refactor: use expanded sidebar tiles for library

This commit is contained in:
Kingkor Roy Tirtho 2025-01-23 20:34:04 +06:00
parent 2411f46877
commit 6e357230ac
21 changed files with 403 additions and 240 deletions

11
.vscode/launch.json vendored
View File

@ -30,6 +30,17 @@
"request": "launch", "request": "launch",
"program": "lib/main.dart", "program": "lib/main.dart",
"flutterMode": "release" "flutterMode": "release"
},
{
"name": "spotube (mobile) (release)",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"flutterMode": "release",
"args": [
"--flavor",
"dev"
]
} }
], ],
"compounds": [] "compounds": []

View File

@ -7,7 +7,11 @@ import 'package:go_router/go_router.dart';
import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/routes.dart';
import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/modules/player/player_controls.dart';
import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/library/library.dart'; import 'package:spotube/pages/library/user_albums.dart';
import 'package:spotube/pages/library/user_artists.dart';
import 'package:spotube/pages/library/user_downloads.dart';
import 'package:spotube/pages/library/user_local_tracks.dart';
import 'package:spotube/pages/library/user_playlists.dart';
import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/search/search.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart';
@ -52,8 +56,13 @@ class NavigationAction extends Action<NavigationIntent> {
enum HomeTabs { enum HomeTabs {
browse, browse,
search, search,
library,
lyrics, lyrics,
userPlaylists,
userArtists,
userAlbums,
userLocalLibrary,
userDownloads,
} }
class HomeTabIntent extends Intent { class HomeTabIntent extends Intent {
@ -73,12 +82,24 @@ class HomeTabAction extends Action<HomeTabIntent> {
case HomeTabs.search: case HomeTabs.search:
router.goNamed(SearchPage.name); router.goNamed(SearchPage.name);
break; break;
case HomeTabs.library:
router.goNamed(LibraryPage.name);
break;
case HomeTabs.lyrics: case HomeTabs.lyrics:
router.goNamed(LyricsPage.name); router.goNamed(LyricsPage.name);
break; break;
case HomeTabs.userPlaylists:
router.goNamed(UserPlaylistsPage.name);
break;
case HomeTabs.userArtists:
router.goNamed(UserArtistsPage.name);
break;
case HomeTabs.userAlbums:
router.goNamed(UserAlbumsPage.name);
break;
case HomeTabs.userLocalLibrary:
router.goNamed(UserLocalLibraryPage.name);
break;
case HomeTabs.userDownloads:
router.goNamed(UserDownloadsPage.name);
break;
} }
return null; return null;
} }

View File

