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,69 +154,74 @@ 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(
) child: ListView.builder(
), itemCount: preferences.localLibraryLocation.length + 1,
Expanded(
child: ListView.builder(
itemCount: preferences.localLibraryLocation.length+1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
late final String location; late final String location;
if (index == 0) { if (index == 0) {
location = preferences.downloadLocation; location = preferences.downloadLocation;
} else { } else {
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(context.l10n.downloads), ? Text(location)
trailing: preferences.downloadLocation != location ? Tooltip( : Text(context.l10n.downloads),
message: context.l10n.remove_library_location, trailing: preferences.downloadLocation != location
child: IconButton( ? Tooltip(
icon: Icon(SpotubeIcons.folderRemove, color: Colors.red[400]), message: context.l10n.remove_library_location,
onPressed: () => removeLocalLibraryLocation(location), child: IconButton(
), icon: Icon(SpotubeIcons.folderRemove,
) : null, color: Colors.red[400]),
onTap: () async { onPressed: () =>
context.go("/library/local${location == preferences.downloadLocation ? "?downloads=1" : ""}", extra: location); removeLocalLibraryLocation(location),
} ),
); )
} : null,
), onTap: () async {
), 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,118 +316,120 @@ 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)
_ => [ PopSheetEntry(
if (mediaQuery.smAndDown) value: TrackOptionValue.album,
PopSheetEntry( leading: const Icon(SpotubeIcons.album),
value: TrackOptionValue.album, title: Text(context.l10n.go_to_album),
leading: const Icon(SpotubeIcons.album), subtitle: Text(track.album!.name!),
title: Text(context.l10n.go_to_album), ),
subtitle: Text(track.album!.name!), if (!playlist.containsTrack(track)) ...[
), PopSheetEntry(
if (!playlist.containsTrack(track)) ...[ value: TrackOptionValue.addToQueue,
PopSheetEntry( leading: const Icon(SpotubeIcons.queueAdd),
value: TrackOptionValue.addToQueue, title: Text(context.l10n.add_to_queue),
leading: const Icon(SpotubeIcons.queueAdd), ),
title: Text(context.l10n.add_to_queue), PopSheetEntry(
), value: TrackOptionValue.playNext,
PopSheetEntry( leading: const Icon(SpotubeIcons.lightning),
value: TrackOptionValue.playNext, title: Text(context.l10n.play_next),
leading: const Icon(SpotubeIcons.lightning), ),
title: Text(context.l10n.play_next), ] else
), PopSheetEntry(
] else value: TrackOptionValue.removeFromQueue,
PopSheetEntry( enabled: playlist.activeTrack?.id != track.id,
value: TrackOptionValue.removeFromQueue, leading: const Icon(SpotubeIcons.queueRemove),
enabled: playlist.activeTrack?.id != track.id, title: Text(context.l10n.remove_from_queue),
leading: const Icon(SpotubeIcons.queueRemove), ),
title: Text(context.l10n.remove_from_queue), if (me.asData?.value != null && !isLocalTrack)
), PopSheetEntry(
if (me.asData?.value != null) value: TrackOptionValue.favorite,
PopSheetEntry( leading: favorites.isLiked
value: TrackOptionValue.favorite, ? const Icon(
leading: favorites.isLiked SpotubeIcons.heartFilled,
? const Icon( color: Colors.pink,
SpotubeIcons.heartFilled, )
color: Colors.pink, : const Icon(SpotubeIcons.heart),
) title: Text(
: const Icon(SpotubeIcons.heart), favorites.isLiked
title: Text( ? context.l10n.remove_from_favorites
favorites.isLiked : context.l10n.save_as_favorite,
? context.l10n.remove_from_favorites
: context.l10n.save_as_favorite,
),
),
if (auth != null) ...[
PopSheetEntry(
value: TrackOptionValue.startRadio,
leading: const Icon(SpotubeIcons.radio),
title: Text(context.l10n.start_a_radio),
),
PopSheetEntry(
value: TrackOptionValue.addToPlaylist,
leading: const Icon(SpotubeIcons.playlistAdd),
title: Text(context.l10n.add_to_playlist),
),
],
if (userPlaylist && auth != null)
PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist,
leading: const Icon(SpotubeIcons.removeFilled),
title: Text(context.l10n.remove_from_playlist),
),
PopSheetEntry(
value: TrackOptionValue.download,
enabled: !isInQueue,
leading: isInQueue
? HookBuilder(builder: (context) {
final progress = useListenable(progressNotifier!);
return CircularProgressIndicator(
value: progress.value,
);
})
: const Icon(SpotubeIcons.download),
title: Text(context.l10n.download_track),
), ),
PopSheetEntry( ),
value: TrackOptionValue.blacklist, if (auth != null && !isLocalTrack) ...[
leading: const Icon(SpotubeIcons.playlistRemove), PopSheetEntry(
iconColor: !isBlackListed ? Colors.red[400] : null, value: TrackOptionValue.startRadio,
textColor: !isBlackListed ? Colors.red[400] : null, leading: const Icon(SpotubeIcons.radio),
title: Text( title: Text(context.l10n.start_a_radio),
isBlackListed ),
? context.l10n.remove_from_blacklist PopSheetEntry(
: context.l10n.add_to_blacklist, value: TrackOptionValue.addToPlaylist,
), leading: const Icon(SpotubeIcons.playlistAdd),
title: Text(context.l10n.add_to_playlist),
),
],
if (userPlaylist && auth != null && !isLocalTrack)
PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist,
leading: const Icon(SpotubeIcons.removeFilled),
title: Text(context.l10n.remove_from_playlist),
),
if (!isLocalTrack)
PopSheetEntry(
value: TrackOptionValue.download,
enabled: !isInQueue,
leading: isInQueue
? HookBuilder(builder: (context) {
final progress = useListenable(progressNotifier!);
return CircularProgressIndicator(
value: progress.value,
);
})
: const Icon(SpotubeIcons.download),
title: Text(context.l10n.download_track),
),
if (!isLocalTrack)
PopSheetEntry(
value: TrackOptionValue.blacklist,
leading: const Icon(SpotubeIcons.playlistRemove),
iconColor: !isBlackListed ? Colors.red[400] : null,
textColor: !isBlackListed ? Colors.red[400] : null,
title: Text(
isBlackListed
? context.l10n.remove_from_blacklist
: context.l10n.add_to_blacklist,
), ),
PopSheetEntry( ),
value: TrackOptionValue.share, if (!isLocalTrack)
leading: const Icon(SpotubeIcons.share), PopSheetEntry(
title: Text(context.l10n.share), value: TrackOptionValue.share,
leading: const Icon(SpotubeIcons.share),
title: Text(context.l10n.share),
),
if (!isLocalTrack)
PopSheetEntry(
value: TrackOptionValue.songlink,
leading: Assets.logos.songlinkTransparent.image(
width: 22,
height: 22,
color: colorScheme.onSurface.withOpacity(0.5),
), ),
PopSheetEntry( title: Text(context.l10n.song_link),
value: TrackOptionValue.songlink, ),
leading: Assets.logos.songlinkTransparent.image( if (!isLocalTrack)
width: 22, PopSheetEntry(
height: 22, value: TrackOptionValue.details,
color: colorScheme.onSurface.withOpacity(0.5), leading: const Icon(SpotubeIcons.info),
), title: Text(context.l10n.details),
title: Text(context.l10n.song_link), ),
), ],
PopSheetEntry(
value: TrackOptionValue.details,
leading: const Icon(SpotubeIcons.info),
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) {
track.name!, LocalTrack() => Text(
"/track/${track.id}", track.name!,
push: true, maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, ),
), _ => LinkText(
track.name!,
"/track/${track.id}",
push: true,
maxLines: 1,
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);
@ -65,172 +64,174 @@ class LocalLibraryPage extends HookConsumerWidget {
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
leading: const BackButton(), leading: const BackButton(),
centerTitle: true, centerTitle: true,
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: [ Padding(
const SizedBox(height: 56), padding: const EdgeInsets.all(8.0),
Padding( child: Row(
padding: const EdgeInsets.all(8.0), children: [
child: Row( const SizedBox(width: 5),
children: [ FilledButton(
const SizedBox(width: 5), onPressed: trackSnapshot.asData?.value != null
FilledButton( ? () async {
onPressed: trackSnapshot.asData?.value != null if (trackSnapshot.asData?.value.isNotEmpty ==
? () async { true) {
if (trackSnapshot.asData?.value.isNotEmpty == true) { if (!isPlaylistPlaying) {
if (!isPlaylistPlaying) { await playLocalTracks(
await playLocalTracks( ref,
ref, trackSnapshot.asData!.value[location] ?? [],
trackSnapshot.asData!.value[location] ?? [], );
); }
} }
} }
} : null,
: null, child: Row(
child: Row( children: [
children: [ Text(context.l10n.play),
Text(context.l10n.play), Icon(
Icon( isPlaylistPlaying
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, ? SpotubeIcons.stop
) : SpotubeIcons.play,
], )
],
),
), ),
), const Spacer(),
const Spacer(), ExpandableSearchButton(
ExpandableSearchButton( isFiltering: isFiltering.value,
isFiltering: isFiltering.value, onPressed: (value) => isFiltering.value = value,
onPressed: (value) => isFiltering.value = value, searchFocus: searchFocus,
searchFocus: searchFocus, ),
), const SizedBox(width: 10),
const SizedBox(width: 10), SortTracksDropdown(
SortTracksDropdown( value: sortBy.value,
value: sortBy.value, onChanged: (value) {
onChanged: (value) { sortBy.value = value;
sortBy.value = value; },
}, ),
), const SizedBox(width: 5),
const SizedBox(width: 5), FilledButton(
FilledButton( child: const Icon(SpotubeIcons.refresh),
child: const Icon(SpotubeIcons.refresh), onPressed: () {
onPressed: () { ref.invalidate(localTracksProvider);
ref.invalidate(localTracksProvider); },
}, )
) ],
], ),
), ),
), ExpandableSearchField(
ExpandableSearchField( searchController: searchController,
searchController: searchController, searchFocus: searchFocus,
searchFocus: searchFocus, isFiltering: isFiltering.value,
isFiltering: isFiltering.value, onChangeFiltering: (value) => isFiltering.value = value,
onChangeFiltering: (value) => isFiltering.value = value, ),
), trackSnapshot.when(
trackSnapshot.when( data: (tracks) {
data: (tracks) { final sortedTracks = useMemoized(() {
final sortedTracks = useMemoized(() { return ServiceUtils.sortTracks(
return ServiceUtils.sortTracks(tracks[location] ?? <LocalTrack>[], sortBy.value); tracks[location] ?? <LocalTrack>[], sortBy.value);
}, [sortBy.value, tracks]); }, [sortBy.value, tracks]);
final filteredTracks = useMemoized(() { final filteredTracks = useMemoized(() {
if (searchController.text.isEmpty) { if (searchController.text.isEmpty) {
return sortedTracks; return sortedTracks;
}
return sortedTracks
.map((e) => (
weightedRatio(
"${e.name} - ${e.artists?.asString() ?? ""}",
searchController.text,
),
e,
))
.toList()
.sorted(
(a, b) => b.$1.compareTo(a.$1),
)
.where((e) => e.$1 > 50)
.map((e) => e.$2)
.toList()
.toList();
}, [searchController.text, sortedTracks]);
if (!trackSnapshot.isLoading && filteredTracks.isEmpty) {
return const Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [NotFound()],
),
);
} }
return sortedTracks
.map((e) => (
weightedRatio(
"${e.name} - ${e.artists?.asString() ?? ""}",
searchController.text,
),
e,
))
.toList()
.sorted(
(a, b) => b.$1.compareTo(a.$1),
)
.where((e) => e.$1 > 50)
.map((e) => e.$2)
.toList()
.toList();
}, [searchController.text, sortedTracks]);
if (!trackSnapshot.isLoading && filteredTracks.isEmpty) { return Expanded(
return const Expanded( child: RefreshIndicator(
child: Row( onRefresh: () async {
mainAxisAlignment: MainAxisAlignment.center, ref.invalidate(localTracksProvider);
children: [NotFound()], },
), child: InterScrollbar(
); controller: controller,
} child: Skeletonizer(
enabled: trackSnapshot.isLoading,
return Expanded( child: ListView.builder(
child: RefreshIndicator( controller: controller,
onRefresh: () async { physics: const AlwaysScrollableScrollPhysics(),
ref.invalidate(localTracksProvider); itemCount: trackSnapshot.isLoading
}, ? 5
child: InterScrollbar( : filteredTracks.length,
controller: controller, itemBuilder: (context, index) {
child: Skeletonizer( if (trackSnapshot.isLoading) {
enabled: trackSnapshot.isLoading, return TrackTile(
child: ListView.builder( playlist: playlist,
controller: controller, track: FakeData.track,
physics: const AlwaysScrollableScrollPhysics(), index: index,
itemCount:
trackSnapshot.isLoading ? 5 : filteredTracks.length,
itemBuilder: (context, index) {
if (trackSnapshot.isLoading) {
return TrackTile(
playlist: playlist,
track: FakeData.track,
index: index,
);
}
final track = filteredTracks[index];
return TrackTile(
index: index,
playlist: playlist,
track: track,
userPlaylist: false,
onTap: () async {
await playLocalTracks(
ref,
sortedTracks,
currentTrack: track,
); );
}, }
);
}, final track = filteredTracks[index];
return TrackTile(
index: index,
playlist: playlist,
track: track,
userPlaylist: false,
onTap: () async {
await playLocalTracks(
ref,
sortedTracks,
currentTrack: track,
);
},
);
},
),
), ),
), ),
), ),
), );
); },
}, loading: () => Expanded(
loading: () => Expanded( child: Skeletonizer(
child: Skeletonizer( enabled: true,
enabled: true, child: ListView.builder(
child: ListView.builder( itemCount: 5,
itemCount: 5, itemBuilder: (context, index) => TrackTile(
itemBuilder: (context, index) => TrackTile( track: FakeData.track,
track: FakeData.track, index: index,
index: index, playlist: playlist,
playlist: playlist, ),
), ),
), ),
), ),
), error: (error, stackTrace) =>
error: (error, stackTrace) => 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",