mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: Local music library (#1479)
* feat: add one additional library folder This folder just doesn't get downloaded to. I think I'm going to rework it so that it can be multiple folders, but I'm going to commit my progress so far anyway. Signed-off-by: Blake Leonard <me@blakes.dev> * chore: update dependencies so that it builds I'm not sure if this breaks CI or something, but I couldn't build it locally to test my changes, so I made these changes and it builds again. Signed-off-by: Blake Leonard <me@blakes.dev> * feat: index multiple folders of local music If you used a previous commit from this branch, this is a breaking change, because it changes the type of a configuration field. but since this is still in development, it should be fine. Signed-off-by: Blake Leonard <me@blakes.dev> * refactor: manage local library in local tracks tab This also refactors the list to use slivers instead. That's the easiest way to have multiple scrolling lists here... The console keeps getting spammed with some intermediate layout error but I can't hold it long enough to figure out what's causing it. Signed-off-by: Blake Leonard <me@blakes.dev> * refactor: use folder add/remove icons in library Signed-off-by: Blake Leonard <me@blakes.dev> * refactor: remove redundant settings page Signed-off-by: Blake Leonard <me@blakes.dev> * refactor: rename "Local Tracks" to just "Local" Not sure if this would be the recommended way to do it... Signed-off-by: Blake Leonard <me@blakes.dev> * fix: console spam about useless Expanded Signed-off-by: Blake Leonard <me@blakes.dev> * chore: remove completed TODO Signed-off-by: Blake Leonard <me@blakes.dev> * chore: use new Platform constants; regenerate plugins Signed-off-by: Blake Leonard <me@blakes.dev> * refactor: put local libraries on separate pages Signed-off-by: Blake Leonard <me@blakes.dev> --------- Signed-off-by: Blake Leonard <me@blakes.dev>
This commit is contained in:
parent
9aea35468f
commit
22caa818f4
@ -14,6 +14,7 @@ import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
|||||||
import 'package:spotube/pages/home/genres/genres.dart';
|
import 'package:spotube/pages/home/genres/genres.dart';
|
||||||
import 'package:spotube/pages/home/home.dart';
|
import 'package:spotube/pages/home/home.dart';
|
||||||
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
|
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
|
||||||
|
import 'package:spotube/pages/library/local_folder.dart';
|
||||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
||||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
|
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
|
||||||
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
|
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
|
||||||
@ -113,6 +114,17 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
GoRoute(
|
||||||
|
path: "local",
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
assert(state.extra is String);
|
||||||
|
return SpotubePage(
|
||||||
|
child: LocalLibraryPage(state.extra as String,
|
||||||
|
isDownloads: state.uri.queryParameters["downloads"] != null
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/lyrics",
|
path: "/lyrics",
|
||||||
|
@ -121,4 +121,6 @@ abstract class SpotubeIcons {
|
|||||||
static const monitor = FeatherIcons.monitor;
|
static const monitor = FeatherIcons.monitor;
|
||||||
static const power = FeatherIcons.power;
|
static const power = FeatherIcons.power;
|
||||||
static const bluetooth = FeatherIcons.bluetooth;
|
static const bluetooth = FeatherIcons.bluetooth;
|
||||||
|
static const folderAdd = FeatherIcons.folderPlus;
|
||||||
|
static const folderRemove = FeatherIcons.folderMinus;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:catcher_2/catcher_2.dart';
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
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:collection/collection.dart';
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
import 'package:fuzzywuzzy/fuzzywuzzy.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';
|
||||||
@ -27,6 +30,7 @@ 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/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/service_utils.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;
|
||||||
@ -59,116 +63,125 @@ enum SortBy {
|
|||||||
album,
|
album,
|
||||||
}
|
}
|
||||||
|
|
||||||
final localTracksProvider = FutureProvider<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 downloadLocation = ref.watch(
|
final downloadLocation = ref.watch(
|
||||||
userPreferencesProvider.select((s) => s.downloadLocation),
|
userPreferencesProvider.select((s) => s.downloadLocation),
|
||||||
);
|
);
|
||||||
if (downloadLocation.isEmpty) return [];
|
|
||||||
final downloadDir = Directory(downloadLocation);
|
final downloadDir = Directory(downloadLocation);
|
||||||
if (!await downloadDir.exists()) {
|
if (!await downloadDir.exists()) {
|
||||||
await downloadDir.create(recursive: true);
|
await downloadDir.create(recursive: true);
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
final entities = downloadDir.listSync(recursive: true);
|
final localLibraryLocations = ref.watch(
|
||||||
|
userPreferencesProvider.select((s) => s.localLibraryLocation),
|
||||||
|
);
|
||||||
|
|
||||||
final filesWithMetadata = (await Future.wait(
|
for (var location in [downloadLocation, ...localLibraryLocations]) {
|
||||||
entities.map((e) => File(e.path)).where((file) {
|
if (location.isEmpty) continue;
|
||||||
final mimetype = lookupMimeType(file.path);
|
final entities = <FileSystemEntity>[];
|
||||||
return mimetype != null && supportedAudioTypes.contains(mimetype);
|
final dir = Directory(location);
|
||||||
}).map(
|
if (await Directory(location).exists()) {
|
||||||
(file) async {
|
entities.addAll(Directory(location).listSync(recursive: true));
|
||||||
try {
|
}
|
||||||
final metadata = await MetadataGod.readMetadata(file: file.path);
|
|
||||||
|
|
||||||
final imageFile = File(join(
|
final filesWithMetadata = (await Future.wait(
|
||||||
(await getTemporaryDirectory()).path,
|
entities.map((e) => File(e.path)).where((file) {
|
||||||
"spotube",
|
final mimetype = lookupMimeType(file.path);
|
||||||
basenameWithoutExtension(file.path) +
|
return mimetype != null && supportedAudioTypes.contains(mimetype);
|
||||||
imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!,
|
}).map(
|
||||||
));
|
(file) async {
|
||||||
if (!await imageFile.exists() && metadata.picture != null) {
|
try {
|
||||||
await imageFile.create(recursive: true);
|
final metadata = await MetadataGod.readMetadata(file: file.path);
|
||||||
await imageFile.writeAsBytes(
|
|
||||||
metadata.picture?.data ?? [],
|
final imageFile = File(join(
|
||||||
mode: FileMode.writeOnly,
|
(await getTemporaryDirectory()).path,
|
||||||
);
|
"spotube",
|
||||||
|
basenameWithoutExtension(file.path) +
|
||||||
|
imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!,
|
||||||
|
));
|
||||||
|
if (!await imageFile.exists() && metadata.picture != null) {
|
||||||
|
await imageFile.create(recursive: true);
|
||||||
|
await imageFile.writeAsBytes(
|
||||||
|
metadata.picture?.data ?? [],
|
||||||
|
mode: FileMode.writeOnly,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"metadata": metadata, "file": file, "art": imageFile.path};
|
||||||
|
} catch (e, stack) {
|
||||||
|
if (e is FfiException) {
|
||||||
|
return {"file": file};
|
||||||
|
}
|
||||||
|
Catcher2.reportCheckedError(e, stack);
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
|
||||||
return {"metadata": metadata, "file": file, "art": imageFile.path};
|
// ignore: no_leading_underscores_for_local_identifiers
|
||||||
} catch (e, stack) {
|
final _tracks = filesWithMetadata
|
||||||
if (e is FfiException) {
|
.map(
|
||||||
return {"file": file};
|
(fileWithMetadata) => LocalTrack.fromTrack(
|
||||||
}
|
track: Track().fromFile(
|
||||||
Catcher2.reportCheckedError(e, stack);
|
fileWithMetadata["file"],
|
||||||
return {};
|
metadata: fileWithMetadata["metadata"],
|
||||||
}
|
art: fileWithMetadata["art"],
|
||||||
},
|
),
|
||||||
),
|
path: fileWithMetadata["file"].path,
|
||||||
))
|
|
||||||
.where((e) => e.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final tracks = filesWithMetadata
|
|
||||||
.map(
|
|
||||||
(fileWithMetadata) => LocalTrack.fromTrack(
|
|
||||||
track: Track().fromFile(
|
|
||||||
fileWithMetadata["file"],
|
|
||||||
metadata: fileWithMetadata["metadata"],
|
|
||||||
art: fileWithMetadata["art"],
|
|
||||||
),
|
),
|
||||||
path: fileWithMetadata["file"].path,
|
)
|
||||||
),
|
.toList();
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
|
tracks[location] = _tracks;
|
||||||
|
}
|
||||||
return tracks;
|
return tracks;
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
Catcher2.reportCheckedError(e, stack);
|
Catcher2.reportCheckedError(e, stack);
|
||||||
return [];
|
return {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
class UserLocalTracks extends HookConsumerWidget {
|
class UserLocalTracks extends HookConsumerWidget {
|
||||||
const UserLocalTracks({super.key});
|
const UserLocalTracks({super.key});
|
||||||
|
|
||||||
Future<void> playLocalTracks(
|
|
||||||
WidgetRef ref,
|
|
||||||
List<LocalTrack> tracks, {
|
|
||||||
LocalTrack? currentTrack,
|
|
||||||
}) async {
|
|
||||||
final playlist = ref.read(proxyPlaylistProvider);
|
|
||||||
final playback = ref.read(proxyPlaylistProvider.notifier);
|
|
||||||
currentTrack ??= tracks.first;
|
|
||||||
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
|
||||||
if (!isPlaylistPlaying) {
|
|
||||||
await playback.load(
|
|
||||||
tracks,
|
|
||||||
initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id),
|
|
||||||
autoPlay: true,
|
|
||||||
);
|
|
||||||
} else if (isPlaylistPlaying &&
|
|
||||||
currentTrack.id != null &&
|
|
||||||
currentTrack.id != playlist.activeTrack?.id) {
|
|
||||||
await playback.jumpToTrack(currentTrack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final sortBy = useState<SortBy>(SortBy.none);
|
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
|
||||||
final trackSnapshot = ref.watch(localTracksProvider);
|
|
||||||
final isPlaylistPlaying =
|
|
||||||
playlist.containsTracks(trackSnapshot.asData?.value ?? []);
|
|
||||||
|
|
||||||
final searchController = useTextEditingController();
|
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
|
||||||
useValueListenable(searchController);
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
final searchFocus = useFocusNode();
|
|
||||||
final isFiltering = useState(false);
|
|
||||||
|
|
||||||
final controller = useScrollController();
|
final addLocalLibraryLocation = useCallback(() async {
|
||||||
|
if (kIsMobile || kIsMacOS) {
|
||||||
|
final dirStr = await FilePicker.platform.getDirectoryPath(
|
||||||
|
initialDirectory: preferences.downloadLocation,
|
||||||
|
);
|
||||||
|
if (dirStr == null) return;
|
||||||
|
if (preferences.localLibraryLocation.contains(dirStr)) return;
|
||||||
|
preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation, dirStr]);
|
||||||
|
} else {
|
||||||
|
String? dirStr = await getDirectoryPath(
|
||||||
|
initialDirectory: preferences.downloadLocation,
|
||||||
|
);
|
||||||
|
if (dirStr == null) return;
|
||||||
|
if (preferences.localLibraryLocation.contains(dirStr)) return;
|
||||||
|
preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation, dirStr]);
|
||||||
|
}
|
||||||
|
}, [preferences.localLibraryLocation]);
|
||||||
|
|
||||||
|
final removeLocalLibraryLocation = useCallback((String location) {
|
||||||
|
if (!preferences.localLibraryLocation.contains(location)) return;
|
||||||
|
preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation]..remove(location));
|
||||||
|
}, [preferences.localLibraryLocation]);
|
||||||
|
|
||||||
|
// This is just to pre-load the tracks.
|
||||||
|
// For now, this gets all of them.
|
||||||
|
ref.watch(localTracksProvider);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@ -177,155 +190,42 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
FilledButton(
|
TextButton.icon(
|
||||||
onPressed: trackSnapshot.asData?.value != null
|
icon: const Icon(SpotubeIcons.folderAdd),
|
||||||
? () async {
|
label: Text(context.l10n.add_library_location),
|
||||||
if (trackSnapshot.asData?.value.isNotEmpty == true) {
|
onPressed: addLocalLibraryLocation,
|
||||||
if (!isPlaylistPlaying) {
|
|
||||||
await playLocalTracks(
|
|
||||||
ref,
|
|
||||||
trackSnapshot.asData!.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(context.l10n.play),
|
|
||||||
Icon(
|
|
||||||
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
ExpandableSearchButton(
|
|
||||||
isFiltering: isFiltering.value,
|
|
||||||
onPressed: (value) => isFiltering.value = value,
|
|
||||||
searchFocus: searchFocus,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
SortTracksDropdown(
|
|
||||||
value: sortBy.value,
|
|
||||||
onChanged: (value) {
|
|
||||||
sortBy.value = value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 5),
|
|
||||||
FilledButton(
|
|
||||||
child: const Icon(SpotubeIcons.refresh),
|
|
||||||
onPressed: () {
|
|
||||||
ref.invalidate(localTracksProvider);
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
],
|
]
|
||||||
),
|
)
|
||||||
),
|
),
|
||||||
ExpandableSearchField(
|
Expanded(
|
||||||
searchController: searchController,
|
child: ListView.builder(
|
||||||
searchFocus: searchFocus,
|
itemCount: preferences.localLibraryLocation.length+1,
|
||||||
isFiltering: isFiltering.value,
|
itemBuilder: (context, index) {
|
||||||
onChangeFiltering: (value) => isFiltering.value = value,
|
late final String location;
|
||||||
),
|
if (index == 0) {
|
||||||
trackSnapshot.when(
|
location = preferences.downloadLocation;
|
||||||
data: (tracks) {
|
} else {
|
||||||
final sortedTracks = useMemoized(() {
|
location = preferences.localLibraryLocation[index-1];
|
||||||
return ServiceUtils.sortTracks(tracks, sortBy.value);
|
|
||||||
}, [sortBy.value, tracks]);
|
|
||||||
|
|
||||||
final filteredTracks = useMemoized(() {
|
|
||||||
if (searchController.text.isEmpty) {
|
|
||||||
return sortedTracks;
|
|
||||||
}
|
}
|
||||||
return sortedTracks
|
return ListTile(
|
||||||
.map((e) => (
|
title: preferences.downloadLocation != location ? Text(location)
|
||||||
weightedRatio(
|
: Text(context.l10n.downloads),
|
||||||
"${e.name} - ${e.artists?.asString() ?? ""}",
|
trailing: preferences.downloadLocation != location ? Tooltip(
|
||||||
searchController.text,
|
message: context.l10n.remove_library_location,
|
||||||
),
|
child: IconButton(
|
||||||
e,
|
icon: Icon(SpotubeIcons.folderRemove, color: Colors.red[400]),
|
||||||
))
|
onPressed: () => removeLocalLibraryLocation(location),
|
||||||
.toList()
|
),
|
||||||
.sorted(
|
) : null,
|
||||||
(a, b) => b.$1.compareTo(a.$1),
|
onTap: () async {
|
||||||
)
|
context.go("/library/local${location == preferences.downloadLocation ? "?downloads=1" : ""}", extra: location);
|
||||||
.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 Expanded(
|
|
||||||
child: RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
ref.invalidate(localTracksProvider);
|
|
||||||
},
|
|
||||||
child: InterScrollbar(
|
|
||||||
controller: controller,
|
|
||||||
child: Skeletonizer(
|
|
||||||
enabled: trackSnapshot.isLoading,
|
|
||||||
child: ListView.builder(
|
|
||||||
controller: controller,
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
loading: () => Expanded(
|
|
||||||
child: Skeletonizer(
|
|
||||||
enabled: true,
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: 5,
|
|
||||||
itemBuilder: (context, index) => TrackTile(
|
|
||||||
track: FakeData.track,
|
|
||||||
index: index,
|
|
||||||
playlist: playlist,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
error: (error, stackTrace) =>
|
),
|
||||||
Text(error.toString() + stackTrace.toString()),
|
]
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,6 +107,9 @@
|
|||||||
"always_on_top": "Always on top",
|
"always_on_top": "Always on top",
|
||||||
"exit_mini_player": "Exit Mini player",
|
"exit_mini_player": "Exit Mini player",
|
||||||
"download_location": "Download location",
|
"download_location": "Download location",
|
||||||
|
"local_library": "Local library",
|
||||||
|
"add_library_location": "Add to library",
|
||||||
|
"remove_library_location": "Remove from library",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"login_with_spotify": "Login with your Spotify account",
|
"login_with_spotify": "Login with your Spotify account",
|
||||||
"connect_with_spotify": "Connect with Spotify",
|
"connect_with_spotify": "Connect with Spotify",
|
||||||
@ -295,6 +298,7 @@
|
|||||||
"delete_playlist": "Delete Playlist",
|
"delete_playlist": "Delete Playlist",
|
||||||
"delete_playlist_confirmation": "Are you sure you want to delete this playlist?",
|
"delete_playlist_confirmation": "Are you sure you want to delete this playlist?",
|
||||||
"local_tracks": "Local Tracks",
|
"local_tracks": "Local Tracks",
|
||||||
|
"local_tab": "Local",
|
||||||
"song_link": "Song Link",
|
"song_link": "Song Link",
|
||||||
"skip_this_nonsense": "Skip this nonsense",
|
"skip_this_nonsense": "Skip this nonsense",
|
||||||
"freedom_of_music": "“Freedom of Music”",
|
"freedom_of_music": "“Freedom of Music”",
|
||||||
@ -321,4 +325,4 @@
|
|||||||
"connect_client_alert": "You're being controlled by {client}",
|
"connect_client_alert": "You're being controlled by {client}",
|
||||||
"this_device": "This Device",
|
"this_device": "This Device",
|
||||||
"remote": "Remote"
|
"remote": "Remote"
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ class LibraryPage extends HookConsumerWidget {
|
|||||||
leading: ThemedButtonsTabBar(
|
leading: ThemedButtonsTabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(text: " ${context.l10n.playlists} "),
|
Tab(text: " ${context.l10n.playlists} "),
|
||||||
Tab(text: " ${context.l10n.local_tracks} "),
|
Tab(text: " ${context.l10n.local_tab} "),
|
||||||
Tab(
|
Tab(
|
||||||
child: Badge(
|
child: Badge(
|
||||||
isLabelVisible: downloadingCount > 0,
|
isLabelVisible: downloadingCount > 0,
|
||||||
|
236
lib/pages/library/local_folder.dart
Normal file
236
lib/pages/library/local_folder.dart
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:spotube/collections/fake.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/library/user_local_tracks.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/page_window_title_bar.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/models/local_track.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
|
class LocalLibraryPage extends HookConsumerWidget {
|
||||||
|
final String location;
|
||||||
|
final bool isDownloads;
|
||||||
|
const LocalLibraryPage(this.location, {super.key, this.isDownloads = false});
|
||||||
|
|
||||||
|
Future<void> playLocalTracks(
|
||||||
|
WidgetRef ref,
|
||||||
|
List<LocalTrack> tracks, {
|
||||||
|
LocalTrack? currentTrack,
|
||||||
|
}) async {
|
||||||
|
final playlist = ref.read(proxyPlaylistProvider);
|
||||||
|
final playback = ref.read(proxyPlaylistProvider.notifier);
|
||||||
|
currentTrack ??= tracks.first;
|
||||||
|
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
||||||
|
if (!isPlaylistPlaying) {
|
||||||
|
await playback.load(
|
||||||
|
tracks,
|
||||||
|
initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
|
autoPlay: true,
|
||||||
|
);
|
||||||
|
} else if (isPlaylistPlaying &&
|
||||||
|
currentTrack.id != null &&
|
||||||
|
currentTrack.id != playlist.activeTrack?.id) {
|
||||||
|
await playback.jumpToTrack(currentTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final sortBy = useState<SortBy>(SortBy.none);
|
||||||
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
|
final trackSnapshot = ref.watch(localTracksProvider);
|
||||||
|
final isPlaylistPlaying =
|
||||||
|
playlist.containsTracks(trackSnapshot.asData?.value.values.flattened.toList() ?? []);
|
||||||
|
|
||||||
|
final searchController = useTextEditingController();
|
||||||
|
useValueListenable(searchController);
|
||||||
|
final searchFocus = useFocusNode();
|
||||||
|
final isFiltering = useState(false);
|
||||||
|
|
||||||
|
final controller = useScrollController();
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: PageWindowTitleBar(
|
||||||
|
leading: const BackButton(),
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text(isDownloads ? context.l10n.downloads : location),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 56),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: trackSnapshot.asData?.value != null
|
||||||
|
? () async {
|
||||||
|
if (trackSnapshot.asData?.value.isNotEmpty == true) {
|
||||||
|
if (!isPlaylistPlaying) {
|
||||||
|
await playLocalTracks(
|
||||||
|
ref,
|
||||||
|
trackSnapshot.asData!.value[location] ?? [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.play),
|
||||||
|
Icon(
|
||||||
|
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
ExpandableSearchButton(
|
||||||
|
isFiltering: isFiltering.value,
|
||||||
|
onPressed: (value) => isFiltering.value = value,
|
||||||
|
searchFocus: searchFocus,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
SortTracksDropdown(
|
||||||
|
value: sortBy.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
sortBy.value = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
FilledButton(
|
||||||
|
child: const Icon(SpotubeIcons.refresh),
|
||||||
|
onPressed: () {
|
||||||
|
ref.invalidate(localTracksProvider);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ExpandableSearchField(
|
||||||
|
searchController: searchController,
|
||||||
|
searchFocus: searchFocus,
|
||||||
|
isFiltering: isFiltering.value,
|
||||||
|
onChangeFiltering: (value) => isFiltering.value = value,
|
||||||
|
),
|
||||||
|
trackSnapshot.when(
|
||||||
|
data: (tracks) {
|
||||||
|
final sortedTracks = useMemoized(() {
|
||||||
|
return ServiceUtils.sortTracks(tracks[location] ?? <LocalTrack>[], sortBy.value);
|
||||||
|
}, [sortBy.value, tracks]);
|
||||||
|
|
||||||
|
final filteredTracks = useMemoized(() {
|
||||||
|
if (searchController.text.isEmpty) {
|
||||||
|
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 Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.invalidate(localTracksProvider);
|
||||||
|
},
|
||||||
|
child: InterScrollbar(
|
||||||
|
controller: controller,
|
||||||
|
child: Skeletonizer(
|
||||||
|
enabled: trackSnapshot.isLoading,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: controller,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => Expanded(
|
||||||
|
child: Skeletonizer(
|
||||||
|
enabled: true,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: 5,
|
||||||
|
itemBuilder: (context, index) => TrackTile(
|
||||||
|
track: FakeData.track,
|
||||||
|
index: index,
|
||||||
|
playlist: playlist,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (error, stackTrace) =>
|
||||||
|
Text(error.toString() + stackTrace.toString()),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import 'package:file_selector/file_selector.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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/settings/section_card_with_heading.dart';
|
import 'package:spotube/components/settings/section_card_with_heading.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
@ -69,6 +69,11 @@ class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
|
|||||||
state = state.copyWith(downloadLocation: downloadDir);
|
state = state.copyWith(downloadLocation: downloadDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setLocalLibraryLocation(List<String> localLibraryDirs) {
|
||||||
|
//if (localLibraryDir.isEmpty) return;
|
||||||
|
state = state.copyWith(localLibraryLocation: localLibraryDirs);
|
||||||
|
}
|
||||||
|
|
||||||
void setLayoutMode(LayoutMode mode) {
|
void setLayoutMode(LayoutMode mode) {
|
||||||
state = state.copyWith(layoutMode: mode);
|
state = state.copyWith(layoutMode: mode);
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,7 @@ class UserPreferences with _$UserPreferences {
|
|||||||
@Default(Market.US) Market recommendationMarket,
|
@Default(Market.US) Market recommendationMarket,
|
||||||
@Default(SearchMode.youtube) SearchMode searchMode,
|
@Default(SearchMode.youtube) SearchMode searchMode,
|
||||||
@Default("") String downloadLocation,
|
@Default("") String downloadLocation,
|
||||||
|
@Default([]) List<String> localLibraryLocation,
|
||||||
@Default("https://pipedapi.kavin.rocks") String pipedInstance,
|
@Default("https://pipedapi.kavin.rocks") String pipedInstance,
|
||||||
@Default(ThemeMode.system) ThemeMode themeMode,
|
@Default(ThemeMode.system) ThemeMode themeMode,
|
||||||
@Default(AudioSource.youtube) AudioSource audioSource,
|
@Default(AudioSource.youtube) AudioSource audioSource,
|
||||||
|
@ -43,6 +43,7 @@ mixin _$UserPreferences {
|
|||||||
Market get recommendationMarket => throw _privateConstructorUsedError;
|
Market get recommendationMarket => throw _privateConstructorUsedError;
|
||||||
SearchMode get searchMode => throw _privateConstructorUsedError;
|
SearchMode get searchMode => throw _privateConstructorUsedError;
|
||||||
String get downloadLocation => throw _privateConstructorUsedError;
|
String get downloadLocation => throw _privateConstructorUsedError;
|
||||||
|
List<String> get localLibraryLocation => throw _privateConstructorUsedError;
|
||||||
String get pipedInstance => throw _privateConstructorUsedError;
|
String get pipedInstance => throw _privateConstructorUsedError;
|
||||||
ThemeMode get themeMode => throw _privateConstructorUsedError;
|
ThemeMode get themeMode => throw _privateConstructorUsedError;
|
||||||
AudioSource get audioSource => throw _privateConstructorUsedError;
|
AudioSource get audioSource => throw _privateConstructorUsedError;
|
||||||
@ -88,6 +89,7 @@ abstract class $UserPreferencesCopyWith<$Res> {
|
|||||||
Market recommendationMarket,
|
Market recommendationMarket,
|
||||||
SearchMode searchMode,
|
SearchMode searchMode,
|
||||||
String downloadLocation,
|
String downloadLocation,
|
||||||
|
List<String> localLibraryLocation,
|
||||||
String pipedInstance,
|
String pipedInstance,
|
||||||
ThemeMode themeMode,
|
ThemeMode themeMode,
|
||||||
AudioSource audioSource,
|
AudioSource audioSource,
|
||||||
@ -126,6 +128,7 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences>
|
|||||||
Object? recommendationMarket = null,
|
Object? recommendationMarket = null,
|
||||||
Object? searchMode = null,
|
Object? searchMode = null,
|
||||||
Object? downloadLocation = null,
|
Object? downloadLocation = null,
|
||||||
|
Object? localLibraryLocation = null,
|
||||||
Object? pipedInstance = null,
|
Object? pipedInstance = null,
|
||||||
Object? themeMode = null,
|
Object? themeMode = null,
|
||||||
Object? audioSource = null,
|
Object? audioSource = null,
|
||||||
@ -196,6 +199,10 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences>
|
|||||||
? _value.downloadLocation
|
? _value.downloadLocation
|
||||||
: downloadLocation // ignore: cast_nullable_to_non_nullable
|
: downloadLocation // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
localLibraryLocation: null == localLibraryLocation
|
||||||
|
? _value.localLibraryLocation
|
||||||
|
: localLibraryLocation // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>,
|
||||||
pipedInstance: null == pipedInstance
|
pipedInstance: null == pipedInstance
|
||||||
? _value.pipedInstance
|
? _value.pipedInstance
|
||||||
: pipedInstance // ignore: cast_nullable_to_non_nullable
|
: pipedInstance // ignore: cast_nullable_to_non_nullable
|
||||||
@ -264,6 +271,7 @@ abstract class _$$UserPreferencesImplCopyWith<$Res>
|
|||||||
Market recommendationMarket,
|
Market recommendationMarket,
|
||||||
SearchMode searchMode,
|
SearchMode searchMode,
|
||||||
String downloadLocation,
|
String downloadLocation,
|
||||||
|
List<String> localLibraryLocation,
|
||||||
String pipedInstance,
|
String pipedInstance,
|
||||||
ThemeMode themeMode,
|
ThemeMode themeMode,
|
||||||
AudioSource audioSource,
|
AudioSource audioSource,
|
||||||
@ -300,6 +308,7 @@ class __$$UserPreferencesImplCopyWithImpl<$Res>
|
|||||||
Object? recommendationMarket = null,
|
Object? recommendationMarket = null,
|
||||||
Object? searchMode = null,
|
Object? searchMode = null,
|
||||||
Object? downloadLocation = null,
|
Object? downloadLocation = null,
|
||||||
|
Object? localLibraryLocation = null,
|
||||||
Object? pipedInstance = null,
|
Object? pipedInstance = null,
|
||||||
Object? themeMode = null,
|
Object? themeMode = null,
|
||||||
Object? audioSource = null,
|
Object? audioSource = null,
|
||||||
@ -370,6 +379,10 @@ class __$$UserPreferencesImplCopyWithImpl<$Res>
|
|||||||
? _value.downloadLocation
|
? _value.downloadLocation
|
||||||
: downloadLocation // ignore: cast_nullable_to_non_nullable
|
: downloadLocation // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
localLibraryLocation: null == localLibraryLocation
|
||||||
|
? _value._localLibraryLocation
|
||||||
|
: localLibraryLocation // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>,
|
||||||
pipedInstance: null == pipedInstance
|
pipedInstance: null == pipedInstance
|
||||||
? _value.pipedInstance
|
? _value.pipedInstance
|
||||||
: pipedInstance // ignore: cast_nullable_to_non_nullable
|
: pipedInstance // ignore: cast_nullable_to_non_nullable
|
||||||
@ -433,6 +446,7 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
|||||||
this.recommendationMarket = Market.US,
|
this.recommendationMarket = Market.US,
|
||||||
this.searchMode = SearchMode.youtube,
|
this.searchMode = SearchMode.youtube,
|
||||||
this.downloadLocation = "",
|
this.downloadLocation = "",
|
||||||
|
final List<String> localLibraryLocation = const [],
|
||||||
this.pipedInstance = "https://pipedapi.kavin.rocks",
|
this.pipedInstance = "https://pipedapi.kavin.rocks",
|
||||||
this.themeMode = ThemeMode.system,
|
this.themeMode = ThemeMode.system,
|
||||||
this.audioSource = AudioSource.youtube,
|
this.audioSource = AudioSource.youtube,
|
||||||
@ -440,7 +454,8 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
|||||||
this.downloadMusicCodec = SourceCodecs.m4a,
|
this.downloadMusicCodec = SourceCodecs.m4a,
|
||||||
this.discordPresence = true,
|
this.discordPresence = true,
|
||||||
this.endlessPlayback = true,
|
this.endlessPlayback = true,
|
||||||
this.enableConnect = false});
|
this.enableConnect = false})
|
||||||
|
: _localLibraryLocation = localLibraryLocation;
|
||||||
|
|
||||||
factory _$UserPreferencesImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$UserPreferencesImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$UserPreferencesImplFromJson(json);
|
_$$UserPreferencesImplFromJson(json);
|
||||||
@ -496,6 +511,16 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
|||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final String downloadLocation;
|
final String downloadLocation;
|
||||||
|
final List<String> _localLibraryLocation;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
List<String> get localLibraryLocation {
|
||||||
|
if (_localLibraryLocation is EqualUnmodifiableListView)
|
||||||
|
return _localLibraryLocation;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_localLibraryLocation);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final String pipedInstance;
|
final String pipedInstance;
|
||||||
@ -523,7 +548,7 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)';
|
return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, localLibraryLocation: $localLibraryLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -560,6 +585,8 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
|||||||
other.searchMode == searchMode) &&
|
other.searchMode == searchMode) &&
|
||||||
(identical(other.downloadLocation, downloadLocation) ||
|
(identical(other.downloadLocation, downloadLocation) ||
|
||||||
other.downloadLocation == downloadLocation) &&
|
other.downloadLocation == downloadLocation) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other._localLibraryLocation, _localLibraryLocation) &&
|
||||||
(identical(other.pipedInstance, pipedInstance) ||
|
(identical(other.pipedInstance, pipedInstance) ||
|
||||||
other.pipedInstance == pipedInstance) &&
|
other.pipedInstance == pipedInstance) &&
|
||||||
(identical(other.themeMode, themeMode) ||
|
(identical(other.themeMode, themeMode) ||
|
||||||
@ -597,6 +624,7 @@ class _$UserPreferencesImpl implements _UserPreferences {
|
|||||||
recommendationMarket,
|
recommendationMarket,
|
||||||
searchMode,
|
searchMode,
|
||||||
downloadLocation,
|
downloadLocation,
|
||||||
|
const DeepCollectionEquality().hash(_localLibraryLocation),
|
||||||
pipedInstance,
|
pipedInstance,
|
||||||
themeMode,
|
themeMode,
|
||||||
audioSource,
|
audioSource,
|
||||||
@ -647,6 +675,7 @@ abstract class _UserPreferences implements UserPreferences {
|
|||||||
final Market recommendationMarket,
|
final Market recommendationMarket,
|
||||||
final SearchMode searchMode,
|
final SearchMode searchMode,
|
||||||
final String downloadLocation,
|
final String downloadLocation,
|
||||||
|
final List<String> localLibraryLocation,
|
||||||
final String pipedInstance,
|
final String pipedInstance,
|
||||||
final ThemeMode themeMode,
|
final ThemeMode themeMode,
|
||||||
final AudioSource audioSource,
|
final AudioSource audioSource,
|
||||||
@ -698,6 +727,8 @@ abstract class _UserPreferences implements UserPreferences {
|
|||||||
@override
|
@override
|
||||||
String get downloadLocation;
|
String get downloadLocation;
|
||||||
@override
|
@override
|
||||||
|
List<String> get localLibraryLocation;
|
||||||
|
@override
|
||||||
String get pipedInstance;
|
String get pipedInstance;
|
||||||
@override
|
@override
|
||||||
ThemeMode get themeMode;
|
ThemeMode get themeMode;
|
||||||
|
@ -44,6 +44,10 @@ _$UserPreferencesImpl _$$UserPreferencesImplFromJson(
|
|||||||
$enumDecodeNullable(_$SearchModeEnumMap, json['searchMode']) ??
|
$enumDecodeNullable(_$SearchModeEnumMap, json['searchMode']) ??
|
||||||
SearchMode.youtube,
|
SearchMode.youtube,
|
||||||
downloadLocation: json['downloadLocation'] as String? ?? "",
|
downloadLocation: json['downloadLocation'] as String? ?? "",
|
||||||
|
localLibraryLocation: (json['localLibraryLocation'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
pipedInstance:
|
pipedInstance:
|
||||||
json['pipedInstance'] as String? ?? "https://pipedapi.kavin.rocks",
|
json['pipedInstance'] as String? ?? "https://pipedapi.kavin.rocks",
|
||||||
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
|
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
|
||||||
@ -81,6 +85,7 @@ Map<String, dynamic> _$$UserPreferencesImplToJson(
|
|||||||
'recommendationMarket': _$MarketEnumMap[instance.recommendationMarket]!,
|
'recommendationMarket': _$MarketEnumMap[instance.recommendationMarket]!,
|
||||||
'searchMode': _$SearchModeEnumMap[instance.searchMode]!,
|
'searchMode': _$SearchModeEnumMap[instance.searchMode]!,
|
||||||
'downloadLocation': instance.downloadLocation,
|
'downloadLocation': instance.downloadLocation,
|
||||||
|
'localLibraryLocation': instance.localLibraryLocation,
|
||||||
'pipedInstance': instance.pipedInstance,
|
'pipedInstance': instance.pipedInstance,
|
||||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
||||||
'audioSource': _$AudioSourceEnumMap[instance.audioSource]!,
|
'audioSource': _$AudioSourceEnumMap[instance.audioSource]!,
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
#include <screen_retriever/screen_retriever_plugin.h>
|
#include <screen_retriever/screen_retriever_plugin.h>
|
||||||
#include <system_theme/system_theme_plugin.h>
|
#include <system_theme/system_theme_plugin.h>
|
||||||
|
#include <system_tray/system_tray_plugin.h>
|
||||||
#include <tray_manager/tray_manager_plugin.h>
|
#include <tray_manager/tray_manager_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
@ -44,6 +45,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) system_theme_registrar =
|
g_autoptr(FlPluginRegistrar) system_theme_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "SystemThemePlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "SystemThemePlugin");
|
||||||
system_theme_plugin_register_with_registrar(system_theme_registrar);
|
system_theme_plugin_register_with_registrar(system_theme_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) system_tray_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "SystemTrayPlugin");
|
||||||
|
system_tray_plugin_register_with_registrar(system_tray_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
|
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
|
||||||
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
|
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
|
||||||
|
@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
media_kit_libs_linux
|
media_kit_libs_linux
|
||||||
screen_retriever
|
screen_retriever
|
||||||
system_theme
|
system_theme
|
||||||
|
system_tray
|
||||||
tray_manager
|
tray_manager
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
window_manager
|
window_manager
|
||||||
|
@ -21,6 +21,7 @@ import screen_retriever
|
|||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite
|
import sqflite
|
||||||
import system_theme
|
import system_theme
|
||||||
|
import system_tray
|
||||||
import tray_manager
|
import tray_manager
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import window_manager
|
import window_manager
|
||||||
@ -43,6 +44,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin"))
|
SystemThemePlugin.register(with: registry.registrar(forPlugin: "SystemThemePlugin"))
|
||||||
|
SystemTrayPlugin.register(with: registry.registrar(forPlugin: "SystemTrayPlugin"))
|
||||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||||
|
20
pubspec.lock
20
pubspec.lock
@ -1455,12 +1455,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.9"
|
version: "1.0.9"
|
||||||
media_kit_native_event_loop:
|
media_kit_native_event_loop:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: media_kit_native_event_loop
|
path: media_kit_native_event_loop
|
||||||
sha256: a605cf185499d14d58935b8784955a92a4bf0ff4e19a23de3d17a9106303930e
|
ref: main
|
||||||
url: "https://pub.dev"
|
resolved-ref: "285f7919bbf4a7d89a62615b14a3766a171ad575"
|
||||||
source: hosted
|
url: "https://github.com/media-kit/media-kit"
|
||||||
|
source: git
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
menu_base:
|
menu_base:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@ -2156,6 +2157,15 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.2"
|
version: "0.0.2"
|
||||||
|
system_tray:
|
||||||
|
dependency: "direct overridden"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: dc7ef410d5cfec897edf060c1c4baff69f7c181c
|
||||||
|
resolved-ref: dc7ef410d5cfec897edf060c1c4baff69f7c181c
|
||||||
|
url: "https://github.com/antler119/system_tray"
|
||||||
|
source: git
|
||||||
|
version: "2.0.2"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
11
pubspec.yaml
11
pubspec.yaml
@ -150,6 +150,17 @@ dev_dependencies:
|
|||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
uuid: ^4.4.0
|
uuid: ^4.4.0
|
||||||
|
system_tray:
|
||||||
|
# TODO: remove this when flutter_desktop_tools gets updated
|
||||||
|
# to use [MenuItemBase] instead of [MenuItem]
|
||||||
|
git:
|
||||||
|
url: https://github.com/antler119/system_tray
|
||||||
|
ref: dc7ef410d5cfec897edf060c1c4baff69f7c181c
|
||||||
|
media_kit_native_event_loop: # to fix "macro name must be an identifier"
|
||||||
|
git:
|
||||||
|
url: https://github.com/media-kit/media-kit
|
||||||
|
path: media_kit_native_event_loop
|
||||||
|
ref: main
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
generate: true
|
generate: true
|
||||||
|
@ -1 +1,155 @@
|
|||||||
{}
|
{
|
||||||
|
"ar": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"bn": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ca": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"cs": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"de": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"es": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"fa": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"fr": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"hi": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"it": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ja": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ko": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ne": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"nl": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"pl": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"pt": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"ru": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"th": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"tr": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"uk": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"vi": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
],
|
||||||
|
|
||||||
|
"zh": [
|
||||||
|
"local_library",
|
||||||
|
"add_library_location",
|
||||||
|
"remove_library_location",
|
||||||
|
"local_tab"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <screen_retriever/screen_retriever_plugin.h>
|
#include <screen_retriever/screen_retriever_plugin.h>
|
||||||
#include <system_theme/system_theme_plugin.h>
|
#include <system_theme/system_theme_plugin.h>
|
||||||
|
#include <system_tray/system_tray_plugin.h>
|
||||||
#include <tray_manager/tray_manager_plugin.h>
|
#include <tray_manager/tray_manager_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
@ -42,6 +43,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||||
SystemThemePluginRegisterWithRegistrar(
|
SystemThemePluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("SystemThemePlugin"));
|
registry->GetRegistrarForPlugin("SystemThemePlugin"));
|
||||||
|
SystemTrayPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SystemTrayPlugin"));
|
||||||
TrayManagerPluginRegisterWithRegistrar(
|
TrayManagerPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
|
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
screen_retriever
|
screen_retriever
|
||||||
system_theme
|
system_theme
|
||||||
|
system_tray
|
||||||
tray_manager
|
tray_manager
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
window_manager
|
window_manager
|
||||||
|
Loading…
Reference in New Issue
Block a user