@ -16,6 +16,11 @@ import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
import 'package:spotube/pages/library/local_folder.dart'; import 'package:spotube/pages/library/local_folder.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
import 'package:spotube/pages/library/user_albums.dart';
import 'package:spotube/pages/library/user_artists.dart';
import 'package:spotube/pages/library/user_downloads.dart';
import 'package:spotube/pages/library/user_local_tracks.dart';
import 'package:spotube/pages/library/user_playlists.dart';
import 'package:spotube/pages/lyrics/mini_lyrics.dart'; import 'package:spotube/pages/lyrics/mini_lyrics.dart';
import 'package:spotube/pages/playlist/liked_playlist.dart'; import 'package:spotube/pages/playlist/liked_playlist.dart';
import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/pages/playlist/playlist.dart';
@ -99,45 +104,73 @@ final routerProvider = Provider((ref) {
pageBuilder: (context, state) => pageBuilder: (context, state) =>
const SpotubePage(child: SearchPage()), const SpotubePage(child: SearchPage()),
), ),
ShellRoute(
pageBuilder: (context, state, child) =>
SpotubePage(child: LibraryPage(child: child)),
routes: [
GoRoute(
path: "/library/playlists",
name: UserPlaylistsPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: UserPlaylistsPage()),
),
GoRoute(
path: "/library/artists",
name: UserArtistsPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: UserArtistsPage()),
),
GoRoute(
path: "/library/album",
name: UserAlbumsPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: UserAlbumsPage()),
),
GoRoute(
path: "/library/local",
name: UserLocalLibraryPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: UserLocalLibraryPage()),
),
GoRoute(
path: "/library/downloads",
name: UserDownloadsPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: UserDownloadsPage()),
),
],
),
GoRoute( GoRoute(
path: "/library", path: "/library/generate",
name: LibraryPage.name, name: PlaylistGeneratorPage.name,
pageBuilder: (context, state) => pageBuilder: (context, state) =>
const SpotubePage(child: LibraryPage()), const SpotubePage(child: PlaylistGeneratorPage()),
routes: [ routes: [
GoRoute( GoRoute(
path: "generate", path: "result",
name: PlaylistGeneratorPage.name, name: PlaylistGenerateResultPage.name,
pageBuilder: (context, state) => pageBuilder: (context, state) => SpotubePage(
const SpotubePage(child: PlaylistGeneratorPage()), child: PlaylistGenerateResultPage(
routes: [ state: state.extra as GeneratePlaylistProviderInput,
GoRoute( ),
path: "result",
name: PlaylistGenerateResultPage.name,
pageBuilder: (context, state) => SpotubePage(
child: PlaylistGenerateResultPage(
state: state.extra as GeneratePlaylistProviderInput,
),
),
)
],
), ),
GoRoute( )
path: "local", ],
name: LocalLibraryPage.name, ),
pageBuilder: (context, state) { GoRoute(
assert(state.extra is String); path: "/library/local",
return SpotubePage( name: LocalLibraryPage.name,
child: LocalLibraryPage( pageBuilder: (context, state) {
state.extra as String, assert(state.extra is String);
isDownloads: return SpotubePage(
state.uri.queryParameters["downloads"] != null, child: LocalLibraryPage(
isCache: state.uri.queryParameters["cache"] != null, state.extra as String,
), isDownloads: state.uri.queryParameters["downloads"] != null,
); isCache: state.uri.queryParameters["cache"] != null,
},
), ),
]), );
},
),
GoRoute( GoRoute(
path: "/lyrics", path: "/lyrics",
name: LyricsPage.name, name: LyricsPage.name,

View File

@ -2,7 +2,10 @@ import 'package:flutter/material.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/library/library.dart'; import 'package:spotube/pages/library/user_albums.dart';
import 'package:spotube/pages/library/user_artists.dart';
import 'package:spotube/pages/library/user_local_tracks.dart';
import 'package:spotube/pages/library/user_playlists.dart';
import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/stats/stats.dart'; import 'package:spotube/pages/stats/stats.dart';
@ -34,12 +37,6 @@ List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
icon: SpotubeIcons.search, icon: SpotubeIcons.search,
title: l10n.search, title: l10n.search,
), ),
SideBarTiles(
id: "library",
name: LibraryPage.name,
icon: SpotubeIcons.library,
title: l10n.library,
),
SideBarTiles( SideBarTiles(
id: "lyrics", id: "lyrics",
name: LyricsPage.name, name: LyricsPage.name,
@ -54,6 +51,33 @@ List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
), ),
]; ];
List<SideBarTiles> getSidebarLibraryTileList(AppLocalizations l10n) => [
SideBarTiles(
id: "playlists",
title: l10n.playlists,
name: UserPlaylistsPage.name,
icon: SpotubeIcons.playlist,
),
SideBarTiles(
id: "artists",
title: l10n.artists,
name: UserArtistsPage.name,
icon: SpotubeIcons.artist,
),
SideBarTiles(
id: "albums",
title: l10n.albums,
name: UserAlbumsPage.name,
icon: SpotubeIcons.album,
),
SideBarTiles(
id: "local_library",
title: l10n.local_library,
name: UserLocalLibraryPage.name,
icon: SpotubeIcons.device,
),
];
List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [ List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
SideBarTiles( SideBarTiles(
id: "browse", id: "browse",
@ -69,7 +93,7 @@ List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
), ),
SideBarTiles( SideBarTiles(
id: "library", id: "library",
name: LibraryPage.name, name: UserPlaylistsPage.name,
icon: SpotubeIcons.library, icon: SpotubeIcons.library,
title: l10n.library, title: l10n.library,
), ),

View File

@ -37,6 +37,7 @@ abstract class SpotubeIcons {
static const share = FeatherIcons.share2; static const share = FeatherIcons.share2;
static const playlistAdd = Icons.playlist_add_rounded; static const playlistAdd = Icons.playlist_add_rounded;
static const playlistRemove = Icons.playlist_remove_rounded; static const playlistRemove = Icons.playlist_remove_rounded;
static const playlist = Icons.playlist_play_rounded;
static const trash = FeatherIcons.trash2; static const trash = FeatherIcons.trash2;
static const clock = FeatherIcons.clock; static const clock = FeatherIcons.clock;
static const lyrics = Icons.lyrics_rounded; static const lyrics = Icons.lyrics_rounded;
@ -132,4 +133,5 @@ abstract class SpotubeIcons {
static const radioUnchecked = Icons.radio_button_off_rounded; static const radioUnchecked = Icons.radio_button_off_rounded;
static const grid = FeatherIcons.grid; static const grid = FeatherIcons.grid;
static const list = FeatherIcons.list; static const list = FeatherIcons.list;
static const device = FeatherIcons.smartphone;
} }

View File

@ -2,7 +2,7 @@ import 'package:collection/collection.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:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/pages/library/user_local_tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';

View File

@ -1,6 +1,6 @@
import 'package:shadcn_flutter/shadcn_flutter.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/user_local_tracks.dart'; import 'package:spotube/pages/library/user_local_tracks.dart';
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';

View File

@ -264,12 +264,32 @@ class Spotube extends HookConsumerWidget {
LogicalKeyboardKey.digit3, LogicalKeyboardKey.digit3,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.library), ): HomeTabIntent(ref, tab: HomeTabs.lyrics),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.digit4, LogicalKeyboardKey.digit4,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.lyrics), ): HomeTabIntent(ref, tab: HomeTabs.userPlaylists),
LogicalKeySet(
LogicalKeyboardKey.digit5,
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.userArtists),
LogicalKeySet(
LogicalKeyboardKey.digit6,
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.userAlbums),
LogicalKeySet(
LogicalKeyboardKey.digit7,
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.userLocalLibrary),
LogicalKeySet(
LogicalKeyboardKey.digit8,
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.userDownloads),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.keyW, LogicalKeyboardKey.keyW,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,

