Compare commits

...

11 Commits

Author SHA1 Message Date
CodeShakingSheep
52e1ee7625
Merge 56bfce06f1 into d3edf07ac9 2025-04-09 16:18:48 +05:30
Kingkor Roy Tirtho
d3edf07ac9 fix: default accent color in orange but it shows blue in settings 2025-04-07 16:16:43 +06:00
Kingkor Roy Tirtho
8fc319d980 fix(mobile): dialogs in bottom sheet are not opening 2025-04-07 14:53:05 +06:00
Seungmin Kim
e986baa0aa
chore: revise filter for ISRC search (#2614) 2025-04-07 13:12:45 +06:00
Kingkor Roy Tirtho
8a7f5c4008 chore: fix weird hovered mode on track tile options 2025-03-28 22:32:05 +06:00
Kingkor Roy Tirtho
9d2ad1c626 chore: upgrade shadcn_flutter version 2025-03-28 22:24:23 +06:00
Kingkor Roy Tirtho
b74c2eab8f fix: calling /track/:streamId endpoint causes active sourced track to be anything 2025-03-28 20:57:46 +06:00
Seungmin Kim
2c4cc94985
feat: add ISRC track search for YouTube (#2594)
* Add ISRC track search for YouTube

* Do not probe Song.Link when ISRC results are valid, fix rate limit
2025-03-28 19:10:54 +06:00
CodeShakingSheep
56bfce06f1 Remove one more empthy path in website SVG 2024-06-20 23:12:03 -05:00
CodeShakingSheep
5f5c055606 Optimize website SVG 2024-06-20 23:10:32 -05:00
CodeShakingSheep
2bc04325b1 Optimize SVG icons 2024-06-20 23:03:22 -05:00
43 changed files with 3963 additions and 266 deletions

View File

@ -9,6 +9,7 @@
"fuzzywuzzy", "fuzzywuzzy",
"gapless", "gapless",
"instrumentalness", "instrumentalness",
"isrc",
"Mpris", "Mpris",
"RGBO", "RGBO",
"riverpod", "riverpod",

View File

@ -3,4 +3,17 @@
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET" /> <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> </manifest>

View File

@ -316,34 +316,4 @@
style="stroke-width:3.28861" /><path style="stroke-width:3.28861" /><path
d="m 188.76763,155.437 v 98.42 c 0,5.867 4.741,10.605 10.60501,10.605 5.854,0 10.605,-4.738 10.605,-10.605 v -98.42 c 0,-5.856 -4.751,-10.605 -10.605,-10.605 -5.86401,0 -10.60501,4.744 -10.60501,10.605 z" d="m 188.76763,155.437 v 98.42 c 0,5.867 4.741,10.605 10.60501,10.605 5.854,0 10.605,-4.738 10.605,-10.605 v -98.42 c 0,-5.856 -4.751,-10.605 -10.605,-10.605 -5.86401,0 -10.60501,4.744 -10.60501,10.605 z"
style="fill:none;stroke:url(#linearGradient5506);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round" style="fill:none;stroke:url(#linearGradient5506);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round"
id="path5502" /></g><g id="path5502" /></g></svg>
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g240" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g242" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g244" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g246" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g248" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g250" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g252" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g254" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g256" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g258" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g260" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g262" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g264" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g266" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g268" /></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -326,34 +326,4 @@
style="stroke-width:3.28861" /><path style="stroke-width:3.28861" /><path
d="m 188.76763,155.437 v 98.42 c 0,5.867 4.741,10.605 10.60501,10.605 5.854,0 10.605,-4.738 10.605,-10.605 v -98.42 c 0,-5.856 -4.751,-10.605 -10.605,-10.605 -5.86401,0 -10.60501,4.744 -10.60501,10.605 z" d="m 188.76763,155.437 v 98.42 c 0,5.867 4.741,10.605 10.60501,10.605 5.854,0 10.605,-4.738 10.605,-10.605 v -98.42 c 0,-5.856 -4.751,-10.605 -10.605,-10.605 -5.86401,0 -10.60501,4.744 -10.60501,10.605 z"
style="fill:none;stroke:url(#linearGradient5506);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round" style="fill:none;stroke:url(#linearGradient5506);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round"
id="path5502" /></g><g id="path5502" /></g></svg>
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g240" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g242" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g244" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g246" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g248" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g250" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g252" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g254" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g256" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g258" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g260" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g262" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g264" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g266" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g268" /></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

View File

@ -94,6 +94,7 @@ abstract class FakeData {
..trackNumber = 1 ..trackNumber = 1
..type = "type" ..type = "type"
..uri = "uri" ..uri = "uri"
..externalIds = externalIds
..isPlayable = true ..isPlayable = true
..explicit = false ..explicit = false
..linkedFrom = trackLink; ..linkedFrom = trackLink;

View File

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

View File

@ -13,7 +13,7 @@ class HeartButton extends HookConsumerWidget {
final IconData? icon; final IconData? icon;
final Color? color; final Color? color;
final String? tooltip; final String? tooltip;
final ButtonVariance variance; final AbstractButtonStyle variance;
final ButtonSize size; final ButtonSize size;
const HeartButton({ const HeartButton({
required this.isLiked, required this.isLiked,

View File

@ -1,7 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/extensions/button_variance.dart';
class ShadcnWindowButton extends StatelessWidget { class ShadcnWindowButton extends StatelessWidget {
final Widget icon; final Widget icon;
@ -22,7 +21,7 @@ class ShadcnWindowButton extends StatelessWidget {
height: 32, height: 32,
child: IconButton( child: IconButton(
variance: ButtonVariance.ghost.copyWith( variance: ButtonVariance.ghost.copyWith(
decoration: (context, states) { decoration: (context, states, value) {
final decoration = ButtonVariance.ghost.decoration(context, states) final decoration = ButtonVariance.ghost.decoration(context, states)
as BoxDecoration; as BoxDecoration;
if (hoverBackgroundColor != null && if (hoverBackgroundColor != null &&

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
@ -17,7 +17,6 @@ import 'package:spotube/components/links/link_text.dart';
import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/components/track_tile/track_options.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/button_variance.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
@ -108,7 +107,7 @@ class TrackTile extends HookConsumerWidget {
? ButtonVariance.destructive ? ButtonVariance.destructive
: ButtonVariance.ghost) : ButtonVariance.ghost)
.copyWith( .copyWith(
padding: (context, states) => padding: (context, states, value) =>
const EdgeInsets.symmetric(vertical: 8, horizontal: 0), const EdgeInsets.symmetric(vertical: 8, horizontal: 0),
), ),
leading: Row( leading: Row(
@ -229,7 +228,8 @@ class TrackTile extends HookConsumerWidget {
Flexible( Flexible(
child: Button( child: Button(
style: ButtonVariance.link.copyWith( style: ButtonVariance.link.copyWith(
padding: (context, states) => EdgeInsets.zero, padding: (context, states, value) =>
EdgeInsets.zero,
), ),
onPressed: () { onPressed: () {
context context

View File

@ -9,7 +9,7 @@ class ButtonTile extends StatelessWidget {
final VoidCallback? onPressed; final VoidCallback? onPressed;
final VoidCallback? onLongPress; final VoidCallback? onLongPress;
final bool selected; final bool selected;
final ButtonVariance style; final AbstractButtonStyle style;
final EdgeInsets? padding; final EdgeInsets? padding;
const ButtonTile({ const ButtonTile({

View File

@ -4,7 +4,9 @@ import 'dart:typed_data';
import 'package:metadata_god/metadata_god.dart'; import 'package:metadata_god/metadata_god.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
extension TrackExtensions on Track { extension TrackExtensions on Track {
Track fromFile( Track fromFile(
@ -67,27 +69,40 @@ extension TrackExtensions on Track {
} }
} }
extension TrackSimpleExtensions on TrackSimple { extension IterableTrackSimpleExtensions on Iterable<TrackSimple> {
Track asTrack(AlbumSimple album) { Future<List<Track>> asTracks(AlbumSimple album, ref) async {
Track track = Track(); try {
track.name = name; final spotify = ref.read(spotifyProvider);
track.album = album; final tracks = await spotify.invoke(
track.artists = artists; (api) => api.tracks.list(map((trackSimple) => trackSimple.id!).toList()));
track.availableMarkets = availableMarkets; return tracks.toList();
track.discNumber = discNumber; } catch (e, stack) {
track.durationMs = durationMs; // Ignore errors and create the track locally
track.explicit = explicit; AppLogger.reportError(e, stack);
track.externalUrls = externalUrls;
track.href = href; List<Track> tracks = [];
track.id = id; for (final trackSimple in this) {
track.isPlayable = isPlayable; Track track = Track();
track.linkedFrom = linkedFrom; track.album = album;
track.name = name; track.name = trackSimple.name;
track.previewUrl = previewUrl; track.artists = trackSimple.artists;
track.trackNumber = trackNumber; track.availableMarkets = trackSimple.availableMarkets;
track.type = type; track.discNumber = trackSimple.discNumber;
track.uri = uri; track.durationMs = trackSimple.durationMs;
return track; track.explicit = trackSimple.explicit;
track.externalUrls = trackSimple.externalUrls;
track.href = trackSimple.href;
track.id = trackSimple.id;
track.isPlayable = trackSimple.isPlayable;
track.linkedFrom = trackSimple.linkedFrom;
track.previewUrl = trackSimple.previewUrl;
track.trackNumber = trackSimple.trackNumber;
track.type = trackSimple.type;
track.uri = trackSimple.uri;
tracks.add(track);
}
return tracks;
}
} }
} }

View File

@ -62,7 +62,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override
int get schemaVersion => 4; int get schemaVersion => 5;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@ -87,6 +87,33 @@ class AppDatabase extends _$AppDatabase {
schema.preferencesTable.youtubeClientEngine, 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, 'accent_color_scheme', aliasedName, false,
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: const Constant("Blue:0xFF2196F3")) defaultValue: const Constant("Orange:0xFFf97315"))
.withConverter<SpotubeColor>( .withConverter<SpotubeColor>(
$PreferencesTableTable.$converteraccentColorScheme); $PreferencesTableTable.$converteraccentColorScheme);
static const VerificationMeta _layoutModeMeta = static const VerificationMeta _layoutModeMeta =

View File

@ -2,7 +2,7 @@
import 'package:drift/internal/versioned_schema.dart' as i0; import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1; import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/services/sourced_track/enums.dart'; // ignore_for_file: type=lint,unused_import import 'package:spotube/services/sourced_track/enums.dart'; // ignore_for_file: type=lint,unused_import
@ -1188,10 +1188,232 @@ i1.GeneratedColumn<String> _column_54(String aliasedName) =>
i1.GeneratedColumn<String>('youtube_client_engine', aliasedName, false, i1.GeneratedColumn<String>('youtube_client_engine', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)); 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({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, 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, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4, required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -1210,6 +1432,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from3To4(migrator, schema); await from3To4(migrator, schema);
return 4; return 4;
case 4:
final schema = Schema5(database: database);
final migrator = i1.Migrator(database, schema);
await from4To5(migrator, schema);
return 5;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -1220,10 +1447,12 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, 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, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4, required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
}) => }) =>
i0.VersionedSchema.stepByStepHelper( i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
from2To3: from2To3, from2To3: from2To3,
from3To4: from3To4, from3To4: from3To4,
from4To5: from4To5,
)); ));

View File

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

View File

@ -54,7 +54,7 @@ class AlbumCard extends HookConsumerWidget {
Future<List<Track>> fetchAllTrack() async { Future<List<Track>> fetchAllTrack() async {
if (album.tracks != null && album.tracks!.isNotEmpty) { if (album.tracks != null && album.tracks!.isNotEmpty) {
return album.tracks!.map((track) => track.asTrack(album)).toList(); return album.tracks!.asTracks(album, ref);
} }
await ref.read(albumTracksProvider(album).future); await ref.read(albumTracksProvider(album).future);
return ref.read(albumTracksProvider(album).notifier).fetchAll(); return ref.read(albumTracksProvider(album).notifier).fetchAll();

View File

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:spotify/spotify.dart' hide Image; import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';

View File

@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
@ -132,7 +132,7 @@ class PlayerView extends HookConsumerWidget {
Tooltip( Tooltip(
tooltip: TooltipContainer( tooltip: TooltipContainer(
child: Text(context.l10n.details), child: Text(context.l10n.details),
), ).call,
child: IconButton.ghost( child: IconButton.ghost(
icon: const Icon(SpotubeIcons.info, size: 18), icon: const Icon(SpotubeIcons.info, size: 18),
onPressed: currentTrack == null onPressed: currentTrack == null

View File

@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
@ -82,7 +82,7 @@ class PlayerActions extends HookConsumerWidget {
children: [ children: [
if (showQueue) if (showQueue)
Tooltip( Tooltip(
tooltip: TooltipContainer(child: Text(context.l10n.queue)), tooltip: TooltipContainer(child: Text(context.l10n.queue)).call,
child: IconButton.ghost( child: IconButton.ghost(
icon: const Icon(SpotubeIcons.queue), icon: const Icon(SpotubeIcons.queue),
enabled: playlist.activeTrack != null, enabled: playlist.activeTrack != null,
@ -119,7 +119,8 @@ class PlayerActions extends HookConsumerWidget {
if (!isLocalTrack) if (!isLocalTrack)
Tooltip( Tooltip(
tooltip: TooltipContainer( tooltip: TooltipContainer(
child: Text(context.l10n.alternative_track_sources)), child: Text(context.l10n.alternative_track_sources),
).call,
child: IconButton.ghost( child: IconButton.ghost(
enabled: playlist.activeTrack != null, enabled: playlist.activeTrack != null,
icon: const Icon(SpotubeIcons.alternativeRoute), icon: const Icon(SpotubeIcons.alternativeRoute),
@ -160,7 +161,8 @@ class PlayerActions extends HookConsumerWidget {
else else
Tooltip( Tooltip(
tooltip: tooltip:
TooltipContainer(child: Text(context.l10n.download_track)), TooltipContainer(child: Text(context.l10n.download_track))
.call,
child: IconButton.ghost( child: IconButton.ghost(
icon: Icon( icon: Icon(
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download, isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,

View File

@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/collections/intents.dart'; import 'package:spotube/collections/intents.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart';
import 'package:spotube/collections/intents.dart'; import 'package:spotube/collections/intents.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:spotify/spotify.dart' hide Offset, Image; import 'package:spotify/spotify.dart' hide Offset, Image;
import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';

View File

@ -1,7 +1,7 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';

View File

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';

View File

@ -2,7 +2,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';

View File

@ -125,28 +125,34 @@ class SearchPage extends HookConsumerWidget {
child: TextField( child: TextField(
autofocus: true, autofocus: true,
controller: controller, controller: controller,
leading: features: [
const Icon(SpotubeIcons.search), const InputFeature.leading(
textInputAction: TextInputAction.search, Icon(SpotubeIcons.search),
placeholder: Text(context.l10n.search), ),
trailing: AnimatedCrossFade( InputFeature.trailing(
duration: AnimatedCrossFade(
const Duration(milliseconds: 300), duration: const Duration(
crossFadeState: milliseconds: 300),
controller.text.isNotEmpty crossFadeState: controller
.text.isNotEmpty
? CrossFadeState.showFirst ? CrossFadeState.showFirst
: CrossFadeState.showSecond, : CrossFadeState.showSecond,
firstChild: IconButton.ghost( firstChild: IconButton.ghost(
size: ButtonSize.small, size: ButtonSize.small,
icon: icon: const Icon(
const Icon(SpotubeIcons.close), SpotubeIcons.close),
onPressed: () { onPressed: () {
controller.clear(); controller.clear();
}, },
), ),
secondChild: const SizedBox.square( secondChild:
dimension: 28), const SizedBox.square(
), dimension: 28),
),
)
],
textInputAction: TextInputAction.search,
placeholder: Text(context.l10n.search),
onSubmitted: onSubmitted, onSubmitted: onSubmitted,
), ),
), ),

View File

@ -11,7 +11,7 @@ import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:piped_client/piped_client.dart'; import 'package:piped_client/piped_client.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/form/text_form_field.dart'; import 'package:spotube/components/form/text_form_field.dart';
@ -106,7 +106,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
Tooltip( Tooltip(
tooltip: TooltipContainer( tooltip: TooltipContainer(
child: Text(context.l10n.add_custom_url), child: Text(context.l10n.add_custom_url),
), ).call,
child: IconButton.outline( child: IconButton.outline(
icon: const Icon(SpotubeIcons.edit), icon: const Icon(SpotubeIcons.edit),
size: ButtonSize.small, size: ButtonSize.small,
@ -261,7 +261,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
Tooltip( Tooltip(
tooltip: TooltipContainer( tooltip: TooltipContainer(
child: Text(context.l10n.add_custom_url), child: Text(context.l10n.add_custom_url),
), ).call,
child: IconButton.outline( child: IconButton.outline(
icon: const Icon(SpotubeIcons.edit), icon: const Icon(SpotubeIcons.edit),
size: ButtonSize.small, size: ButtonSize.small,

View File

@ -128,7 +128,10 @@ class ServerPlaybackRoutes {
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier) .read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
.refreshStreamingUrl(); .refreshStreamingUrl();
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); if (playlist.activeTrack?.id == sourcedTrack?.id &&
sourcedTrack != null) {
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
}
return await dio.get<Uint8List>( return await dio.get<Uint8List>(
sourcedTrack!.url, sourcedTrack!.url,
@ -199,7 +202,10 @@ class ServerPlaybackRoutes {
? activeSourcedTrack ? activeSourcedTrack
: await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future); : await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future);
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); if (playlist.activeTrack?.id == sourcedTrack?.id &&
sourcedTrack != null) {
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
}
final (bytes: audioBytes, response: res) = final (bytes: audioBytes, response: res) =
await streamTrack(sourcedTrack!, request.headers); await streamTrack(sourcedTrack!, request.headers);

View File

@ -33,7 +33,7 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
final tracks = await spotify.invoke( final tracks = await spotify.invoke(
(api) => api.albums.tracks(arg.id!).getPage(limit, offset), (api) => api.albums.tracks(arg.id!).getPage(limit, offset),
); );
final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? []; final items = await tracks.items!.asTracks(arg, ref);
return ( return (
items: items, items: items,

View File

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

View File

@ -236,32 +236,75 @@ class YoutubeSourcedTrack extends SourcedTrack {
.toList(); .toList();
} }
static Future<List<YoutubeVideoInfo>> fetchFromIsrc({
required Track track,
required Ref ref,
}) async {
final isrcResults = <YoutubeVideoInfo>[];
final isrc = track.externalIds?.isrc;
if (isrc != null && isrc.isNotEmpty) {
final searchedVideos =
await ref.read(youtubeEngineProvider).searchVideos(isrc.toString());
if (searchedVideos.isNotEmpty) {
isrcResults.addAll(searchedVideos
.map<YoutubeVideoInfo>(YoutubeVideoInfo.fromVideo)
.map((YoutubeVideoInfo videoInfo) {
final ytWords = videoInfo.title
.toLowerCase()
.replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '')
.split(RegExp(r'\p{Z}+', unicode: true))
.where((item) => item.isNotEmpty);
final spWords = track.name!
.toLowerCase()
.replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '')
.split(RegExp(r'\p{Z}+', unicode: true))
.where((item) => item.isNotEmpty);
// Single word and duration match with 3 second tolerance
if (ytWords.any((word) => spWords.contains(word)) &&
(videoInfo.duration - track.duration!)
.abs().inMilliseconds <= 3000) {
return videoInfo;
}
return null;
})
.whereType<YoutubeVideoInfo>()
.toList());
}
}
return isrcResults;
}
static Future<List<SiblingType>> fetchSiblings({ static Future<List<SiblingType>> fetchSiblings({
required Track track, required Track track,
required Ref ref, required Ref ref,
}) async { }) async {
final links = await SongLinkService.links(track.id!); final videoResults = <YoutubeVideoInfo>[];
final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube");
if (ytLink?.url != null if (track is! SourcedTrack) {
// allows to fetch siblings more results for already sourced track final isrcResults = await fetchFromIsrc(
&& track: track,
track is! SourcedTrack) { ref: ref,
try { );
return [
await toSiblingType( videoResults.addAll(isrcResults);
0,
YoutubeVideoInfo.fromVideo( if (isrcResults.isEmpty) {
await ref.read(youtubeEngineProvider).getVideo( final links = await SongLinkService.links(track.id!);
Uri.parse(ytLink!.url!).queryParameters["v"]!, final ytLink = links.firstWhereOrNull(
), (link) => link.platform == "youtube",
), );
ref, if (ytLink?.url != null) {
) try {
]; videoResults.add(
} on VideoUnplayableException catch (e, stack) { YoutubeVideoInfo.fromVideo(await ref
// Ignore this error and continue with the search .read(youtubeEngineProvider)
AppLogger.reportError(e, stack); .getVideo(Uri.parse(ytLink!.url!).queryParameters["v"]!)),
);
} on VideoUnplayableException catch (e, stack) {
// Ignore this error and continue with the search
AppLogger.reportError(e, stack);
}
}
} }
} }
@ -271,20 +314,27 @@ class YoutubeSourcedTrack extends SourcedTrack {
await ref.read(youtubeEngineProvider).searchVideos(query); await ref.read(youtubeEngineProvider).searchVideos(query);
if (ServiceUtils.onlyContainsEnglish(query)) { if (ServiceUtils.onlyContainsEnglish(query)) {
return await Future.wait(searchResults videoResults
.map(YoutubeVideoInfo.fromVideo) .addAll(searchResults.map(YoutubeVideoInfo.fromVideo).toList());
.mapIndexed((index, info) => toSiblingType(index, info, ref))); } else {
videoResults.addAll(rankResults(
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
track,
));
} }
final rankedSiblings = rankResults( final seenIds = <String>{};
searchResults.map(YoutubeVideoInfo.fromVideo).toList(), int index = 0;
track,
);
return await Future.wait( return await Future.wait(
rankedSiblings videoResults.map((videoResult) async {
.mapIndexed((index, info) => toSiblingType(index, info, ref)), // Deduplicate results
); if (!seenIds.contains(videoResult.id)) {
seenIds.add(videoResult.id);
return await toSiblingType(index++, videoResult, ref);
}
return null;
}),
).then((s) => s.whereType<SiblingType>().toList());
} }
@override @override

View File

@ -32,6 +32,7 @@ function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_options(${TARGET} PRIVATE -Wall -Werror) target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>") target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>") target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
target_compile_options(${TARGET} PRIVATE -Wno-error=deprecated-declarations)
endfunction() endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")

View File

@ -2012,10 +2012,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shadcn_flutter name: shadcn_flutter
sha256: "1e5f40484a42217a69af254952168783d1305025d56dabc45ab16396dba84d5e" sha256: "2b6faf9a93628469c29a534e653295e26781f2799efe5dc971b91e91062ebf52"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.26" version: "0.0.32"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2449,10 +2449,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: tray_manager name: tray_manager
sha256: f231031c5c0eb4ad514e18ddaab27a912ddbe50335c594bc28fb0f9972ab6a84 sha256: c2da0f0f1ddb455e721cf68d05d1281fec75cf5df0a1d3cb67b6ca0bdfd5709d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.1" version: "0.4.0"
type_plus: type_plus:
dependency: transitive dependency: transitive
description: description:

View File

@ -102,7 +102,7 @@ dependencies:
ref: dart-3-support ref: dart-3-support
url: https://github.com/KRTirtho/scrobblenaut.git url: https://github.com/KRTirtho/scrobblenaut.git
scroll_to_index: ^3.0.1 scroll_to_index: ^3.0.1
shadcn_flutter: ^0.0.26 shadcn_flutter: ^0.0.32
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
shelf: ^1.4.1 shelf: ^1.4.1
shelf_router: ^1.1.4 shelf_router: ^1.1.4
@ -120,7 +120,7 @@ dependencies:
test: ^1.25.7 test: ^1.25.7
timezone: ^0.10.0 timezone: ^0.10.0
titlebar_buttons: ^1.0.0 titlebar_buttons: ^1.0.0
tray_manager: ^0.3.0 tray_manager: ^0.4.0
url_launcher: ^6.2.6 url_launcher: ^6.2.6
uuid: ^4.4.0 uuid: ^4.4.0
version: ^3.0.2 version: ^3.0.2

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -316,34 +316,4 @@
style="stroke-width:3.28861" /><path style="stroke-width:3.28861" /><path
d="m 188.76763,155.437 v 98.42 c 0,5.867 4.741,10.605 10.60501,10.605 5.854,0 10.605,-4.738 10.605,-10.605 v -98.42 c 0,-5.856 -4.751,-10.605 -10.605,-10.605 -5.86401,0 -10.60501,4.744 -10.60501,10.605 z" d="m 188.76763,155.437 v 98.42 c 0,5.867 4.741,10.605 10.60501,10.605 5.854,0 10.605,-4.738 10.605,-10.605 v -98.42 c 0,-5.856 -4.751,-10.605 -10.605,-10.605 -5.86401,0 -10.60501,4.744 -10.60501,10.605 z"
style="fill:none;stroke:url(#linearGradient5506);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round" style="fill:none;stroke:url(#linearGradient5506);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round"
id="path5502" /></g><g id="path5502" /></g></svg>
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g240" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g242" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g244" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g246" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g248" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g250" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g252" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g254" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g256" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g258" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g260" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g262" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g264" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g266" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g268" /></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB