Compare commits

..

1 Commits

Author SHA1 Message Date
Guanciottaman
eee7e08ca1
Merge ff252d6b14 into e986baa0aa 2025-04-07 14:20:35 +06:00
15 changed files with 68 additions and 3774 deletions

View File

@ -3,17 +3,4 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="${applicationName}"
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name_en"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true">
<!-- Disable Impeller -->
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
</application>
</manifest>

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart' show showModalBottomSheet;
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart';
@ -25,7 +26,7 @@ class AdaptiveMenuButton<T> extends MenuButton {
/// An adaptive widget that shows a [PopupMenuButton] when screen size is above
/// or equal to 640px
/// In smaller screen, a [IconButton] with a [openDrawer] is shown
/// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown
class AdaptivePopSheetList<T> extends StatelessWidget {
final List<AdaptiveMenuButton<T>> Function(BuildContext context) items;
final Widget? icon;
@ -101,13 +102,15 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
return;
}
await openDrawer(
showModalBottomSheet(
context: context,
draggable: true,
enableDrag: true,
showDragHandle: true,
position: OverlayPosition.bottom,
borderRadius: context.theme.borderRadiusMd,
transformBackdrop: false,
useRootNavigator: true,
shape: RoundedRectangleBorder(
borderRadius: context.theme.borderRadiusMd,
),
backgroundColor: context.theme.colorScheme.card,
builder: (context) {
final children = childrenModified(context);
return ListView.builder(
@ -124,7 +127,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
onPressed: () {
data.onPressed?.call(context);
if (data.autoClose) {
closeDrawer(context);
Navigator.of(context).pop();
}
},
leading: data.leading,

View File

@ -74,26 +74,6 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
ref.watch(presentationStateProvider(options.collection).notifier);
final selectedTracks = state.selectedTracks;
Future<void> actionDownloadTracks({
required BuildContext context,
required List<Track> tracks,
required String action,
}) async {
final confirmed = audioSource == AudioSource.piped ||
(await showDialog<bool>(
context: context,
builder: (context) {
return const ConfirmDownloadDialog();
},
) ??
false);
if (confirmed != true) return;
downloader.batchAddToQueue(tracks);
notifier.deselectAllTracks();
if (!context.mounted) return;
showToastForAction(context, action, tracks.length);
}
return AdaptivePopSheetList(
tooltip: context.l10n.more_actions,
headings: [
@ -115,12 +95,22 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
switch (action) {
case "download":
await actionDownloadTracks(
context: context,
tracks: tracks,
action: action,
);
break;
{
final confirmed = audioSource == AudioSource.piped ||
(await showDialog<bool?>(
context: context,
builder: (context) {
return const ConfirmDownloadDialog();
},
) ??
false);
if (confirmed != true) return;
downloader.batchAddToQueue(tracks);
notifier.deselectAllTracks();
if (!context.mounted) return;
showToastForAction(context, action, tracks.length);
break;
}
case "add-to-playlist":
{
if (context.mounted) {

View File

@ -57,7 +57,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.shuffle_playlist),
).call,
),
child: IconButton.secondary(
icon: isLoading
? const Center(
@ -73,7 +73,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_to_queue),
).call,
),
child: IconButton.secondary(
icon: const Icon(SpotubeIcons.queueAdd),
enabled: !isLoading && !isActive,
@ -126,7 +126,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.share),
).call,
),
child: IconButton.outline(
icon: const Icon(SpotubeIcons.share),
size: ButtonSize.small,

View File

@ -91,14 +91,24 @@ class TrackOptions extends HookConsumerWidget {
) {
/// showDialog doesn't work for some reason. So we have to
/// manually push a Dialog Route in the Navigator to get it working
showDialog(
context: context,
builder: (context) {
return PlaylistAddTrackDialog(
tracks: [track],
openFromPlaylist: playlistId,
);
},
Navigator.push(
context,
DialogRoute(
alignment: Alignment.bottomCenter,
transitionBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
context: context,
barrierColor: Colors.black.withValues(alpha: 0.5),
builder: (context) {
return Center(
child: PlaylistAddTrackDialog(
tracks: [track],
openFromPlaylist: playlistId,
),
);
},
),
);
}
@ -328,7 +338,6 @@ class TrackOptions extends HookConsumerWidget {
}
},
icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
variance: ButtonVariance.outline,
headings: [
Basic(
leading: AspectRatio(

View File

@ -62,7 +62,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 5;
int get schemaVersion => 4;
@override
MigrationStrategy get migration {
@ -87,33 +87,6 @@ class AppDatabase extends _$AppDatabase {
schema.preferencesTable.youtubeClientEngine,
);
},
from4To5: (m, schema) async {
final columnName = schema.preferencesTable.accentColorScheme
.escapedNameFor(SqlDialect.sqlite);
final columnNameOld =
'"${schema.preferencesTable.accentColorScheme.name}_old"';
final tableName = schema.preferencesTable.actualTableName;
await customStatement(
"ALTER TABLE $tableName "
"RENAME COLUMN $columnName to $columnNameOld",
);
await customStatement(
"ALTER TABLE $tableName "
"ADD COLUMN $columnName TEXT NOT NULL DEFAULT 'Orange:0xFFf97315'",
);
await customStatement(
"UPDATE $tableName "
"SET $columnName = $columnNameOld",
);
await customStatement(
"ALTER TABLE $tableName "
"DROP COLUMN $columnNameOld",
);
await customStatement(
"UPDATE $tableName "
"SET $columnName = 'Orange:0xFFf97315' WHERE $columnName = 'Blue:0xFF2196F3'",
);
},
),
);
}

View File

@ -666,7 +666,7 @@ class $PreferencesTableTable extends PreferencesTable
'accent_color_scheme', aliasedName, false,
type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant("Orange:0xFFf97315"))
defaultValue: const Constant("Blue:0xFF2196F3"))
.withConverter<SpotubeColor>(
$PreferencesTableTable.$converteraccentColorScheme);
static const VerificationMeta _layoutModeMeta =

View File

@ -2,7 +2,7 @@
import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/services/sourced_track/enums.dart'; // ignore_for_file: type=lint,unused_import
@ -1188,232 +1188,10 @@ i1.GeneratedColumn<String> _column_54(String aliasedName) =>
i1.GeneratedColumn<String>('youtube_client_engine', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name));
final class Schema5 extends i0.VersionedSchema {
Schema5({required super.database}) : super(version: 5);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
authenticationTable,
blacklistTable,
preferencesTable,
scrobblerTable,
skipSegmentTable,
sourceMatchTable,
audioPlayerStateTable,
playlistTable,
playlistMediaTable,
historyTable,
lyricsTable,
uniqueBlacklist,
uniqTrackMatch,
];
late final Shape0 authenticationTable = Shape0(
source: i0.VersionedTable(
entityName: 'authentication_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 blacklistTable = Shape1(
source: i0.VersionedTable(
entityName: 'blacklist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_5,
_column_6,
],
attachedDatabase: database,
),
alias: null);
late final Shape12 preferencesTable = Shape12(
source: i0.VersionedTable(
entityName: 'preferences_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_7,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_15,
_column_55,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_23,
_column_24,
_column_25,
_column_26,
_column_54,
_column_27,
_column_28,
_column_29,
_column_30,
_column_31,
_column_53,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 scrobblerTable = Shape3(
source: i0.VersionedTable(
entityName: 'scrobbler_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_33,
_column_34,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 skipSegmentTable = Shape4(
source: i0.VersionedTable(
entityName: 'skip_segment_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_35,
_column_36,
_column_37,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 sourceMatchTable = Shape5(
source: i0.VersionedTable(
entityName: 'source_match_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_38,
_column_39,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 audioPlayerStateTable = Shape6(
source: i0.VersionedTable(
entityName: 'audio_player_state_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_40,
_column_41,
_column_42,
_column_43,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 playlistTable = Shape7(
source: i0.VersionedTable(
entityName: 'playlist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_44,
_column_45,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 playlistMediaTable = Shape8(
source: i0.VersionedTable(
entityName: 'playlist_media_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_46,
_column_47,
_column_48,
_column_49,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 historyTable = Shape9(
source: i0.VersionedTable(
entityName: 'history_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_50,
_column_51,
_column_52,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 lyricsTable = Shape10(
source: i0.VersionedTable(
entityName: 'lyrics_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_52,
],
attachedDatabase: database,
),
alias: null);
final i1.Index uniqueBlacklist = i1.Index('unique_blacklist',
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
final i1.Index uniqTrackMatch = i1.Index('uniq_track_match',
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
}
i1.GeneratedColumn<String> _column_55(String aliasedName) =>
i1.GeneratedColumn<String>('accent_color_scheme', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: const Constant("Orange:0xFFf97315"));
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@ -1432,11 +1210,6 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from3To4(migrator, schema);
return 4;
case 4:
final schema = Schema5(database: database);
final migrator = i1.Migrator(database, schema);
await from4To5(migrator, schema);
return 5;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@ -1447,12 +1220,10 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
from2To3: from2To3,
from3To4: from3To4,
from4To5: from4To5,
));

View File

@ -79,7 +79,7 @@ class PreferencesTable extends Table {
TextColumn get closeBehavior => textEnum<CloseBehavior>()
.withDefault(Constant(CloseBehavior.close.name))();
TextColumn get accentColorScheme => text()
.withDefault(const Constant("Orange:0xFFf97315"))
.withDefault(const Constant("Blue:0xFF2196F3"))
.map(const SpotubeColorConverter())();
TextColumn get layoutMode =>
textEnum<LayoutMode>().withDefault(Constant(LayoutMode.adaptive.name))();
@ -130,7 +130,7 @@ class PreferencesTable extends Table {
systemTitleBar: false,
skipNonMusic: false,
closeBehavior: CloseBehavior.close,
accentColorScheme: SpotubeColor(Colors.orange.value, name: "Orange"),
accentColorScheme: SpotubeColor(Colors.blue.value, name: "Blue"),
layoutMode: LayoutMode.adaptive,
locale: const Locale("system", "system"),
market: Market.US,

View File

@ -132,7 +132,7 @@ class PlayerView extends HookConsumerWidget {
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.details),
).call,
),
child: IconButton.ghost(
icon: const Icon(SpotubeIcons.info, size: 18),
onPressed: currentTrack == null

View File

@ -82,7 +82,7 @@ class PlayerActions extends HookConsumerWidget {
children: [
if (showQueue)
Tooltip(
tooltip: TooltipContainer(child: Text(context.l10n.queue)).call,
tooltip: TooltipContainer(child: Text(context.l10n.queue)),
child: IconButton.ghost(
icon: const Icon(SpotubeIcons.queue),
enabled: playlist.activeTrack != null,
@ -119,8 +119,7 @@ class PlayerActions extends HookConsumerWidget {
if (!isLocalTrack)
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.alternative_track_sources),
).call,
child: Text(context.l10n.alternative_track_sources)),
child: IconButton.ghost(
enabled: playlist.activeTrack != null,
icon: const Icon(SpotubeIcons.alternativeRoute),
@ -161,8 +160,7 @@ class PlayerActions extends HookConsumerWidget {
else
Tooltip(
tooltip:
TooltipContainer(child: Text(context.l10n.download_track))
.call,
TooltipContainer(child: Text(context.l10n.download_track)),
child: IconButton.ghost(
icon: Icon(
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,

View File

@ -90,9 +90,9 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
Future<void> reset() async {
final db = ref.read(databaseProvider);
final query = db.update(db.preferencesTable);
final query = db.update(db.preferencesTable)..where((t) => t.id.equals(0));
await query.replace(PreferencesTableCompanion.insert(id: const Value(0)));
await query.replace(PreferencesTableCompanion.insert());
}
static Future<String> getMusicCacheDir() async {

View File

@ -3,30 +3,27 @@
// ignore_for_file: type=lint
import 'package:drift/drift.dart';
import 'package:drift/internal/migrations.dart';
import 'schema_v3.dart' as v3;
import 'schema_v5.dart' as v5;
import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2;
import 'schema_v4.dart' as v4;
import 'schema_v3.dart' as v3;
import 'schema_v2.dart' as v2;
import 'schema_v1.dart' as v1;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
switch (version) {
case 3:
return v3.DatabaseAtV3(db);
case 5:
return v5.DatabaseAtV5(db);
case 1:
return v1.DatabaseAtV1(db);
case 2:
return v2.DatabaseAtV2(db);
case 4:
return v4.DatabaseAtV4(db);
case 3:
return v3.DatabaseAtV3(db);
case 2:
return v2.DatabaseAtV2(db);
case 1:
return v1.DatabaseAtV1(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3, 4, 5];
static const versions = const [1, 2, 3, 4];
}

File diff suppressed because it is too large Load Diff