mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-05-08 16:24:36 +00:00
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:
parent
b051ad4637
commit
20be5dbbcc
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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",
|
||||
|
||||
100
lib/pages/settings/library.dart
Normal file
100
lib/pages/settings/library.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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']) ??
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user