View File

@ -1,6 +1,7 @@
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.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/connect/connect.dart'; import 'package:spotube/pages/connect/connect.dart';
import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/connect/clients.dart';
@ -19,6 +20,17 @@ class ConnectDeviceButton extends HookConsumerWidget {
connectClients.asData?.value.services.isNotEmpty == true; connectClients.asData?.value.services.isNotEmpty == true;
if (_sidebar) { if (_sidebar) {
final mediaQuery = MediaQuery.sizeOf(context);
if (mediaQuery.mdAndDown) {
return IconButton.ghost(
icon: const Icon(SpotubeIcons.speaker),
onPressed: () {
ServiceUtils.pushNamed(context, ConnectPage.name);
},
);
}
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: Button.primary( child: Button.primary(

View File

@ -3,7 +3,6 @@ import 'package:flutter_hooks/flutter_hooks.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:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/side_bar_tiles.dart';
@ -14,6 +13,7 @@ import 'package:spotube/models/database/database.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/modules/connect/connect_device.dart'; import 'package:spotube/modules/connect/connect_device.dart';
import 'package:spotube/pages/library/user_downloads.dart';
import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/pages/profile/profile.dart';
import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
@ -47,8 +47,6 @@ class Sidebar extends HookConsumerWidget {
final routerState = GoRouterState.of(context); final routerState = GoRouterState.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
final layoutMode = final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
@ -57,7 +55,14 @@ class Sidebar extends HookConsumerWidget {
[context.l10n], [context.l10n],
); );
final selectedIndex = sidebarTileList.indexWhere( final sidebarLibraryTileList = useMemoized(
() => getSidebarLibraryTileList(context.l10n),
[context.l10n],
);
final tileList = [...sidebarTileList, ...sidebarLibraryTileList];
final selectedIndex = tileList.indexWhere(
(e) => routerState.namedLocation(e.name) == routerState.matchedLocation, (e) => routerState.namedLocation(e.name) == routerState.matchedLocation,
); );
@ -73,16 +78,8 @@ class Sidebar extends HookConsumerWidget {
for (final tile in sidebarTileList) for (final tile in sidebarTileList)
NavigationButton( NavigationButton(
label: mediaQuery.lgAndUp ? Text(tile.title) : null, label: mediaQuery.lgAndUp ? Text(tile.title) : null,
child: Badge( child: Tooltip(
backgroundColor: context.theme.colorScheme.primary, tooltip: TooltipContainer(child: Text(tile.title)),
isLabelVisible: tile.title == "Library" && downloadCount > 0,
label: Text(
downloadCount.toString(),
style: TextStyle(
color: context.theme.colorScheme.primaryForeground,
fontSize: 10,
),
),
child: Icon(tile.icon), child: Icon(tile.icon),
), ),
onChanged: (value) { onChanged: (value) {
@ -91,6 +88,22 @@ class Sidebar extends HookConsumerWidget {
} }
}, },
), ),
const NavigationDivider(),
if (mediaQuery.lgAndUp)
NavigationLabel(child: Text(context.l10n.library)),
for (final tile in sidebarLibraryTileList)
NavigationButton(
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
onChanged: (value) {
if (value) {
context.goNamed(tile.name);
}
},
child: Tooltip(
tooltip: TooltipContainer(child: Text(tile.title)),
child: Icon(tile.icon),
),
),
]; ];
return Row( return Row(
@ -103,7 +116,7 @@ class Sidebar extends HookConsumerWidget {
? NavigationSidebar( ? NavigationSidebar(
index: selectedIndex, index: selectedIndex,
onSelected: (index) { onSelected: (index) {
final tile = sidebarTileList[index]; final tile = tileList[index];
context.goNamed(tile.name); context.goNamed(tile.name);
}, },
children: navigationButtons, children: navigationButtons,
@ -112,7 +125,7 @@ class Sidebar extends HookConsumerWidget {
alignment: NavigationRailAlignment.start, alignment: NavigationRailAlignment.start,
index: selectedIndex, index: selectedIndex,
onSelected: (index) { onSelected: (index) {
final tile = sidebarTileList[index]; final tile = tileList[index];
context.goNamed(tile.name); context.goNamed(tile.name);
}, },
children: navigationButtons, children: navigationButtons,
@ -138,8 +151,10 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final me = ref.watch(meProvider); final routerState = GoRouterState.of(context);
final data = me.asData?.value; final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
final userSnapshot = ref.watch(meProvider);
final data = userSnapshot.asData?.value;
final avatarImg = (data?.images).asUrlString( final avatarImg = (data?.images).asUrlString(
index: (data?.images?.length ?? 1) - 1, index: (data?.images?.length ?? 1) - 1,
@ -149,10 +164,30 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
if (mediaQuery.mdAndDown) { if (mediaQuery.mdAndDown) {
return IconButton( return Column(
variance: ButtonVariance.ghost, mainAxisSize: MainAxisSize.min,
icon: const Icon(SpotubeIcons.settings), spacing: 10,
onPressed: () => ServiceUtils.navigateNamed(context, SettingsPage.name), children: [
Badge(
isLabelVisible: downloadCount > 0,
label: Text(downloadCount.toString()),
child: IconButton(
variance: routerState.topRoute?.name == UserDownloadsPage.name
? ButtonVariance.secondary
: ButtonVariance.ghost,
icon: const Icon(SpotubeIcons.download),
onPressed: () =>
ServiceUtils.navigateNamed(context, UserDownloadsPage.name),
),
),
const ConnectDeviceButton.sidebar(),
IconButton(
variance: ButtonVariance.ghost,
icon: const Icon(SpotubeIcons.settings),
onPressed: () =>
ServiceUtils.navigateNamed(context, SettingsPage.name),
),
],
); );
} }
@ -161,9 +196,27 @@ class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
width: 180, width: 180,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [ children: [
SizedBox(
width: double.infinity,
child: Button(
style: routerState.topRoute?.name == UserDownloadsPage.name
? ButtonVariance.secondary
: ButtonVariance.outline,
onPressed: () {
ServiceUtils.navigateNamed(context, UserDownloadsPage.name);
},
leading: const Icon(SpotubeIcons.download),
trailing: downloadCount > 0
? PrimaryBadge(
child: Text(downloadCount.toString()),
)
: null,
child: Text(context.l10n.downloads),
),
),
const ConnectDeviceButton.sidebar(), const ConnectDeviceButton.sidebar(),
const Gap(10),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,

View File

@ -34,32 +34,35 @@ class GettingStarting extends HookConsumerWidget {
return Scaffold( return Scaffold(
headers: [ headers: [
TitleBar( SafeArea(
backgroundColor: Colors.transparent, child: TitleBar(
surfaceBlur: 0, backgroundColor: Colors.transparent,
trailing: [ surfaceBlur: 0,
ListenableBuilder( trailing: [
listenable: pageController, ListenableBuilder(
builder: (context, _) { listenable: pageController,
return AnimatedSwitcher( builder: (context, _) {
duration: const Duration(milliseconds: 300), return AnimatedSwitcher(
child: pageController.hasClients && duration: const Duration(milliseconds: 300),
(pageController.page == 0 || pageController.page == 3) child: pageController.hasClients &&
? const SizedBox() (pageController.page == 0 ||
: Button.secondary( pageController.page == 3)
onPressed: () { ? const SizedBox()
pageController.animateToPage( : Button.secondary(
3, onPressed: () {
duration: const Duration(milliseconds: 300), pageController.animateToPage(
curve: Curves.easeInOut, 3,
); duration: const Duration(milliseconds: 300),
}, curve: Curves.easeInOut,
child: Text(context.l10n.skip_this_nonsense), );
), },
); child: Text(context.l10n.skip_this_nonsense),
}, ),
), );
], },
),
],
),
), ),
], ],
floatingHeader: true, floatingHeader: true,

View File

@ -1,71 +1,63 @@
import 'package:flutter/material.dart' show Badge; import 'package:flutter/material.dart' show Badge;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.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.dart';
import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/modules/library/user_albums.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/modules/library/user_artists.dart';
import 'package:spotube/modules/library/user_downloads.dart';
import 'package:spotube/modules/library/user_playlists.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
class LibraryPage extends HookConsumerWidget { class LibraryPage extends HookConsumerWidget {
static const name = "library"; final Widget child;
const LibraryPage({super.key, required this.child});
const LibraryPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final downloadingCount = ref.watch(downloadManagerProvider).$downloadCount; final downloadingCount = ref.watch(downloadManagerProvider).$downloadCount;
final index = useState(0); final routerState = GoRouterState.of(context);
final sidebarLibraryTileList = useMemoized(
final children = [ () => getSidebarLibraryTileList(context.l10n),
Text(context.l10n.playlists), [context.l10n],
Text(context.l10n.local_tab), );
Badge( final index = sidebarLibraryTileList.indexWhere(
isLabelVisible: downloadingCount > 0, (e) => routerState.namedLocation(e.name) == routerState.matchedLocation,
label: Text(downloadingCount.toString()), );
child: Text(context.l10n.downloads),
),
Text(context.l10n.artists),
Text(context.l10n.albums),
];
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: LayoutBuilder(builder: (context, constraints) {
headers: [ return Scaffold(
TitleBar( headers: [
child: SingleChildScrollView( if (constraints.smAndDown)
scrollDirection: Axis.horizontal, TitleBar(
child: TabList( child: SingleChildScrollView(
index: index.value, scrollDirection: Axis.horizontal,
children: [ child: TabList(
for (final child in children) index: index,
TabButton( children: [
child: child, for (final tile in sidebarLibraryTileList)
onPressed: () { TabButton(
index.value = children.indexOf(child); child: Badge(
}, isLabelVisible:
), tile.id == 'downloads' && downloadingCount > 0,
], label: Text(downloadingCount.toString()),
child: Text(tile.title),
),
onPressed: () {
context.goNamed(tile.name);
},
),
],
),
),
), ),
), const Gap(10),
),
const Gap(10),
],
child: IndexedStack(
index: index.value,
children: const [
UserPlaylists(),
UserLocalTracks(),
UserDownloads(),
UserArtists(),
UserAlbums(),
], ],
), child: child,
), );
}),
); );
} }
} }

View File

@ -16,9 +16,8 @@ import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/extensions/constrains.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/pages/library/user_local_tracks.dart';
import 'package:spotube/components/expandable_search/expandable_search.dart'; import 'package:spotube/components/expandable_search/expandable_search.dart';
import 'package:spotube/components/fallbacks/not_found.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/track_presentation/sort_tracks_dropdown.dart'; import 'package:spotube/components/track_presentation/sort_tracks_dropdown.dart';

View File

@ -16,8 +16,9 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class UserAlbums extends HookConsumerWidget { class UserAlbumsPage extends HookConsumerWidget {
const UserAlbums({super.key}); static const name = 'user_albums';
const UserAlbumsPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -19,8 +19,9 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class UserArtists extends HookConsumerWidget { class UserArtistsPage extends HookConsumerWidget {
const UserArtists({super.key}); static const name = 'user_artists';
const UserArtistsPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -6,8 +6,9 @@ import 'package:spotube/modules/library/user_downloads/download_item.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
class UserDownloads extends HookConsumerWidget { class UserDownloadsPage extends HookConsumerWidget {
const UserDownloads({super.key}); static const name = 'user_downloads';
const UserDownloadsPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -24,8 +24,9 @@ enum SortBy {
album, album,
} }
class UserLocalTracks extends HookConsumerWidget { class UserLocalLibraryPage extends HookConsumerWidget {
const UserLocalTracks({super.key}); static const name = 'user_local_library';
const UserLocalLibraryPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -20,8 +20,9 @@ import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class UserPlaylists extends HookConsumerWidget { class UserPlaylistsPage extends HookConsumerWidget {
const UserPlaylists({super.key}); static const name = 'user_playlists';
const UserPlaylistsPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -8,7 +8,6 @@ import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.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/hooks/utils/use_custom_status_bar_color.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart';
@ -33,15 +32,8 @@ class LyricsPage extends HookConsumerWidget {
[playlist.activeTrack?.album?.images], [playlist.activeTrack?.album?.images],
); );
final palette = usePaletteColor(albumArt, ref); final palette = usePaletteColor(albumArt, ref);
final route = ModalRoute.of(context);
final selectedIndex = useState(0); final selectedIndex = useState(0);
final resetStatusBar = useCustomStatusBarColor(
palette.color,
route?.isCurrent ?? false,
noSetBGColor: true,
);
Widget tabbar = Padding( Widget tabbar = Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: isModal child: isModal
@ -93,51 +85,47 @@ class LyricsPage extends HookConsumerWidget {
); );
if (isModal) { if (isModal) {
return PopScope( return SafeArea(
canPop: true, bottom: false,
onPopInvokedWithResult: (_, __) => resetStatusBar(), child: SurfaceCard(
child: SafeArea( surfaceBlur: context.theme.surfaceBlur,
bottom: false, surfaceOpacity: context.theme.surfaceOpacity,
child: SurfaceCard( padding: EdgeInsets.zero,
surfaceBlur: context.theme.surfaceBlur, borderRadius: BorderRadius.zero,
surfaceOpacity: context.theme.surfaceOpacity, borderWidth: 0,
padding: EdgeInsets.zero, child: Column(
borderRadius: BorderRadius.zero, children: [
borderWidth: 0, const SizedBox(height: 20),
child: Column( Container(
children: [ height: 7,
const SizedBox(height: 5), width: 150,
Container( decoration: BoxDecoration(
height: 7, color: palette.titleTextColor,
width: 150, borderRadius: BorderRadius.circular(10),
decoration: BoxDecoration(
color: palette.titleTextColor,
borderRadius: BorderRadius.circular(10),
),
), ),
Row( ),
Row(
children: [
Expanded(
child: tabbar,
),
IconButton.ghost(
icon: const Icon(SpotubeIcons.minimize),
onPressed: () => Navigator.of(context).pop(),
),
const SizedBox(width: 5),
],
),
Expanded(
child: IndexedStack(
index: selectedIndex.value,
children: [ children: [
Expanded( SyncedLyrics(palette: palette, isModal: isModal),
child: tabbar, PlainLyrics(palette: palette, isModal: isModal),
),
IconButton.ghost(
icon: const Icon(SpotubeIcons.minimize),
onPressed: () => Navigator.of(context).pop(),
),
const SizedBox(width: 5),
], ],
), ),
Expanded( ),
child: IndexedStack( ],
index: selectedIndex.value,
children: [
SyncedLyrics(palette: palette, isModal: isModal),
PlainLyrics(palette: palette, isModal: isModal),
],
),
),
],
),
), ),
), ),
); );

View File

@ -6,7 +6,7 @@ import 'package:go_router/go_router.dart';
import 'package:html/dom.dart' hide Text; import 'package:html/dom.dart' hide Text;
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Element; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Element;
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/pages/library/user_local_tracks.dart';
import 'package:spotube/modules/root/update_dialog.dart'; import 'package:spotube/modules/root/update_dialog.dart';
import 'package:spotube/models/lyrics.dart'; import 'package:spotube/models/lyrics.dart';

View File

@ -168,35 +168,35 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468
audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9 audio_service: 0d9e4e25347bb3efb768f3b9f005911a81e587a7
audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 audio_session: 48ab6500f7a5e7c64363e206565a5dfe5a0c1441
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842 bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 connectivity_plus: 2256d3e20624a7749ed21653aafe291a46446fee
desktop_webview_window: 89bb3d691f4c80314a10be312f4cd35db93a9d5a desktop_webview_window: 2f0cdefecc06e21208a51589bd3d1580a87a703c
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
flutter_discord_rpc: 67a7c10ea24d9d3bf35d01af643f48fbcfa7c24f flutter_discord_rpc: 90614fcca26f3cebfd33263557ea7875936d184b
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 flutter_secure_storage_macos: b2d62a774c23b060f0b99d0173b0b36abb4a8632
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
media_kit_libs_macos_audio: 3871782a4f3f84c77f04d7666c87800a781c24da media_kit_libs_macos_audio: 06f3cf88d6d89c7c3c87eae57689d1c6adb335b2
media_kit_native_event_loop: 7321675377cb9ae8596a29bddf3a3d2b5e8792c5 media_kit_native_event_loop: a5833d1e4d5bedb6f691e9909fa57f15f436f2c8
metadata_god: 829f61208b44ac1173e7cd32ab740d8776be5435 metadata_god: 8029e6ff4b1400ae4f13c38d2c478e8633f0e58b
open_file_mac: 0e554648e2a87ce59e9438e3e5ca3e552e90d89a open_file_mac: 01874b6d6a2c1485ac9b126d7105b99102dea2cf
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c package_info_plus: a8a591e70e87ce97ce5d21b2594f69cea9e0312f
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 7559e33dae4c78538df563795af3a86fc887ee71 sqlite3: 7559e33dae4c78538df563795af3a86fc887ee71
sqlite3_flutter_libs: 1b4e98da20ebd4e9b1240269b78cdcf492dbe9f3 sqlite3_flutter_libs: f0b59f6bb2a18597d0796558725007e5a7428397
system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc system_theme: ed74293ad07d3a05e3e2d0059ff342360346f1a0
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0 PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0