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>
This commit is contained in:
Blake Leonard 2024-05-05 17:06:43 -04:00
parent b051ad4637
commit 20be5dbbcc
No known key found for this signature in database
GPG Key ID: 3B1965C22D07D9F6
10 changed files with 243 additions and 75 deletions

View File

@ -36,6 +36,7 @@ import 'package:spotube/pages/desktop_login/desktop_login.dart';
import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/root/root_app.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/pages/settings/library.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart';
final rootNavigatorKey = Catcher2.navigatorKey;
@ -132,6 +133,12 @@ final routerProvider = Provider((ref) {
child: const BlackListPage(),
),
),
GoRoute(
path: "local_library",
pageBuilder: (context, state) => SpotubeSlidePage(
child: const LocalLibrariesPage(),
),
),
if (!kIsWeb)
GoRoute(
path: "logs",

View File

@ -71,20 +71,17 @@ final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
await downloadDir.create(recursive: true);
return [];
}
final downloadEntities = downloadDir.listSync(recursive: true);
final entities = downloadDir.listSync(recursive: true);
final localLibraryLocation = ref.watch(
final localLibraryLocations = ref.watch(
userPreferencesProvider.select((s) => s.localLibraryLocation),
);
if (localLibraryLocation.isEmpty) return [];
final localLibraryDir = Directory(localLibraryLocation);
if (!await localLibraryDir.exists()) {
await localLibraryDir.create(recursive: true);
return [];
for (final location in localLibraryLocations) {
final dir = Directory(location);
if (await Directory(location).exists()) {
entities.addAll(Directory(location).listSync(recursive: true));
}
}
final localLibraryEntities = localLibraryDir.listSync(recursive: true);
final entities = [...downloadEntities, ...localLibraryEntities];
final filesWithMetadata = (await Future.wait(
entities.map((e) => File(e.path)).where((file) {

View File

@ -107,7 +107,10 @@
"always_on_top": "Always on top",
"exit_mini_player": "Exit Mini player",
"download_location": "Download location",
"local_library_location": "Local library location",
"local_library": "Local library",
"local_library_description": "Music saved on your device that wasn't downloaded from Spotube",
"add_library_location": "Add to library",
"remove_library_location": "Remove from library",
"account": "Account",
"login_with_spotify": "Login with your Spotify account",
"connect_with_spotify": "Connect with Spotify",

View File

@ -0,0 +1,100 @@
import 'package:file_picker/file_picker.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
class LocalLibrariesPage extends HookConsumerWidget {
const LocalLibrariesPage({super.key});
@override
Widget build(BuildContext context, ref) {
final controller = useScrollController();
// final blacklist = ref.watch(blacklistProvider);
// final searchText = useState("");
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final preferences = ref.watch(userPreferencesProvider);
final addLocalLibraryLocation = useCallback(() async {
if (DesktopTools.platform.isMobile || DesktopTools.platform.isMacOS) {
final dirStr = await FilePicker.platform.getDirectoryPath(
initialDirectory: preferences.downloadLocation,
);
if (dirStr == null) return;
preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation, dirStr]);
} else {
String? dirStr = await getDirectoryPath(
initialDirectory: preferences.downloadLocation,
);
if (dirStr == null) return;
preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation, dirStr]);
}
}, [preferences.localLibraryLocation]);
final removeLocalLibraryLocation = useCallback((int index) {
if (index < 0 || index >= preferences.localLibraryLocation.length) return;
preferencesNotifier.setLocalLibraryLocation([...preferences.localLibraryLocation]..removeAt(index));
}, [preferences.localLibraryLocation]);
return Scaffold(
appBar: PageWindowTitleBar(
title: Text(context.l10n.local_library),
centerTitle: true,
leading: const BackButton(),
),
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
/* Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
onChanged: (value) => searchText.value = value,
decoration: InputDecoration(
hintText: context.l10n.search,
prefixIcon: const Icon(SpotubeIcons.search),
),
),
), */
Padding(
padding: const EdgeInsets.all(8.0),
child: FilledButton(
child: const Icon(SpotubeIcons.add),
onPressed: addLocalLibraryLocation,
),
),
InterScrollbar(
controller: controller,
child: ListView.builder(
controller: controller,
shrinkWrap: true,
itemCount: preferences.localLibraryLocation.length,
itemBuilder: (context, index) {
final item = preferences.localLibraryLocation.elementAt(index);
return ListTile(
title: Text(item),
trailing: Tooltip(
message: context.l10n.remove_library_location,
child: IconButton(
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
onPressed: () => removeLocalLibraryLocation(index),
),
),
);
},
),
),
],
),
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/components/settings/section_card_with_heading.dart';
import 'package:spotube/extensions/context.dart';
@ -33,22 +34,6 @@ class SettingsDownloadsSection extends HookConsumerWidget {
}
}, [preferences.downloadLocation]);
final pickLocalLibraryLocation = useCallback(() async {
if (DesktopTools.platform.isMobile || DesktopTools.platform.isMacOS) {
final dirStr = await FilePicker.platform.getDirectoryPath(
initialDirectory: preferences.localLibraryLocation,
);
if (dirStr == null) return;
preferencesNotifier.setLocalLibraryLocation(dirStr);
} else {
String? dirStr = await getDirectoryPath(
initialDirectory: preferences.localLibraryLocation,
);
if (dirStr == null) return;
preferencesNotifier.setLocalLibraryLocation(dirStr);
}
}, [preferences.localLibraryLocation]);
return SectionCardWithHeading(
heading: context.l10n.downloads,
children: [
@ -64,13 +49,12 @@ class SettingsDownloadsSection extends HookConsumerWidget {
),
ListTile(
leading: const Icon(SpotubeIcons.folder),
title: Text(context.l10n.local_library_location),
subtitle: Text(preferences.localLibraryLocation),
trailing: FilledButton(
onPressed: pickLocalLibraryLocation,
child: const Icon(SpotubeIcons.folder),
),
onTap: pickLocalLibraryLocation,
title: Text(context.l10n.local_library),
subtitle: Text(context.l10n.local_library_description),
onTap: () {
GoRouter.of(context).push("/settings/local_library");
},
trailing: const Icon(SpotubeIcons.angleRight),
),
],
);

View File

@ -69,9 +69,9 @@ class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
state = state.copyWith(downloadLocation: downloadDir);
}
void setLocalLibraryLocation(String localLibraryDir) {
if (localLibraryDir.isEmpty) return;
state = state.copyWith(localLibraryLocation: localLibraryDir);
void setLocalLibraryLocation(List<String> localLibraryDirs) {
//if (localLibraryDir.isEmpty) return;
state = state.copyWith(localLibraryLocation: localLibraryDirs);
}
void setLayoutMode(LayoutMode mode) {

View File

@ -84,7 +84,7 @@ class UserPreferences with _$UserPreferences {
@Default(Market.US) Market recommendationMarket,
@Default(SearchMode.youtube) SearchMode searchMode,
@Default("") String downloadLocation,
@Default("") String localLibraryLocation,
@Default([]) List<String> localLibraryLocation,
@Default("https://pipedapi.kavin.rocks") String pipedInstance,
@Default(ThemeMode.system) ThemeMode themeMode,
@Default(AudioSource.youtube) AudioSource audioSource,

View File

@ -43,7 +43,7 @@ mixin _$UserPreferences {
Market get recommendationMarket => throw _privateConstructorUsedError;
SearchMode get searchMode => throw _privateConstructorUsedError;
String get downloadLocation => throw _privateConstructorUsedError;
String get localLibraryLocation => throw _privateConstructorUsedError;
List<String> get localLibraryLocation => throw _privateConstructorUsedError;
String get pipedInstance => throw _privateConstructorUsedError;
ThemeMode get themeMode => throw _privateConstructorUsedError;
AudioSource get audioSource => throw _privateConstructorUsedError;
@ -89,7 +89,7 @@ abstract class $UserPreferencesCopyWith<$Res> {
Market recommendationMarket,
SearchMode searchMode,
String downloadLocation,
String localLibraryLocation,
List<String> localLibraryLocation,
String pipedInstance,
ThemeMode themeMode,
AudioSource audioSource,
@ -202,7 +202,7 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences>
localLibraryLocation: null == localLibraryLocation
? _value.localLibraryLocation
: localLibraryLocation // ignore: cast_nullable_to_non_nullable
as String,
as List<String>,
pipedInstance: null == pipedInstance
? _value.pipedInstance
: pipedInstance // ignore: cast_nullable_to_non_nullable
@ -271,7 +271,7 @@ abstract class _$$UserPreferencesImplCopyWith<$Res>
Market recommendationMarket,
SearchMode searchMode,
String downloadLocation,
String localLibraryLocation,
List<String> localLibraryLocation,
String pipedInstance,
ThemeMode themeMode,
AudioSource audioSource,
@ -380,9 +380,9 @@ class __$$UserPreferencesImplCopyWithImpl<$Res>
: downloadLocation // ignore: cast_nullable_to_non_nullable
as String,
localLibraryLocation: null == localLibraryLocation
? _value.localLibraryLocation
? _value._localLibraryLocation
: localLibraryLocation // ignore: cast_nullable_to_non_nullable
as String,
as List<String>,
pipedInstance: null == pipedInstance
? _value.pipedInstance
: pipedInstance // ignore: cast_nullable_to_non_nullable
@ -446,7 +446,7 @@ class _$UserPreferencesImpl implements _UserPreferences {
this.recommendationMarket = Market.US,
this.searchMode = SearchMode.youtube,
this.downloadLocation = "",
this.localLibraryLocation = "",
final List<String> localLibraryLocation = const [],
this.pipedInstance = "https://pipedapi.kavin.rocks",
this.themeMode = ThemeMode.system,
this.audioSource = AudioSource.youtube,
@ -454,7 +454,8 @@ class _$UserPreferencesImpl implements _UserPreferences {
this.downloadMusicCodec = SourceCodecs.m4a,
this.discordPresence = true,
this.endlessPlayback = true,
this.enableConnect = false});
this.enableConnect = false})
: _localLibraryLocation = localLibraryLocation;
factory _$UserPreferencesImpl.fromJson(Map<String, dynamic> json) =>
_$$UserPreferencesImplFromJson(json);
@ -510,9 +511,16 @@ class _$UserPreferencesImpl implements _UserPreferences {
@override
@JsonKey()
final String downloadLocation;
final List<String> _localLibraryLocation;
@override
@JsonKey()
final String localLibraryLocation;
List<String> get localLibraryLocation {
if (_localLibraryLocation is EqualUnmodifiableListView)
return _localLibraryLocation;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_localLibraryLocation);
}
@override
@JsonKey()
final String pipedInstance;
@ -577,8 +585,8 @@ class _$UserPreferencesImpl implements _UserPreferences {
other.searchMode == searchMode) &&
(identical(other.downloadLocation, downloadLocation) ||
other.downloadLocation == downloadLocation) &&
(identical(other.localLibraryLocation, localLibraryLocation) ||
other.localLibraryLocation == localLibraryLocation) &&
const DeepCollectionEquality()
.equals(other._localLibraryLocation, _localLibraryLocation) &&
(identical(other.pipedInstance, pipedInstance) ||
other.pipedInstance == pipedInstance) &&
(identical(other.themeMode, themeMode) ||
@ -616,7 +624,7 @@ class _$UserPreferencesImpl implements _UserPreferences {
recommendationMarket,
searchMode,
downloadLocation,
localLibraryLocation,
const DeepCollectionEquality().hash(_localLibraryLocation),
pipedInstance,
themeMode,
audioSource,
@ -667,7 +675,7 @@ abstract class _UserPreferences implements UserPreferences {
final Market recommendationMarket,
final SearchMode searchMode,
final String downloadLocation,
final String localLibraryLocation,
final List<String> localLibraryLocation,
final String pipedInstance,
final ThemeMode themeMode,
final AudioSource audioSource,
@ -719,7 +727,7 @@ abstract class _UserPreferences implements UserPreferences {
@override
String get downloadLocation;
@override
String get localLibraryLocation;
List<String> get localLibraryLocation;
@override
String get pipedInstance;
@override

View File

@ -44,7 +44,10 @@ _$UserPreferencesImpl _$$UserPreferencesImplFromJson(
$enumDecodeNullable(_$SearchModeEnumMap, json['searchMode']) ??
SearchMode.youtube,
downloadLocation: json['downloadLocation'] as String? ?? "",
localLibraryLocation: json['localLibraryLocation'] as String? ?? "",
localLibraryLocation: (json['localLibraryLocation'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
pipedInstance:
json['pipedInstance'] as String? ?? "https://pipedapi.kavin.rocks",
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??

View File

@ -1,89 +1,155 @@
{
"ar": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"bn": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"ca": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"cs": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"de": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"es": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"fa": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"fr": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"hi": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"it": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"ja": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"ko": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"ne": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"nl": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"pl": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"pt": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"ru": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"th": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"tr": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"uk": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"vi": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
],
"zh": [
"local_library_location"
"local_library",
"local_library_description",
"add_library_location",
"remove_library_location"
]
}