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,58 +68,54 @@ 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,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (tracks.isEmpty)
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
SpotubeIcons.folder,
|
||||
size: mediaQuery.smAndDown
|
||||
? 95
|
||||
: mediaQuery.mdAndDown
|
||||
? 100
|
||||
: 142,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: max((tracks.length / 2).ceil(), 2),
|
||||
),
|
||||
itemCount: tracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final track = tracks[index];
|
||||
return UniversalImage(
|
||||
path: (track.album?.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
),
|
||||
style: ButtonVariance.card.copyWith(
|
||||
padding: (context, states, value) {
|
||||
return const EdgeInsets.all(8);
|
||||
},
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (tracks.isEmpty)
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
SpotubeIcons.folder,
|
||||
size: mediaQuery.smAndDown
|
||||
? 95
|
||||
: mediaQuery.mdAndDown
|
||||
? 100
|
||||
: 142,
|
||||
),
|
||||
const Gap(8),
|
||||
Stack(
|
||||
),
|
||||
)
|
||||
else
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: max((tracks.length / 2).ceil(), 2),
|
||||
),
|
||||
itemCount: tracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final track = tracks[index];
|
||||
return UniversalImage(
|
||||
path: (track.album?.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
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)
|
||||
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:
|
||||
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: 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,43 +178,18 @@ 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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,49 +57,48 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
// For now, this gets all of them.
|
||||
ref.watch(localTracksProvider);
|
||||
|
||||
return LayoutBuilder(builder: (context, constrains) {
|
||||
return 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),
|
||||
onPressed: addLocalLibraryLocation,
|
||||
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: Button.secondary(
|
||||
leading: const Icon(SpotubeIcons.folderAdd),
|
||||
onPressed: addLocalLibraryLocation,
|
||||
child: Text(context.l10n.add_library_location),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: constrains.isXs
|
||||
? 210
|
||||
: constrains.mdAndDown
|
||||
? 280
|
||||
: 250,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
),
|
||||
itemCount: locations.length,
|
||||
itemBuilder: (context, index) {
|
||||
return LocalFolderItem(
|
||||
folder: locations[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: constrains.isXs
|
||||
? 210
|
||||
: constrains.mdAndDown
|
||||
? 280
|
||||
: 250,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
),
|
||||
itemCount: preferences.localLibraryLocation.length +
|
||||
1 +
|
||||
(cacheDir.hasData ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
return LocalFolderItem(
|
||||
folder: index == 0
|
||||
? preferences.downloadLocation
|
||||
: index == 1 && cacheDir.hasData
|
||||
? cacheDir.data!
|
||||
: preferences.localLibraryLocation[index - 1],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -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,282 +96,303 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
appBar: TitleBar(
|
||||
leading: const [BackButton()],
|
||||
title: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isDownloads
|
||||
? context.l10n.downloads
|
||||
: 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,
|
||||
);
|
||||
},
|
||||
)
|
||||
headers: [
|
||||
TitleBar(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 0,
|
||||
),
|
||||
surfaceBlur: 0,
|
||||
leading: const [BackButton()],
|
||||
title: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isDownloads
|
||||
? context.l10n.downloads
|
||||
: isCache
|
||||
? context.l10n.cache_folder.capitalize()
|
||||
: location,
|
||||
),
|
||||
FutureBuilder<String>(
|
||||
future: directorySize,
|
||||
builder: (context, snapshot) {
|
||||
return Text(
|
||||
"${(snapshot.data ?? 0)} GB",
|
||||
).xSmall().muted();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
trailingGap: 10,
|
||||
trailing: [
|
||||
if (isCache) ...[
|
||||
IconButton.outline(
|
||||
size: ButtonSize.small,
|
||||
icon: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(SpotubeIcons.delete),
|
||||
Text(context.l10n.clear_cache)
|
||||
],
|
||||
).xSmall().iconSmall(),
|
||||
onPressed: () async {
|
||||
final accepted = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.l10n.clear_cache_confirmation),
|
||||
actions: [
|
||||
Button.outline(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(context.l10n.decline),
|
||||
),
|
||||
Button.destructive(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(context.l10n.accept),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (accepted ?? false) return;
|
||||
|
||||
final cacheDir = Directory(
|
||||
await UserPreferencesNotifier.getMusicCacheDir(),
|
||||
);
|
||||
|
||||
if (cacheDir.existsSync()) {
|
||||
await cacheDir.delete(recursive: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton.outline(
|
||||
size: ButtonSize.small,
|
||||
icon: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(SpotubeIcons.export),
|
||||
Text(
|
||||
context.l10n.export,
|
||||
)
|
||||
],
|
||||
).xSmall().iconSmall(),
|
||||
onPressed: () async {
|
||||
final exportPath =
|
||||
await FilePicker.platform.getDirectoryPath();
|
||||
|
||||
if (exportPath == null) return;
|
||||
final exportDirectory = Directory(exportPath);
|
||||
|
||||
if (!exportDirectory.existsSync()) {
|
||||
await exportDirectory.create(recursive: true);
|
||||
}
|
||||
|
||||
final cacheDir = Directory(
|
||||
await UserPreferencesNotifier.getMusicCacheDir());
|
||||
|
||||
if (!context.mounted) return;
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return LocalFolderCacheExportDialog(
|
||||
cacheDir: cacheDir,
|
||||
exportDir: exportDirectory,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
trailing: [
|
||||
if (isCache) ...[
|
||||
IconButton(
|
||||
iconSize: 16,
|
||||
icon: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
],
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => Column(
|
||||
children: [
|
||||
const Icon(SpotubeIcons.delete),
|
||||
Text(
|
||||
context.l10n.clear_cache,
|
||||
style: textTheme.labelSmall,
|
||||
)
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
final accepted = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog.adaptive(
|
||||
title: Text(context.l10n.clear_cache_confirmation),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(context.l10n.decline),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(context.l10n.accept),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (accepted ?? false) return;
|
||||
|
||||
final cacheDir = Directory(
|
||||
await UserPreferencesNotifier.getMusicCacheDir(),
|
||||
);
|
||||
|
||||
if (cacheDir.existsSync()) {
|
||||
await cacheDir.delete(recursive: true);
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
iconSize: 16,
|
||||
icon: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(SpotubeIcons.export),
|
||||
Text(
|
||||
context.l10n.export,
|
||||
style: textTheme.labelSmall,
|
||||
)
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
final exportPath =
|
||||
await FilePicker.platform.getDirectoryPath();
|
||||
|
||||
if (exportPath == null) return;
|
||||
final exportDirectory = Directory(exportPath);
|
||||
|
||||
if (!exportDirectory.existsSync()) {
|
||||
await exportDirectory.create(recursive: true);
|
||||
}
|
||||
|
||||
final cacheDir = Directory(
|
||||
await UserPreferencesNotifier.getMusicCacheDir());
|
||||
|
||||
if (!context.mounted) return;
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return LocalFolderCacheExportDialog(
|
||||
cacheDir: cacheDir,
|
||||
exportDir: exportDirectory,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
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,
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(5),
|
||||
Button.primary(
|
||||
onPressed: trackSnapshot.asData?.value != null
|
||||
? () async {
|
||||
if (trackSnapshot
|
||||
.asData?.value.isNotEmpty ==
|
||||
true) {
|
||||
if (!isPlaylistPlaying) {
|
||||
await playLocalTracks(
|
||||
ref,
|
||||
trackSnapshot
|
||||
.asData!.value[location] ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
leading: Icon(
|
||||
isPlaylistPlaying
|
||||
? SpotubeIcons.stop
|
||||
: SpotubeIcons.play,
|
||||
),
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
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,
|
||||
),
|
||||
child: ExpandableSearchField(
|
||||
isFiltering: true,
|
||||
onChangeFiltering: (value) {},
|
||||
searchController: searchController,
|
||||
searchFocus: searchFocus,
|
||||
),
|
||||
),
|
||||
const Gap(5),
|
||||
SortTracksDropdown(
|
||||
value: sortBy.value,
|
||||
onChanged: (value) {
|
||||
sortBy.value = value;
|
||||
},
|
||||
),
|
||||
const Gap(5),
|
||||
IconButton.outline(
|
||||
icon: const Icon(SpotubeIcons.refresh),
|
||||
onPressed: () {
|
||||
ref.invalidate(localTracksProvider);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => Expanded(
|
||||
child: Skeletonizer(
|
||||
enabled: true,
|
||||
child: ListView.builder(
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) => TrackTile(
|
||||
track: FakeData.track,
|
||||
index: index,
|
||||
playlist: playlist,
|
||||
ExpandableSearchField(
|
||||
searchController: searchController,
|
||||
searchFocus: searchFocus,
|
||||
isFiltering: isFiltering.value,
|
||||
onChangeFiltering: (value) => isFiltering.value = value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
error: (error, stackTrace) =>
|
||||
Text(error.toString() + stackTrace.toString()),
|
||||
)
|
||||
],
|
||||
)),
|
||||
HookBuilder(builder: (context) {
|
||||
return 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: RefreshTrigger(
|
||||
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()),
|
||||
);
|
||||
})
|
||||
],
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user