diff --git a/lib/components/expandable_search/expandable_search.dart b/lib/components/expandable_search/expandable_search.dart index 157e180f..0c40b843 100644 --- a/lib/components/expandable_search/expandable_search.dart +++ b/lib/components/expandable_search/expandable_search.dart @@ -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(); diff --git a/lib/main.dart b/lib/main.dart index 57cfbd88..5b20acd8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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, ), diff --git a/lib/modules/library/local_folder/local_folder_item.dart b/lib/modules/library/local_folder/local_folder_item.dart index a965a42d..84e43e96 100644 --- a/lib/modules/library/local_folder/local_folder_item.dart +++ b/lib/modules/library/local_folder/local_folder_item.dart @@ -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(), + ], ), ); } diff --git a/lib/modules/library/user_local_tracks.dart b/lib/modules/library/user_local_tracks.dart index 23fb3be0..86b36dfb 100644 --- a/lib/modules/library/user_local_tracks.dart +++ b/lib/modules/library/user_local_tracks.dart @@ -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], - ); - }, - ), - ), - ], - ), - ); - }); + )); } } diff --git a/lib/pages/library/local_folder.dart b/lib/pages/library/local_folder.dart index 8cfec3a8..23326c68 100644 --- a/lib/pages/library/local_folder.dart +++ b/lib/pages/library/local_folder.dart @@ -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.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( - 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( + 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( + 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( - 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] ?? [], 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] ?? [], + 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()), + ); + }) + ], + ))), ); } }