From 8c3979ca8bab215ee4b34c7dda3e22844d3ca192 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:41:41 +0900 Subject: [PATCH] library screen refactor --- lib/app/lists/albums_grid.dart | 27 ++- lib/app/screens/library_screen.dart | 356 ++++++++++++++-------------- 2 files changed, 194 insertions(+), 189 deletions(-) diff --git a/lib/app/lists/albums_grid.dart b/lib/app/lists/albums_grid.dart index 16b8903..20bf604 100644 --- a/lib/app/lists/albums_grid.dart +++ b/lib/app/lists/albums_grid.dart @@ -35,18 +35,21 @@ class AlbumsGrid extends HookConsumerWidget { return PagingListener( controller: controller, builder: (context, state, fetchNextPage) { - return PagedSliverGrid( - state: state, - fetchNextPage: fetchNextPage, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - ), - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => AlbumGridTile( - album: item, - onTap: () async { - context.push('/album/${item.id}'); - }, + return SliverPadding( + padding: const EdgeInsets.all(8.0), + sliver: PagedSliverGrid( + state: state, + fetchNextPage: fetchNextPage, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + ), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => AlbumGridTile( + album: item, + onTap: () async { + context.push('/album/${item.id}'); + }, + ), ), ), ); diff --git a/lib/app/screens/library_screen.dart b/lib/app/screens/library_screen.dart index a1d139e..4455ce2 100644 --- a/lib/app/screens/library_screen.dart +++ b/lib/app/screens/library_screen.dart @@ -1,173 +1,69 @@ 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 '../lists/albums_grid.dart'; import '../lists/artists_list.dart'; import '../state/services.dart'; import '../util/custom_scroll_fix.dart'; -class LibraryScreen extends StatefulWidget { +const kIconSize = 26.0; +const kTabHeight = 36.0; + +enum LibraryTab { + home(Icon(Symbols.home_rounded)), + albums(Icon(Symbols.album_rounded)), + artists(Icon(Symbols.person_rounded)), + songs(Icon(Symbols.music_note_rounded)), + playlists(Icon(Symbols.playlist_play_rounded)); + + const LibraryTab(this.icon); + + final Widget icon; + + @override + toString() => name; +} + +class LibraryScreen extends HookConsumerWidget { const LibraryScreen({super.key}); @override - State createState() => _LibraryScreenState(); -} - -class _LibraryScreenState extends State - with SingleTickerProviderStateMixin { - late final TabController tabController; - - final iconSize = 26.0; - final tabHeight = 36.0; - - late final List<(String, Widget)> tabs = [ - ('Home', Icon(Symbols.home_rounded, size: iconSize)), - ('Albums', Icon(Symbols.album_rounded, size: iconSize)), - ('Artists', Icon(Symbols.person_rounded, size: iconSize)), - ('Songs', Icon(Symbols.music_note_rounded, size: iconSize)), - ('Playlists', Icon(Symbols.playlist_play_rounded, size: iconSize)), - ]; - - @override - void initState() { - super.initState(); - tabController = TabController( - length: tabs.length, + Widget build(BuildContext context, WidgetRef ref) { + final tabController = useTabController( + initialLength: LibraryTab.values.length, initialIndex: 1, - vsync: this, ); - } - @override - void dispose() { - tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return IconTheme( - data: IconThemeData( - fill: 1, - color: TextTheme.of(context).headlineLarge?.color, - weight: 600, - opticalSize: iconSize, - ), - child: Scaffold( - body: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context, - ), - sliver: SliverAppBar( - flexibleSpace: FlexibleSpaceBar( - collapseMode: CollapseMode.pin, - background: SafeArea( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 18, - vertical: 16, - ), - child: Text( - 'Albums', - style: TextTheme.of(context).headlineLarge?.copyWith( - fontWeight: FontWeight.w800, - ), - ), - ), + return Scaffold( + body: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: LibraryTabsHeader(tabController: tabController), + ), + ], + body: Builder( + builder: (context) => CustomScrollProvider( + tabController: tabController, + parent: PrimaryScrollController.of(context), + child: TabBarView( + controller: tabController, + children: LibraryTab.values + .map( + (tab) => TabScrollView( + index: LibraryTab.values.indexOf(tab), + sliver: switch (tab) { + LibraryTab.albums => AlbumsGrid(), + _ => ArtistsList(), + }, ), - ), - pinned: true, - floating: true, - bottom: PreferredSize( - preferredSize: Size.fromHeight(tabHeight + 18), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - TabBar( - controller: tabController, - dividerColor: Colors.transparent, - isScrollable: true, - tabAlignment: TabAlignment.start, - indicatorSize: TabBarIndicatorSize.label, - labelPadding: EdgeInsets.symmetric( - horizontal: 2, - ), - labelColor: Theme.of(context).primaryColorDark, - unselectedLabelColor: Theme.of( - context, - ).textTheme.headlineLarge?.color, - padding: EdgeInsets.symmetric( - // horizontal: 12, - vertical: 8, - ), - splashBorderRadius: BorderRadius.circular(8), - indicator: BoxDecoration( - color: Theme.of( - context, - ).primaryTextTheme.headlineLarge?.color, - borderRadius: BorderRadius.circular(8), - ), - tabs: tabs - .map( - (tab) => Tab( - height: tabHeight, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 6, - ), - child: tab.$2, - ), - ), - ) - .toList(), - ), - Row( - children: [ - SyncButton(), - IconButton( - onPressed: () { - context.push('/settings'); - }, - icon: Icon( - Symbols.settings_rounded, - ), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ]; - }, - body: Builder( - builder: (context) { - return CustomScrollProvider( - tabController: tabController, - parent: PrimaryScrollController.of(context), - child: TabBarView( - // These are the contents of the tab views, below the tabs. - controller: tabController, - children: tabs.map((tab) { - final index = tabs.indexOf(tab); - return SafeArea( - top: false, - bottom: false, - child: NewWidget(index: index, tab: tab), - ); - }).toList(), - ), - ); - }, + ) + .toList(), + ), ), ), ), @@ -175,43 +71,135 @@ class _LibraryScreenState extends State } } -class NewWidget extends StatefulWidget { - const NewWidget({ +class LibraryTabsHeader extends HookConsumerWidget { + const LibraryTabsHeader({ + super.key, + required this.tabController, + }); + + final TabController tabController; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + + return SliverAppBar( + pinned: true, + floating: true, + flexibleSpace: FlexibleSpaceBar( + collapseMode: CollapseMode.pin, + background: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 18, vertical: 16), + child: TabTitleText(tabController: tabController), + ), + ), + ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(kTabHeight + 18), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: IconTheme( + data: IconThemeData( + fill: 1, + color: theme.textTheme.headlineLarge?.color, + weight: 600, + opticalSize: kIconSize, + size: kIconSize, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TabBar( + controller: tabController, + dividerColor: Colors.transparent, + isScrollable: true, + tabAlignment: TabAlignment.start, + indicatorSize: TabBarIndicatorSize.label, + labelPadding: EdgeInsets.symmetric(horizontal: 2), + labelColor: theme.primaryColorDark, + unselectedLabelColor: theme.textTheme.headlineLarge?.color, + padding: EdgeInsets.symmetric(vertical: 8), + splashBorderRadius: BorderRadius.circular(8), + indicator: BoxDecoration( + color: theme.primaryTextTheme.headlineLarge?.color, + borderRadius: BorderRadius.circular(8), + ), + tabs: LibraryTab.values + .map( + (tab) => Tab( + height: kTabHeight, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: tab.icon, + ), + ), + ) + .toList(), + ), + Spacer(), + SyncButton(), + SettingsButton(), + ], + ), + ), + ), + ), + ); + } +} + +class TabTitleText extends HookConsumerWidget { + const TabTitleText({ + super.key, + required this.tabController, + }); + + final TabController tabController; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final tabText = useState(LibraryTab.home.toString()); + + useListenable(tabController); + useEffect(() { + tabText.value = LibraryTab.values[tabController.index].toString(); + return; + }, [tabController.index]); + + return Text( + tabText.value, + style: theme.textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.w800, + ), + ); + } +} + +class TabScrollView extends HookConsumerWidget { + const TabScrollView({ super.key, required this.index, - required this.tab, + required this.sliver, }); final int index; - final (String, Widget) tab; + final Widget sliver; @override - State createState() => _NewWidgetState(); -} - -class _NewWidgetState extends State - with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); + Widget build(BuildContext context, WidgetRef ref) { + useAutomaticKeepAlive(); final scrollProvider = CustomScrollProviderData.of(context); return CustomScrollView( - controller: scrollProvider.scrollControllers[widget.index], + controller: scrollProvider.scrollControllers[index], slivers: [ SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context, - ), - ), - SliverPadding( - padding: const EdgeInsets.all(8.0), - sliver: ArtistsList(), + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), + sliver, ], ); } @@ -232,3 +220,17 @@ class SyncButton extends HookConsumerWidget { ); } } + +class SettingsButton extends HookConsumerWidget { + const SettingsButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return IconButton( + icon: Icon(Symbols.settings_rounded), + onPressed: () { + context.push('/settings'); + }, + ); + } +}