refactor: user local library

This commit is contained in:
Kingkor Roy Tirtho 2025-01-05 11:54:50 +06:00
parent 4afe0cca68
commit b8f2495acb
5 changed files with 441 additions and 439 deletions

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
@ -39,11 +39,8 @@ class ExpandableSearchField extends StatelessWidget {
child: TextField( child: TextField(
focusNode: searchFocus, focusNode: searchFocus,
controller: searchController, controller: searchController,
decoration: InputDecoration( placeholder: Text(context.l10n.search_tracks),
hintText: context.l10n.search_tracks, leading: const Icon(SpotubeIcons.search),
isDense: true,
prefixIcon: const Icon(SpotubeIcons.search),
),
), ),
), ),
), ),
@ -69,16 +66,9 @@ class ExpandableSearchButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
return IconButton( return IconButton(
icon: icon, icon: icon,
style: IconButton.styleFrom( variance: isFiltering ? ButtonVariance.secondary : ButtonVariance.outline,
backgroundColor:
isFiltering ? theme.colorScheme.secondaryContainer : null,
foregroundColor: isFiltering ? theme.colorScheme.secondary : null,
minimumSize: const Size(25, 25),
),
onPressed: () { onPressed: () {
if (isFiltering) { if (isFiltering) {
searchFocus.requestFocus(); searchFocus.requestFocus();

View File

@ -215,7 +215,7 @@ class Spotube extends HookConsumerWidget {
theme: ThemeData( theme: ThemeData(
radius: .5, radius: .5,
iconTheme: const IconThemeProperties(), iconTheme: const IconThemeProperties(),
colorScheme: ColorSchemes.lightBlue(), colorScheme: ColorSchemes.lightOrange(),
surfaceOpacity: .8, surfaceOpacity: .8,
surfaceBlur: 10, surfaceBlur: 10,
), ),

View File

@ -1,18 +1,17 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/string.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/pages/library/local_folder.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart'; import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_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 ThemeData(:colorScheme) = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final lerpValue = useBrightnessValue(.9, .7);
final downloadFolder = final downloadFolder =
ref.watch(userPreferencesProvider.select((s) => s.downloadLocation)); ref.watch(userPreferencesProvider.select((s) => s.downloadLocation));
final cacheFolder = useFuture(UserPreferencesNotifier.getMusicCacheDir()); final cacheFolder = useFuture(UserPreferencesNotifier.getMusicCacheDir());
@ -60,8 +57,8 @@ class LocalFolderItem extends HookConsumerWidget {
final tracks = trackSnapshot.value ?? []; final tracks = trackSnapshot.value ?? [];
return InkWell( return Button(
onTap: () { onPressed: () {
context.goNamed( context.goNamed(
LocalLibraryPage.name, LocalLibraryPage.name,
queryParameters: { queryParameters: {
@ -71,58 +68,54 @@ class LocalFolderItem extends HookConsumerWidget {
extra: folder, extra: folder,
); );
}, },
borderRadius: BorderRadius.circular(8), style: ButtonVariance.card.copyWith(
child: Ink( padding: (context, states, value) {
decoration: BoxDecoration( return const EdgeInsets.all(8);
borderRadius: BorderRadius.circular(8), },
color: Color.lerp( ),
colorScheme.surfaceContainerHighest, child: Column(
colorScheme.surface, mainAxisSize: MainAxisSize.min,
lerpValue, children: [
), if (tracks.isEmpty)
), Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Icon(
mainAxisSize: MainAxisSize.min, SpotubeIcons.folder,
children: [ size: mediaQuery.smAndDown
if (tracks.isEmpty) ? 95
Card( : mediaQuery.mdAndDown
child: Padding( ? 100
padding: const EdgeInsets.all(8.0), : 142,
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,
);
},
),
), ),
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: [ children: [
Center( Center(
child: Text( child: Text(
@ -133,25 +126,47 @@ class LocalFolderItem extends HookConsumerWidget {
: basename(folder), : basename(folder),
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
), ),
if (!isDownloadFolder) Wrap(
Align( spacing: 2,
alignment: Alignment.topRight, runSpacing: 2,
child: PopupMenuButton( children: [
child: const Padding( for (final MapEntry(key: index, value: segment)
padding: EdgeInsets.all(3), in segments.asMap().entries)
child: Icon(Icons.more_vert), Text.rich(
), TextSpan(
itemBuilder: (context) { children: [
return [ if (index != 0) const TextSpan(text: "/ "),
PopupMenuItem( TextSpan(text: segment),
child: ListTile( ],
leading: const Icon(SpotubeIcons.folderRemove), ),
iconColor: colorScheme.error, maxLines: 2,
title: ).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), Text(context.l10n.remove_library_location),
onTap: () { onPressed: (context) {
final libraryLocations = ref final libraryLocations = ref
.read(userPreferencesProvider) .read(userPreferencesProvider)
.localLibraryLocation; .localLibraryLocation;
@ -163,43 +178,18 @@ class LocalFolderItem extends HookConsumerWidget {
.toList(), .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(),
],
), ),
); );
} }

View File

@ -1,9 +1,8 @@
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:file_selector/file_selector.dart'; import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/collections/spotube_icons.dart';
import 'package:spotube/modules/library/local_folder/local_folder_item.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. // For now, this gets all of them.
ref.watch(localTracksProvider); ref.watch(localTracksProvider);
return LayoutBuilder(builder: (context, constrains) { final locations = [
return Padding( preferences.downloadLocation,
padding: const EdgeInsets.symmetric(horizontal: 12.0), if (cacheDir.hasData) cacheDir.data!,
child: Column( ...preferences.localLibraryLocation,
children: [ ];
Align(
alignment: Alignment.centerRight, return LayoutBuilder(
child: TextButton.icon( builder: (context, constrains) => Padding(
icon: const Icon(SpotubeIcons.folderAdd), padding: const EdgeInsets.symmetric(horizontal: 12.0),
label: Text(context.l10n.add_library_location), child: Column(
onPressed: addLocalLibraryLocation, 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],
);
},
),
),
],
),
);
});
} }
} }

View File

@ -3,13 +3,16 @@ import 'dart:math';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.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/extensions/string.dart';
import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart'; import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart';
import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/modules/library/user_local_tracks.dart';
@ -65,7 +68,7 @@ class LocalLibraryPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme) = Theme.of(context); final scale = context.theme.scaling;
final sortBy = useState<SortBy>(SortBy.none); final sortBy = useState<SortBy>(SortBy.none);
final playlist = ref.watch(audioPlayerProvider); final playlist = ref.watch(audioPlayerProvider);
@ -93,282 +96,303 @@ class LocalLibraryPage extends HookConsumerWidget {
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
appBar: TitleBar( headers: [
leading: const [BackButton()], TitleBar(
title: Column( padding: const EdgeInsets.symmetric(
mainAxisSize: MainAxisSize.min, horizontal: 10,
crossAxisAlignment: CrossAxisAlignment.start, vertical: 0,
children: [ ),
Text( surfaceBlur: 0,
isDownloads leading: const [BackButton()],
? context.l10n.downloads title: Column(
: isCache mainAxisSize: MainAxisSize.min,
? context.l10n.cache_folder.capitalize() crossAxisAlignment: CrossAxisAlignment.start,
: location, children: [
style: textTheme.titleLarge, Text(
), isDownloads
FutureBuilder<String>( ? context.l10n.downloads
future: directorySize, : isCache
builder: (context, snapshot) { ? context.l10n.cache_folder.capitalize()
return Text( : location,
"${(snapshot.data ?? 0)} GB", ),
style: textTheme.labelSmall, 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: [ child: LayoutBuilder(
if (isCache) ...[ builder: (context, constraints) => Column(
IconButton(
iconSize: 16,
icon: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(SpotubeIcons.delete), Padding(
Text( padding: const EdgeInsets.all(8.0),
context.l10n.clear_cache, child: Row(
style: textTheme.labelSmall, children: [
) const Gap(5),
], Button.primary(
), onPressed: trackSnapshot.asData?.value != null
onPressed: () async { ? () async {
final accepted = await showDialog<bool>( if (trackSnapshot
context: context, .asData?.value.isNotEmpty ==
builder: (context) => AlertDialog.adaptive( true) {
title: Text(context.l10n.clear_cache_confirmation), if (!isPlaylistPlaying) {
actions: [ await playLocalTracks(
TextButton( ref,
onPressed: () { trackSnapshot
Navigator.of(context).pop(false); .asData!.value[location] ??
}, [],
child: Text(context.l10n.decline), );
), }
TextButton( }
onPressed: () async { }
Navigator.of(context).pop(true); : null,
}, leading: Icon(
child: Text(context.l10n.accept), isPlaylistPlaying
), ? SpotubeIcons.stop
], : SpotubeIcons.play,
),
);
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,
), ),
e, child: Text(context.l10n.play),
)) ),
.toList() const Spacer(),
.sorted( if (constraints.smAndDown)
(a, b) => b.$1.compareTo(a.$1), ExpandableSearchButton(
) isFiltering: isFiltering.value,
.where((e) => e.$1 > 50) onPressed: (value) => isFiltering.value = value,
.map((e) => e.$2) searchFocus: searchFocus,
.toList() )
.toList(); else
}, [searchController.text, sortedTracks]); ConstrainedBox(
constraints: BoxConstraints(
if (!trackSnapshot.isLoading && filteredTracks.isEmpty) { maxWidth: 300 * scale,
return const Expanded( maxHeight: 38 * scale,
child: Row( ),
mainAxisAlignment: MainAxisAlignment.center, child: ExpandableSearchField(
children: [NotFound()], isFiltering: true,
), onChangeFiltering: (value) {},
); searchController: searchController,
} searchFocus: searchFocus,
),
return Expanded( ),
child: RefreshIndicator( const Gap(5),
onRefresh: () async { SortTracksDropdown(
ref.invalidate(localTracksProvider); value: sortBy.value,
}, onChanged: (value) {
child: InterScrollbar( sortBy.value = value;
controller: controller, },
child: Skeletonizer( ),
enabled: trackSnapshot.isLoading, const Gap(5),
child: ListView.builder( IconButton.outline(
controller: controller, icon: const Icon(SpotubeIcons.refresh),
physics: const AlwaysScrollableScrollPhysics(), onPressed: () {
itemCount: trackSnapshot.isLoading ref.invalidate(localTracksProvider);
? 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,
);
},
);
},
),
), ),
), ),
), ExpandableSearchField(
); searchController: searchController,
}, searchFocus: searchFocus,
loading: () => Expanded( isFiltering: isFiltering.value,
child: Skeletonizer( onChangeFiltering: (value) => isFiltering.value = value,
enabled: true,
child: ListView.builder(
itemCount: 5,
itemBuilder: (context, index) => TrackTile(
track: FakeData.track,
index: index,
playlist: playlist,
), ),
), HookBuilder(builder: (context) {
), return trackSnapshot.when(
), data: (tracks) {
error: (error, stackTrace) => final sortedTracks = useMemoized(() {
Text(error.toString() + stackTrace.toString()), 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()),
);
})
],
))),
); );
} }
} }