From ad6d534286e096b3e64f395573b25becd82ed6fb Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:27:41 +0900 Subject: [PATCH] context menu base and move query to state --- lib/app/screens/library_screen.dart | 62 ++++++++--------- lib/app/screens/settings_source_screen.dart | 2 +- lib/app/state/lists.dart | 31 +++++++++ lib/app/ui/lists/albums_grid.dart | 28 +++++--- lib/app/ui/lists/albums_list.dart | 2 + lib/app/ui/lists/artists_list.dart | 2 + lib/app/ui/lists/playlists_list.dart | 2 + lib/app/ui/lists/songs_list.dart | 5 +- lib/app/ui/menus.dart | 76 +++++++++++++++++++++ lib/app/util/padding.dart | 12 ---- 10 files changed, 169 insertions(+), 53 deletions(-) create mode 100644 lib/app/state/lists.dart create mode 100644 lib/app/ui/menus.dart delete mode 100644 lib/app/util/padding.dart diff --git a/lib/app/screens/library_screen.dart b/lib/app/screens/library_screen.dart index a737a6b..3031b10 100644 --- a/lib/app/screens/library_screen.dart +++ b/lib/app/screens/library_screen.dart @@ -1,19 +1,18 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; -import '../../database/query.dart'; import '../../l10n/generated/app_localizations.dart'; +import '../state/lists.dart'; import '../state/services.dart'; -import '../state/source.dart'; import '../ui/lists/albums_grid.dart'; import '../ui/lists/artists_list.dart'; import '../ui/lists/items.dart'; import '../ui/lists/playlists_list.dart'; import '../ui/lists/songs_list.dart'; +import '../ui/menus.dart'; import '../util/custom_scroll_fix.dart'; const kIconSize = 26.0; @@ -75,25 +74,7 @@ class LibraryTabBarView extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final sourceId = ref.watch(sourceIdProvider); - - final albumsQuery = AlbumsQuery( - sourceId: sourceId, - sort: IList([ - SortingTerm.albumsDesc(AlbumsColumn.created), - ]), - ); - - final songsQuery = SongsQuery( - sourceId: sourceId, - sort: IList([ - SortingTerm.songsAsc(SongsColumn.albumArtist), - SortingTerm.songsAsc(SongsColumn.album), - SortingTerm.songsAsc(SongsColumn.disc), - SortingTerm.songsAsc(SongsColumn.track), - SortingTerm.songsAsc(SongsColumn.title), - ]), - ); + final songsQuery = ref.watch(songsQueryProvider); return TabBarView( controller: tabController, @@ -102,7 +83,7 @@ class LibraryTabBarView extends HookConsumerWidget { (tab) => TabScrollView( index: LibraryTab.values.indexOf(tab), sliver: switch (tab) { - LibraryTab.albums => AlbumsGrid(query: albumsQuery), + LibraryTab.albums => AlbumsGrid(), LibraryTab.artists => ArtistsList(), LibraryTab.playlists => PlaylistsList(), LibraryTab.songs => SongsList( @@ -116,6 +97,13 @@ class LibraryTabBarView extends HookConsumerWidget { ), // _ => SliverToBoxAdapter(child: Container()), }, + menuBuilder: switch (tab) { + LibraryTab.albums => (_) => AlbumsGridFilters(), + // LibraryTab.artists => (_) => AlbumsGridFilters(), + // LibraryTab.playlists => (_) => AlbumsGridFilters(), + // LibraryTab.songs => (_) => AlbumsGridFilters(), + _ => null, + }, ), ) .toList(), @@ -240,10 +228,12 @@ class TabScrollView extends HookConsumerWidget { super.key, required this.index, required this.sliver, + this.menuBuilder, }); final int index; final Widget sliver; + final WidgetBuilder? menuBuilder; @override Widget build(BuildContext context, WidgetRef ref) { @@ -251,14 +241,24 @@ class TabScrollView extends HookConsumerWidget { final scrollProvider = CustomScrollProviderData.of(context); - return CustomScrollView( - controller: scrollProvider.scrollControllers[index], - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - sliver, - ], + final listBuilder = menuBuilder; + final floatingActionButton = listBuilder != null + ? FabFilter( + listBuilder: listBuilder, + ) + : null; + + return Scaffold( + floatingActionButton: floatingActionButton, + body: CustomScrollView( + controller: scrollProvider.scrollControllers[index], + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + sliver, + ], + ), ); } } diff --git a/lib/app/screens/settings_source_screen.dart b/lib/app/screens/settings_source_screen.dart index 3fc9045..3a03cdd 100644 --- a/lib/app/screens/settings_source_screen.dart +++ b/lib/app/screens/settings_source_screen.dart @@ -9,7 +9,7 @@ import '../../database/database.dart'; import '../../l10n/generated/app_localizations.dart'; import '../../util/logger.dart'; import '../state/database.dart'; -import '../util/padding.dart'; +import '../ui/menus.dart'; class SettingsSourceScreen extends HookConsumerWidget { const SettingsSourceScreen({ diff --git a/lib/app/state/lists.dart b/lib/app/state/lists.dart new file mode 100644 index 0000000..13a75b4 --- /dev/null +++ b/lib/app/state/lists.dart @@ -0,0 +1,31 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../database/query.dart'; +import 'source.dart'; + +final albumsQueryProvider = Provider((ref) { + final sourceId = ref.watch(sourceIdProvider); + + return AlbumsQuery( + sourceId: sourceId, + sort: IList([ + SortingTerm.albumsDesc(AlbumsColumn.created), + ]), + ); +}); + +final songsQueryProvider = Provider((ref) { + final sourceId = ref.watch(sourceIdProvider); + + return SongsQuery( + sourceId: sourceId, + sort: IList([ + SortingTerm.songsAsc(SongsColumn.albumArtist), + SortingTerm.songsAsc(SongsColumn.album), + SortingTerm.songsAsc(SongsColumn.disc), + SortingTerm.songsAsc(SongsColumn.track), + SortingTerm.songsAsc(SongsColumn.title), + ]), + ); +}); diff --git a/lib/app/ui/lists/albums_grid.dart b/lib/app/ui/lists/albums_grid.dart index f44dba4..9cb35ed 100644 --- a/lib/app/ui/lists/albums_grid.dart +++ b/lib/app/ui/lists/albums_grid.dart @@ -1,29 +1,28 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import '../../../database/query.dart'; import '../../../sources/models.dart'; import '../../hooks/use_on_source.dart'; import '../../hooks/use_paging_controller.dart'; import '../../state/database.dart'; +import '../../state/lists.dart'; import '../../state/source.dart'; +import '../menus.dart'; import 'items.dart'; const kPageSize = 60; class AlbumsGrid extends HookConsumerWidget { - const AlbumsGrid({ - super.key, - required this.query, - }); - - final AlbumsQuery query; + const AlbumsGrid({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final db = ref.watch(databaseProvider); + final query = ref.watch(albumsQueryProvider); + final controller = usePagingController( getNextPageKey: (state) => state.lastPageIsEmpty ? null : state.nextIntPageKey, @@ -36,8 +35,8 @@ class AlbumsGrid extends HookConsumerWidget { ), ); - useOnSourceChange(ref, (_) => controller.refresh()); useOnSourceSync(ref, controller.refresh); + useValueChanged(query, (_, _) => controller.refresh()); return PagingListener( controller: controller, @@ -50,7 +49,9 @@ class AlbumsGrid extends HookConsumerWidget { gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, ), + showNoMoreItemsIndicatorAsGridChild: false, builderDelegate: PagedChildBuilderDelegate( + noMoreItemsIndicatorBuilder: (context) => FabPadding(), itemBuilder: (context, item, index) => AlbumGridTile( album: item, onTap: () async { @@ -64,3 +65,14 @@ class AlbumsGrid extends HookConsumerWidget { ); } } + +class AlbumsGridFilters extends HookConsumerWidget { + const AlbumsGridFilters({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListView( + children: [], + ); + } +} diff --git a/lib/app/ui/lists/albums_list.dart b/lib/app/ui/lists/albums_list.dart index 1bd74a5..126cbef 100644 --- a/lib/app/ui/lists/albums_list.dart +++ b/lib/app/ui/lists/albums_list.dart @@ -9,6 +9,7 @@ import '../../hooks/use_on_source.dart'; import '../../hooks/use_paging_controller.dart'; import '../../state/database.dart'; import '../../state/source.dart'; +import '../menus.dart'; import 'items.dart'; const kPageSize = 30; @@ -46,6 +47,7 @@ class AlbumsList extends HookConsumerWidget { state: state, fetchNextPage: fetchNextPage, builderDelegate: PagedChildBuilderDelegate( + noMoreItemsIndicatorBuilder: (context) => FabPadding(), itemBuilder: (context, item, index) { final tile = AlbumListTile( album: item, diff --git a/lib/app/ui/lists/artists_list.dart b/lib/app/ui/lists/artists_list.dart index a6177a9..ff53eba 100644 --- a/lib/app/ui/lists/artists_list.dart +++ b/lib/app/ui/lists/artists_list.dart @@ -10,6 +10,7 @@ import '../../hooks/use_on_source.dart'; import '../../hooks/use_paging_controller.dart'; import '../../state/database.dart'; import '../../state/source.dart'; +import '../menus.dart'; import 'items.dart'; const kPageSize = 30; @@ -45,6 +46,7 @@ class ArtistsList extends HookConsumerWidget { state: state, fetchNextPage: fetchNextPage, builderDelegate: PagedChildBuilderDelegate( + noMoreItemsIndicatorBuilder: (context) => FabPadding(), itemBuilder: (context, item, index) { final (:artist, :albumCount) = item; diff --git a/lib/app/ui/lists/playlists_list.dart b/lib/app/ui/lists/playlists_list.dart index aea957f..0baf9b7 100644 --- a/lib/app/ui/lists/playlists_list.dart +++ b/lib/app/ui/lists/playlists_list.dart @@ -10,6 +10,7 @@ import '../../hooks/use_on_source.dart'; import '../../hooks/use_paging_controller.dart'; import '../../state/database.dart'; import '../../state/source.dart'; +import '../menus.dart'; import 'items.dart'; const kPageSize = 30; @@ -45,6 +46,7 @@ class PlaylistsList extends HookConsumerWidget { state: state, fetchNextPage: fetchNextPage, builderDelegate: PagedChildBuilderDelegate( + noMoreItemsIndicatorBuilder: (context) => FabPadding(), itemBuilder: (context, item, index) { return PlaylistListTile( playlist: item, diff --git a/lib/app/ui/lists/songs_list.dart b/lib/app/ui/lists/songs_list.dart index 7e03181..8382ae2 100644 --- a/lib/app/ui/lists/songs_list.dart +++ b/lib/app/ui/lists/songs_list.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; @@ -8,6 +9,7 @@ import '../../hooks/use_on_source.dart'; import '../../hooks/use_paging_controller.dart'; import '../../state/database.dart'; import '../../state/source.dart'; +import '../menus.dart'; const kPageSize = 30; @@ -37,8 +39,8 @@ class SongsList extends HookConsumerWidget { ), ); - useOnSourceChange(ref, (_) => controller.refresh()); useOnSourceSync(ref, controller.refresh); + useValueChanged(query, (_, _) => controller.refresh()); return PagingListener( controller: controller, @@ -47,6 +49,7 @@ class SongsList extends HookConsumerWidget { state: state, fetchNextPage: fetchNextPage, builderDelegate: PagedChildBuilderDelegate( + noMoreItemsIndicatorBuilder: (context) => FabPadding(), itemBuilder: itemBuilder, ), ); diff --git a/lib/app/ui/menus.dart b/lib/app/ui/menus.dart new file mode 100644 index 0000000..6af530b --- /dev/null +++ b/lib/app/ui/menus.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +Future showContextMenu({ + required BuildContext context, + required WidgetBuilder listBuilder, +}) => showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) => DraggableScrollableSheet( + expand: false, + snap: true, + initialChildSize: 0.3, + minChildSize: 0.3, + maxChildSize: 0.4, + builder: (context, scrollController) => PrimaryScrollController( + controller: scrollController, + child: listBuilder(context), + ), + ), +); + +class ContextMenuList extends StatelessWidget { + const ContextMenuList({ + super.key, + required this.children, + }); + + final List children; + + @override + Widget build(BuildContext context) { + return ListView( + children: children, + ); + } +} + +class FabFilter extends StatelessWidget { + const FabFilter({ + super.key, + required this.listBuilder, + }); + + final WidgetBuilder listBuilder; + + @override + Widget build(BuildContext context) { + return FloatingActionButton( + onPressed: () { + showContextMenu( + context: context, + listBuilder: listBuilder, + ); + }, + child: Icon( + Symbols.filter_list_rounded, + weight: 500, + opticalSize: 28, + size: 28, + ), + ); + } +} + +class FabPadding extends StatelessWidget { + const FabPadding({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const SizedBox(height: 86); + } +} diff --git a/lib/app/util/padding.dart b/lib/app/util/padding.dart deleted file mode 100644 index ff959e8..0000000 --- a/lib/app/util/padding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter/material.dart'; - -class FabPadding extends StatelessWidget { - const FabPadding({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return const SizedBox(height: 86); - } -}