mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
refactor: user local library
This commit is contained in:
parent
4afe0cca68
commit
b8f2495acb
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
|
||||
@ -39,11 +39,8 @@ class ExpandableSearchField extends StatelessWidget {
|
||||
child: TextField(
|
||||
focusNode: searchFocus,
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.search_tracks,
|
||||
isDense: true,
|
||||
prefixIcon: const Icon(SpotubeIcons.search),
|
||||
),
|
||||
placeholder: Text(context.l10n.search_tracks),
|
||||
leading: const Icon(SpotubeIcons.search),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -69,16 +66,9 @@ class ExpandableSearchButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return IconButton(
|
||||
icon: icon,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor:
|
||||
isFiltering ? theme.colorScheme.secondaryContainer : null,
|
||||
foregroundColor: isFiltering ? theme.colorScheme.secondary : null,
|
||||
minimumSize: const Size(25, 25),
|
||||
),
|
||||
variance: isFiltering ? ButtonVariance.secondary : ButtonVariance.outline,
|
||||
onPressed: () {
|
||||
if (isFiltering) {
|
||||
searchFocus.requestFocus();
|
||||
|
@ -215,7 +215,7 @@ class Spotube extends HookConsumerWidget {
|
||||
theme: ThemeData(
|
||||
radius: .5,
|
||||
iconTheme: const IconThemeProperties(),
|
||||
colorScheme: ColorSchemes.lightBlue(),
|
||||
colorScheme: ColorSchemes.lightOrange(),
|
||||
surfaceOpacity: .8,
|
||||
surfaceBlur: 10,
|
||||
),
|
||||
|
@ -1,18 +1,17 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/image.dart';
|
||||
import 'package:spotube/extensions/string.dart';
|
||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||
import 'package:spotube/pages/library/local_folder.dart';
|
||||
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
@ -26,8 +25,6 @@ class LocalFolderItem extends HookConsumerWidget {
|
||||
final ThemeData(:colorScheme) = Theme.of(context);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
|
||||
final lerpValue = useBrightnessValue(.9, .7);
|
||||
|
||||
final downloadFolder =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.downloadLocation));
|
||||
final cacheFolder = useFuture(UserPreferencesNotifier.getMusicCacheDir());
|
||||
@ -60,8 +57,8 @@ class LocalFolderItem extends HookConsumerWidget {
|
||||
|
||||
final tracks = trackSnapshot.value ?? [];
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
return Button(
|
||||
onPressed: () {
|
||||
context.goNamed(
|
||||
LocalLibraryPage.name,
|
||||
queryParameters: {
|
||||
@ -71,18 +68,11 @@ class LocalFolderItem extends HookConsumerWidget {
|
||||
extra: folder,
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Color.lerp(
|
||||
colorScheme.surfaceContainerHighest,
|
||||
colorScheme.surface,
|
||||
lerpValue,
|
||||
style: ButtonVariance.card.copyWith(
|
||||
padding: (context, states, value) {
|
||||
return const EdgeInsets.all(8);
|
||||
},
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -123,6 +113,9 @@ class LocalFolderItem extends HookConsumerWidget {
|
||||
),
|
||||
const Gap(8),
|
||||
Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
@ -133,25 +126,47 @@ class LocalFolderItem extends HookConsumerWidget {
|
||||
: basename(folder),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (!isDownloadFolder)
|
||||
Wrap(
|
||||
spacing: 2,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
for (final MapEntry(key: index, value: segment)
|
||||
in segments.asMap().entries)
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
if (index != 0) const TextSpan(text: "/ "),
|
||||
TextSpan(text: segment),
|
||||
],
|
||||
),
|
||||
maxLines: 2,
|
||||
).xSmall().muted(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!isDownloadFolder && !isCacheFolder)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: PopupMenuButton(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(3),
|
||||
child: Icon(Icons.more_vert),
|
||||
),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: const Icon(SpotubeIcons.folderRemove),
|
||||
iconColor: colorScheme.error,
|
||||
title:
|
||||
child: IconButton.ghost(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
size: ButtonSize.small,
|
||||
onPressed: () {
|
||||
showDropdown(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return DropdownMenu(
|
||||
children: [
|
||||
MenuButton(
|
||||
leading: Icon(SpotubeIcons.folderRemove,
|
||||
color: colorScheme.destructive),
|
||||
child:
|
||||
Text(context.l10n.remove_library_location),
|
||||
onTap: () {
|
||||
onPressed: (context) {
|
||||
final libraryLocations = ref
|
||||
.read(userPreferencesProvider)
|
||||
.localLibraryLocation;
|
||||
@ -163,44 +178,19 @@ class LocalFolderItem extends HookConsumerWidget {
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
];
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Wrap(
|
||||
spacing: 2,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
for (final MapEntry(key: index, value: segment)
|
||||
in segments.asMap().entries)
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
if (index != 0)
|
||||
TextSpan(
|
||||
text: "/ ",
|
||||
style: TextStyle(color: colorScheme.primary),
|
||||
),
|
||||
TextSpan(text: segment),
|
||||
],
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/modules/library/local_folder/local_folder_item.dart';
|
||||
@ -58,17 +57,23 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
// For now, this gets all of them.
|
||||
ref.watch(localTracksProvider);
|
||||
|
||||
return LayoutBuilder(builder: (context, constrains) {
|
||||
return Padding(
|
||||
final locations = [
|
||||
preferences.downloadLocation,
|
||||
if (cacheDir.hasData) cacheDir.data!,
|
||||
...preferences.localLibraryLocation,
|
||||
];
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constrains) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(SpotubeIcons.folderAdd),
|
||||
label: Text(context.l10n.add_library_location),
|
||||
child: Button.secondary(
|
||||
leading: const Icon(SpotubeIcons.folderAdd),
|
||||
onPressed: addLocalLibraryLocation,
|
||||
child: Text(context.l10n.add_library_location),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@ -84,23 +89,16 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
),
|
||||
itemCount: preferences.localLibraryLocation.length +
|
||||
1 +
|
||||
(cacheDir.hasData ? 1 : 0),
|
||||
itemCount: locations.length,
|
||||
itemBuilder: (context, index) {
|
||||
return LocalFolderItem(
|
||||
folder: index == 0
|
||||
? preferences.downloadLocation
|
||||
: index == 1 && cacheDir.hasData
|
||||
? cacheDir.data!
|
||||
: preferences.localLibraryLocation[index - 1],
|
||||
folder: locations[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,16 @@ import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:file_picker/file_picker.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:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/fake.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/button/back_button.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/string.dart';
|
||||
import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart';
|
||||
import 'package:spotube/modules/library/user_local_tracks.dart';
|
||||
@ -65,7 +68,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme) = Theme.of(context);
|
||||
final scale = context.theme.scaling;
|
||||
|
||||
final sortBy = useState<SortBy>(SortBy.none);
|
||||
final playlist = ref.watch(audioPlayerProvider);
|
||||
@ -93,7 +96,13 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
appBar: TitleBar(
|
||||
headers: [
|
||||
TitleBar(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 0,
|
||||
),
|
||||
surfaceBlur: 0,
|
||||
leading: const [BackButton()],
|
||||
title: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -105,47 +114,43 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
: isCache
|
||||
? context.l10n.cache_folder.capitalize()
|
||||
: location,
|
||||
style: textTheme.titleLarge,
|
||||
),
|
||||
FutureBuilder<String>(
|
||||
future: directorySize,
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
"${(snapshot.data ?? 0)} GB",
|
||||
style: textTheme.labelSmall,
|
||||
);
|
||||
).xSmall().muted();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
trailingGap: 10,
|
||||
trailing: [
|
||||
if (isCache) ...[
|
||||
IconButton(
|
||||
iconSize: 16,
|
||||
IconButton.outline(
|
||||
size: ButtonSize.small,
|
||||
icon: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(SpotubeIcons.delete),
|
||||
Text(
|
||||
context.l10n.clear_cache,
|
||||
style: textTheme.labelSmall,
|
||||
)
|
||||
Text(context.l10n.clear_cache)
|
||||
],
|
||||
),
|
||||
).xSmall().iconSmall(),
|
||||
onPressed: () async {
|
||||
final accepted = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog.adaptive(
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.l10n.clear_cache_confirmation),
|
||||
actions: [
|
||||
TextButton(
|
||||
Button.outline(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(context.l10n.decline),
|
||||
),
|
||||
TextButton(
|
||||
Button.destructive(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
@ -166,18 +171,17 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
iconSize: 16,
|
||||
IconButton.outline(
|
||||
size: ButtonSize.small,
|
||||
icon: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(SpotubeIcons.export),
|
||||
Text(
|
||||
context.l10n.export,
|
||||
style: textTheme.labelSmall,
|
||||
)
|
||||
],
|
||||
),
|
||||
).xSmall().iconSmall(),
|
||||
onPressed: () async {
|
||||
final exportPath =
|
||||
await FilePicker.platform.getDirectoryPath();
|
||||
@ -207,54 +211,69 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
]
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
],
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 5),
|
||||
FilledButton(
|
||||
const Gap(5),
|
||||
Button.primary(
|
||||
onPressed: trackSnapshot.asData?.value != null
|
||||
? () async {
|
||||
if (trackSnapshot.asData?.value.isNotEmpty ==
|
||||
if (trackSnapshot
|
||||
.asData?.value.isNotEmpty ==
|
||||
true) {
|
||||
if (!isPlaylistPlaying) {
|
||||
await playLocalTracks(
|
||||
ref,
|
||||
trackSnapshot.asData!.value[location] ?? [],
|
||||
trackSnapshot
|
||||
.asData!.value[location] ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(context.l10n.play),
|
||||
Icon(
|
||||
leading: Icon(
|
||||
isPlaylistPlaying
|
||||
? SpotubeIcons.stop
|
||||
: SpotubeIcons.play,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Text(context.l10n.play),
|
||||
),
|
||||
const Spacer(),
|
||||
if (constraints.smAndDown)
|
||||
ExpandableSearchButton(
|
||||
isFiltering: isFiltering.value,
|
||||
onPressed: (value) => isFiltering.value = value,
|
||||
searchFocus: searchFocus,
|
||||
)
|
||||
else
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 300 * scale,
|
||||
maxHeight: 38 * scale,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
child: ExpandableSearchField(
|
||||
isFiltering: true,
|
||||
onChangeFiltering: (value) {},
|
||||
searchController: searchController,
|
||||
searchFocus: searchFocus,
|
||||
),
|
||||
),
|
||||
const Gap(5),
|
||||
SortTracksDropdown(
|
||||
value: sortBy.value,
|
||||
onChanged: (value) {
|
||||
sortBy.value = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
FilledButton(
|
||||
child: const Icon(SpotubeIcons.refresh),
|
||||
const Gap(5),
|
||||
IconButton.outline(
|
||||
icon: const Icon(SpotubeIcons.refresh),
|
||||
onPressed: () {
|
||||
ref.invalidate(localTracksProvider);
|
||||
},
|
||||
@ -268,11 +287,13 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
isFiltering: isFiltering.value,
|
||||
onChangeFiltering: (value) => isFiltering.value = value,
|
||||
),
|
||||
trackSnapshot.when(
|
||||
HookBuilder(builder: (context) {
|
||||
return trackSnapshot.when(
|
||||
data: (tracks) {
|
||||
final sortedTracks = useMemoized(() {
|
||||
return ServiceUtils.sortTracks(
|
||||
tracks[location] ?? <LocalTrack>[], sortBy.value);
|
||||
tracks[location] ?? <LocalTrack>[],
|
||||
sortBy.value);
|
||||
}, [sortBy.value, tracks]);
|
||||
|
||||
final filteredTracks = useMemoized(() {
|
||||
@ -297,7 +318,8 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
.toList();
|
||||
}, [searchController.text, sortedTracks]);
|
||||
|
||||
if (!trackSnapshot.isLoading && filteredTracks.isEmpty) {
|
||||
if (!trackSnapshot.isLoading &&
|
||||
filteredTracks.isEmpty) {
|
||||
return const Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@ -307,9 +329,9 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: RefreshIndicator(
|
||||
child: RefreshTrigger(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(localTracksProvider);
|
||||
// ref.invalidate(localTracksProvider);
|
||||
},
|
||||
child: InterScrollbar(
|
||||
controller: controller,
|
||||
@ -317,7 +339,8 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
enabled: trackSnapshot.isLoading,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
physics:
|
||||
const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: trackSnapshot.isLoading
|
||||
? 5
|
||||
: filteredTracks.length,
|
||||
@ -366,9 +389,10 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
),
|
||||
error: (error, stackTrace) =>
|
||||
Text(error.toString() + stackTrace.toString()),
|
||||
)
|
||||
);
|
||||
})
|
||||
],
|
||||
)),
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user