fix: local track not showing up in queue

This commit is contained in:
Kingkor Roy Tirtho 2024-05-23 16:56:52 +06:00
parent 22caa818f4
commit d82261cb25
8 changed files with 385 additions and 335 deletions

View File

@ -6,32 +6,20 @@ import 'package:file_selector/file_selector.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:metadata_god/metadata_god.dart'; import 'package:metadata_god/metadata_god.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
import 'package:spotube/components/shared/fallbacks/not_found.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/track.dart'; import 'package:spotube/extensions/track.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
// ignore: depend_on_referenced_packages // ignore: depend_on_referenced_packages
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException; import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
@ -63,7 +51,8 @@ enum SortBy {
album, album,
} }
final localTracksProvider = FutureProvider<Map<String, List<LocalTrack>>>((ref) async { final localTracksProvider =
FutureProvider<Map<String, List<LocalTrack>>>((ref) async {
try { try {
if (kIsWeb) return {}; if (kIsWeb) return {};
final Map<String, List<LocalTrack>> tracks = {}; final Map<String, List<LocalTrack>> tracks = {};
@ -82,7 +71,6 @@ final localTracksProvider = FutureProvider<Map<String, List<LocalTrack>>>((ref)
for (var location in [downloadLocation, ...localLibraryLocations]) { for (var location in [downloadLocation, ...localLibraryLocations]) {
if (location.isEmpty) continue; if (location.isEmpty) continue;
final entities = <FileSystemEntity>[]; final entities = <FileSystemEntity>[];
final dir = Directory(location);
if (await Directory(location).exists()) { if (await Directory(location).exists()) {
entities.addAll(Directory(location).listSync(recursive: true)); entities.addAll(Directory(location).listSync(recursive: true));
} }
@ -110,7 +98,11 @@ final localTracksProvider = FutureProvider<Map<String, List<LocalTrack>>>((ref)
); );
} }
return {"metadata": metadata, "file": file, "art": imageFile.path}; return {
"metadata": metadata,
"file": file,
"art": imageFile.path
};
} catch (e, stack) { } catch (e, stack) {
if (e is FfiException) { if (e is FfiException) {
return {"file": file}; return {"file": file};
@ -152,7 +144,6 @@ class UserLocalTracks extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
@ -163,41 +154,41 @@ class UserLocalTracks extends HookConsumerWidget {
); );
if (dirStr == null) return; if (dirStr == null) return;
if (preferences.localLibraryLocation.contains(dirStr)) return; if (preferences.localLibraryLocation.contains(dirStr)) return;
preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation, dirStr]); preferencesNotifier.setLocalLibraryLocation(
[...preferences.localLibraryLocation, dirStr]);
} else { } else {
String? dirStr = await getDirectoryPath( String? dirStr = await getDirectoryPath(
initialDirectory: preferences.downloadLocation, initialDirectory: preferences.downloadLocation,
); );
if (dirStr == null) return; if (dirStr == null) return;
if (preferences.localLibraryLocation.contains(dirStr)) return; if (preferences.localLibraryLocation.contains(dirStr)) return;
preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation, dirStr]); preferencesNotifier.setLocalLibraryLocation(
[...preferences.localLibraryLocation, dirStr]);
} }
}, [preferences.localLibraryLocation]); }, [preferences.localLibraryLocation]);
final removeLocalLibraryLocation = useCallback((String location) { final removeLocalLibraryLocation = useCallback((String location) {
if (!preferences.localLibraryLocation.contains(location)) return; if (!preferences.localLibraryLocation.contains(location)) return;
preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation]..remove(location)); preferencesNotifier.setLocalLibraryLocation(
[...preferences.localLibraryLocation]..remove(location),
);
}, [preferences.localLibraryLocation]); }, [preferences.localLibraryLocation]);
// This is just to pre-load the tracks. // This is just to pre-load the tracks.
// For now, this gets all of them. // For now, this gets all of them.
ref.watch(localTracksProvider); ref.watch(localTracksProvider);
return Column( return Column(children: [
children: [
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(children: [
children: [
const SizedBox(width: 5), const SizedBox(width: 5),
TextButton.icon( TextButton.icon(
icon: const Icon(SpotubeIcons.folderAdd), icon: const Icon(SpotubeIcons.folderAdd),
label: Text(context.l10n.add_library_location), label: Text(context.l10n.add_library_location),
onPressed: addLocalLibraryLocation, onPressed: addLocalLibraryLocation,
) )
] ])),
)
),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: preferences.localLibraryLocation.length + 1, itemCount: preferences.localLibraryLocation.length + 1,
@ -209,23 +200,28 @@ class UserLocalTracks extends HookConsumerWidget {
location = preferences.localLibraryLocation[index - 1]; location = preferences.localLibraryLocation[index - 1];
} }
return ListTile( return ListTile(
title: preferences.downloadLocation != location ? Text(location) title: preferences.downloadLocation != location
? Text(location)
: Text(context.l10n.downloads), : Text(context.l10n.downloads),
trailing: preferences.downloadLocation != location ? Tooltip( trailing: preferences.downloadLocation != location
? Tooltip(
message: context.l10n.remove_library_location, message: context.l10n.remove_library_location,
child: IconButton( child: IconButton(
icon: Icon(SpotubeIcons.folderRemove, color: Colors.red[400]), icon: Icon(SpotubeIcons.folderRemove,
onPressed: () => removeLocalLibraryLocation(location), color: Colors.red[400]),
onPressed: () =>
removeLocalLibraryLocation(location),
), ),
) : null, )
: null,
onTap: () async { onTap: () async {
context.go("/library/local${location == preferences.downloadLocation ? "?downloads=1" : ""}", extra: location); context.go(
} "/library/local${location == preferences.downloadLocation ? "?downloads=1" : ""}",
extra: location,
); );
} });
}),
), ),
), ]);
]
);
} }
} }

