mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
fix: local track not showing up in queue
This commit is contained in:
parent
22caa818f4
commit
d82261cb25
@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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()),
|
)
|
||||||
)
|
],
|
||||||
],
|
)),
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user