View File

@ -197,6 +197,8 @@ class TrackOptions extends HookConsumerWidget {
return downloadManager.getProgressNotifier(spotubeTrack); return downloadManager.getProgressNotifier(spotubeTrack);
}); });
final isLocalTrack = track is LocalTrack;
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>( final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
onSelected: (value) async { onSelected: (value) async {
switch (value) { switch (value) {
@ -314,15 +316,13 @@ class TrackOptions extends HookConsumerWidget {
), ),
), ),
], ],
children: switch (track.runtimeType) { children: [
LocalTrack() => [ if (isLocalTrack)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.delete, value: TrackOptionValue.delete,
leading: const Icon(SpotubeIcons.trash), leading: const Icon(SpotubeIcons.trash),
title: Text(context.l10n.delete), title: Text(context.l10n.delete),
) ),
],
_ => [
if (mediaQuery.smAndDown) if (mediaQuery.smAndDown)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.album, value: TrackOptionValue.album,
@ -348,7 +348,7 @@ class TrackOptions extends HookConsumerWidget {
leading: const Icon(SpotubeIcons.queueRemove), leading: const Icon(SpotubeIcons.queueRemove),
title: Text(context.l10n.remove_from_queue), title: Text(context.l10n.remove_from_queue),
), ),
if (me.asData?.value != null) if (me.asData?.value != null && !isLocalTrack)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.favorite, value: TrackOptionValue.favorite,
leading: favorites.isLiked leading: favorites.isLiked
@ -363,7 +363,7 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.save_as_favorite, : context.l10n.save_as_favorite,
), ),
), ),
if (auth != null) ...[ if (auth != null && !isLocalTrack) ...[
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.startRadio, value: TrackOptionValue.startRadio,
leading: const Icon(SpotubeIcons.radio), leading: const Icon(SpotubeIcons.radio),
@ -375,12 +375,13 @@ class TrackOptions extends HookConsumerWidget {
title: Text(context.l10n.add_to_playlist), title: Text(context.l10n.add_to_playlist),
), ),
], ],
if (userPlaylist && auth != null) if (userPlaylist && auth != null && !isLocalTrack)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist, value: TrackOptionValue.removeFromPlaylist,
leading: const Icon(SpotubeIcons.removeFilled), leading: const Icon(SpotubeIcons.removeFilled),
title: Text(context.l10n.remove_from_playlist), title: Text(context.l10n.remove_from_playlist),
), ),
if (!isLocalTrack)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.download, value: TrackOptionValue.download,
enabled: !isInQueue, enabled: !isInQueue,
@ -394,6 +395,7 @@ class TrackOptions extends HookConsumerWidget {
: const Icon(SpotubeIcons.download), : const Icon(SpotubeIcons.download),
title: Text(context.l10n.download_track), title: Text(context.l10n.download_track),
), ),
if (!isLocalTrack)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.blacklist, value: TrackOptionValue.blacklist,
leading: const Icon(SpotubeIcons.playlistRemove), leading: const Icon(SpotubeIcons.playlistRemove),
@ -405,11 +407,13 @@ class TrackOptions extends HookConsumerWidget {
: context.l10n.add_to_blacklist, : context.l10n.add_to_blacklist,
), ),
), ),
if (!isLocalTrack)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.share, value: TrackOptionValue.share,
leading: const Icon(SpotubeIcons.share), leading: const Icon(SpotubeIcons.share),
title: Text(context.l10n.share), title: Text(context.l10n.share),
), ),
if (!isLocalTrack)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.songlink, value: TrackOptionValue.songlink,
leading: Assets.logos.songlinkTransparent.image( leading: Assets.logos.songlinkTransparent.image(
@ -419,13 +423,13 @@ class TrackOptions extends HookConsumerWidget {
), ),
title: Text(context.l10n.song_link), title: Text(context.l10n.song_link),
), ),
if (!isLocalTrack)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.details, value: TrackOptionValue.details,
leading: const Icon(SpotubeIcons.info), leading: const Icon(SpotubeIcons.info),
title: Text(context.l10n.details), title: Text(context.l10n.details),
), ),
] ],
},
); );
//! This is the most ANTI pattern I've ever done, but it works //! This is the most ANTI pattern I've ever done, but it works

View File

@ -195,19 +195,26 @@ class TrackTile extends HookConsumerWidget {
children: [ children: [
Expanded( Expanded(
flex: 6, flex: 6,
child: LinkText( child: switch (track) {
LocalTrack() => Text(
track.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
_ => LinkText(
track.name!, track.name!,
"/track/${track.id}", "/track/${track.id}",
push: true, push: true,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
},
), ),
if (constrains.mdAndUp) ...[ if (constrains.mdAndUp) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
flex: 4, flex: 4,
child: switch (track.runtimeType) { child: switch (track) {
LocalTrack() => Text( LocalTrack() => Text(
track.album!.name!, track.album!.name!,
maxLines: 1, maxLines: 1,

View File

@ -1,5 +1,4 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
@ -52,8 +51,8 @@ class LocalLibraryPage extends HookConsumerWidget {
final sortBy = useState<SortBy>(SortBy.none); final sortBy = useState<SortBy>(SortBy.none);
final playlist = ref.watch(proxyPlaylistProvider); final playlist = ref.watch(proxyPlaylistProvider);
final trackSnapshot = ref.watch(localTracksProvider); final trackSnapshot = ref.watch(localTracksProvider);
final isPlaylistPlaying = final isPlaylistPlaying = playlist.containsTracks(
playlist.containsTracks(trackSnapshot.asData?.value.values.flattened.toList() ?? []); trackSnapshot.asData?.value.values.flattened.toList() ?? []);
final searchController = useTextEditingController(); final searchController = useTextEditingController();
useValueListenable(searchController); useValueListenable(searchController);
@ -71,10 +70,8 @@ class LocalLibraryPage extends HookConsumerWidget {
title: Text(isDownloads ? context.l10n.downloads : location), title: Text(isDownloads ? context.l10n.downloads : location),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
), ),
extendBodyBehindAppBar: true,
body: Column( body: Column(
children: [ children: [
const SizedBox(height: 56),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(
@ -83,7 +80,8 @@ class LocalLibraryPage extends HookConsumerWidget {
FilledButton( FilledButton(
onPressed: trackSnapshot.asData?.value != null onPressed: trackSnapshot.asData?.value != null
? () async { ? () async {
if (trackSnapshot.asData?.value.isNotEmpty == true) { if (trackSnapshot.asData?.value.isNotEmpty ==
true) {
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
await playLocalTracks( await playLocalTracks(
ref, ref,
@ -97,7 +95,9 @@ class LocalLibraryPage extends HookConsumerWidget {
children: [ children: [
Text(context.l10n.play), Text(context.l10n.play),
Icon( Icon(
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, isPlaylistPlaying
? SpotubeIcons.stop
: SpotubeIcons.play,
) )
], ],
), ),
@ -134,7 +134,8 @@ class LocalLibraryPage extends HookConsumerWidget {
trackSnapshot.when( trackSnapshot.when(
data: (tracks) { data: (tracks) {
final sortedTracks = useMemoized(() { final sortedTracks = useMemoized(() {
return ServiceUtils.sortTracks(tracks[location] ?? <LocalTrack>[], sortBy.value); return ServiceUtils.sortTracks(
tracks[location] ?? <LocalTrack>[], sortBy.value);
}, [sortBy.value, tracks]); }, [sortBy.value, tracks]);
final filteredTracks = useMemoized(() { final filteredTracks = useMemoized(() {
@ -180,8 +181,9 @@ class LocalLibraryPage extends HookConsumerWidget {
child: ListView.builder( child: ListView.builder(
controller: controller, controller: controller,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: itemCount: trackSnapshot.isLoading
trackSnapshot.isLoading ? 5 : filteredTracks.length, ? 5
: filteredTracks.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (trackSnapshot.isLoading) { if (trackSnapshot.isLoading) {
return TrackTile( return TrackTile(
@ -229,8 +231,7 @@ class LocalLibraryPage extends HookConsumerWidget {
Text(error.toString() + stackTrace.toString()), Text(error.toString() + stackTrace.toString()),
) )
], ],
) )),
),
); );
} }
} }

View File

@ -1,4 +1,4 @@
// ignore_for_file: invalid_use_of_protected_member // ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
import 'dart:async'; import 'dart:async';

View File

@ -45,7 +45,14 @@ class ProxyPlaylist {
} }
bool containsTrack(TrackSimple track) { bool containsTrack(TrackSimple track) {
return tracks.firstWhereOrNull((element) => element.id == track.id) != null; return tracks.firstWhereOrNull((element) {
if (element is LocalTrack && track is LocalTrack) {
return element.path == track.path;
}
return element.id == track.id;
}) !=
null;
} }
bool containsTracks(Iterable<TrackSimple> tracks) { bool containsTracks(Iterable<TrackSimple> tracks) {
@ -65,8 +72,8 @@ class ProxyPlaylist {
/// Otherwise default super.toJson() is used /// Otherwise default super.toJson() is used
static Map<String, dynamic> _makeAppropriateTrackJson(Track track) { static Map<String, dynamic> _makeAppropriateTrackJson(Track track) {
return switch (track.runtimeType) { return switch (track.runtimeType) {
LocalTrack() => track.toJson(), LocalTrack() => (track as LocalTrack).toJson(),
SourcedTrack() => track.toJson(), SourcedTrack() => (track as SourcedTrack).toJson(),
_ => track.toJson(), _ => track.toJson(),
}; };
} }

View File

@ -13,6 +13,7 @@ import 'package:media_kit/media_kit.dart' as mk;
import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:spotube/services/audio_player/loop_mode.dart';
import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/audio_player/playback_state.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
part 'audio_players_streams_mixin.dart'; part 'audio_players_streams_mixin.dart';
part 'audio_player_impl.dart'; part 'audio_player_impl.dart';
@ -30,12 +31,18 @@ class SpotubeMedia extends mk.Media {
: "http://${InternetAddress.loopbackIPv4.address}:${PlaybackServer.port}/stream/${track.id}", : "http://${InternetAddress.loopbackIPv4.address}:${PlaybackServer.port}/stream/${track.id}",
extras: { extras: {
...?extras, ...?extras,
"track": track.toJson(), "track": switch (track) {
LocalTrack() => track.toJson(),
SourcedTrack() => track.toJson(),
_ => track.toJson(),
},
}, },
); );
factory SpotubeMedia.fromMedia(mk.Media media) { factory SpotubeMedia.fromMedia(mk.Media media) {
final track = Track.fromJson(media.extras?["track"]); final track = media.uri.startsWith("http")
? Track.fromJson(media.extras?["track"])
: LocalTrack.fromJson(media.extras?["track"]);
return SpotubeMedia(track); return SpotubeMedia(track);
} }
} }

View File

@ -41,6 +41,13 @@
"local_tab" "local_tab"
], ],
"eu": [
"local_library",
"add_library_location",
"remove_library_location",
"local_tab"
],
"fa": [ "fa": [
"local_library", "local_library",
"add_library_location", "add_library_location",
@ -48,6 +55,13 @@
"local_tab" "local_tab"
], ],
"fi": [
"local_library",
"add_library_location",
"remove_library_location",
"local_tab"
],
"fr": [ "fr": [
"local_library", "local_library",
"add_library_location", "add_library_location",
@ -62,6 +76,13 @@
"local_tab" "local_tab"
], ],
"id": [
"local_library",
"add_library_location",
"remove_library_location",
"local_tab"
],
"it": [ "it": [
"local_library", "local_library",
"add_library_location", "add_library_location",
@ -76,6 +97,13 @@
"local_tab" "local_tab"
], ],
"ka": [
"local_library",
"add_library_location",
"remove_library_location",
"local_tab"
],
"ko": [ "ko": [
"local_library", "local_library",
"add_library_location", "add_library_location",