Merge pull request #2291 from KRTirtho/migrate/auto-routes

refactor: migrate to auto_route from go_router
This commit is contained in:
Kingkor Roy Tirtho 2025-02-01 17:22:51 +06:00 committed by GitHub
commit cd39bbf87c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
89 changed files with 3852 additions and 2888 deletions

View File

@ -4,6 +4,16 @@ targets:
exclude: exclude:
- bin/*.dart - bin/*.dart
builders: builders:
auto_route_generator:auto_route_generator: # this for @RoutePage
options:
enable_cached_builds: true
generate_for:
- lib/pages/**/*.dart
auto_route_generator:auto_router_generator: # this for @AutoRouterConfig
options:
enable_cached_builds: true
generate_for:
- lib/collections/routes.dart
json_serializable: json_serializable:
options: options:
any_map: true any_map: true

View File

@ -3,17 +3,9 @@ import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/routes.gr.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/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/user_local_tracks.dart';
import 'package:spotube/pages/library/user_playlists.dart';
import 'package:spotube/pages/lyrics/lyrics.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';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -40,7 +32,7 @@ class PlayPauseAction extends Action<PlayPauseIntent> {
} }
class NavigationIntent extends Intent { class NavigationIntent extends Intent {
final GoRouter router; final AppRouter router;
final String path; final String path;
const NavigationIntent(this.router, this.path); const NavigationIntent(this.router, this.path);
} }
@ -48,7 +40,7 @@ class NavigationIntent extends Intent {
class NavigationAction extends Action<NavigationIntent> { class NavigationAction extends Action<NavigationIntent> {
@override @override
invoke(intent) { invoke(intent) {
intent.router.go(intent.path); intent.router.navigateNamed(intent.path);
return null; return null;
} }
} }
@ -66,39 +58,39 @@ enum HomeTabs {
} }
class HomeTabIntent extends Intent { class HomeTabIntent extends Intent {
final WidgetRef ref; final AppRouter router;
final HomeTabs tab; final HomeTabs tab;
const HomeTabIntent(this.ref, {required this.tab}); const HomeTabIntent(this.router, {required this.tab});
} }
class HomeTabAction extends Action<HomeTabIntent> { class HomeTabAction extends Action<HomeTabIntent> {
@override @override
invoke(intent) { invoke(intent) {
final router = intent.ref.read(routerProvider); final router = intent.router;
switch (intent.tab) { switch (intent.tab) {
case HomeTabs.browse: case HomeTabs.browse:
router.goNamed(HomePage.name); router.navigate(const HomeRoute());
break; break;
case HomeTabs.search: case HomeTabs.search:
router.goNamed(SearchPage.name); router.navigate(const SearchRoute());
break; break;
case HomeTabs.lyrics: case HomeTabs.lyrics:
router.goNamed(LyricsPage.name); router.navigate(LyricsRoute());
break; break;
case HomeTabs.userPlaylists: case HomeTabs.userPlaylists:
router.goNamed(UserPlaylistsPage.name); router.navigate(const UserPlaylistsRoute());
break; break;
case HomeTabs.userArtists: case HomeTabs.userArtists:
router.goNamed(UserArtistsPage.name); router.navigate(const UserArtistsRoute());
break; break;
case HomeTabs.userAlbums: case HomeTabs.userAlbums:
router.goNamed(UserAlbumsPage.name); router.navigate(const UserAlbumsRoute());
break; break;
case HomeTabs.userLocalLibrary: case HomeTabs.userLocalLibrary:
router.goNamed(UserLocalLibraryPage.name); router.navigate(const UserLocalLibraryRoute());
break; break;
case HomeTabs.userDownloads: case HomeTabs.userDownloads:
router.goNamed(UserDownloadsPage.name); router.navigate(const UserDownloadsRoute());
break; break;
} }
return null; return null;

View File

@ -1,365 +1,216 @@
import 'package:flutter/foundation.dart' hide Category; import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/foundation.dart';
import 'package:go_router/go_router.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Search; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/pages/album/album.dart';
import 'package:spotube/pages/connect/connect.dart';
import 'package:spotube/pages/connect/control/control.dart';
import 'package:spotube/pages/getting_started/getting_started.dart';
import 'package:spotube/pages/home/feed/feed_section.dart';
import 'package:spotube/pages/home/genres/genre_playlists.dart';
import 'package:spotube/pages/home/genres/genres.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
import 'package:spotube/pages/library/user_local_tracks/local_folder.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/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/user_local_tracks.dart';
import 'package:spotube/pages/library/user_playlists.dart';
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
import 'package:spotube/pages/playlist/liked_playlist.dart';
import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/pages/profile/profile.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart';
import 'package:spotube/pages/settings/logs.dart';
import 'package:spotube/pages/stats/albums/albums.dart';
import 'package:spotube/pages/stats/artists/artists.dart';
import 'package:spotube/pages/stats/fees/fees.dart';
import 'package:spotube/pages/stats/minutes/minutes.dart';
import 'package:spotube/pages/stats/playlists/playlists.dart';
import 'package:spotube/pages/stats/stats.dart';
import 'package:spotube/pages/stats/streams/streams.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/components/spotube_page_route.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/pages/library/library.dart';
import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/root/root_app.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart';
final rootNavigatorKey = GlobalKey<NavigatorState>(); final rootNavigatorKey = GlobalKey<NavigatorState>();
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
final routerProvider = Provider((ref) {
return GoRouter(
navigatorKey: rootNavigatorKey,
routes: [
ShellRoute(
navigatorKey: shellRouteNavigatorKey,
builder: (context, state, child) => RootApp(child: child),
routes: [
GoRoute(
path: "/",
name: HomePage.name,
redirect: (context, state) async {
final auth = await ref.read(authenticationProvider.future);
if (auth == null && !KVStoreService.doneGettingStarted) { @AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route')
return "/getting-started"; class AppRouter extends RootStackRouter {
} final WidgetRef ref;
return null; AppRouter(this.ref) : super(navigatorKey: rootNavigatorKey);
},
pageBuilder: (context, state) => @override
const SpotubePage(child: HomePage()), List<AutoRouteGuard> get guards => [
routes: [ AutoRouteGuardCallback(
GoRoute( (resolver, router) async {
path: "genres", final auth = await ref.read(authenticationProvider.future);
name: GenrePage.name,
pageBuilder: (context, state) => if (auth == null && !KVStoreService.doneGettingStarted) {
const SpotubePage(child: GenrePage()), resolver.redirect(const GettingStartedRoute());
), } else {
GoRoute( resolver.next(true);
path: "genre/:categoryId", }
name: GenrePlaylistsPage.name, },
pageBuilder: (context, state) => SpotubePage(
child: GenrePlaylistsPage(
category: state.extra as Category,
),
),
),
GoRoute(
path: "feeds/:feedId",
name: HomeFeedSectionPage.name,
pageBuilder: (context, state) => SpotubePage(
child: HomeFeedSectionPage(
sectionUri: state.pathParameters["feedId"] as String,
),
),
)
],
),
GoRoute(
path: "/search",
name: SearchPage.name,
pageBuilder: (context, state) =>
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()),
routes: [
GoRoute(
path: "folder",
name: LocalLibraryPage.name,
parentNavigatorKey: shellRouteNavigatorKey,
pageBuilder: (context, state) {
assert(state.extra is String);
return SpotubePage(
child: LocalLibraryPage(
state.extra as String,
isDownloads:
state.uri.queryParameters["downloads"] != null,
isCache: state.uri.queryParameters["cache"] != null,
),
);
},
),
]),
GoRoute(
path: "/library/downloads",
name: UserDownloadsPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: UserDownloadsPage()),
),
],
),
GoRoute(
path: "/library/generate",
name: PlaylistGeneratorPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: PlaylistGeneratorPage()),
routes: [
GoRoute(
path: "result",
name: PlaylistGenerateResultPage.name,
pageBuilder: (context, state) => SpotubePage(
child: PlaylistGenerateResultPage(
state: state.extra as GeneratePlaylistProviderInput,
),
),
)
],
),
GoRoute(
path: "/lyrics",
name: LyricsPage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: LyricsPage()),
),
GoRoute(
path: "/settings",
name: SettingsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: SettingsPage(),
),
routes: [
GoRoute(
path: "blacklist",
name: BlackListPage.name,
pageBuilder: (context, state) => SpotubeSlidePage(
child: const BlackListPage(),
),
),
if (!kIsWeb)
GoRoute(
path: "logs",
name: LogsPage.name,
pageBuilder: (context, state) => SpotubeSlidePage(
child: const LogsPage(),
),
),
GoRoute(
path: "about",
name: AboutSpotube.name,
pageBuilder: (context, state) => SpotubeSlidePage(
child: const AboutSpotube(),
),
),
],
),
GoRoute(
path: "/album/:id",
name: AlbumPage.name,
pageBuilder: (context, state) {
assert(state.extra is AlbumSimple);
return SpotubePage(
child: AlbumPage(album: state.extra as AlbumSimple),
);
},
),
GoRoute(
path: "/artist/:id",
name: ArtistPage.name,
pageBuilder: (context, state) {
assert(state.pathParameters["id"] != null);
return SpotubePage(
child: ArtistPage(state.pathParameters["id"]!));
},
),
GoRoute(
path: "/playlist/:id",
name: PlaylistPage.name,
pageBuilder: (context, state) {
assert(state.extra is PlaylistSimple);
return SpotubePage(
child: state.pathParameters["id"] == "user-liked-tracks"
? LikedPlaylistPage(playlist: state.extra as PlaylistSimple)
: PlaylistPage(playlist: state.extra as PlaylistSimple),
);
},
),
GoRoute(
path: "/track/:id",
name: TrackPage.name,
pageBuilder: (context, state) {
final id = state.pathParameters["id"]!;
return SpotubePage(
child: TrackPage(trackId: id),
);
},
),
GoRoute(
path: "/connect",
name: ConnectPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: ConnectPage(),
),
routes: [
GoRoute(
path: "control",
name: ConnectControlPage.name,
pageBuilder: (context, state) {
return const SpotubePage(
child: ConnectControlPage(),
);
},
)
],
),
GoRoute(
path: "/profile",
name: ProfilePage.name,
pageBuilder: (context, state) =>
const SpotubePage(child: ProfilePage()),
),
GoRoute(
path: "/stats",
name: StatsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsPage(),
),
routes: [
GoRoute(
path: "minutes",
name: StatsMinutesPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsMinutesPage(),
),
),
GoRoute(
path: "streams",
name: StatsStreamsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsStreamsPage(),
),
),
GoRoute(
path: "fees",
name: StatsStreamFeesPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsStreamFeesPage(),
),
),
GoRoute(
path: "artists",
name: StatsArtistsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsArtistsPage(),
),
),
GoRoute(
path: "albums",
name: StatsAlbumsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsAlbumsPage(),
),
),
GoRoute(
path: "playlists",
name: StatsPlaylistsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsPlaylistsPage(),
),
),
],
)
],
),
GoRoute(
path: "/mini-player",
name: MiniLyricsPage.name,
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => SpotubePage(
child: MiniLyricsPage(prevSize: state.extra as Size),
), ),
), ];
GoRoute(
path: "/getting-started", @override
name: GettingStarting.name, List<AutoRoute> get routes => [
parentNavigatorKey: rootNavigatorKey, AutoRoute(
pageBuilder: (context, state) => const SpotubePage( page: RootAppRoute.page,
child: GettingStarting(), path: "/",
initial: true,
children: [
AutoRoute(
path: "home",
page: HomeRoute.page,
initial: true,
),
AutoRoute(
path: "home/genres",
page: GenreRoute.page,
),
AutoRoute(
path: "home/genre/:categoryId",
page: GenrePlaylistsRoute.page,
),
AutoRoute(
path: "home/feeds/:feedId",
page: HomeFeedSectionRoute.page,
),
AutoRoute(
path: "search",
page: SearchRoute.page,
),
AutoRoute(
path: "library",
page: LibraryRoute.page,
children: [
AutoRoute(
path: "playlists",
page: UserPlaylistsRoute.page,
),
AutoRoute(
path: "artists",
page: UserArtistsRoute.page,
),
AutoRoute(
path: "albums",
page: UserAlbumsRoute.page,
),
AutoRoute(
path: "local",
page: UserLocalLibraryRoute.page,
),
AutoRoute(
path: "local/folder",
page: LocalLibraryRoute.page,
// parentNavigatorKey: shellRouteNavigatorKey,
),
AutoRoute(
path: "downloads",
page: UserDownloadsRoute.page,
),
],
),
AutoRoute(
path: "library/generate",
page: PlaylistGeneratorRoute.page,
),
AutoRoute(
path: "library/generate/result",
page: PlaylistGenerateResultRoute.page,
),
AutoRoute(
path: "lyrics",
page: LyricsRoute.page,
),
AutoRoute(
path: "settings",
page: SettingsRoute.page,
),
AutoRoute(
path: "settings/blacklist",
page: BlackListRoute.page,
),
if (!kIsWeb)
AutoRoute(
path: "settings/logs",
page: LogsRoute.page,
),
AutoRoute(
path: "settings/about",
page: AboutSpotubeRoute.page,
),
AutoRoute(
path: "album/:id",
page: AlbumRoute.page,
),
AutoRoute(
path: "artist/:id",
page: ArtistRoute.page,
),
AutoRoute(
path: "liked-tracks",
page: LikedPlaylistRoute.page,
),
AutoRoute(
path: "playlist/:id",
page: PlaylistRoute.page,
guards: [
AutoRouteGuard.redirect(
(resolver) {
final PlaylistRouteArgs(:id, :playlist) =
resolver.route.args as PlaylistRouteArgs;
if (id == "user-liked-tracks") {
return LikedPlaylistRoute(playlist: playlist);
}
return null;
},
),
],
),
AutoRoute(
path: "track/:id",
page: TrackRoute.page,
),
AutoRoute(
path: "connect",
page: ConnectRoute.page,
),
AutoRoute(
path: "connect/control",
page: ConnectControlRoute.page,
),
AutoRoute(
path: "profile",
page: ProfileRoute.page,
),
AutoRoute(
path: "stats",
page: StatsRoute.page,
),
AutoRoute(
path: "stats/minutes",
page: StatsMinutesRoute.page,
),
AutoRoute(
path: "stats/streams",
page: StatsStreamsRoute.page,
),
AutoRoute(
path: "stats/fees",
page: StatsStreamFeesRoute.page,
),
AutoRoute(
path: "stats/artists",
page: StatsArtistsRoute.page,
),
AutoRoute(
path: "stats/albums",
page: StatsAlbumsRoute.page,
),
AutoRoute(
path: "stats/playlists",
page: StatsPlaylistsRoute.page,
),
],
), ),
), AutoRoute(
GoRoute( path: "/mini-player",
path: "/login", page: MiniLyricsRoute.page,
name: WebViewLogin.name, // parentNavigatorKey: rootNavigatorKey,
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => const SpotubePage(
child: WebViewLogin(),
), ),
), AutoRoute(
GoRoute( path: "/getting-started",
path: "/lastfm-login", page: GettingStartedRoute.page,
name: LastFMLoginPage.name, // parentNavigatorKey: rootNavigatorKey,
parentNavigatorKey: rootNavigatorKey, ),
pageBuilder: (context, state) => AutoRoute(
const SpotubePage(child: LastFMLoginPage()), path: "/login",
), page: WebViewLoginRoute.page,
], // parentNavigatorKey: rootNavigatorKey,
); ),
}); AutoRoute(
path: "/lastfm-login",
page: LastFMLoginRoute.page,
// parentNavigatorKey: rootNavigatorKey,
),
];
}

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,51 @@
import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.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/library/user_albums.dart';
import 'package:spotube/pages/library/user_artists.dart';
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
import 'package:spotube/pages/library/user_playlists.dart';
import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/stats/stats.dart';
class SideBarTiles { class SideBarTiles {
final IconData icon; final IconData icon;
final String title; final String title;
final String id; final String id;
final String name; final String pathPrefix;
final PageRouteInfo route;
SideBarTiles({ SideBarTiles({
required this.icon, required this.icon,
required this.title, required this.title,
required this.id, required this.id,
required this.name, required this.route,
required this.pathPrefix,
}); });
} }
List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [ List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
SideBarTiles( SideBarTiles(
id: "browse", id: "home",
name: HomePage.name, pathPrefix: "/home",
route: const HomeRoute(),
icon: SpotubeIcons.home, icon: SpotubeIcons.home,
title: l10n.browse, title: l10n.browse,
), ),
SideBarTiles( SideBarTiles(
id: "search", id: "search",
name: SearchPage.name, pathPrefix: "/search",
route: const SearchRoute(),
icon: SpotubeIcons.search, icon: SpotubeIcons.search,
title: l10n.search, title: l10n.search,
), ),
SideBarTiles( SideBarTiles(
id: "lyrics", id: "lyrics",
name: LyricsPage.name, pathPrefix: "/lyrics",
route: LyricsRoute(),
icon: SpotubeIcons.music, icon: SpotubeIcons.music,
title: l10n.lyrics, title: l10n.lyrics,
), ),
SideBarTiles( SideBarTiles(
id: "stats", id: "stats",
name: StatsPage.name, pathPrefix: "/stats",
route: const StatsRoute(),
icon: SpotubeIcons.chart, icon: SpotubeIcons.chart,
title: l10n.stats, title: l10n.stats,
), ),
@ -54,52 +54,60 @@ List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
List<SideBarTiles> getSidebarLibraryTileList(AppLocalizations l10n) => [ List<SideBarTiles> getSidebarLibraryTileList(AppLocalizations l10n) => [
SideBarTiles( SideBarTiles(
id: "playlists", id: "playlists",
pathPrefix: "/library/playlists",
title: l10n.playlists, title: l10n.playlists,
name: UserPlaylistsPage.name, route: const UserPlaylistsRoute(),
icon: SpotubeIcons.playlist, icon: SpotubeIcons.playlist,
), ),
SideBarTiles( SideBarTiles(
id: "artists", id: "artists",
pathPrefix: "/library/artists",
title: l10n.artists, title: l10n.artists,
name: UserArtistsPage.name, route: const UserArtistsRoute(),
icon: SpotubeIcons.artist, icon: SpotubeIcons.artist,
), ),
SideBarTiles( SideBarTiles(
id: "albums", id: "albums",
pathPrefix: "/library/albums",
title: l10n.albums, title: l10n.albums,
name: UserAlbumsPage.name, route: const UserAlbumsRoute(),
icon: SpotubeIcons.album, icon: SpotubeIcons.album,
), ),
SideBarTiles( SideBarTiles(
id: "local_library", id: "local_library",
pathPrefix: "/library/local",
title: l10n.local_library, title: l10n.local_library,
name: UserLocalLibraryPage.name, route: const UserLocalLibraryRoute(),
icon: SpotubeIcons.device, icon: SpotubeIcons.device,
), ),
]; ];
List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [ List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
SideBarTiles( SideBarTiles(
id: "browse", id: "home",
name: HomePage.name, pathPrefix: "/home",
route: const HomeRoute(),
icon: SpotubeIcons.home, icon: SpotubeIcons.home,
title: l10n.browse, title: l10n.browse,
), ),
SideBarTiles( SideBarTiles(
id: "search", id: "search",
name: SearchPage.name, pathPrefix: "/search",
route: const SearchRoute(),
icon: SpotubeIcons.search, icon: SpotubeIcons.search,
title: l10n.search, title: l10n.search,
), ),
SideBarTiles( SideBarTiles(
id: "library", id: "library",
name: UserPlaylistsPage.name, pathPrefix: "/library",
route: const UserPlaylistsRoute(),
icon: SpotubeIcons.library, icon: SpotubeIcons.library,
title: l10n.library, title: l10n.library,
), ),
SideBarTiles( SideBarTiles(
id: "stats", id: "stats",
name: StatsPage.name, pathPrefix: "/stats",
route: const StatsRoute(),
icon: SpotubeIcons.chart, icon: SpotubeIcons.chart,
title: l10n.stats, title: l10n.stats,
), ),

View File

@ -1,6 +1,7 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/links/hyper_link.dart'; import 'package:spotube/components/links/hyper_link.dart';
@ -32,8 +33,7 @@ class TrackDetailsDialog extends HookWidget {
), ),
context.l10n.album: LinkText( context.l10n.album: LinkText(
track.album!.name!, track.album!.name!,
"/album/${track.album?.id}", AlbumRoute(album: track.album!, id: track.album!.id!),
extra: track.album,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.blue), style: const TextStyle(color: Colors.blue),
), ),

View File

@ -1,13 +1,13 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_undraw/flutter_undraw.dart'; import 'package:flutter_undraw/flutter_undraw.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:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
class AnonymousFallback extends ConsumerWidget { class AnonymousFallback extends ConsumerWidget {
final Widget? child; final Widget? child;
@ -40,7 +40,7 @@ class AnonymousFallback extends ConsumerWidget {
Text(context.l10n.not_logged_in), Text(context.l10n.not_logged_in),
Button.primary( Button.primary(
child: Text(context.l10n.login_with_spotify), child: Text(context.l10n.login_with_spotify),
onPressed: () => ServiceUtils.pushNamed(context, SettingsPage.name), onPressed: () => context.navigateTo(const SettingsRoute()),
) )
], ],
), ),

View File

@ -1,9 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/links/anchor_button.dart'; import 'package:spotube/components/links/anchor_button.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/utils/service_utils.dart';
class ArtistLink extends StatelessWidget { class ArtistLink extends StatelessWidget {
final List<ArtistSimple> artists; final List<ArtistSimple> artists;
@ -49,13 +49,8 @@ class ArtistLink extends StatelessWidget {
if (onRouteChange != null) { if (onRouteChange != null) {
onRouteChange?.call("/artist/${artist.value.id}"); onRouteChange?.call("/artist/${artist.value.id}");
} else { } else {
ServiceUtils.pushNamed( context
context, .navigateTo(ArtistRoute(artistId: artist.value.id!));
ArtistPage.name,
pathParameters: {
"id": artist.value.id!,
},
);
} }
}, },
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

View File

@ -1,15 +1,14 @@
import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/links/anchor_button.dart'; import 'package:spotube/components/links/anchor_button.dart';
import 'package:spotube/utils/service_utils.dart';
class LinkText<T> extends StatelessWidget { class LinkText<T> extends StatelessWidget {
final String text; final String text;
final TextStyle style; final TextStyle style;
final TextAlign? textAlign; final TextAlign? textAlign;
final TextOverflow? overflow; final TextOverflow? overflow;
final String route; final PageRouteInfo route;
final int? maxLines; final int? maxLines;
final T? extra;
final bool push; final bool push;
const LinkText( const LinkText(
@ -17,7 +16,6 @@ class LinkText<T> extends StatelessWidget {
this.route, { this.route, {
super.key, super.key,
this.textAlign, this.textAlign,
this.extra,
this.overflow, this.overflow,
this.style = const TextStyle(), this.style = const TextStyle(),
this.maxLines, this.maxLines,
@ -30,9 +28,9 @@ class LinkText<T> extends StatelessWidget {
text, text,
onTap: () { onTap: () {
if (push) { if (push) {
ServiceUtils.push(context, route, extra: extra); context.navigateTo(route);
} else { } else {
ServiceUtils.navigate(context, route, extra: extra); context.navigateTo(route);
} }
}, },
key: key, key: key,

View File

@ -1,25 +1,24 @@
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:go_router/go_router.dart';
class SpotubePage<T> extends MaterialPage<T> { class SpotubePage<T> extends MaterialPage<T> {
const SpotubePage({required super.child}); const SpotubePage({required super.child});
} }
class SpotubeSlidePage extends CustomTransitionPage { // class SpotubeSlidePage extends CustomTransitionPage {
SpotubeSlidePage({ // SpotubeSlidePage({
required super.child, // required super.child,
super.key, // super.key,
}) : super( // }) : super(
reverseTransitionDuration: const Duration(milliseconds: 150), // reverseTransitionDuration: const Duration(milliseconds: 150),
transitionDuration: const Duration(milliseconds: 150), // transitionDuration: const Duration(milliseconds: 150),
transitionsBuilder: (context, animation, secondaryAnimation, child) { // transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition( // return SlideTransition(
position: Tween<Offset>( // position: Tween<Offset>(
begin: const Offset(1, 0), // begin: const Offset(1, 0),
end: Offset.zero, // end: Offset.zero,
).animate(animation), // ).animate(animation),
child: child, // child: child,
); // );
}, // },
); // );
} // }

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.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';
@ -73,6 +74,10 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
final hasFullscreen = final hasFullscreen =
MediaQuery.sizeOf(context).width == constraints.maxWidth; MediaQuery.sizeOf(context).width == constraints.maxWidth;
final canPop = leading.isEmpty &&
automaticallyImplyLeading &&
(Navigator.canPop(context) || context.watchRouter.canPop());
return GestureDetector( return GestureDetector(
onHorizontalDragStart: (_) => onDrag(ref), onHorizontalDragStart: (_) => onDrag(ref),
onVerticalDragStart: (_) => onDrag(ref), onVerticalDragStart: (_) => onDrag(ref),
@ -94,13 +99,7 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
} }
}, },
child: AppBar( child: AppBar(
leading: leading.isEmpty && leading: canPop ? [const BackButton()] : leading,
automaticallyImplyLeading &&
Navigator.canPop(context)
? [
const BackButton(),
]
: leading,
trailing: [ trailing: [
...trailing, ...trailing,
Align( Align(

View File

@ -1,14 +1,16 @@
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
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:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotify/spotify.dart' hide Offset; import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart'; import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
@ -22,7 +24,6 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
@ -30,7 +31,6 @@ import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -166,7 +166,6 @@ class TrackOptions extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final router = GoRouter.of(context);
final ThemeData(:colorScheme) = Theme.of(context); final ThemeData(:colorScheme) = Theme.of(context);
final playlist = ref.watch(audioPlayerProvider); final playlist = ref.watch(audioPlayerProvider);
@ -211,9 +210,8 @@ class TrackOptions extends HookConsumerWidget {
onSelected: (value) async { onSelected: (value) async {
switch (value) { switch (value) {
case TrackOptionValue.album: case TrackOptionValue.album:
await router.push( await context.navigateTo(
'/album/${track.album!.id}', AlbumRoute(id: track.album!.id!, album: track.album!),
extra: track.album!,
); );
break; break;
case TrackOptionValue.delete: case TrackOptionValue.delete:
@ -347,12 +345,8 @@ class TrackOptions extends HookConsumerWidget {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: ArtistLink( child: ArtistLink(
artists: track.artists!, artists: track.artists!,
onOverflowArtistClick: () => ServiceUtils.pushNamed( onOverflowArtistClick: () => context.navigateTo(
context, TrackRoute(trackId: track.id!),
TrackPage.name,
pathParameters: {
"id": track.id!,
},
), ),
), ),
), ),

View File

@ -1,13 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
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:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/hover_builder.dart'; import 'package:spotube/components/hover_builder.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
@ -21,12 +22,10 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
class TrackTile extends HookConsumerWidget { class TrackTile extends HookConsumerWidget {
/// [index] will not be shown if null /// [index] will not be shown if null
@ -234,12 +233,8 @@ class TrackTile extends HookConsumerWidget {
padding: (context, states) => EdgeInsets.zero, padding: (context, states) => EdgeInsets.zero,
), ),
onPressed: () { onPressed: () {
context.pushNamed( context
TrackPage.name, .navigateTo(TrackRoute(trackId: track.id!));
pathParameters: {
"id": track.id!,
},
);
}, },
child: Text( child: Text(
track.name!, track.name!,
@ -266,8 +261,8 @@ class TrackTile extends HookConsumerWidget {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: LinkText( child: LinkText(
track.album!.name!, track.album!.name!,
"/album/${track.album?.id}", AlbumRoute(
extra: track.album, album: track.album!, id: track.album!.id!),
push: true, push: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -288,13 +283,11 @@ class TrackTile extends HookConsumerWidget {
constraints: const BoxConstraints(maxHeight: 40), constraints: const BoxConstraints(maxHeight: 40),
child: ArtistLink( child: ArtistLink(
artists: track.artists ?? [], artists: track.artists ?? [],
onOverflowArtistClick: () => ServiceUtils.pushNamed( onOverflowArtistClick: () {
context, context.navigateTo(
TrackPage.name, TrackRoute(trackId: track.id!),
pathParameters: { );
"id": track.id!, },
},
),
), ),
), ),
), ),

View File

@ -4,6 +4,7 @@ import 'package:app_links/app_links.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart';
@ -13,10 +14,9 @@ import 'package:spotube/utils/platform.dart';
final appLinks = AppLinks(); final appLinks = AppLinks();
final linkStream = appLinks.stringLinkStream.asBroadcastStream(); final linkStream = appLinks.stringLinkStream.asBroadcastStream();
void useDeepLinking(WidgetRef ref) { void useDeepLinking(WidgetRef ref, AppRouter router) {
// single instance no worries // single instance no worries
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final router = ref.watch(routerProvider);
useEffect(() { useEffect(() {
void uriListener(List<SharedFile> files) async { void uriListener(List<SharedFile> files) async {
@ -27,24 +27,21 @@ void useDeepLinking(WidgetRef ref) {
switch (url.pathSegments.first) { switch (url.pathSegments.first) {
case "album": case "album":
router.push( final album = await spotify.albums.get(url.pathSegments.last);
"/album/${url.pathSegments.last}", router.navigate(
extra: await spotify.albums.get(url.pathSegments.last), AlbumRoute(id: album.id!, album: album),
); );
break; break;
case "artist": case "artist":
router.push("/artist/${url.pathSegments.last}"); router.navigate(ArtistRoute(artistId: url.pathSegments.last));
break; break;
case "playlist": case "playlist":
router.push( final playlist = await spotify.playlists.get(url.pathSegments.last);
"/playlist/${url.pathSegments.last}", router
extra: await spotify.playlists.get(url.pathSegments.last), .navigate(PlaylistRoute(id: playlist.id!, playlist: playlist));
);
break; break;
case "track": case "track":
router.push( router.navigate(TrackRoute(trackId: url.pathSegments.last));
"/track/${url.pathSegments.last}",
);
break; break;
default: default:
break; break;
@ -68,21 +65,21 @@ void useDeepLinking(WidgetRef ref) {
switch (startSegment) { switch (startSegment) {
case "spotify:album": case "spotify:album":
await router.push( final album = await spotify.albums.get(endSegment);
"/album/$endSegment", await router.navigate(
extra: await spotify.albums.get(endSegment), AlbumRoute(id: album.id!, album: album),
); );
break; break;
case "spotify:artist": case "spotify:artist":
await router.push("/artist/$endSegment"); await router.navigate(ArtistRoute(artistId: endSegment));
break; break;
case "spotify:track": case "spotify:track":
await router.push("/track/$endSegment"); await router.navigate(TrackRoute(trackId: endSegment));
break; break;
case "spotify:playlist": case "spotify:playlist":
await router.push( final playlist = await spotify.playlists.get(endSegment);
"/playlist/$endSegment", await router.navigate(
extra: await spotify.playlists.get(endSegment), PlaylistRoute(id: playlist.id!, playlist: playlist),
); );
break; break;
default: default:

View File

@ -17,8 +17,8 @@ import 'package:metadata_god/metadata_god.dart';
import 'package:smtc_windows/smtc_windows.dart'; import 'package:smtc_windows/smtc_windows.dart';
import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/initializers.dart'; import 'package:spotube/collections/initializers.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/intents.dart'; import 'package:spotube/collections/intents.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/hooks/configurators/use_close_behavior.dart'; import 'package:spotube/hooks/configurators/use_close_behavior.dart';
import 'package:spotube/hooks/configurators/use_deep_linking.dart'; import 'package:spotube/hooks/configurators/use_deep_linking.dart';
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart'; import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
@ -133,7 +133,7 @@ class Spotube extends HookConsumerWidget {
final locale = ref.watch(userPreferencesProvider.select((s) => s.locale)); final locale = ref.watch(userPreferencesProvider.select((s) => s.locale));
final accentMaterialColor = final accentMaterialColor =
ref.watch(userPreferencesProvider.select((s) => s.accentColorScheme)); ref.watch(userPreferencesProvider.select((s) => s.accentColorScheme));
final router = ref.watch(routerProvider); final router = useMemoized(() => AppRouter(ref), []);
final hasTouchSupport = useHasTouch(); final hasTouchSupport = useHasTouch();
ref.listen(audioPlayerStreamListenersProvider, (_, __) {}); ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
@ -144,7 +144,7 @@ class Spotube extends HookConsumerWidget {
useFixWindowStretching(); useFixWindowStretching();
useDisableBatteryOptimizations(); useDisableBatteryOptimizations();
useDeepLinking(ref); useDeepLinking(ref, router);
useCloseBehavior(ref); useCloseBehavior(ref);
useGetStoragePermissions(ref); useGetStoragePermissions(ref);
@ -171,7 +171,7 @@ class Spotube extends HookConsumerWidget {
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
routerConfig: router, routerConfig: router.config(),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Spotube', title: 'Spotube',
builder: (context, child) { builder: (context, child) {
@ -240,42 +240,42 @@ class Spotube extends HookConsumerWidget {
LogicalKeyboardKey.digit1, LogicalKeyboardKey.digit1,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.browse), ): HomeTabIntent(router, tab: HomeTabs.browse),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.digit2, LogicalKeyboardKey.digit2,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.search), ): HomeTabIntent(router, tab: HomeTabs.search),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.digit3, LogicalKeyboardKey.digit3,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.lyrics), ): HomeTabIntent(router, tab: HomeTabs.lyrics),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.digit4, LogicalKeyboardKey.digit4,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.userPlaylists), ): HomeTabIntent(router, tab: HomeTabs.userPlaylists),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.digit5, LogicalKeyboardKey.digit5,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.userArtists), ): HomeTabIntent(router, tab: HomeTabs.userArtists),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.digit6, LogicalKeyboardKey.digit6,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.userAlbums), ): HomeTabIntent(router, tab: HomeTabs.userAlbums),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.digit7, LogicalKeyboardKey.digit7,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.userLocalLibrary), ): HomeTabIntent(router, tab: HomeTabs.userLocalLibrary),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.digit8, LogicalKeyboardKey.digit8,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,
LogicalKeyboardKey.shift, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.userDownloads), ): HomeTabIntent(router, tab: HomeTabs.userDownloads),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.keyW, LogicalKeyboardKey.keyW,
LogicalKeyboardKey.control, LogicalKeyboardKey.control,

View File

@ -1,7 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.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:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/playbutton_view/playbutton_card.dart'; import 'package:spotube/components/playbutton_view/playbutton_card.dart';
import 'package:spotube/components/playbutton_view/playbutton_tile.dart'; import 'package:spotube/components/playbutton_view/playbutton_tile.dart';
@ -10,14 +12,12 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/track.dart'; import 'package:spotube/extensions/track.dart';
import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/pages/album/album.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart';
extension FormattedAlbumType on AlbumType { extension FormattedAlbumType on AlbumType {
String get formatted => name.replaceFirst(name[0], name[0].toUpperCase()); String get formatted => name.replaceFirst(name[0], name[0].toUpperCase());
@ -69,14 +69,7 @@ class AlbumCard extends HookConsumerWidget {
"${album.albumType?.formatted}${album.artists?.asString() ?? ""}"; "${album.albumType?.formatted}${album.artists?.asString() ?? ""}";
void onTap() { void onTap() {
ServiceUtils.pushNamed( context.navigateTo(AlbumRoute(id: album.id!, album: album));
context,
AlbumPage.name,
pathParameters: {
"id": album.id!,
},
extra: album,
);
} }
void onPlaybuttonPressed() async { void onPlaybuttonPressed() async {

View File

@ -1,16 +1,16 @@
import 'package:auto_route/auto_route.dart';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.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:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.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/pages/artist/artist.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/utils/service_utils.dart';
class ArtistCard extends HookConsumerWidget { class ArtistCard extends HookConsumerWidget {
final Artist artist; final Artist artist;
@ -36,13 +36,7 @@ class ArtistCard extends HookConsumerWidget {
width: 180, width: 180,
child: Button.card( child: Button.card(
onPressed: () { onPressed: () {
ServiceUtils.pushNamed( context.navigateTo(ArtistRoute(artistId: artist.id!));
context,
ArtistPage.name,
pathParameters: {
"id": artist.id!,
},
);
}, },
child: Column( child: Column(
children: [ children: [

View File

@ -1,11 +1,11 @@
import 'package:auto_route/auto_route.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/collections/routes.gr.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/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/connect/connect.dart';
import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/utils/service_utils.dart';
class ConnectDeviceButton extends HookConsumerWidget { class ConnectDeviceButton extends HookConsumerWidget {
final bool _sidebar; final bool _sidebar;
@ -26,7 +26,7 @@ class ConnectDeviceButton extends HookConsumerWidget {
return IconButton.ghost( return IconButton.ghost(
icon: const Icon(SpotubeIcons.speaker), icon: const Icon(SpotubeIcons.speaker),
onPressed: () { onPressed: () {
ServiceUtils.pushNamed(context, ConnectPage.name); context.navigateTo(const ConnectRoute());
}, },
); );
} }
@ -35,7 +35,7 @@ class ConnectDeviceButton extends HookConsumerWidget {
width: double.infinity, width: double.infinity,
child: Button.primary( child: Button.primary(
onPressed: () { onPressed: () {
ServiceUtils.pushNamed(context, ConnectPage.name); context.navigateTo(const ConnectRoute());
}, },
trailing: const Icon(SpotubeIcons.speaker), trailing: const Icon(SpotubeIcons.speaker),
child: Text( child: Text(
@ -50,7 +50,7 @@ class ConnectDeviceButton extends HookConsumerWidget {
children: [ children: [
SecondaryBadge( SecondaryBadge(
onPressed: () { onPressed: () {
ServiceUtils.pushNamed(context, ConnectPage.name); context.navigateTo(const ConnectRoute());
}, },
style: const ButtonStyle.secondary(size: ButtonSize(.8)), style: const ButtonStyle.secondary(size: ButtonSize(.8)),
leading: connectClients.asData?.value.resolvedService != null leading: connectClients.asData?.value.resolvedService != null
@ -70,7 +70,7 @@ class ConnectDeviceButton extends HookConsumerWidget {
IconButton.primary( IconButton.primary(
icon: const Icon(SpotubeIcons.speaker), icon: const Icon(SpotubeIcons.speaker),
onPressed: () { onPressed: () {
ServiceUtils.pushNamed(context, ConnectPage.name); context.navigateTo(const ConnectRoute());
}, },
) )
], ],

View File

@ -1,10 +1,10 @@
import 'package:auto_route/auto_route.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/collections/routes.gr.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/home/feed/feed_section.dart';
import 'package:spotube/provider/spotify/views/home.dart'; import 'package:spotube/provider/spotify/views/home.dart';
import 'package:spotube/utils/service_utils.dart';
class HomePageFeedSection extends HookConsumerWidget { class HomePageFeedSection extends HookConsumerWidget {
const HomePageFeedSection({super.key}); const HomePageFeedSection({super.key});
@ -39,13 +39,9 @@ class HomePageFeedSection extends HookConsumerWidget {
onFetchMore: () {}, onFetchMore: () {},
titleTrailing: Button.text( titleTrailing: Button.text(
child: Text(context.l10n.browse_all), child: Text(context.l10n.browse_all),
onPressed: () => ServiceUtils.pushNamed( onPressed: () {
context, context.navigateTo(HomeFeedSectionRoute(sectionUri: section.uri));
HomeFeedSectionPage.name, },
pathParameters: {
"feedId": section.uri,
},
),
), ),
); );
}, },

View File

@ -1,14 +1,13 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.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:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.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/models/spotify_friends.dart'; import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/pages/album/album.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
class FriendItem extends HookConsumerWidget { class FriendItem extends HookConsumerWidget {
@ -50,9 +49,8 @@ class FriendItem extends HookConsumerWidget {
text: friend.track.name, text: friend.track.name,
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () {
context.pushNamed(TrackPage.name, pathParameters: { context
"id": friend.track.id, .navigateTo(TrackRoute(trackId: friend.track.id));
});
}, },
), ),
const TextSpan(text: ""), const TextSpan(text: ""),
@ -66,12 +64,8 @@ class FriendItem extends HookConsumerWidget {
text: " ${friend.track.artist.name}", text: " ${friend.track.artist.name}",
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () {
context.pushNamed( context.navigateTo(
ArtistPage.name, ArtistRoute(artistId: friend.track.artist.id),
pathParameters: {
"id": friend.track.artist.id,
},
extra: friend.track.artist,
); );
}, },
), ),
@ -80,13 +74,13 @@ class FriendItem extends HookConsumerWidget {
text: friend.track.context.name, text: friend.track.context.name,
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () async { ..onTap = () async {
context.push( context.router.navigateNamed(
"/${friend.track.context.path}", "/${friend.track.context.path}",
extra: // extra:
!friend.track.context.path.startsWith("album") // !friend.track.context.path.startsWith("album")
? null // ? null
: await spotify.albums // : await spotify.albums
.get(friend.track.context.id), // .get(friend.track.context.id),
); );
}, },
), ),
@ -104,12 +98,8 @@ class FriendItem extends HookConsumerWidget {
final album = final album =
await spotify.albums.get(friend.track.album.id); await spotify.albums.get(friend.track.album.id);
if (context.mounted) { if (context.mounted) {
context.pushNamed( context.navigateTo(
AlbumPage.name, AlbumRoute(id: album.id!, album: album),
pathParameters: {
"id": friend.track.album.id,
},
extra: album,
); );
} }
}, },

View File

@ -1,17 +1,17 @@
import 'dart:math'; import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:go_router/go_router.dart'; import 'package:auto_route/auto_route.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:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart' hide Offset; import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/gradients.dart'; import 'package:spotube/collections/gradients.dart';
import 'package:spotube/collections/routes.gr.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/modules/home/sections/genres/genre_card_playlist_card.dart'; import 'package:spotube/modules/home/sections/genres/genre_card_playlist_card.dart';
import 'package:spotube/pages/home/genres/genre_playlists.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
final random = Random(); final random = Random();
@ -76,10 +76,11 @@ class GenreSectionCard extends HookConsumerWidget {
).h3(), ).h3(),
Button.link( Button.link(
onPressed: () { onPressed: () {
context.pushNamed( context.navigateTo(
GenrePlaylistsPage.name, GenrePlaylistsRoute(
pathParameters: {'categoryId': category.id!}, id: category.id!,
extra: category, category: category,
),
); );
}, },
child: Text( child: Text(

View File

@ -1,12 +1,12 @@
import 'package:go_router/go_router.dart'; import 'package:auto_route/auto_route.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:spotify/spotify.dart' hide Image; import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.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/pages/playlist/playlist.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:stroke_text/stroke_text.dart'; import 'package:stroke_text/stroke_text.dart';
@ -47,12 +47,8 @@ class GenreSectionCardPlaylistCard extends HookConsumerWidget {
}, },
), ),
onPressed: () { onPressed: () {
context.pushNamed( context.navigateTo(
PlaylistPage.name, PlaylistRoute(id: playlist.id!, playlist: playlist),
pathParameters: {
"id": playlist.id!,
},
extra: playlist,
); );
}, },
child: Column( child: Column(

View File

@ -1,15 +1,16 @@
import 'package:auto_route/auto_route.dart';
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:shadcn_flutter/shadcn_flutter_extension.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/routes.gr.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/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/home/sections/genres/genre_card.dart'; import 'package:spotube/modules/home/sections/genres/genre_card.dart';
import 'package:spotube/pages/home/genres/genres.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class HomeGenresSection extends HookConsumerWidget { class HomeGenresSection extends HookConsumerWidget {
@ -47,7 +48,7 @@ class HomeGenresSection extends HookConsumerWidget {
), ),
Button.link( Button.link(
onPressed: () { onPressed: () {
context.pushNamed(GenrePage.name); context.navigateTo(const GenreRoute());
}, },
child: Text( child: Text(
context.l10n.browse_all, context.l10n.browse_all,

View File

@ -1,18 +1,18 @@
import 'dart:math'; import 'dart:math';
import 'package:auto_route/auto_route.dart';
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:path/path.dart'; import 'package:path/path.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.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/pages/library/user_local_tracks/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';
@ -59,13 +59,12 @@ class LocalFolderItem extends HookConsumerWidget {
return Button( return Button(
onPressed: () { onPressed: () {
context.pushNamed( context.navigateTo(
LocalLibraryPage.name, LocalLibraryRoute(
queryParameters: { location: folder,
if (isDownloadFolder) "downloads": "true", isCache: isCacheFolder,
if (isCacheFolder) "cache": "true", isDownloads: isDownloadFolder,
}, ),
extra: folder,
); );
}, },
style: ButtonVariance.card.copyWith( style: ButtonVariance.card.copyWith(

View File

@ -1,18 +1,18 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.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:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.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/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.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/pages/track/track.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/services/download_manager/download_status.dart'; import 'package:spotube/services/download_manager/download_status.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/service_utils.dart';
class DownloadItem extends HookConsumerWidget { class DownloadItem extends HookConsumerWidget {
final Track track; final Track track;
@ -66,13 +66,9 @@ class DownloadItem extends HookConsumerWidget {
subtitle: ArtistLink( subtitle: ArtistLink(
artists: track.artists ?? <Artist>[], artists: track.artists ?? <Artist>[],
mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
onOverflowArtistClick: () => ServiceUtils.pushNamed( onOverflowArtistClick: () {
context, context.navigateTo(TrackRoute(trackId: track.id!));
TrackPage.name, },
pathParameters: {
"id": track.id!,
},
),
), ),
trailing: isQueryingSourceInfo trailing: isQueryingSourceInfo
? Text(context.l10n.querying_info).small() ? Text(context.l10n.querying_info).small()

View File

@ -1,13 +1,14 @@
import 'package:auto_route/auto_route.dart';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart' show showModalBottomSheet; import 'package:flutter/material.dart' show showModalBottomSheet;
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:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/framework/app_pop_scope.dart'; import 'package:spotube/components/framework/app_pop_scope.dart';
import 'package:spotube/modules/player/player_actions.dart'; import 'package:spotube/modules/player/player_actions.dart';
@ -25,13 +26,11 @@ import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart';
import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -94,7 +93,7 @@ class PlayerView extends HookConsumerWidget {
}, [panelController.isAttached && panelController.isPanelOpen]); }, [panelController.isAttached && panelController.isPanelOpen]);
return AppPopScope( return AppPopScope(
canPop: context.canPop(), canPop: false,
onPopInvoked: (didPop) async { onPopInvoked: (didPop) async {
await panelController.close(); await panelController.close();
}, },
@ -210,14 +209,10 @@ class PlayerView extends HookConsumerWidget {
.copyWith(fontWeight: FontWeight.bold), .copyWith(fontWeight: FontWeight.bold),
onRouteChange: (route) { onRouteChange: (route) {
panelController.close(); panelController.close();
GoRouter.of(context).push(route); context.router.navigateNamed(route);
}, },
onOverflowArtistClick: () => ServiceUtils.pushNamed( onOverflowArtistClick: () => context.navigateTo(
context, TrackRoute(trackId: currentTrack!.id!),
TrackPage.name,
pathParameters: {
"id": currentTrack!.id!,
},
), ),
), ),
], ],

View File

@ -1,4 +1,3 @@
import 'package:flutter_hooks/flutter_hooks.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:sliding_up_panel/sliding_up_panel.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart';
@ -8,6 +7,10 @@ import 'package:spotube/modules/root/spotube_navigation_bar.dart';
import 'package:spotube/modules/player/player.dart'; import 'package:spotube/modules/player/player.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
final playerOverlayControllerProvider = StateProvider<PanelController>((ref) {
return PanelController();
});
class PlayerOverlay extends HookConsumerWidget { class PlayerOverlay extends HookConsumerWidget {
final String albumArt; final String albumArt;
@ -23,7 +26,7 @@ class PlayerOverlay extends HookConsumerWidget {
final screenSize = MediaQuery.sizeOf(context); final screenSize = MediaQuery.sizeOf(context);
final panelController = useMemoized(() => PanelController(), []); final panelController = ref.watch(playerOverlayControllerProvider);
return SlidingUpPanel( return SlidingUpPanel(
maxHeight: screenSize.height, maxHeight: screenSize.height,

View File

@ -1,17 +1,18 @@
import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.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/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/links/link_text.dart'; import 'package:spotube/components/links/link_text.dart';
import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart';
class PlayerTrackDetails extends HookConsumerWidget { class PlayerTrackDetails extends HookConsumerWidget {
final Color? color; final Color? color;
@ -50,7 +51,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
LinkText( LinkText(
playback.activeTrack?.name ?? "", playback.activeTrack?.name ?? "",
"/track/${playback.activeTrack?.id}", TrackRoute(trackId: playback.activeTrack?.id ?? ""),
push: true, push: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: theme.typography.normal.copyWith( style: theme.typography.normal.copyWith(
@ -72,7 +73,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
children: [ children: [
LinkText( LinkText(
playback.activeTrack?.name ?? "", playback.activeTrack?.name ?? "",
"/track/${playback.activeTrack?.id}", TrackRoute(trackId: playback.activeTrack?.id ?? ""),
push: true, push: true,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.bold, color: color), style: TextStyle(fontWeight: FontWeight.bold, color: color),
@ -80,15 +81,10 @@ class PlayerTrackDetails extends HookConsumerWidget {
ArtistLink( ArtistLink(
artists: playback.activeTrack?.artists ?? [], artists: playback.activeTrack?.artists ?? [],
onRouteChange: (route) { onRouteChange: (route) {
ServiceUtils.push(context, route); context.router.navigateNamed(route);
}, },
onOverflowArtistClick: () => ServiceUtils.pushNamed( onOverflowArtistClick: () =>
context, context.navigateTo(TrackRoute(trackId: track!.id!)),
TrackPage.name,
pathParameters: {
"id": track!.id!,
},
),
) )
], ],
), ),

View File

@ -1,8 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.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:spotify/spotify.dart' hide Offset, Image; import 'package:spotify/spotify.dart' hide Offset, Image;
import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/dialogs/select_device_dialog.dart'; import 'package:spotube/components/dialogs/select_device_dialog.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/playbutton_view/playbutton_card.dart'; import 'package:spotube/components/playbutton_view/playbutton_card.dart';
@ -10,14 +12,12 @@ import 'package:spotube/components/playbutton_view/playbutton_tile.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/models/connect/connect.dart'; import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:stroke_text/stroke_text.dart'; import 'package:stroke_text/stroke_text.dart';
class PlaylistCard extends HookConsumerWidget { class PlaylistCard extends HookConsumerWidget {
@ -73,14 +73,7 @@ class PlaylistCard extends HookConsumerWidget {
} }
void onTap() { void onTap() {
ServiceUtils.pushNamed( context.navigateTo(PlaylistRoute(id: playlist.id!, playlist: playlist));
context,
PlaylistPage.name,
pathParameters: {
"id": playlist.id!,
},
extra: playlist,
);
} }
void onPlaybuttonPressed() async { void onPlaybuttonPressed() async {

View File

@ -1,11 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:form_builder_validators/form_builder_validators.dart';
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:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -105,7 +105,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
isSubmitting.value = false; isSubmitting.value = false;
if (context.mounted && if (context.mounted &&
!ref.read(playlistProvider(playlistId ?? "")).hasError) { !ref.read(playlistProvider(playlistId ?? "")).hasError) {
context.pop(); context.router.maybePop();
} }
} }
} }

View File

@ -1,10 +1,11 @@
import 'package:auto_route/auto_route.dart';
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:shadcn_flutter/shadcn_flutter_extension.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/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/player/player_actions.dart'; import 'package:spotube/modules/player/player_actions.dart';
@ -96,9 +97,8 @@ class BottomPlayer extends HookConsumerWidget {
const Duration(milliseconds: 100), const Duration(milliseconds: 100),
() async { () async {
if (context.mounted) { if (context.mounted) {
context.go( context.navigateTo(
'/mini-player', MiniLyricsRoute(prevSize: prevSize),
extra: prevSize,
); );
} }
}, },

View File

@ -1,270 +0,0 @@
import 'package:flutter/material.dart' show Badge;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.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/settings/settings.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/service_utils.dart';
class Sidebar extends HookConsumerWidget {
final Widget child;
const Sidebar({
required this.child,
super.key,
});
static Widget brandLogo() {
return Container(
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(50),
),
child: Assets.spotubeLogoPng.image(height: 50),
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final routerState = GoRouterState.of(context);
final mediaQuery = MediaQuery.of(context);
final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
final sidebarTileList = useMemoized(
() => getSidebarTileList(context.l10n),
[context.l10n],
);
final sidebarLibraryTileList = useMemoized(
() => getSidebarLibraryTileList(context.l10n),
[context.l10n],
);
final tileList = [...sidebarTileList, ...sidebarLibraryTileList];
final selectedIndex = tileList.indexWhere(
(e) => routerState.namedLocation(e.name) == routerState.matchedLocation,
);
if (layoutMode == LayoutMode.compact ||
(mediaQuery.smAndDown && layoutMode == LayoutMode.adaptive)) {
return Scaffold(child: child);
}
final navigationButtons = [
NavigationLabel(
child: mediaQuery.lgAndUp ? const Text("Spotube") : const Text(""),
),
for (final tile in sidebarTileList)
NavigationButton(
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
child: Tooltip(
tooltip: TooltipContainer(child: Text(tile.title)),
child: Icon(tile.icon),
),
onChanged: (value) {
if (value) {
context.goNamed(tile.name);
}
},
),
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
Expanded(
child: mediaQuery.lgAndUp
? NavigationSidebar(
index: selectedIndex,
onSelected: (index) {
final tile = tileList[index];
context.goNamed(tile.name);
},
children: navigationButtons,
)
: NavigationRail(
alignment: NavigationRailAlignment.start,
index: selectedIndex,
onSelected: (index) {
final tile = tileList[index];
context.goNamed(tile.name);
},
children: navigationButtons,
),
),
const SidebarFooter(),
if (mediaQuery.lgAndUp) const Gap(130) else const Gap(65),
],
),
const VerticalDivider(),
Expanded(child: child),
],
);
}
}
class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
const SidebarFooter({
super.key,
});
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final routerState = GoRouterState.of(context);
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
final userSnapshot = ref.watch(meProvider);
final data = userSnapshot.asData?.value;
final avatarImg = (data?.images).asUrlString(
index: (data?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.artist,
);
final auth = ref.watch(authenticationProvider);
if (mediaQuery.mdAndDown) {
return Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
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),
),
],
);
}
return Container(
padding: const EdgeInsets.only(left: 12),
width: 180,
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
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(),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (auth.asData?.value != null && data == null)
const CircularProgressIndicator()
else if (data != null)
Flexible(
child: GestureDetector(
onTap: () {
ServiceUtils.pushNamed(context, ProfilePage.name);
},
child: Row(
children: [
Avatar(
initials:
Avatar.getInitials(data.displayName ?? "User"),
provider: UniversalImage.imageProvider(avatarImg),
),
const SizedBox(width: 10),
Flexible(
child: Text(
data.displayName ?? context.l10n.guest,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
style: theme.typography.normal
.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
),
),
IconButton(
variance: ButtonVariance.ghost,
icon: const Icon(SpotubeIcons.settings),
onPressed: () {
ServiceUtils.pushNamed(context, SettingsPage.name);
},
),
],
),
],
),
);
}
@override
bool get selectable => false;
}

View File

@ -0,0 +1,132 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/root/sidebar/sidebar_footer.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
class Sidebar extends HookConsumerWidget {
final Widget child;
const Sidebar({
required this.child,
super.key,
});
static Widget brandLogo() {
return Container(
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(50),
),
child: Assets.spotubeLogoPng.image(height: 50),
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final mediaQuery = MediaQuery.of(context);
final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
final sidebarTileList = useMemoized(
() => getSidebarTileList(context.l10n),
[context.l10n],
);
final sidebarLibraryTileList = useMemoized(
() => getSidebarLibraryTileList(context.l10n),
[context.l10n],
);
final tileList = [...sidebarTileList, ...sidebarLibraryTileList];
final router = context.watchRouter;
final selectedIndex = tileList.indexWhere(
(e) => router.currentPath.startsWith(e.pathPrefix),
);
if (layoutMode == LayoutMode.compact ||
(mediaQuery.smAndDown && layoutMode == LayoutMode.adaptive)) {
return child;
}
final navigationButtons = [
NavigationLabel(
child: mediaQuery.lgAndUp ? const Text("Spotube") : const Text(""),
),
for (final tile in sidebarTileList)
NavigationButton(
label: mediaQuery.lgAndUp ? Text(tile.title) : null,
child: Tooltip(
tooltip: TooltipContainer(child: Text(tile.title)),
child: Icon(tile.icon),
),
onChanged: (value) {
if (value) {
context.navigateTo(tile.route);
}
},
),
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.navigateTo(tile.route);
}
},
child: Tooltip(
tooltip: TooltipContainer(child: Text(tile.title)),
child: Icon(tile.icon),
),
),
];
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
Expanded(
child: mediaQuery.lgAndUp
? NavigationSidebar(
index: selectedIndex,
onSelected: (index) {
final tile = tileList[index];
context.navigateTo(tile.route);
},
children: navigationButtons,
)
: NavigationRail(
alignment: NavigationRailAlignment.start,
index: selectedIndex,
onSelected: (index) {
final tile = tileList[index];
context.navigateTo(tile.route);
},
children: navigationButtons,
),
),
const SidebarFooter(),
if (mediaQuery.lgAndUp) const Gap(130) else const Gap(65),
],
),
const VerticalDivider(),
Expanded(child: child),
],
);
}
}

View File

@ -0,0 +1,140 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart' show Badge;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/connect/connect_device.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
const SidebarFooter({
super.key,
});
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final router = AutoRouter.of(context, watch: true);
final mediaQuery = MediaQuery.of(context);
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
final userSnapshot = ref.watch(meProvider);
final data = userSnapshot.asData?.value;
final avatarImg = (data?.images).asUrlString(
index: (data?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.artist,
);
final auth = ref.watch(authenticationProvider);
if (mediaQuery.mdAndDown) {
return Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
Badge(
isLabelVisible: downloadCount > 0,
label: Text(downloadCount.toString()),
child: IconButton(
variance: router.topRoute.name == UserDownloadsRoute.name
? ButtonVariance.secondary
: ButtonVariance.ghost,
icon: const Icon(SpotubeIcons.download),
onPressed: () => context.navigateTo(const UserDownloadsRoute()),
),
),
const ConnectDeviceButton.sidebar(),
IconButton(
variance: ButtonVariance.ghost,
icon: const Icon(SpotubeIcons.settings),
onPressed: () => context.navigateTo(const SettingsRoute()),
),
],
);
}
return Container(
padding: const EdgeInsets.only(left: 12),
width: 180,
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
SizedBox(
width: double.infinity,
child: Button(
style: router.topRoute.name == UserDownloadsRoute.name
? ButtonVariance.secondary
: ButtonVariance.outline,
onPressed: () {
context.navigateTo(const UserDownloadsRoute());
},
leading: const Icon(SpotubeIcons.download),
trailing: downloadCount > 0
? PrimaryBadge(
child: Text(downloadCount.toString()),
)
: null,
child: Text(context.l10n.downloads),
),
),
const ConnectDeviceButton.sidebar(),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (auth.asData?.value != null && data == null)
const CircularProgressIndicator()
else if (data != null)
Flexible(
child: GestureDetector(
onTap: () {
context.navigateTo(const ProfileRoute());
},
child: Row(
children: [
Avatar(
initials:
Avatar.getInitials(data.displayName ?? "User"),
provider: UniversalImage.imageProvider(avatarImg),
),
const SizedBox(width: 10),
Flexible(
child: Text(
data.displayName ?? context.l10n.guest,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
style: theme.typography.normal
.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
),
),
IconButton(
variance: ButtonVariance.ghost,
icon: const Icon(SpotubeIcons.settings),
onPressed: () {
context.navigateTo(const SettingsRoute());
},
),
],
),
],
),
);
}
@override
bool get selectable => false;
}

View File

@ -1,6 +1,8 @@
import 'dart:math';
import 'package:auto_route/auto_route.dart';
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:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
@ -12,8 +14,6 @@ import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/service_utils.dart';
final navigationPanelHeight = StateProvider<double>((ref) => 50); final navigationPanelHeight = StateProvider<double>((ref) => 50);
class SpotubeNavigationBar extends HookConsumerWidget { class SpotubeNavigationBar extends HookConsumerWidget {
@ -23,10 +23,9 @@ class SpotubeNavigationBar extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final routerState = GoRouterState.of(context); final mediaQuery = MediaQuery.of(context);
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount; final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
final mediaQuery = MediaQuery.of(context);
final layoutMode = final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
@ -37,13 +36,13 @@ class SpotubeNavigationBar extends HookConsumerWidget {
final panelHeight = ref.watch(navigationPanelHeight); final panelHeight = ref.watch(navigationPanelHeight);
final selectedIndex = useMemoized(() { final router = context.watchRouter;
final index = navbarTileList.indexWhere( final selectedIndex = max(
(e) => routerState.namedLocation(e.name) == routerState.matchedLocation, 0,
); navbarTileList.indexWhere(
(e) => router.currentPath.startsWith(e.pathPrefix),
return index == -1 ? 0 : index; ),
}, [navbarTileList, routerState.matchedLocation]); );
if (layoutMode == LayoutMode.extended || if (layoutMode == LayoutMode.extended ||
(mediaQuery.mdAndUp && layoutMode == LayoutMode.adaptive) || (mediaQuery.mdAndUp && layoutMode == LayoutMode.adaptive) ||
@ -63,7 +62,7 @@ class SpotubeNavigationBar extends HookConsumerWidget {
surfaceBlur: context.theme.surfaceBlur, surfaceBlur: context.theme.surfaceBlur,
surfaceOpacity: context.theme.surfaceOpacity, surfaceOpacity: context.theme.surfaceOpacity,
onSelected: (i) { onSelected: (i) {
ServiceUtils.navigateNamed(context, navbarTileList[i].name); context.navigateTo(navbarTileList[i].route);
}, },
children: [ children: [
for (final tile in navbarTileList) for (final tile in navbarTileList)

View File

@ -1,12 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/modules/album/album_card.dart'; import 'package:spotube/modules/album/album_card.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/album/album.dart';
import 'package:spotube/utils/service_utils.dart';
class StatsAlbumItem extends StatelessWidget { class StatsAlbumItem extends StatelessWidget {
final AlbumSimple album; final AlbumSimple album;
@ -36,25 +36,15 @@ class StatsAlbumItem extends StatelessWidget {
child: ArtistLink( child: ArtistLink(
artists: album.artists ?? [], artists: album.artists ?? [],
mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
onOverflowArtistClick: () => ServiceUtils.pushNamed( onOverflowArtistClick: () =>
context, context.navigateTo(AlbumRoute(id: album.id!, album: album)),
AlbumPage.name,
pathParameters: {
"id": album.id!,
},
),
), ),
), ),
], ],
), ),
trailing: info, trailing: info,
onPressed: () { onPressed: () {
ServiceUtils.pushNamed( context.navigateTo(AlbumRoute(id: album.id!, album: album));
context,
AlbumPage.name,
pathParameters: {"id": album.id!},
extra: album,
);
}, },
); );
} }

View File

@ -1,10 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/utils/service_utils.dart';
class StatsArtistItem extends StatelessWidget { class StatsArtistItem extends StatelessWidget {
final Artist artist; final Artist artist;
@ -30,11 +30,7 @@ class StatsArtistItem extends StatelessWidget {
), ),
trailing: info, trailing: info,
onPressed: () { onPressed: () {
ServiceUtils.pushNamed( context.navigateTo(ArtistRoute(artistId: artist.id!));
context,
ArtistPage.name,
pathParameters: {"id": artist.id!},
);
}, },
); );
} }

View File

@ -1,11 +1,11 @@
import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.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/pages/playlist/playlist.dart';
import 'package:spotube/utils/service_utils.dart';
class StatsPlaylistItem extends StatelessWidget { class StatsPlaylistItem extends StatelessWidget {
final PlaylistSimple playlist; final PlaylistSimple playlist;
@ -35,12 +35,7 @@ class StatsPlaylistItem extends StatelessWidget {
), ),
trailing: info, trailing: info,
onPressed: () { onPressed: () {
ServiceUtils.pushNamed( context.navigateTo(PlaylistRoute(id: playlist.id!, playlist: playlist));
context,
PlaylistPage.name,
pathParameters: {"id": playlist.id!},
extra: playlist,
);
}, },
); );
} }

View File

@ -1,11 +1,11 @@
import 'package:auto_route/auto_route.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/utils/service_utils.dart';
class StatsTrackItem extends StatelessWidget { class StatsTrackItem extends StatelessWidget {
final Track track; final Track track;
@ -34,23 +34,13 @@ class StatsTrackItem extends StatelessWidget {
subtitle: ArtistLink( subtitle: ArtistLink(
artists: track.artists!, artists: track.artists!,
mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
onOverflowArtistClick: () => ServiceUtils.pushNamed( onOverflowArtistClick: () {
context, context.navigateTo(TrackRoute(trackId: track.id!));
TrackPage.name, },
pathParameters: {
"id": track.id!,
},
),
), ),
trailing: info, trailing: info,
onPressed: () { onPressed: () {
ServiceUtils.pushNamed( context.navigateTo(TrackRoute(trackId: track.id!));
context,
TrackPage.name,
pathParameters: {
"id": track.id!,
},
);
}, },
); );
} }

View File

@ -1,19 +1,14 @@
import 'package:auto_route/auto_route.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:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/modules/stats/summary/summary_card.dart'; import 'package:spotube/modules/stats/summary/summary_card.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/pages/stats/albums/albums.dart';
import 'package:spotube/pages/stats/artists/artists.dart';
import 'package:spotube/pages/stats/fees/fees.dart';
import 'package:spotube/pages/stats/minutes/minutes.dart';
import 'package:spotube/pages/stats/playlists/playlists.dart';
import 'package:spotube/pages/stats/streams/streams.dart';
import 'package:spotube/provider/history/summary.dart'; import 'package:spotube/provider/history/summary.dart';
import 'package:spotube/utils/service_utils.dart';
class StatsPageSummarySection extends HookConsumerWidget { class StatsPageSummarySection extends HookConsumerWidget {
const StatsPageSummarySection({super.key}); const StatsPageSummarySection({super.key});
@ -50,7 +45,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
description: context.l10n.summary_listened_to_music, description: context.l10n.summary_listened_to_music,
color: Colors.indigo, color: Colors.indigo,
onTap: () { onTap: () {
ServiceUtils.pushNamed(context, StatsMinutesPage.name); context.navigateTo(const StatsMinutesRoute());
}, },
), ),
SummaryCard( SummaryCard(
@ -59,7 +54,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
description: context.l10n.summary_streamed_overall, description: context.l10n.summary_streamed_overall,
color: Colors.blue, color: Colors.blue,
onTap: () { onTap: () {
ServiceUtils.pushNamed(context, StatsStreamsPage.name); context.navigateTo(const StatsStreamsRoute());
}, },
), ),
SummaryCard.unformatted( SummaryCard.unformatted(
@ -68,7 +63,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
description: context.l10n.summary_owed_to_artists, description: context.l10n.summary_owed_to_artists,
color: Colors.green, color: Colors.green,
onTap: () { onTap: () {
ServiceUtils.pushNamed(context, StatsStreamFeesPage.name); context.navigateTo(const StatsStreamsRoute());
}, },
), ),
SummaryCard( SummaryCard(
@ -77,7 +72,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
description: context.l10n.summary_music_reached_you, description: context.l10n.summary_music_reached_you,
color: Colors.yellow, color: Colors.yellow,
onTap: () { onTap: () {
ServiceUtils.pushNamed(context, StatsArtistsPage.name); context.navigateTo(const StatsArtistsRoute());
}, },
), ),
SummaryCard( SummaryCard(
@ -86,7 +81,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
description: context.l10n.summary_got_your_love, description: context.l10n.summary_got_your_love,
color: Colors.pink, color: Colors.pink,
onTap: () { onTap: () {
ServiceUtils.pushNamed(context, StatsAlbumsPage.name); context.navigateTo(const StatsAlbumsRoute());
}, },
), ),
SummaryCard( SummaryCard(
@ -95,7 +90,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
description: context.l10n.summary_were_on_repeat, description: context.l10n.summary_were_on_repeat,
color: Colors.teal, color: Colors.teal,
onTap: () { onTap: () {
ServiceUtils.pushNamed(context, StatsPlaylistsPage.name); context.navigateTo(const StatsPlaylistsRoute());
}, },
), ),
]), ]),

View File

@ -56,6 +56,7 @@ class StatsPageTopSection extends HookConsumerWidget {
floating: true, floating: true,
elevation: 0, elevation: 0,
backgroundColor: context.theme.colorScheme.background, backgroundColor: context.theme.colorScheme.background,
automaticallyImplyLeading: false,
flexibleSpace: Padding( flexibleSpace: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.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:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -7,12 +8,15 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
@RoutePage()
class AlbumPage extends HookConsumerWidget { class AlbumPage extends HookConsumerWidget {
static const name = "album"; static const name = "album";
final AlbumSimple album; final AlbumSimple album;
final String id;
const AlbumPage({ const AlbumPage({
super.key, super.key,
@PathParam("id") required this.id,
required this.album, required this.album,
}); });

View File

@ -13,12 +13,17 @@ import 'package:spotube/pages/artist/section/header.dart';
import 'package:spotube/pages/artist/section/related_artists.dart'; import 'package:spotube/pages/artist/section/related_artists.dart';
import 'package:spotube/pages/artist/section/top_tracks.dart'; import 'package:spotube/pages/artist/section/top_tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class ArtistPage extends HookConsumerWidget { class ArtistPage extends HookConsumerWidget {
static const name = "artist"; static const name = "artist";
final String artistId; final String artistId;
const ArtistPage(this.artistId, {super.key}); const ArtistPage(
@PathParam("id") this.artistId, {
super.key,
});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -1,14 +1,15 @@
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/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/modules/connect/local_devices.dart'; import 'package:spotube/modules/connect/local_devices.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/connect/control/control.dart';
import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:auto_route/auto_route.dart';
@RoutePage()
class ConnectPage extends HookConsumerWidget { class ConnectPage extends HookConsumerWidget {
static const name = "connect"; static const name = "connect";
@ -22,68 +23,65 @@ class ConnectPage extends HookConsumerWidget {
final connectClientsNotifier = ref.read(connectClientsProvider.notifier); final connectClientsNotifier = ref.read(connectClientsProvider.notifier);
final discoveredDevices = connectClients.asData?.value.services; final discoveredDevices = connectClients.asData?.value.services;
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
automaticallyImplyLeading: true, headers: [
title: Text(context.l10n.devices), TitleBar(title: Text(context.l10n.devices)),
) ],
], child: Padding(
child: Padding( padding: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0), child: CustomScrollView(
child: CustomScrollView( slivers: [
slivers: [ SliverPadding(
SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0), sliver: SliverToBoxAdapter(
sliver: SliverToBoxAdapter( child: Text(
child: Text( context.l10n.remote,
context.l10n.remote, style: typography.bold,
style: typography.bold, ),
), ),
), ),
), const SliverGap(10),
const SliverGap(10), SliverList.separated(
SliverList.separated( itemCount: discoveredDevices?.length ?? 0,
itemCount: discoveredDevices?.length ?? 0, separatorBuilder: (context, index) => const Gap(10),
separatorBuilder: (context, index) => const Gap(10), itemBuilder: (context, index) {
itemBuilder: (context, index) { final device = discoveredDevices![index];
final device = discoveredDevices![index]; final selected =
final selected = connectClients.asData?.value.resolvedService?.name ==
connectClients.asData?.value.resolvedService?.name == device.name;
device.name; return ButtonTile(
return ButtonTile( selected: selected,
selected: selected, leading: const Icon(SpotubeIcons.monitor),
leading: const Icon(SpotubeIcons.monitor), title: Text(device.name),
title: Text(device.name), subtitle: selected
subtitle: selected ? Text(
? Text( "${connectClients.asData?.value.resolvedService?.host}"
"${connectClients.asData?.value.resolvedService?.host}" ":${connectClients.asData?.value.resolvedService?.port}",
":${connectClients.asData?.value.resolvedService?.port}", )
) : null,
: null, trailing: selected
trailing: selected ? IconButton.outline(
? IconButton.outline( icon: const Icon(SpotubeIcons.power),
icon: const Icon(SpotubeIcons.power), size: ButtonSize.small,
size: ButtonSize.small, onPressed: () =>
onPressed: () => connectClientsNotifier.clearResolvedService(),
connectClientsNotifier.clearResolvedService(), )
) : null,
: null, onPressed: () {
onPressed: () { if (selected) {
if (selected) { context.navigateTo(const ConnectControlRoute());
ServiceUtils.pushNamed( } else {
context, connectClientsNotifier.resolveService(device);
ConnectControlPage.name, }
); },
} else { );
connectClientsNotifier.resolveService(device); },
} ),
}, const ConnectPageLocalDevices(),
); ],
}, ),
),
const ConnectPageLocalDevices(),
],
), ),
), ),
); );

View File

@ -1,7 +1,8 @@
import 'package:go_router/go_router.dart'; import 'package:auto_route/auto_route.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:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/modules/player/volume_slider.dart'; import 'package:spotube/modules/player/volume_slider.dart';
@ -13,11 +14,9 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/connect/clients.dart'; import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:media_kit/media_kit.dart' hide Track; import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/utils/service_utils.dart';
class RemotePlayerQueue extends ConsumerWidget { class RemotePlayerQueue extends ConsumerWidget {
const RemotePlayerQueue({super.key}); const RemotePlayerQueue({super.key});
@ -46,6 +45,7 @@ class RemotePlayerQueue extends ConsumerWidget {
} }
} }
@RoutePage()
class ConnectControlPage extends HookConsumerWidget { class ConnectControlPage extends HookConsumerWidget {
static const name = "connect_control"; static const name = "connect_control";
@ -65,7 +65,7 @@ class ConnectControlPage extends HookConsumerWidget {
ref.listen(connectClientsProvider, (prev, next) { ref.listen(connectClientsProvider, (prev, next) {
if (next.asData?.value.resolvedService == null) { if (next.asData?.value.resolvedService == null) {
context.pop(); context.back();
} }
}); });
@ -75,7 +75,6 @@ class ConnectControlPage extends HookConsumerWidget {
headers: [ headers: [
TitleBar( TitleBar(
title: Text(resolvedService!.name), title: Text(resolvedService!.name),
automaticallyImplyLeading: true,
) )
], ],
child: LayoutBuilder(builder: (context, constrains) { child: LayoutBuilder(builder: (context, constrains) {
@ -115,12 +114,9 @@ class ConnectControlPage extends HookConsumerWidget {
style: typography.h4, style: typography.h4,
onTap: () { onTap: () {
if (playlist.activeTrack == null) return; if (playlist.activeTrack == null) return;
ServiceUtils.pushNamed( context.navigateTo(
context, TrackRoute(
TrackPage.name, trackId: playlist.activeTrack!.id!),
pathParameters: {
"id": playlist.activeTrack!.id!,
},
); );
}, },
), ),
@ -130,13 +126,8 @@ class ConnectControlPage extends HookConsumerWidget {
artists: playlist.activeTrack?.artists ?? [], artists: playlist.activeTrack?.artists ?? [],
textStyle: typography.normal, textStyle: typography.normal,
mainAxisAlignment: WrapAlignment.start, mainAxisAlignment: WrapAlignment.start,
onOverflowArtistClick: () => onOverflowArtistClick: () => context.navigateTo(
ServiceUtils.pushNamed( TrackRoute(trackId: playlist.activeTrack!.id!),
context,
TrackPage.name,
pathParameters: {
"id": playlist.activeTrack!.id!,
},
), ),
), ),
), ),

View File

@ -8,11 +8,13 @@ import 'package:spotube/pages/getting_started/sections/greeting.dart';
import 'package:spotube/pages/getting_started/sections/playback.dart'; import 'package:spotube/pages/getting_started/sections/playback.dart';
import 'package:spotube/pages/getting_started/sections/region.dart'; import 'package:spotube/pages/getting_started/sections/region.dart';
import 'package:spotube/pages/getting_started/sections/support.dart'; import 'package:spotube/pages/getting_started/sections/support.dart';
import 'package:auto_route/auto_route.dart';
class GettingStarting extends HookConsumerWidget { @RoutePage()
class GettingStartedPage extends HookConsumerWidget {
static const name = "getting_started"; static const name = "getting_started";
const GettingStarting({super.key}); const GettingStartedPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -1,11 +1,11 @@
import 'package:go_router/go_router.dart'; import 'package:auto_route/auto_route.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/collections/env.dart'; import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/modules/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/mobile_login/hooks/login_callback.dart'; import 'package:spotube/pages/mobile_login/hooks/login_callback.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -112,7 +112,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
onPressed: () async { onPressed: () async {
await KVStoreService.setDoneGettingStarted(true); await KVStoreService.setDoneGettingStarted(true);
if (context.mounted) { if (context.mounted) {
context.goNamed(HomePage.name); context.navigateTo(const HomeRoute());
} }
}, },
child: Text(context.l10n.browse_anonymously), child: Text(context.l10n.browse_anonymously),

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.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';
@ -10,11 +11,15 @@ import 'package:spotube/modules/playlist/playlist_card.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/provider/spotify/views/home_section.dart'; import 'package:spotube/provider/spotify/views/home_section.dart';
@RoutePage()
class HomeFeedSectionPage extends HookConsumerWidget { class HomeFeedSectionPage extends HookConsumerWidget {
static const name = "home_feed_section"; static const name = "home_feed_section";
final String sectionUri; final String sectionUri;
const HomeFeedSectionPage({super.key, required this.sectionUri}); const HomeFeedSectionPage({
super.key,
@PathParam("feedId") required this.sectionUri,
});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -23,68 +28,71 @@ class HomeFeedSectionPage extends HookConsumerWidget {
final controller = useScrollController(); final controller = useScrollController();
final isArtist = section.items.every((item) => item.artist != null); final isArtist = section.items.every((item) => item.artist != null);
return Skeletonizer( return SafeArea(
enabled: homeFeedSection.isLoading, bottom: false,
child: Scaffold( child: Skeletonizer(
headers: [ enabled: homeFeedSection.isLoading,
TitleBar( child: Scaffold(
title: Text(section.title ?? ""), headers: [
automaticallyImplyLeading: true, TitleBar(
) title: Text(section.title ?? ""),
], )
child: Padding( ],
padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Padding(
child: CustomScrollView( padding: const EdgeInsets.symmetric(horizontal: 8.0),
controller: controller, child: CustomScrollView(
slivers: [ controller: controller,
if (isArtist) slivers: [
SliverGrid.builder( if (isArtist)
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( SliverGrid.builder(
maxCrossAxisExtent: 200, gridDelegate:
mainAxisExtent: 250, const SliverGridDelegateWithMaxCrossAxisExtent(
crossAxisSpacing: 8, maxCrossAxisExtent: 200,
mainAxisSpacing: 8, mainAxisExtent: 250,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: section.items.length,
itemBuilder: (context, index) {
final item = section.items[index];
return ArtistCard(item.artist!.asArtist);
},
)
else
PlaybuttonView(
controller: controller,
itemCount: section.items.length,
hasMore: false,
isLoading: false,
onRequestMore: () => {},
listItemBuilder: (context, index) {
final item = section.items[index];
if (item.album != null) {
return AlbumCard.tile(item.album!.asAlbum);
}
if (item.playlist != null) {
return PlaylistCard.tile(item.playlist!.asPlaylist);
}
return const SizedBox.shrink();
},
gridItemBuilder: (context, index) {
final item = section.items[index];
if (item.album != null) {
return AlbumCard(item.album!.asAlbum);
}
if (item.playlist != null) {
return PlaylistCard(item.playlist!.asPlaylist);
}
return const SizedBox.shrink();
},
),
const SliverToBoxAdapter(
child: SafeArea(
child: SizedBox(),
), ),
itemCount: section.items.length,
itemBuilder: (context, index) {
final item = section.items[index];
return ArtistCard(item.artist!.asArtist);
},
)
else
PlaybuttonView(
controller: controller,
itemCount: section.items.length,
hasMore: false,
isLoading: false,
onRequestMore: () => {},
listItemBuilder: (context, index) {
final item = section.items[index];
if (item.album != null) {
return AlbumCard.tile(item.album!.asAlbum);
}
if (item.playlist != null) {
return PlaylistCard.tile(item.playlist!.asPlaylist);
}
return const SizedBox.shrink();
},
gridItemBuilder: (context, index) {
final item = section.items[index];
if (item.album != null) {
return AlbumCard(item.album!.asAlbum);
}
if (item.playlist != null) {
return PlaylistCard(item.playlist!.asPlaylist);
}
return const SizedBox.shrink();
},
), ),
const SliverToBoxAdapter( ],
child: SafeArea( ),
child: SizedBox(),
),
),
],
), ),
), ),
), ),

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart' show CollapseMode, FlexibleSpaceBar; import 'package:flutter/material.dart' show CollapseMode, FlexibleSpaceBar;
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:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotify/spotify.dart' hide Offset; import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/playbutton_view/playbutton_view.dart'; import 'package:spotube/components/playbutton_view/playbutton_view.dart';
import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
@ -16,12 +16,19 @@ import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class GenrePlaylistsPage extends HookConsumerWidget { class GenrePlaylistsPage extends HookConsumerWidget {
static const name = "genre_playlists"; static const name = "genre_playlists";
final Category category; final Category category;
const GenrePlaylistsPage({super.key, required this.category}); final String id;
const GenrePlaylistsPage({
super.key,
@PathParam("categoryId") required this.id,
required this.category,
});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -30,102 +37,106 @@ class GenrePlaylistsPage extends HookConsumerWidget {
final playlistsNotifier = final playlistsNotifier =
ref.read(categoryPlaylistsProvider(category.id!).notifier); ref.read(categoryPlaylistsProvider(category.id!).notifier);
final scrollController = useScrollController(); final scrollController = useScrollController();
final routeName = GoRouterState.of(context).name;
useCustomStatusBarColor( useCustomStatusBarColor(
Colors.black, Colors.black,
routeName == GenrePlaylistsPage.name, context.watchRouter.topRoute.name == GenrePlaylistsRoute.name,
noSetBGColor: true, noSetBGColor: true,
automaticSystemUiAdjustment: false, automaticSystemUiAdjustment: false,
); );
return Scaffold( return SafeArea(
headers: [ child: Scaffold(
if (kIsDesktop) headers: [
const TitleBar( if (kIsDesktop)
leading: [ const TitleBar(
BackButton(), leading: [
], BackButton(),
backgroundColor: Colors.transparent, ],
surfaceOpacity: 0, backgroundColor: Colors.transparent,
surfaceBlur: 0, surfaceOpacity: 0,
) surfaceBlur: 0,
], )
floatingHeader: true, ],
child: DecoratedBox( floatingHeader: true,
decoration: BoxDecoration( child: DecoratedBox(
image: DecorationImage( decoration: BoxDecoration(
image: UniversalImage.imageProvider(category.icons!.first.url!), image: DecorationImage(
alignment: Alignment.topCenter, image: UniversalImage.imageProvider(category.icons!.first.url!),
fit: BoxFit.cover, alignment: Alignment.topCenter,
repeat: ImageRepeat.noRepeat, fit: BoxFit.cover,
matchTextDirection: true, repeat: ImageRepeat.noRepeat,
matchTextDirection: true,
),
), ),
), child: SurfaceCard(
child: SurfaceCard( borderRadius: BorderRadius.zero,
borderRadius: BorderRadius.zero, padding: EdgeInsets.zero,
padding: EdgeInsets.zero, child: CustomScrollView(
child: CustomScrollView( controller: scrollController,
controller: scrollController, slivers: [
slivers: [ SliverSafeArea(
SliverAppBar( bottom: false,
automaticallyImplyLeading: false, sliver: SliverAppBar(
leading: kIsMobile ? const BackButton() : null, automaticallyImplyLeading: false,
expandedHeight: mediaQuery.mdAndDown ? 200 : 150, leading: kIsMobile ? const BackButton() : null,
title: const Text(""), expandedHeight: mediaQuery.mdAndDown ? 200 : 150,
backgroundColor: Colors.transparent, title: const Text(""),
flexibleSpace: FlexibleSpaceBar( backgroundColor: Colors.transparent,
centerTitle: kIsDesktop, flexibleSpace: FlexibleSpaceBar(
title: Text( centerTitle: kIsDesktop,
category.name!, title: Text(
style: context.theme.typography.h3.copyWith( category.name!,
color: Colors.white, style: context.theme.typography.h3.copyWith(
letterSpacing: 3, color: Colors.white,
shadows: [ letterSpacing: 3,
Shadow( shadows: [
offset: const Offset(-1.5, -1.5), Shadow(
color: Colors.black.withAlpha(138), offset: const Offset(-1.5, -1.5),
color: Colors.black.withAlpha(138),
),
Shadow(
offset: const Offset(1.5, -1.5),
color: Colors.black.withAlpha(138),
),
Shadow(
offset: const Offset(1.5, 1.5),
color: Colors.black.withAlpha(138),
),
Shadow(
offset: const Offset(-1.5, 1.5),
color: Colors.black.withAlpha(138),
),
],
), ),
Shadow( ),
offset: const Offset(1.5, -1.5), collapseMode: CollapseMode.parallax,
color: Colors.black.withAlpha(138),
),
Shadow(
offset: const Offset(1.5, 1.5),
color: Colors.black.withAlpha(138),
),
Shadow(
offset: const Offset(-1.5, 1.5),
color: Colors.black.withAlpha(138),
),
],
), ),
), ),
collapseMode: CollapseMode.parallax,
), ),
), const SliverGap(20),
const SliverGap(20), SliverSafeArea(
SliverSafeArea( top: false,
top: false, sliver: SliverPadding(
sliver: SliverPadding( padding: EdgeInsets.symmetric(
padding: EdgeInsets.symmetric( horizontal: mediaQuery.mdAndDown ? 12 : 24,
horizontal: mediaQuery.mdAndDown ? 12 : 24, ),
), sliver: PlaybuttonView(
sliver: PlaybuttonView( controller: scrollController,
controller: scrollController, itemCount: playlists.asData?.value.items.length ?? 0,
itemCount: playlists.asData?.value.items.length ?? 0, isLoading: playlists.isLoading,
isLoading: playlists.isLoading, hasMore: playlists.asData?.value.hasMore == true,
hasMore: playlists.asData?.value.hasMore == true, onRequestMore: playlistsNotifier.fetchMore,
onRequestMore: playlistsNotifier.fetchMore, listItemBuilder: (context, index) => PlaylistCard.tile(
listItemBuilder: (context, index) => playlists.asData!.value.items[index]),
PlaylistCard.tile(playlists.asData!.value.items[index]), gridItemBuilder: (context, index) =>
gridItemBuilder: (context, index) => PlaylistCard(playlists.asData!.value.items[index]),
PlaylistCard(playlists.asData!.value.items[index]), ),
), ),
), ),
), const SliverGap(20),
const SliverGap(20), ],
], ),
), ),
), ),
), ),

View File

@ -3,17 +3,19 @@ import 'dart:math';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
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:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/gradients.dart'; import 'package:spotube/collections/gradients.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.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/pages/home/genres/genre_playlists.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class GenrePage extends HookConsumerWidget { class GenrePage extends HookConsumerWidget {
static const name = "genre"; static const name = "genre";
const GenrePage({super.key}); const GenrePage({super.key});
@ -30,7 +32,6 @@ class GenrePage extends HookConsumerWidget {
headers: [ headers: [
TitleBar( TitleBar(
title: Text(context.l10n.explore_genres), title: Text(context.l10n.explore_genres),
automaticallyImplyLeading: true,
) )
], ],
child: GridView.builder( child: GridView.builder(
@ -49,12 +50,11 @@ class GenrePage extends HookConsumerWidget {
final gradient = gradients[Random().nextInt(gradients.length)]; final gradient = gradients[Random().nextInt(gradients.length)];
return CardImage( return CardImage(
onPressed: () { onPressed: () {
context.pushNamed( context.navigateTo(
GenrePlaylistsPage.name, GenrePlaylistsRoute(
pathParameters: { id: category.id!,
"categoryId": category.id!, category: category,
}, ),
extra: category,
); );
}, },
image: Stack( image: Stack(

View File

@ -1,9 +1,11 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.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:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/connect/connect_device.dart'; import 'package:spotube/modules/connect/connect_device.dart';
@ -16,11 +18,10 @@ import 'package:spotube/modules/home/sections/new_releases.dart';
import 'package:spotube/modules/home/sections/recent.dart'; import 'package:spotube/modules/home/sections/recent.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
@RoutePage()
class HomePage extends HookConsumerWidget { class HomePage extends HookConsumerWidget {
static const name = "home"; static const name = "home";
const HomePage({super.key}); const HomePage({super.key});
@ -53,7 +54,7 @@ class HomePage extends HookConsumerWidget {
IconButton.ghost( IconButton.ghost(
icon: const Icon(SpotubeIcons.settings, size: 20), icon: const Icon(SpotubeIcons.settings, size: 20),
onPressed: () { onPressed: () {
ServiceUtils.pushNamed(context, SettingsPage.name); context.navigateTo(const SettingsRoute());
}, },
), ),
const Gap(10), const Gap(10),

View File

@ -1,6 +1,5 @@
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/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
@ -9,14 +8,15 @@ import 'package:spotube/components/dialogs/prompt_dialog.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class LastFMLoginPage extends HookConsumerWidget { class LastFMLoginPage extends HookConsumerWidget {
static const name = "lastfm_login"; static const name = "lastfm_login";
const LastFMLoginPage({super.key}); const LastFMLoginPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final router = GoRouter.of(context);
final scrobblerNotifier = ref.read(scrobblerProvider.notifier); final scrobblerNotifier = ref.read(scrobblerProvider.notifier);
final usernameKey = final usernameKey =
@ -31,6 +31,7 @@ class LastFMLoginPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
headers: const [ headers: const [
SafeArea( SafeArea(
bottom: false,
child: TitleBar( child: TitleBar(
leading: [BackButton()], leading: [BackButton()],
), ),
@ -39,100 +40,104 @@ class LastFMLoginPage extends HookConsumerWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( Flexible(
constraints: const BoxConstraints(maxWidth: 400), child: Container(
alignment: Alignment.center, constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(16), alignment: Alignment.center,
child: Card( padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16.0), child: Card(
child: Form( padding: const EdgeInsets.all(16.0),
onSubmit: (context, values) async { child: Form(
try { onSubmit: (context, values) async {
isLoading.value = true; try {
await scrobblerNotifier.login( isLoading.value = true;
values[usernameKey].trim(), await scrobblerNotifier.login(
values[passwordKey], values[usernameKey].trim(),
); values[passwordKey],
router.pop();
} catch (e) {
if (context.mounted) {
showPromptDialog(
context: context,
title: context.l10n.error("Authentication failed"),
message: e.toString(),
cancelText: null,
); );
if (context.mounted) {
context.back();
}
} catch (e) {
if (context.mounted) {
showPromptDialog(
context: context,
title: context.l10n.error("Authentication failed"),
message: e.toString(),
cancelText: null,
);
}
} finally {
isLoading.value = false;
} }
} finally { },
isLoading.value = false; child: Column(
} mainAxisSize: MainAxisSize.min,
}, spacing: 10,
child: Column( children: [
mainAxisSize: MainAxisSize.min, Container(
spacing: 10, decoration: BoxDecoration(
children: [ borderRadius: BorderRadius.circular(30),
Container( color: const Color.fromARGB(255, 186, 0, 0),
decoration: BoxDecoration( ),
borderRadius: BorderRadius.circular(30), padding: const EdgeInsets.all(12),
color: const Color.fromARGB(255, 186, 0, 0), child: const Icon(
SpotubeIcons.lastFm,
color: Colors.white,
size: 60,
),
), ),
padding: const EdgeInsets.all(12), const Text("last.fm").h3(),
child: const Icon( Text(context.l10n.login_with_your_lastfm),
SpotubeIcons.lastFm, AutofillGroup(
color: Colors.white, child: Column(
size: 60, spacing: 10,
), children: [
), FormField(
const Text("last.fm").h3(), label: Text(context.l10n.username),
Text(context.l10n.login_with_your_lastfm), key: usernameKey,
AutofillGroup( validator: const NotEmptyValidator(),
child: Column( child: TextField(
spacing: 10, autofillHints: const [
children: [ AutofillHints.username,
FormField( AutofillHints.email,
label: Text(context.l10n.username), ],
key: usernameKey, placeholder: Text(context.l10n.username),
validator: const NotEmptyValidator(),
child: TextField(
autofillHints: const [
AutofillHints.username,
AutofillHints.email,
],
placeholder: Text(context.l10n.username),
),
),
FormField(
key: passwordKey,
validator: const NotEmptyValidator(),
label: Text(context.l10n.password),
child: TextField(
autofillHints: const [
AutofillHints.password,
],
obscureText: !passwordVisible.value,
placeholder: Text(context.l10n.password),
trailing: IconButton.ghost(
icon: Icon(
passwordVisible.value
? SpotubeIcons.eye
: SpotubeIcons.noEye,
),
onPressed: () => passwordVisible.value =
!passwordVisible.value,
), ),
), ),
), FormField(
], key: passwordKey,
validator: const NotEmptyValidator(),
label: Text(context.l10n.password),
child: TextField(
autofillHints: const [
AutofillHints.password,
],
obscureText: !passwordVisible.value,
placeholder: Text(context.l10n.password),
trailing: IconButton.ghost(
icon: Icon(
passwordVisible.value
? SpotubeIcons.eye
: SpotubeIcons.noEye,
),
onPressed: () => passwordVisible.value =
!passwordVisible.value,
),
),
),
],
),
), ),
), FormErrorBuilder(builder: (context, errors, child) {
FormErrorBuilder(builder: (context, errors, child) { return Button.primary(
return Button.primary( onPressed: () => context.submitForm(),
onPressed: () => context.submitForm(), enabled: errors.isEmpty && !isLoading.value,
enabled: errors.isEmpty && !isLoading.value, child: Text(context.l10n.login),
child: Text(context.l10n.login), );
); }),
}), ],
], ),
), ),
), ),
), ),

View File

@ -1,73 +1,81 @@
import 'package:auto_route/auto_route.dart';
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/collections/routes.gr.dart';
import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.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/pages/library/user_downloads.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
@RoutePage()
class LibraryPage extends HookConsumerWidget { class LibraryPage extends HookConsumerWidget {
final Widget child; const LibraryPage({super.key});
const LibraryPage({super.key, required this.child});
@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 routerState = GoRouterState.of(context); final router = context.watchRouter;
final sidebarLibraryTileList = useMemoized( final sidebarLibraryTileList = useMemoized(
() => [ () => [
...getSidebarLibraryTileList(context.l10n), ...getSidebarLibraryTileList(context.l10n),
SideBarTiles( SideBarTiles(
id: "downloads", id: "downloads",
pathPrefix: "library/downloads",
title: context.l10n.downloads, title: context.l10n.downloads,
name: UserDownloadsPage.name, route: const UserDownloadsRoute(),
icon: SpotubeIcons.download, icon: SpotubeIcons.download,
), ),
], ],
[context.l10n], [context.l10n],
); );
final index = sidebarLibraryTileList.indexWhere( final index = sidebarLibraryTileList.indexWhere(
(e) => routerState.namedLocation(e.name) == routerState.matchedLocation, (e) => router.currentPath.startsWith(e.pathPrefix),
); );
return SafeArea( return PopScope(
bottom: false, canPop: false,
child: LayoutBuilder(builder: (context, constraints) { onPopInvokedWithResult: (didPop, result) {
return Scaffold( context.navigateTo(const HomeRoute());
headers: [ },
if (constraints.smAndDown) child: SafeArea(
TitleBar( bottom: false,
child: SingleChildScrollView( child: LayoutBuilder(builder: (context, constraints) {
scrollDirection: Axis.horizontal, return Scaffold(
child: TabList( headers: [
index: index, if (constraints.smAndDown)
children: [ TitleBar(
for (final tile in sidebarLibraryTileList) automaticallyImplyLeading: false,
TabButton( child: SingleChildScrollView(
child: Badge( scrollDirection: Axis.horizontal,
isLabelVisible: child: TabList(
tile.id == 'downloads' && downloadingCount > 0, index: index,
label: Text(downloadingCount.toString()), children: [
child: Text(tile.title), for (final tile in sidebarLibraryTileList)
TabButton(
child: Badge(
isLabelVisible: tile.id == 'downloads' &&
downloadingCount > 0,
label: Text(downloadingCount.toString()),
child: Text(tile.title),
),
onPressed: () {
context.navigateTo(tile.route);
},
), ),
onPressed: () { ],
context.goNamed(tile.name); ),
},
),
],
), ),
), ),
), const Gap(10),
const Gap(10), ],
], child: const AutoRouter(),
child: child, );
); }),
}), ),
); );
} }
} }

View File

@ -1,10 +1,11 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
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:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotify_markets.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/components/button/back_button.dart';
@ -23,9 +24,11 @@ import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:auto_route/auto_route.dart';
const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0); const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0);
@RoutePage()
class PlaylistGeneratorPage extends HookConsumerWidget { class PlaylistGeneratorPage extends HookConsumerWidget {
static const name = "playlist_generator"; static const name = "playlist_generator";
@ -253,425 +256,430 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
final controller = useScrollController(); final controller = useScrollController();
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
leading: const [BackButton()], headers: [
title: Text(context.l10n.generate), TitleBar(
) leading: const [BackButton()],
], title: Text(context.l10n.generate),
child: Scrollbar( )
controller: controller, ],
child: Center( child: Scrollbar(
child: ConstrainedBox( controller: controller,
constraints: BoxConstraints(maxWidth: Breakpoints.lg), child: Center(
child: SafeArea( child: ConstrainedBox(
child: LayoutBuilder(builder: (context, constrains) { constraints: BoxConstraints(maxWidth: Breakpoints.lg),
return ScrollConfiguration( child: SafeArea(
behavior: ScrollConfiguration.of(context) child: LayoutBuilder(builder: (context, constrains) {
.copyWith(scrollbars: false), return ScrollConfiguration(
child: ListView( behavior: ScrollConfiguration.of(context)
controller: controller, .copyWith(scrollbars: false),
padding: const EdgeInsets.all(16), child: ListView(
children: [ controller: controller,
ValueListenableBuilder( padding: const EdgeInsets.all(16),
valueListenable: limit, children: [
builder: (context, value, child) { ValueListenableBuilder(
return Column( valueListenable: limit,
crossAxisAlignment: CrossAxisAlignment.start, builder: (context, value, child) {
children: [ return Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
context.l10n.number_of_tracks_generate, children: [
style: typography.semiBold, Text(
), context.l10n.number_of_tracks_generate,
Row( style: typography.semiBold,
spacing: 5, ),
children: [ Row(
Container( spacing: 5,
width: 40, children: [
height: 40, Container(
alignment: Alignment.center, width: 40,
decoration: BoxDecoration( height: 40,
color: theme.colorScheme.primary alignment: Alignment.center,
.withAlpha(25), decoration: BoxDecoration(
shape: BoxShape.circle, color: theme.colorScheme.primary
), .withAlpha(25),
child: Text( shape: BoxShape.circle,
value.round().toString(), ),
style: typography.large.copyWith( child: Text(
color: theme.colorScheme.primary, value.round().toString(),
style: typography.large.copyWith(
color: theme.colorScheme.primary,
),
), ),
), ),
), Expanded(
Expanded( child: Slider(
child: Slider( value: SliderValue.single(
value: value.toDouble()),
SliderValue.single(value.toDouble()), min: 10,
min: 10, max: 100,
max: 100, divisions: 9,
divisions: 9, onChanged: (value) {
onChanged: (value) { limit.value = value.value.round();
limit.value = value.value.round(); },
}, ),
), )
) ],
], )
) ],
);
},
),
const SizedBox(height: 16),
if (constrains.mdAndUp)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: countrySelector,
),
const SizedBox(width: 16),
Expanded(
child: genreSelector,
),
], ],
); )
}, else ...[
), countrySelector,
const SizedBox(height: 16), const SizedBox(height: 16),
if (constrains.mdAndUp) genreSelector,
Row( ],
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: countrySelector,
),
const SizedBox(width: 16),
Expanded(
child: genreSelector,
),
],
)
else ...[
countrySelector,
const SizedBox(height: 16), const SizedBox(height: 16),
genreSelector, if (constrains.mdAndUp)
], Row(
const SizedBox(height: 16), crossAxisAlignment: CrossAxisAlignment.start,
if (constrains.mdAndUp) children: [
Row( Expanded(
crossAxisAlignment: CrossAxisAlignment.start, child: artistAutoComplete,
children: [ ),
Expanded( const SizedBox(width: 16),
child: artistAutoComplete, Expanded(
), child: tracksAutocomplete,
const SizedBox(width: 16), ),
Expanded( ],
child: tracksAutocomplete, )
), else ...[
], artistAutoComplete,
) const SizedBox(height: 16),
else ...[ tracksAutocomplete,
artistAutoComplete, ],
const SizedBox(height: 16), const SizedBox(height: 16),
tracksAutocomplete, RecommendationAttributeDials(
], title: Text(context.l10n.acousticness),
const SizedBox(height: 16), values: (
RecommendationAttributeDials( target: target.value.acousticness?.toDouble() ?? 0,
title: Text(context.l10n.acousticness), min: min.value.acousticness?.toDouble() ?? 0,
values: ( max: max.value.acousticness?.toDouble() ?? 0,
target: target.value.acousticness?.toDouble() ?? 0,
min: min.value.acousticness?.toDouble() ?? 0,
max: max.value.acousticness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
acousticness: value.target,
);
min.value = min.value.copyWith(
acousticness: value.min,
);
max.value = max.value.copyWith(
acousticness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.danceability),
values: (
target: target.value.danceability?.toDouble() ?? 0,
min: min.value.danceability?.toDouble() ?? 0,
max: max.value.danceability?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
danceability: value.target,
);
min.value = min.value.copyWith(
danceability: value.min,
);
max.value = max.value.copyWith(
danceability: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.energy),
values: (
target: target.value.energy?.toDouble() ?? 0,
min: min.value.energy?.toDouble() ?? 0,
max: max.value.energy?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
energy: value.target,
);
min.value = min.value.copyWith(
energy: value.min,
);
max.value = max.value.copyWith(
energy: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.instrumentalness),
values: (
target:
target.value.instrumentalness?.toDouble() ?? 0,
min: min.value.instrumentalness?.toDouble() ?? 0,
max: max.value.instrumentalness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
instrumentalness: value.target,
);
min.value = min.value.copyWith(
instrumentalness: value.min,
);
max.value = max.value.copyWith(
instrumentalness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.liveness),
values: (
target: target.value.liveness?.toDouble() ?? 0,
min: min.value.liveness?.toDouble() ?? 0,
max: max.value.liveness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
liveness: value.target,
);
min.value = min.value.copyWith(
liveness: value.min,
);
max.value = max.value.copyWith(
liveness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.loudness),
values: (
target: target.value.loudness?.toDouble() ?? 0,
min: min.value.loudness?.toDouble() ?? 0,
max: max.value.loudness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
loudness: value.target,
);
min.value = min.value.copyWith(
loudness: value.min,
);
max.value = max.value.copyWith(
loudness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.speechiness),
values: (
target: target.value.speechiness?.toDouble() ?? 0,
min: min.value.speechiness?.toDouble() ?? 0,
max: max.value.speechiness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
speechiness: value.target,
);
min.value = min.value.copyWith(
speechiness: value.min,
);
max.value = max.value.copyWith(
speechiness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.valence),
values: (
target: target.value.valence?.toDouble() ?? 0,
min: min.value.valence?.toDouble() ?? 0,
max: max.value.valence?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
valence: value.target,
);
min.value = min.value.copyWith(
valence: value.min,
);
max.value = max.value.copyWith(
valence: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.popularity),
base: 100,
values: (
target: target.value.popularity?.toDouble() ?? 0,
min: min.value.popularity?.toDouble() ?? 0,
max: max.value.popularity?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
popularity: value.target,
);
min.value = min.value.copyWith(
popularity: value.min,
);
max.value = max.value.copyWith(
popularity: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.key),
base: 11,
values: (
target: target.value.key?.toDouble() ?? 0,
min: min.value.key?.toDouble() ?? 0,
max: max.value.key?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
key: value.target,
);
min.value = min.value.copyWith(
key: value.min,
);
max.value = max.value.copyWith(
key: value.max,
);
},
),
RecommendationAttributeFields(
title: Text(context.l10n.duration),
values: (
max: (max.value.durationMs ?? 0) / 1000,
target: (target.value.durationMs ?? 0) / 1000,
min: (min.value.durationMs ?? 0) / 1000,
),
onChanged: (value) {
target.value = target.value.copyWith(
durationMs: (value.target * 1000).toInt(),
);
min.value = min.value.copyWith(
durationMs: (value.min * 1000).toInt(),
);
max.value = max.value.copyWith(
durationMs: (value.max * 1000).toInt(),
);
},
presets: {
context.l10n.short: (min: 50, target: 90, max: 120),
context.l10n.medium: (
min: 120,
target: 180,
max: 200
), ),
context.l10n.long: (min: 480, target: 560, max: 640) onChanged: (value) {
}, target.value = target.value.copyWith(
), acousticness: value.target,
RecommendationAttributeFields( );
title: Text(context.l10n.tempo), min.value = min.value.copyWith(
values: ( acousticness: value.min,
max: max.value.tempo?.toDouble() ?? 0, );
target: target.value.tempo?.toDouble() ?? 0, max.value = max.value.copyWith(
min: min.value.tempo?.toDouble() ?? 0, acousticness: value.max,
);
},
), ),
onChanged: (value) { RecommendationAttributeDials(
target.value = target.value.copyWith( title: Text(context.l10n.danceability),
tempo: value.target, values: (
); target: target.value.danceability?.toDouble() ?? 0,
min.value = min.value.copyWith( min: min.value.danceability?.toDouble() ?? 0,
tempo: value.min, max: max.value.danceability?.toDouble() ?? 0,
); ),
max.value = max.value.copyWith( onChanged: (value) {
tempo: value.max, target.value = target.value.copyWith(
); danceability: value.target,
}, );
), min.value = min.value.copyWith(
RecommendationAttributeFields( danceability: value.min,
title: Text(context.l10n.mode), );
values: ( max.value = max.value.copyWith(
max: max.value.mode?.toDouble() ?? 0, danceability: value.max,
target: target.value.mode?.toDouble() ?? 0, );
min: min.value.mode?.toDouble() ?? 0, },
), ),
onChanged: (value) { RecommendationAttributeDials(
target.value = target.value.copyWith( title: Text(context.l10n.energy),
mode: value.target, values: (
); target: target.value.energy?.toDouble() ?? 0,
min.value = min.value.copyWith( min: min.value.energy?.toDouble() ?? 0,
mode: value.min, max: max.value.energy?.toDouble() ?? 0,
); ),
max.value = max.value.copyWith( onChanged: (value) {
mode: value.max, target.value = target.value.copyWith(
); energy: value.target,
}, );
), min.value = min.value.copyWith(
RecommendationAttributeFields( energy: value.min,
title: Text(context.l10n.time_signature), );
values: ( max.value = max.value.copyWith(
max: max.value.timeSignature?.toDouble() ?? 0, energy: value.max,
target: target.value.timeSignature?.toDouble() ?? 0, );
min: min.value.timeSignature?.toDouble() ?? 0, },
), ),
onChanged: (value) { RecommendationAttributeDials(
target.value = target.value.copyWith( title: Text(context.l10n.instrumentalness),
timeSignature: value.target, values: (
); target:
min.value = min.value.copyWith( target.value.instrumentalness?.toDouble() ?? 0,
timeSignature: value.min, min: min.value.instrumentalness?.toDouble() ?? 0,
); max: max.value.instrumentalness?.toDouble() ?? 0,
max.value = max.value.copyWith( ),
timeSignature: value.max, onChanged: (value) {
); target.value = target.value.copyWith(
}, instrumentalness: value.target,
), );
const Gap(20), min.value = min.value.copyWith(
Center( instrumentalness: value.min,
child: Button.primary( );
leading: const Icon(SpotubeIcons.magic), max.value = max.value.copyWith(
onPressed: artists.value.isEmpty && instrumentalness: value.max,
tracks.value.isEmpty && );
genres.value.isEmpty },
? null
: () {
final routeState =
GeneratePlaylistProviderInput(
seedArtists: artists.value
.map((a) => a.id!)
.toList(),
seedTracks:
tracks.value.map((t) => t.id!).toList(),
seedGenres: genres.value,
limit: limit.value,
max: max.value,
min: min.value,
target: target.value,
);
GoRouter.of(context).push(
"/library/generate/result",
extra: routeState,
);
},
child: Text(context.l10n.generate),
), ),
), RecommendationAttributeDials(
], title: Text(context.l10n.liveness),
), values: (
); target: target.value.liveness?.toDouble() ?? 0,
}), min: min.value.liveness?.toDouble() ?? 0,
max: max.value.liveness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
liveness: value.target,
);
min.value = min.value.copyWith(
liveness: value.min,
);
max.value = max.value.copyWith(
liveness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.loudness),
values: (
target: target.value.loudness?.toDouble() ?? 0,
min: min.value.loudness?.toDouble() ?? 0,
max: max.value.loudness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
loudness: value.target,
);
min.value = min.value.copyWith(
loudness: value.min,
);
max.value = max.value.copyWith(
loudness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.speechiness),
values: (
target: target.value.speechiness?.toDouble() ?? 0,
min: min.value.speechiness?.toDouble() ?? 0,
max: max.value.speechiness?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
speechiness: value.target,
);
min.value = min.value.copyWith(
speechiness: value.min,
);
max.value = max.value.copyWith(
speechiness: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.valence),
values: (
target: target.value.valence?.toDouble() ?? 0,
min: min.value.valence?.toDouble() ?? 0,
max: max.value.valence?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
valence: value.target,
);
min.value = min.value.copyWith(
valence: value.min,
);
max.value = max.value.copyWith(
valence: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.popularity),
base: 100,
values: (
target: target.value.popularity?.toDouble() ?? 0,
min: min.value.popularity?.toDouble() ?? 0,
max: max.value.popularity?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
popularity: value.target,
);
min.value = min.value.copyWith(
popularity: value.min,
);
max.value = max.value.copyWith(
popularity: value.max,
);
},
),
RecommendationAttributeDials(
title: Text(context.l10n.key),
base: 11,
values: (
target: target.value.key?.toDouble() ?? 0,
min: min.value.key?.toDouble() ?? 0,
max: max.value.key?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
key: value.target,
);
min.value = min.value.copyWith(
key: value.min,
);
max.value = max.value.copyWith(
key: value.max,
);
},
),
RecommendationAttributeFields(
title: Text(context.l10n.duration),
values: (
max: (max.value.durationMs ?? 0) / 1000,
target: (target.value.durationMs ?? 0) / 1000,
min: (min.value.durationMs ?? 0) / 1000,
),
onChanged: (value) {
target.value = target.value.copyWith(
durationMs: (value.target * 1000).toInt(),
);
min.value = min.value.copyWith(
durationMs: (value.min * 1000).toInt(),
);
max.value = max.value.copyWith(
durationMs: (value.max * 1000).toInt(),
);
},
presets: {
context.l10n.short: (min: 50, target: 90, max: 120),
context.l10n.medium: (
min: 120,
target: 180,
max: 200
),
context.l10n.long: (min: 480, target: 560, max: 640)
},
),
RecommendationAttributeFields(
title: Text(context.l10n.tempo),
values: (
max: max.value.tempo?.toDouble() ?? 0,
target: target.value.tempo?.toDouble() ?? 0,
min: min.value.tempo?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
tempo: value.target,
);
min.value = min.value.copyWith(
tempo: value.min,
);
max.value = max.value.copyWith(
tempo: value.max,
);
},
),
RecommendationAttributeFields(
title: Text(context.l10n.mode),
values: (
max: max.value.mode?.toDouble() ?? 0,
target: target.value.mode?.toDouble() ?? 0,
min: min.value.mode?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
mode: value.target,
);
min.value = min.value.copyWith(
mode: value.min,
);
max.value = max.value.copyWith(
mode: value.max,
);
},
),
RecommendationAttributeFields(
title: Text(context.l10n.time_signature),
values: (
max: max.value.timeSignature?.toDouble() ?? 0,
target: target.value.timeSignature?.toDouble() ?? 0,
min: min.value.timeSignature?.toDouble() ?? 0,
),
onChanged: (value) {
target.value = target.value.copyWith(
timeSignature: value.target,
);
min.value = min.value.copyWith(
timeSignature: value.min,
);
max.value = max.value.copyWith(
timeSignature: value.max,
);
},
),
const Gap(20),
Center(
child: Button.primary(
leading: const Icon(SpotubeIcons.magic),
onPressed: artists.value.isEmpty &&
tracks.value.isEmpty &&
genres.value.isEmpty
? null
: () {
final routeState =
GeneratePlaylistProviderInput(
seedArtists: artists.value
.map((a) => a.id!)
.toList(),
seedTracks: tracks.value
.map((t) => t.id!)
.toList(),
seedGenres: genres.value,
limit: limit.value,
max: max.value,
min: min.value,
target: target.value,
);
context.navigateTo(
PlaylistGenerateResultRoute(
state: routeState,
),
);
},
child: Text(context.l10n.generate),
),
),
],
),
);
}),
),
), ),
), ),
), ),

View File

@ -1,8 +1,10 @@
import 'package:auto_route/auto_route.dart';
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:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.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/components/button/back_button.dart';
import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart'; import 'package:spotube/modules/library/playlist_generate/simple_track_tile.dart';
@ -11,10 +13,10 @@ import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/spotify/recommendation_seeds.dart'; import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
@RoutePage()
class PlaylistGenerateResultPage extends HookConsumerWidget { class PlaylistGenerateResultPage extends HookConsumerWidget {
static const name = "playlist_generate_result"; static const name = "playlist_generate_result";
@ -27,8 +29,6 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final router = GoRouter.of(context);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final generatedPlaylist = ref.watch(generatePlaylistProvider(state)); final generatedPlaylist = ref.watch(generatePlaylistProvider(state));
@ -48,219 +48,225 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
final isAllTrackSelected = selectedTracks.value.length == final isAllTrackSelected = selectedTracks.value.length ==
(generatedPlaylist.asData?.value.length ?? 0); (generatedPlaylist.asData?.value.length ?? 0);
return Scaffold( return SafeArea(
headers: const [ bottom: false,
TitleBar(leading: [BackButton()]) child: Scaffold(
], headers: const [
child: generatedPlaylist.isLoading TitleBar(leading: [BackButton()])
? Center( ],
child: Column( child: generatedPlaylist.isLoading
mainAxisAlignment: MainAxisAlignment.center, ? Center(
crossAxisAlignment: CrossAxisAlignment.center, child: Column(
children: [ mainAxisAlignment: MainAxisAlignment.center,
const CircularProgressIndicator(), crossAxisAlignment: CrossAxisAlignment.center,
Text(context.l10n.generating_playlist), children: [
], const CircularProgressIndicator(),
), Text(context.l10n.generating_playlist),
) ],
: Padding( ),
padding: const EdgeInsets.all(8.0), )
child: ListView( : Padding(
children: [ padding: const EdgeInsets.all(8.0),
GridView( child: ListView(
gridDelegate: children: [
const SliverGridDelegateWithFixedCrossAxisCount( GridView(
crossAxisCount: 2, gridDelegate:
crossAxisSpacing: 8, const SliverGridDelegateWithFixedCrossAxisCount(
mainAxisSpacing: 8, crossAxisCount: 2,
mainAxisExtent: 32, crossAxisSpacing: 8,
), mainAxisSpacing: 8,
shrinkWrap: true, mainAxisExtent: 32,
children: [
Button.primary(
leading: const Icon(SpotubeIcons.play),
onPressed: selectedTracks.value.isEmpty
? null
: () async {
await playlistNotifier.load(
generatedPlaylist.asData!.value
.where(
(e) => selectedTracks.value
.contains(e.id!),
)
.toList(),
autoPlay: true,
);
},
child: Text(context.l10n.play),
), ),
Button.primary( shrinkWrap: true,
leading: const Icon(SpotubeIcons.queueAdd), children: [
onPressed: selectedTracks.value.isEmpty Button.primary(
? null leading: const Icon(SpotubeIcons.play),
: () async { onPressed: selectedTracks.value.isEmpty
await playlistNotifier.addTracks( ? null
generatedPlaylist.asData!.value.where( : () async {
(e) => selectedTracks.value.contains(e.id!), await playlistNotifier.load(
), generatedPlaylist.asData!.value
); .where(
if (context.mounted) { (e) => selectedTracks.value
showToast( .contains(e.id!),
context: context,
location: ToastLocation.topRight,
builder: (context, overlay) {
return SurfaceCard(
child: Text(
context.l10n.add_count_to_queue(
selectedTracks.value.length,
),
),
);
},
);
}
},
child: Text(context.l10n.add_to_queue),
),
Button.primary(
leading: const Icon(SpotubeIcons.addFilled),
onPressed: selectedTracks.value.isEmpty
? null
: () async {
final playlist = await showDialog<Playlist>(
context: context,
builder: (context) => PlaylistCreateDialog(
trackIds: selectedTracks.value,
),
);
if (playlist != null) {
router.goNamed(
PlaylistPage.name,
pathParameters: {
"id": playlist.id!,
},
extra: playlist,
);
}
},
child: Text(context.l10n.create_a_playlist),
),
Button.primary(
leading: const Icon(SpotubeIcons.playlistAdd),
onPressed: selectedTracks.value.isEmpty
? null
: () async {
final hasAdded = await showDialog<bool>(
context: context,
builder: (context) => PlaylistAddTrackDialog(
openFromPlaylist: null,
tracks: selectedTracks.value
.map(
(e) => generatedPlaylist.asData!.value
.firstWhere(
(element) => element.id == e,
),
) )
.toList(), .toList(),
), autoPlay: true,
);
if (context.mounted && hasAdded == true) {
showToast(
context: context,
location: ToastLocation.topRight,
builder: (context, overlay) {
return SurfaceCard(
child: Text(
context.l10n.add_count_to_playlist(
selectedTracks.value.length,
),
),
);
},
); );
} },
}, child: Text(context.l10n.play),
child: Text(context.l10n.add_to_playlist),
)
],
),
const SizedBox(height: 16),
if (generatedPlaylist.asData?.value != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.l10n.selected_count_tracks(
selectedTracks.value.length,
),
), ),
Button.secondary( Button.primary(
onPressed: () { leading: const Icon(SpotubeIcons.queueAdd),
if (isAllTrackSelected) { onPressed: selectedTracks.value.isEmpty
selectedTracks.value = []; ? null
} else { : () async {
selectedTracks.value = generatedPlaylist await playlistNotifier.addTracks(
.asData?.value generatedPlaylist.asData!.value.where(
.map((e) => e.id!) (e) =>
.toList() ?? selectedTracks.value.contains(e.id!),
[]; ),
} );
}, if (context.mounted) {
leading: const Icon(SpotubeIcons.selectionCheck), showToast(
child: Text( context: context,
isAllTrackSelected location: ToastLocation.topRight,
? context.l10n.deselect_all builder: (context, overlay) {
: context.l10n.select_all, return SurfaceCard(
), child: Text(
context.l10n.add_count_to_queue(
selectedTracks.value.length,
),
),
);
},
);
}
},
child: Text(context.l10n.add_to_queue),
), ),
Button.primary(
leading: const Icon(SpotubeIcons.addFilled),
onPressed: selectedTracks.value.isEmpty
? null
: () async {
final playlist = await showDialog<Playlist>(
context: context,
builder: (context) => PlaylistCreateDialog(
trackIds: selectedTracks.value,
),
);
if (playlist != null && context.mounted) {
context.navigateTo(
PlaylistRoute(
id: playlist.id!,
playlist: playlist,
),
);
}
},
child: Text(context.l10n.create_a_playlist),
),
Button.primary(
leading: const Icon(SpotubeIcons.playlistAdd),
onPressed: selectedTracks.value.isEmpty
? null
: () async {
final hasAdded = await showDialog<bool>(
context: context,
builder: (context) =>
PlaylistAddTrackDialog(
openFromPlaylist: null,
tracks: selectedTracks.value
.map(
(e) => generatedPlaylist
.asData!.value
.firstWhere(
(element) => element.id == e,
),
)
.toList(),
),
);
if (context.mounted && hasAdded == true) {
showToast(
context: context,
location: ToastLocation.topRight,
builder: (context, overlay) {
return SurfaceCard(
child: Text(
context.l10n.add_count_to_playlist(
selectedTracks.value.length,
),
),
);
},
);
}
},
child: Text(context.l10n.add_to_playlist),
)
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 16),
SafeArea( if (generatedPlaylist.asData?.value != null)
child: Column( Row(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
for (final track Text(
in generatedPlaylist.asData?.value ?? []) context.l10n.selected_count_tracks(
Row( selectedTracks.value.length,
spacing: 5, ),
children: [ ),
Checkbox( Button.secondary(
state: selectedTracks.value.contains(track.id) onPressed: () {
? CheckboxState.checked if (isAllTrackSelected) {
: CheckboxState.unchecked, selectedTracks.value = [];
onChanged: (value) { } else {
if (value == CheckboxState.checked) { selectedTracks.value = generatedPlaylist
selectedTracks.value.add(track.id!); .asData?.value
} else { .map((e) => e.id!)
selectedTracks.value.remove(track.id); .toList() ??
} [];
selectedTracks.value = }
selectedTracks.value.toList(); },
}, leading: const Icon(SpotubeIcons.selectionCheck),
), child: Text(
Expanded( isAllTrackSelected
child: GestureDetector( ? context.l10n.deselect_all
onTap: () { : context.l10n.select_all,
selectedTracks.value.contains(track.id) ),
? selectedTracks.value.remove(track.id) ),
: selectedTracks.value.add(track.id!); ],
),
const SizedBox(height: 8),
SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final track
in generatedPlaylist.asData?.value ?? [])
Row(
spacing: 5,
children: [
Checkbox(
state: selectedTracks.value.contains(track.id)
? CheckboxState.checked
: CheckboxState.unchecked,
onChanged: (value) {
if (value == CheckboxState.checked) {
selectedTracks.value.add(track.id!);
} else {
selectedTracks.value.remove(track.id);
}
selectedTracks.value = selectedTracks.value =
selectedTracks.value.toList(); selectedTracks.value.toList();
}, },
child: SimpleTrackTile(track: track),
), ),
), Expanded(
], child: GestureDetector(
) onTap: () {
], selectedTracks.value.contains(track.id)
? selectedTracks.value
.remove(track.id)
: selectedTracks.value.add(track.id!);
selectedTracks.value =
selectedTracks.value.toList();
},
child: SimpleTrackTile(track: track),
),
),
],
)
],
),
), ),
), ],
], ),
), ),
), ),
); );
} }
} }

View File

@ -15,7 +15,9 @@ import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
import 'package:spotube/extensions/context.dart'; 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';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class UserAlbumsPage extends HookConsumerWidget { class UserAlbumsPage extends HookConsumerWidget {
static const name = 'user_albums'; static const name = 'user_albums';
const UserAlbumsPage({super.key}); const UserAlbumsPage({super.key});
@ -63,6 +65,7 @@ class UserAlbumsPage extends HookConsumerWidget {
controller: controller, controller: controller,
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
automaticallyImplyLeading: false,
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
floating: true, floating: true,
flexibleSpace: Padding( flexibleSpace: Padding(

View File

@ -18,7 +18,9 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; 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';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class UserArtistsPage extends HookConsumerWidget { class UserArtistsPage extends HookConsumerWidget {
static const name = 'user_artists'; static const name = 'user_artists';
const UserArtistsPage({super.key}); const UserArtistsPage({super.key});
@ -70,6 +72,7 @@ class UserArtistsPage extends HookConsumerWidget {
controller: controller, controller: controller,
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
automaticallyImplyLeading: false,
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
floating: true, floating: true,
flexibleSpace: SizedBox( flexibleSpace: SizedBox(

View File

@ -5,7 +5,9 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/modules/library/user_downloads/download_item.dart'; 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';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class UserDownloadsPage extends HookConsumerWidget { class UserDownloadsPage extends HookConsumerWidget {
static const name = 'user_downloads'; static const name = 'user_downloads';
const UserDownloadsPage({super.key}); const UserDownloadsPage({super.key});

View File

@ -29,7 +29,9 @@ import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class LocalLibraryPage extends HookConsumerWidget { class LocalLibraryPage extends HookConsumerWidget {
static const name = "local_library_page"; static const name = "local_library_page";

View File

@ -1,3 +1,4 @@
import 'package:auto_route/auto_route.dart';
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_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -24,6 +25,7 @@ enum SortBy {
album, album,
} }
@RoutePage()
class UserLocalLibraryPage extends HookConsumerWidget { class UserLocalLibraryPage extends HookConsumerWidget {
static const name = 'user_local_library'; static const name = 'user_local_library';
const UserLocalLibraryPage({super.key}); const UserLocalLibraryPage({super.key});

View File

@ -7,6 +7,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image;
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/playbutton_view/playbutton_view.dart'; import 'package:spotube/components/playbutton_view/playbutton_view.dart';
import 'package:spotube/modules/playlist/playlist_create_dialog.dart'; import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
@ -14,12 +15,12 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
import 'package:spotube/modules/playlist/playlist_card.dart'; import 'package:spotube/modules/playlist/playlist_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.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';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:auto_route/auto_route.dart';
@RoutePage()
class UserPlaylistsPage extends HookConsumerWidget { class UserPlaylistsPage extends HookConsumerWidget {
static const name = 'user_playlists'; static const name = 'user_playlists';
const UserPlaylistsPage({super.key}); const UserPlaylistsPage({super.key});
@ -90,6 +91,7 @@ class UserPlaylistsPage extends HookConsumerWidget {
controller: controller, controller: controller,
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
automaticallyImplyLeading: false,
floating: true, floating: true,
backgroundColor: context.theme.colorScheme.background, backgroundColor: context.theme.colorScheme.background,
flexibleSpace: Container( flexibleSpace: Container(
@ -113,10 +115,7 @@ class UserPlaylistsPage extends HookConsumerWidget {
leading: const Icon(SpotubeIcons.magic), leading: const Icon(SpotubeIcons.magic),
child: Text(context.l10n.generate), child: Text(context.l10n.generate),
onPressed: () { onPressed: () {
ServiceUtils.pushNamed( context.navigateTo(const PlaylistGeneratorRoute());
context,
PlaylistGeneratorPage.name,
);
}, },
), ),
const Gap(10), const Gap(10),

View File

@ -14,7 +14,9 @@ import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class LyricsPage extends HookConsumerWidget { class LyricsPage extends HookConsumerWidget {
static const name = "lyrics"; static const name = "lyrics";

View File

@ -1,9 +1,10 @@
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:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.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:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/modules/player/player_controls.dart';
import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/player_queue.dart';
@ -14,7 +15,9 @@ import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class MiniLyricsPage extends HookConsumerWidget { class MiniLyricsPage extends HookConsumerWidget {
static const name = "mini_lyrics"; static const name = "mini_lyrics";
@ -265,7 +268,7 @@ class MiniLyricsPage extends HookConsumerWidget {
const Duration(milliseconds: 200)); const Duration(milliseconds: 200));
} finally { } finally {
if (context.mounted) { if (context.mounted) {
GoRouter.of(context).go('/lyrics'); context.navigateTo(LyricsRoute());
} }
} }
}, },

View File

@ -1,14 +1,15 @@
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide join; import 'package:shadcn_flutter/shadcn_flutter.dart' hide join;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
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:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/pages/mobile_login/no_webview_runtime_dialog.dart'; import 'package:spotube/pages/mobile_login/no_webview_runtime_dialog.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -20,7 +21,7 @@ Future<void> Function() useLoginCallback(WidgetRef ref) {
return useCallback(() async { return useCallback(() async {
if (kIsMobile || kIsMacOS) { if (kIsMobile || kIsMacOS) {
context.pushNamed(WebViewLogin.name); context.navigateTo(const WebViewLoginRoute());
return; return;
} }
@ -57,7 +58,7 @@ Future<void> Function() useLoginCallback(WidgetRef ref) {
webview.close(); webview.close();
if (context.mounted) { if (context.mounted) {
context.go("/"); context.navigateTo(const HomeRoute());
} }
}); });
} }
@ -76,5 +77,5 @@ Future<void> Function() useLoginCallback(WidgetRef ref) {
}); });
} }
} }
}, [authNotifier, theme, context.go, context.pushNamed]); }, [authNotifier, theme, context.navigateTo]);
} }

View File

@ -1,16 +1,19 @@
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:auto_route/auto_route.dart';
class WebViewLogin extends HookConsumerWidget { @RoutePage()
class WebViewLoginPage extends HookConsumerWidget {
static const name = "login"; static const name = "login";
const WebViewLogin({super.key}); const WebViewLoginPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -24,50 +27,53 @@ class WebViewLogin extends HookConsumerWidget {
); );
} }
return Scaffold( return SafeArea(
headers: const [ bottom: false,
TitleBar( child: Scaffold(
leading: [BackButton(color: Colors.white)], headers: const [
backgroundColor: Colors.transparent, TitleBar(
), leading: [BackButton(color: Colors.white)],
], backgroundColor: Colors.transparent,
floatingHeader: true, ),
child: InAppWebView( ],
initialSettings: InAppWebViewSettings( floatingHeader: true,
userAgent: child: InAppWebView(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36", initialSettings: InAppWebViewSettings(
), userAgent:
initialUrlRequest: URLRequest( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36",
url: WebUri("https://accounts.spotify.com/"), ),
), initialUrlRequest: URLRequest(
onPermissionRequest: (controller, permissionRequest) async { url: WebUri("https://accounts.spotify.com/"),
return PermissionResponse( ),
resources: permissionRequest.resources, onPermissionRequest: (controller, permissionRequest) async {
action: PermissionResponseAction.GRANT, return PermissionResponse(
); resources: permissionRequest.resources,
}, action: PermissionResponseAction.GRANT,
onLoadStop: (controller, action) async { );
if (action == null) return; },
String url = action.toString(); onLoadStop: (controller, action) async {
if (url.endsWith("/")) { if (action == null) return;
url = url.substring(0, url.length - 1); String url = action.toString();
} if (url.endsWith("/")) {
url = url.substring(0, url.length - 1);
final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status");
if (exp.hasMatch(url)) {
final cookies =
await CookieManager.instance().getCookies(url: action);
final cookieHeader =
"sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}";
await authenticationNotifier.login(cookieHeader);
if (context.mounted) {
// ignore: use_build_context_synchronously
GoRouter.of(context).go("/");
} }
}
}, final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status");
if (exp.hasMatch(url)) {
final cookies =
await CookieManager.instance().getCookies(url: action);
final cookieHeader =
"sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}";
await authenticationNotifier.login(cookieHeader);
if (context.mounted) {
// ignore: use_build_context_synchronously
context.navigateTo(const HomeRoute());
}
}
},
),
), ),
); );
} }

View File

@ -5,7 +5,9 @@ import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/components/track_presentation/track_presentation.dart'; import 'package:spotube/components/track_presentation/track_presentation.dart';
import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class LikedPlaylistPage extends HookConsumerWidget { class LikedPlaylistPage extends HookConsumerWidget {
static const name = PlaylistPage.name; static const name = PlaylistPage.name;

View File

@ -9,13 +9,17 @@ import 'package:spotube/components/track_presentation/use_is_user_playlist.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/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class PlaylistPage extends HookConsumerWidget { class PlaylistPage extends HookConsumerWidget {
static const name = "playlist"; static const name = "playlist";
final PlaylistSimple _playlist; final PlaylistSimple _playlist;
final String id;
const PlaylistPage({ const PlaylistPage({
super.key, super.key,
@PathParam("id") required this.id,
required PlaylistSimple playlist, required PlaylistSimple playlist,
}) : _playlist = playlist; }) : _playlist = playlist;

View File

@ -12,7 +12,9 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class ProfilePage extends HookConsumerWidget { class ProfilePage extends HookConsumerWidget {
static const name = "profile"; static const name = "profile";
@ -42,7 +44,6 @@ class ProfilePage extends HookConsumerWidget {
headers: [ headers: [
TitleBar( TitleBar(
title: Text(context.l10n.profile), title: Text(context.l10n.profile),
automaticallyImplyLeading: true,
) )
], ],
child: Skeletonizer( child: Skeletonizer(

View File

@ -1,27 +1,19 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
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/collections/side_bar_tiles.dart';
import 'package:spotube/components/framework/app_pop_scope.dart';
import 'package:spotube/modules/root/bottom_player.dart'; import 'package:spotube/modules/root/bottom_player.dart';
import 'package:spotube/modules/root/sidebar.dart'; import 'package:spotube/modules/root/sidebar/sidebar.dart';
import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/configurators/use_endless_playback.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.dart';
import 'package:spotube/modules/root/use_downloader_dialogs.dart'; import 'package:spotube/modules/root/use_downloader_dialogs.dart';
import 'package:spotube/modules/root/use_global_subscriptions.dart'; import 'package:spotube/modules/root/use_global_subscriptions.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/provider/glance/glance.dart'; import 'package:spotube/provider/glance/glance.dart';
import 'package:spotube/utils/platform.dart';
class RootApp extends HookConsumerWidget { @RoutePage()
final Widget child; class RootAppPage extends HookConsumerWidget {
const RootApp({ const RootAppPage({super.key});
required this.child,
super.key,
});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -45,42 +37,19 @@ class RootApp extends HookConsumerWidget {
return null; return null;
}, [backgroundColor, brightness]); }, [backgroundColor, brightness]);
final navTileNames = useMemoized(() {
return getSidebarTileList(context.l10n).map((s) => s.name).toList();
}, []);
final scaffold = MediaQuery.removeViewInsets( final scaffold = MediaQuery.removeViewInsets(
context: context, context: context,
removeBottom: true, removeBottom: true,
child: Scaffold( child: const Scaffold(
footers: const [ footers: [
BottomPlayer(), BottomPlayer(),
SpotubeNavigationBar(), SpotubeNavigationBar(),
], ],
floatingFooter: true, floatingFooter: true,
child: Sidebar(child: child), child: Sidebar(child: AutoRouter()),
), ),
); );
if (!kIsAndroid) { return scaffold;
return scaffold;
}
final topRoute = GoRouterState.of(context).topRoute;
final canPop = topRoute != null && !navTileNames.contains(topRoute.name);
return AppPopScope(
canPop: canPop,
onPopInvoked: (didPop) {
if (didPop) return;
if (topRoute?.name == HomePage.name) {
SystemNavigator.pop();
} else {
context.goNamed(HomePage.name);
}
},
child: scaffold,
);
} }
} }

View File

@ -6,6 +6,7 @@ import 'package:spotify/spotify.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:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
@ -20,7 +21,9 @@ import 'package:spotube/pages/search/sections/tracks.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';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class SearchPage extends HookConsumerWidget { class SearchPage extends HookConsumerWidget {
static const name = "search"; static const name = "search";
@ -66,165 +69,174 @@ class SearchPage extends HookConsumerWidget {
); );
} }
return SafeArea( return PopScope(
bottom: false, canPop: false,
child: Scaffold( onPopInvokedWithResult: (didPop, result) {
headers: [ context.navigateTo(const HomeRoute());
if (kTitlebarVisible) },
const TitleBar(automaticallyImplyLeading: true, height: 30) child: SafeArea(
], bottom: false,
child: auth.asData?.value == null child: Scaffold(
? const AnonymousFallback() headers: [
: Column( if (kTitlebarVisible)
children: [ const TitleBar(automaticallyImplyLeading: false, height: 30)
Row( ],
crossAxisAlignment: CrossAxisAlignment.center, child: auth.asData?.value == null
children: [ ? const AnonymousFallback()
Expanded( : Column(
child: Padding( children: [
padding: const EdgeInsets.all(20), Row(
child: ListenableBuilder( crossAxisAlignment: CrossAxisAlignment.center,
listenable: controller, children: [
builder: (context, _) { Expanded(
final suggestions = controller.text.isEmpty child: Padding(
? KVStoreService.recentSearches padding: const EdgeInsets.all(20),
: KVStoreService.recentSearches child: ListenableBuilder(
.where( listenable: controller,
(s) => builder: (context, _) {
weightedRatio( final suggestions = controller.text.isEmpty
s.toLowerCase(), ? KVStoreService.recentSearches
controller.text.toLowerCase(), : KVStoreService.recentSearches
) > .where(
50, (s) =>
) weightedRatio(
.toList(); s.toLowerCase(),
controller.text.toLowerCase(),
) >
50,
)
.toList();
return KeyboardListener( return KeyboardListener(
focusNode: focusNode, focusNode: focusNode,
autofocus: true,
onKeyEvent: (value) {
final isEnter = value.logicalKey ==
LogicalKeyboardKey.enter;
if (isEnter) {
onSubmitted(controller.text);
focusNode.unfocus();
}
},
child: AutoComplete(
autofocus: true, autofocus: true,
controller: controller, onKeyEvent: (value) {
suggestions: suggestions, final isEnter = value.logicalKey ==
leading: const Icon(SpotubeIcons.search), LogicalKeyboardKey.enter;
textInputAction: TextInputAction.search,
placeholder: Text(context.l10n.search), if (isEnter) {
trailing: AnimatedCrossFade( onSubmitted(controller.text);
duration: focusNode.unfocus();
const Duration(milliseconds: 300), }
crossFadeState: controller.text.isNotEmpty
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: IconButton.ghost(
size: ButtonSize.small,
icon: const Icon(SpotubeIcons.close),
onPressed: () {
controller.clear();
},
),
secondChild:
const SizedBox.square(dimension: 28),
),
onAcceptSuggestion: (index) {
controller.text =
KVStoreService.recentSearches[index];
ref
.read(searchTermStateProvider
.notifier)
.state =
KVStoreService.recentSearches[index];
}, },
onChanged: (value) {}, child: AutoComplete(
onSubmitted: onSubmitted, autofocus: true,
), controller: controller,
); suggestions: suggestions,
}), leading: const Icon(SpotubeIcons.search),
), textInputAction: TextInputAction.search,
), placeholder: Text(context.l10n.search),
], trailing: AnimatedCrossFade(
), duration:
Expanded( const Duration(milliseconds: 300),
child: AnimatedSwitcher( crossFadeState:
duration: const Duration(milliseconds: 300), controller.text.isNotEmpty
child: switch ((searchTerm.isEmpty, isFetching)) { ? CrossFadeState.showFirst
(true, false) => Column( : CrossFadeState.showSecond,
children: [ firstChild: IconButton.ghost(
SizedBox( size: ButtonSize.small,
height: mediaQuery.height * 0.2, icon: const Icon(SpotubeIcons.close),
), onPressed: () {
Undraw( controller.clear();
illustration: UndrawIllustration.explore, },
color: theme.colorScheme.primary, ),
height: 200 * theme.scaling, secondChild: const SizedBox.square(
), dimension: 28),
const SizedBox(height: 20), ),
Text(context.l10n.search_to_get_results).large(), onAcceptSuggestion: (index) {
], controller.text = KVStoreService
.recentSearches[index];
ref
.read(searchTermStateProvider
.notifier)
.state =
KVStoreService
.recentSearches[index];
},
onChanged: (value) {},
onSubmitted: onSubmitted,
),
);
}),
), ),
(false, true) => Container( ),
constraints: BoxConstraints( ],
maxWidth: mediaQuery.lgAndUp ),
? mediaQuery.width * 0.5 Expanded(
: mediaQuery.width, child: AnimatedSwitcher(
), duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric( child: switch ((searchTerm.isEmpty, isFetching)) {
horizontal: 20, (true, false) => Column(
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( SizedBox(
context.l10n.crunching_results, height: mediaQuery.height * 0.2,
style: TextStyle( ),
fontSize: 20, Undraw(
fontWeight: FontWeight.w900, illustration: UndrawIllustration.explore,
color: theme.colorScheme.foreground color: theme.colorScheme.primary,
.withOpacity(0.7), height: 200 * theme.scaling,
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
const LinearProgressIndicator(), Text(context.l10n.search_to_get_results)
.large(),
], ],
), ),
), (false, true) => Container(
_ => InterScrollbar( constraints: BoxConstraints(
controller: scrollController, maxWidth: mediaQuery.lgAndUp
child: SingleChildScrollView( ? mediaQuery.width * 0.5
: mediaQuery.width,
),
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
context.l10n.crunching_results,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: theme.colorScheme.foreground
.withOpacity(0.7),
),
),
const SizedBox(height: 20),
const LinearProgressIndicator(),
],
),
),
_ => InterScrollbar(
controller: scrollController, controller: scrollController,
child: const Padding( child: SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 8), controller: scrollController,
child: SafeArea( child: const Padding(
child: Column( padding: EdgeInsets.symmetric(vertical: 8),
crossAxisAlignment: child: SafeArea(
CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment:
SearchTracksSection(), CrossAxisAlignment.start,
SearchPlaylistsSection(), children: [
Gap(20), SearchTracksSection(),
SearchArtistsSection(), SearchPlaylistsSection(),
Gap(20), Gap(20),
SearchAlbumsSection(), SearchArtistsSection(),
], Gap(20),
SearchAlbumsSection(),
],
),
), ),
), ),
), ),
), ),
), },
}, ),
), ),
), ],
], ),
), ),
), ),
); );
} }

View File

@ -11,15 +11,17 @@ import 'package:spotube/hooks/controllers/use_package_info.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:auto_route/auto_route.dart';
final _licenseProvider = FutureProvider<String>((ref) async { final _licenseProvider = FutureProvider<String>((ref) async {
return await rootBundle.loadString("LICENSE"); return await rootBundle.loadString("LICENSE");
}); });
class AboutSpotube extends HookConsumerWidget { @RoutePage()
class AboutSpotubePage extends HookConsumerWidget {
static const name = "about"; static const name = "about";
const AboutSpotube({super.key}); const AboutSpotubePage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -29,163 +31,166 @@ class AboutSpotube extends HookConsumerWidget {
const colon = TableCell(child: Text(":")); const colon = TableCell(child: Text(":"));
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
leading: const [BackButton()], headers: [
title: Text(context.l10n.about_spotube), TitleBar(
) leading: const [BackButton()],
], title: Text(context.l10n.about_spotube),
child: SingleChildScrollView( )
child: Padding( ],
padding: const EdgeInsets.symmetric(horizontal: 16.0), child: SingleChildScrollView(
child: Column( child: Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 16.0),
Assets.spotubeLogoPng.image( child: Column(
height: 200, children: [
width: 200, Assets.spotubeLogoPng.image(
), height: 200,
Center( width: 200,
child: Column( ),
children: [ Center(
Text(context.l10n.spotube_description).semiBold().large(), child: Column(
const SizedBox(height: 20), children: [
Table( Text(context.l10n.spotube_description).semiBold().large(),
columnWidths: const { const SizedBox(height: 20),
0: FixedTableSize(95), Table(
1: FixedTableSize(10), columnWidths: const {
2: IntrinsicTableSize(), 0: FixedTableSize(95),
}, 1: FixedTableSize(10),
defaultRowHeight: const FixedTableSize(40), 2: IntrinsicTableSize(),
rows: [ },
TableRow( defaultRowHeight: const FixedTableSize(40),
cells: [ rows: [
TableCell(child: Text(context.l10n.founder)), TableRow(
colon, cells: [
TableCell( TableCell(child: Text(context.l10n.founder)),
child: Hyperlink( colon,
context.l10n.kingkor_roy_tirtho, TableCell(
"https://github.com/KRTirtho", child: Hyperlink(
context.l10n.kingkor_roy_tirtho,
"https://github.com/KRTirtho",
),
)
],
),
TableRow(
cells: [
TableCell(child: Text(context.l10n.version)),
colon,
TableCell(child: Text("v${packageInfo.version}"))
],
),
TableRow(
cells: [
TableCell(child: Text(context.l10n.channel)),
colon,
TableCell(child: Text(Env.releaseChannel.name))
],
),
TableRow(
cells: [
TableCell(child: Text(context.l10n.build_number)),
colon,
TableCell(
child: Text(packageInfo.buildNumber
.replaceAll(".", " ")),
)
],
),
TableRow(
cells: [
TableCell(child: Text(context.l10n.repository)),
colon,
const TableCell(
child: Hyperlink(
"github.com/KRTirtho/spotube",
"https://github.com/KRTirtho/spotube",
),
), ),
) ],
], ),
), TableRow(
TableRow( cells: [
cells: [ TableCell(child: Text(context.l10n.license)),
TableCell(child: Text(context.l10n.version)), colon,
colon, const TableCell(
TableCell(child: Text("v${packageInfo.version}")) child: Hyperlink(
], "BSD-4-Clause",
), "https://raw.githubusercontent.com/KRTirtho/spotube/master/LICENSE",
TableRow( ),
cells: [
TableCell(child: Text(context.l10n.channel)),
colon,
TableCell(child: Text(Env.releaseChannel.name))
],
),
TableRow(
cells: [
TableCell(child: Text(context.l10n.build_number)),
colon,
TableCell(
child: Text(
packageInfo.buildNumber.replaceAll(".", " ")),
)
],
),
TableRow(
cells: [
TableCell(child: Text(context.l10n.repository)),
colon,
const TableCell(
child: Hyperlink(
"github.com/KRTirtho/spotube",
"https://github.com/KRTirtho/spotube",
), ),
), ],
], ),
), TableRow(
TableRow( cells: [
cells: [ TableCell(child: Text(context.l10n.bug_issues)),
TableCell(child: Text(context.l10n.license)), colon,
colon, const TableCell(
const TableCell( child: Hyperlink(
child: Hyperlink( "github.com/KRTirtho/spotube/issues",
"BSD-4-Clause", "https://github.com/KRTirtho/spotube/issues",
"https://raw.githubusercontent.com/KRTirtho/spotube/master/LICENSE", ),
), ),
), ],
], ),
), ],
TableRow( ),
cells: [ ],
TableCell(child: Text(context.l10n.bug_issues)), ),
colon, ),
const TableCell( const SizedBox(height: 20),
child: Hyperlink( MouseRegion(
"github.com/KRTirtho/spotube/issues", cursor: SystemMouseCursors.click,
"https://github.com/KRTirtho/spotube/issues", child: GestureDetector(
), onTap: () => launchUrl(
), Uri.parse("https://discord.gg/uJ94vxB6vg"),
], mode: LaunchMode.externalApplication,
), ),
], child: const UniversalImage(
path:
"https://discord.com/api/guilds/1012234096237350943/widget.png?style=banner2",
), ),
],
),
),
const SizedBox(height: 20),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => launchUrl(
Uri.parse("https://discord.gg/uJ94vxB6vg"),
mode: LaunchMode.externalApplication,
),
child: const UniversalImage(
path:
"https://discord.com/api/guilds/1012234096237350943/widget.png?style=banner2",
), ),
), ),
), const SizedBox(height: 20),
const SizedBox(height: 20), Text(
Text( context.l10n.made_with,
context.l10n.made_with, textAlign: TextAlign.center,
textAlign: TextAlign.center, style: theme.typography.small,
style: theme.typography.small, ),
), Text(
Text( context.l10n.copyright(DateTime.now().year),
context.l10n.copyright(DateTime.now().year), textAlign: TextAlign.center,
textAlign: TextAlign.center, style: theme.typography.small,
style: theme.typography.small, ),
), const SizedBox(height: 20),
const SizedBox(height: 20), ConstrainedBox(
ConstrainedBox( constraints: const BoxConstraints(maxWidth: 750),
constraints: const BoxConstraints(maxWidth: 750), child: SafeArea(
child: SafeArea( child: license.when(
child: license.when( data: (data) {
data: (data) { return Text(
return Text( data,
data, style: theme.typography.small,
style: theme.typography.small, );
); },
}, loading: () {
loading: () { return const Center(
return const Center( child: CircularProgressIndicator(),
child: CircularProgressIndicator(), );
); },
}, error: (e, s) {
error: (e, s) { return Text(
return Text( e.toString(),
e.toString(), style: theme.typography.small,
style: theme.typography.small, );
); },
}, ),
), ),
), ),
), ],
], ),
), ),
), ),
), ),

View File

@ -11,7 +11,9 @@ import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class BlackListPage extends HookConsumerWidget { class BlackListPage extends HookConsumerWidget {
static const name = "blacklist"; static const name = "blacklist";
@ -45,50 +47,52 @@ class BlackListPage extends HookConsumerWidget {
[blacklist, searchText.value], [blacklist, searchText.value],
); );
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
title: Text(context.l10n.blacklist), headers: [
leading: const [BackButton()], TitleBar(
) title: Text(context.l10n.blacklist),
], leading: const [BackButton()],
child: Column( )
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
onChanged: (value) => searchText.value = value,
placeholder: Text(context.l10n.search),
leading: const Icon(SpotubeIcons.search),
),
),
InterScrollbar(
controller: controller,
child: ListView.builder(
controller: controller,
shrinkWrap: true,
itemCount: filteredBlacklist.length,
itemBuilder: (context, index) {
final item = filteredBlacklist.elementAt(index);
return ButtonTile(
style: ButtonVariance.ghost,
leading: Text("${index + 1}."),
title: Text("${item.name} (${item.elementType.name})"),
subtitle: Text(item.elementId),
trailing: IconButton.ghost(
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
onPressed: () {
ref
.read(blacklistProvider.notifier)
.remove(filteredBlacklist.elementAt(index).elementId);
},
),
);
},
),
),
], ],
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
onChanged: (value) => searchText.value = value,
placeholder: Text(context.l10n.search),
leading: const Icon(SpotubeIcons.search),
),
),
InterScrollbar(
controller: controller,
child: ListView.builder(
controller: controller,
shrinkWrap: true,
itemCount: filteredBlacklist.length,
itemBuilder: (context, index) {
final item = filteredBlacklist.elementAt(index);
return ButtonTile(
style: ButtonVariance.ghost,
leading: Text("${index + 1}."),
title: Text("${item.name} (${item.elementType.name})"),
subtitle: Text(item.elementId),
trailing: IconButton.ghost(
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
onPressed: () {
ref.read(blacklistProvider.notifier).remove(
filteredBlacklist.elementAt(index).elementId);
},
),
);
},
),
),
],
),
), ),
); );
} }

View File

@ -11,7 +11,9 @@ import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/logs/logs_provider.dart'; import 'package:spotube/provider/logs/logs_provider.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class LogsPage extends HookConsumerWidget { class LogsPage extends HookConsumerWidget {
static const name = "logs"; static const name = "logs";

View File

@ -1,9 +1,11 @@
import 'package:auto_route/auto_route.dart';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart' show ListTile; import 'package:flutter/material.dart' show ListTile;
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' hide ButtonStyle; import 'package:shadcn_flutter/shadcn_flutter.dart' hide ButtonStyle;
import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/components/adaptive/adaptive_list_tile.dart'; import 'package:spotube/components/adaptive/adaptive_list_tile.dart';
@ -88,7 +90,7 @@ class SettingsAboutSection extends HookConsumerWidget {
title: Text(context.l10n.about_spotube), title: Text(context.l10n.about_spotube),
trailing: const Icon(SpotubeIcons.angleRight), trailing: const Icon(SpotubeIcons.angleRight),
onTap: () { onTap: () {
GoRouter.of(context).push("/settings/about"); context.navigateTo(const AboutSpotubeRoute());
}, },
) )
], ],

View File

@ -1,20 +1,20 @@
import 'package:auto_route/auto_route.dart';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart' show ListTile; import 'package:flutter/material.dart' show ListTile;
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/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.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/pages/profile/profile.dart';
import 'package:spotube/pages/mobile_login/hooks/login_callback.dart'; import 'package:spotube/pages/mobile_login/hooks/login_callback.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/service_utils.dart';
class SettingsAccountSection extends HookConsumerWidget { class SettingsAccountSection extends HookConsumerWidget {
const SettingsAccountSection({super.key}); const SettingsAccountSection({super.key});
@ -22,7 +22,6 @@ class SettingsAccountSection extends HookConsumerWidget {
@override @override
Widget build(context, ref) { Widget build(context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final router = GoRouter.of(context);
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final scrobbler = ref.watch(scrobblerProvider); final scrobbler = ref.watch(scrobblerProvider);
@ -50,7 +49,7 @@ class SettingsAccountSection extends HookConsumerWidget {
), ),
), ),
onTap: () { onTap: () {
ServiceUtils.pushNamed(context, ProfilePage.name); context.navigateTo(ProfileRoute());
}, },
), ),
if (auth.asData?.value == null) if (auth.asData?.value == null)
@ -99,7 +98,7 @@ class SettingsAccountSection extends HookConsumerWidget {
trailing: Button.destructive( trailing: Button.destructive(
onPressed: () async { onPressed: () async {
ref.read(authenticationProvider.notifier).logout(); ref.read(authenticationProvider.notifier).logout();
GoRouter.of(context).pop(); context.maybePop();
}, },
child: Text(context.l10n.logout), child: Text(context.l10n.logout),
), ),
@ -113,7 +112,7 @@ class SettingsAccountSection extends HookConsumerWidget {
trailing: Button.secondary( trailing: Button.secondary(
leading: const Icon(SpotubeIcons.lastFm), leading: const Icon(SpotubeIcons.lastFm),
onPressed: () { onPressed: () {
router.push("/lastfm-login"); context.navigateTo(const LastFMLoginRoute());
}, },
child: Text(context.l10n.connect), child: Text(context.l10n.connect),
), ),

View File

@ -1,7 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart' show ListTile; import 'package:flutter/material.dart' show ListTile;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
@ -19,7 +21,7 @@ class SettingsDevelopersSection extends HookWidget {
title: Text(context.l10n.logs), title: Text(context.l10n.logs),
trailing: const Icon(SpotubeIcons.angleRight), trailing: const Icon(SpotubeIcons.angleRight),
onTap: () { onTap: () {
GoRouter.of(context).push("/settings/logs"); context.navigateTo(const LogsRoute());
}, },
) )
], ],

View File

@ -1,12 +1,13 @@
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show ListTile; import 'package:flutter/material.dart' show ListTile;
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:piped_client/piped_client.dart'; import 'package:piped_client/piped_client.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart';
@ -267,7 +268,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
title: Text(context.l10n.blacklist), title: Text(context.l10n.blacklist),
subtitle: Text(context.l10n.blacklist_description), subtitle: Text(context.l10n.blacklist_description),
onTap: () { onTap: () {
GoRouter.of(context).push("/settings/blacklist"); context.navigateTo(const BlackListRoute());
}, },
trailing: const Icon(SpotubeIcons.angleRight), trailing: const Icon(SpotubeIcons.angleRight),
), ),

View File

@ -15,7 +15,9 @@ import 'package:spotube/pages/settings/sections/language_region.dart';
import 'package:spotube/pages/settings/sections/playback.dart'; import 'package:spotube/pages/settings/sections/playback.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class SettingsPage extends HookConsumerWidget { class SettingsPage extends HookConsumerWidget {
static const name = "settings"; static const name = "settings";
@ -32,7 +34,6 @@ class SettingsPage extends HookConsumerWidget {
headers: [ headers: [
TitleBar( TitleBar(
title: Text(context.l10n.settings), title: Text(context.l10n.settings),
automaticallyImplyLeading: true,
) )
], ],
child: Scrollbar( child: Scrollbar(

View File

@ -10,7 +10,9 @@ import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/albums.dart'; import 'package:spotube/provider/history/top/albums.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class StatsAlbumsPage extends HookConsumerWidget { class StatsAlbumsPage extends HookConsumerWidget {
static const name = "stats_albums"; static const name = "stats_albums";
const StatsAlbumsPage({super.key}); const StatsAlbumsPage({super.key});
@ -24,31 +26,33 @@ class StatsAlbumsPage extends HookConsumerWidget {
final albumsData = topAlbums.asData?.value.items ?? []; final albumsData = topAlbums.asData?.value.items ?? [];
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
automaticallyImplyLeading: true, headers: [
title: Text(context.l10n.albums), TitleBar(
) title: Text(context.l10n.albums),
], )
child: Skeletonizer( ],
enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage, child: Skeletonizer(
child: InfiniteList( enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
onFetchData: () async { child: InfiniteList(
await topAlbumsNotifier.fetchMore(); onFetchData: () async {
}, await topAlbumsNotifier.fetchMore();
hasError: topAlbums.hasError, },
isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage, hasError: topAlbums.hasError,
hasReachedMax: topAlbums.asData?.value.hasMore ?? true, isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
itemCount: albumsData.length, hasReachedMax: topAlbums.asData?.value.hasMore ?? true,
itemBuilder: (context, index) { itemCount: albumsData.length,
final album = albumsData[index]; itemBuilder: (context, index) {
return StatsAlbumItem( final album = albumsData[index];
album: album.album, return StatsAlbumItem(
info: Text(context.l10n album: album.album,
.count_plays(compactNumberFormatter.format(album.count))), info: Text(context.l10n
); .count_plays(compactNumberFormatter.format(album.count))),
}, );
},
),
), ),
), ),
); );

View File

@ -11,7 +11,9 @@ import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart'; import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class StatsArtistsPage extends HookConsumerWidget { class StatsArtistsPage extends HookConsumerWidget {
static const name = "stats_artists"; static const name = "stats_artists";
const StatsArtistsPage({super.key}); const StatsArtistsPage({super.key});
@ -27,31 +29,33 @@ class StatsArtistsPage extends HookConsumerWidget {
final artistsData = useMemoized( final artistsData = useMemoized(
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
automaticallyImplyLeading: true, headers: [
title: Text(context.l10n.artists), TitleBar(
) title: Text(context.l10n.artists),
], )
child: Skeletonizer( ],
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, child: Skeletonizer(
child: InfiniteList( enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
onFetchData: () async { child: InfiniteList(
await topTracksNotifier.fetchMore(); onFetchData: () async {
}, await topTracksNotifier.fetchMore();
hasError: topTracks.hasError, },
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, hasError: topTracks.hasError,
hasReachedMax: topTracks.asData?.value.hasMore ?? true, isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
itemCount: artistsData.length, hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemBuilder: (context, index) { itemCount: artistsData.length,
final artist = artistsData[index]; itemBuilder: (context, index) {
return StatsArtistItem( final artist = artistsData[index];
artist: artist.artist, return StatsArtistItem(
info: Text(context.l10n artist: artist.artist,
.count_plays(compactNumberFormatter.format(artist.count))), info: Text(context.l10n
); .count_plays(compactNumberFormatter.format(artist.count))),
}, );
},
),
), ),
), ),
); );

View File

@ -12,7 +12,9 @@ import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart'; import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class StatsStreamFeesPage extends HookConsumerWidget { class StatsStreamFeesPage extends HookConsumerWidget {
static const name = "stats_stream_fees"; static const name = "stats_stream_fees";
@ -48,79 +50,83 @@ class StatsStreamFeesPage extends HookConsumerWidget {
HistoryDuration.allTime: context.l10n.all_time, HistoryDuration.allTime: context.l10n.all_time,
}; };
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
automaticallyImplyLeading: true, headers: [
title: Text(context.l10n.streaming_fees_hypothetical), TitleBar(
) title: Text(context.l10n.streaming_fees_hypothetical),
], )
child: CustomScrollView(
slivers: [
SliverCrossAxisConstrained(
maxCrossAxisExtent: 600,
alignment: -1,
child: SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverToBoxAdapter(
child: Text(
context.l10n.spotify_hipotetical_calculation,
).small().muted(),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.l10n.total_money(usdFormatter.format(total)),
).semiBold().large(),
Select<HistoryDuration>(
value: duration.value,
onChanged: (value) {
if (value == null) return;
duration.value = value;
},
itemBuilder: (context, value) => Text(translations[value]!),
constraints: const BoxConstraints(maxWidth: 150),
popupWidthConstraint: PopoverConstraint.anchorMaxSize,
children: [
for (final entry in translations.entries)
SelectItemButton(
value: entry.key,
child: Text(entry.value),
),
],
),
],
),
),
),
SliverSafeArea(
sliver: Skeletonizer.sliver(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: SliverInfiniteList(
onFetchData: () async {
await topTracksNotifier.fetchMore();
},
hasError: topTracks.hasError,
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemCount: artistsData.length,
itemBuilder: (context, index) {
final artist = artistsData[index];
return StatsArtistItem(
artist: artist.artist,
info: Text(usdFormatter.format(artist.count * 0.005)),
);
},
),
),
),
], ],
child: CustomScrollView(
slivers: [
SliverCrossAxisConstrained(
maxCrossAxisExtent: 600,
alignment: -1,
child: SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverToBoxAdapter(
child: Text(
context.l10n.spotify_hipotetical_calculation,
).small().muted(),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.l10n.total_money(usdFormatter.format(total)),
).semiBold().large(),
Select<HistoryDuration>(
value: duration.value,
onChanged: (value) {
if (value == null) return;
duration.value = value;
},
itemBuilder: (context, value) =>
Text(translations[value]!),
constraints: const BoxConstraints(maxWidth: 150),
popupWidthConstraint: PopoverConstraint.anchorMaxSize,
children: [
for (final entry in translations.entries)
SelectItemButton(
value: entry.key,
child: Text(entry.value),
),
],
),
],
),
),
),
SliverSafeArea(
sliver: Skeletonizer.sliver(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: SliverInfiniteList(
onFetchData: () async {
await topTracksNotifier.fetchMore();
},
hasError: topTracks.hasError,
isLoading:
topTracks.isLoading && !topTracks.isLoadingNextPage,
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemCount: artistsData.length,
itemBuilder: (context, index) {
final artist = artistsData[index];
return StatsArtistItem(
artist: artist.artist,
info: Text(usdFormatter.format(artist.count * 0.005)),
);
},
),
),
),
],
),
), ),
); );
} }

View File

@ -10,7 +10,9 @@ import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart'; import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class StatsMinutesPage extends HookConsumerWidget { class StatsMinutesPage extends HookConsumerWidget {
static const name = "stats_minutes"; static const name = "stats_minutes";
@ -26,34 +28,36 @@ class StatsMinutesPage extends HookConsumerWidget {
final tracksData = topTracks.asData?.value.items ?? []; final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
title: Text(context.l10n.minutes_listened), headers: [
automaticallyImplyLeading: true, TitleBar(
) title: Text(context.l10n.minutes_listened),
], )
child: Skeletonizer( ],
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, child: Skeletonizer(
child: InfiniteList( enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
separatorBuilder: (context, index) => const Gap(8), child: InfiniteList(
onFetchData: () async { separatorBuilder: (context, index) => const Gap(8),
await topTracksNotifier.fetchMore(); onFetchData: () async {
}, await topTracksNotifier.fetchMore();
hasError: topTracks.hasError, },
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, hasError: topTracks.hasError,
hasReachedMax: topTracks.asData?.value.hasMore ?? true, isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
itemCount: tracksData.length, hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemBuilder: (context, index) { itemCount: tracksData.length,
final track = tracksData[index]; itemBuilder: (context, index) {
return StatsTrackItem( final track = tracksData[index];
track: track.track, return StatsTrackItem(
info: Text( track: track.track,
context.l10n.count_mins(compactNumberFormatter info: Text(
.format(track.count * track.track.duration!.inMinutes)), context.l10n.count_mins(compactNumberFormatter
), .format(track.count * track.track.duration!.inMinutes)),
); ),
}, );
},
),
), ),
), ),
); );

View File

@ -10,7 +10,9 @@ import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/playlists.dart'; import 'package:spotube/provider/history/top/playlists.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class StatsPlaylistsPage extends HookConsumerWidget { class StatsPlaylistsPage extends HookConsumerWidget {
static const name = "stats_playlists"; static const name = "stats_playlists";
const StatsPlaylistsPage({super.key}); const StatsPlaylistsPage({super.key});
@ -25,33 +27,36 @@ class StatsPlaylistsPage extends HookConsumerWidget {
final playlistsData = topPlaylists.asData?.value.items ?? []; final playlistsData = topPlaylists.asData?.value.items ?? [];
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
automaticallyImplyLeading: true, headers: [
title: Text(context.l10n.playlists), TitleBar(
) title: Text(context.l10n.playlists),
], )
child: Skeletonizer( ],
enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, child: Skeletonizer(
child: InfiniteList( enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
onFetchData: () async { child: InfiniteList(
await topPlaylistsNotifier.fetchMore(); onFetchData: () async {
}, await topPlaylistsNotifier.fetchMore();
hasError: topPlaylists.hasError, },
isLoading: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, hasError: topPlaylists.hasError,
hasReachedMax: topPlaylists.asData?.value.hasMore ?? true, isLoading:
itemCount: playlistsData.length, topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
itemBuilder: (context, index) { hasReachedMax: topPlaylists.asData?.value.hasMore ?? true,
final playlist = playlistsData[index]; itemCount: playlistsData.length,
return StatsPlaylistItem( itemBuilder: (context, index) {
playlist: playlist.playlist, final playlist = playlistsData[index];
info: Text( return StatsPlaylistItem(
context.l10n playlist: playlist.playlist,
.count_plays(compactNumberFormatter.format(playlist.count)), info: Text(
), context.l10n.count_plays(
); compactNumberFormatter.format(playlist.count)),
}, ),
);
},
),
), ),
), ),
); );

View File

@ -1,10 +1,13 @@
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/routes.gr.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/modules/stats/summary/summary.dart'; import 'package:spotube/modules/stats/summary/summary.dart';
import 'package:spotube/modules/stats/top/top.dart'; import 'package:spotube/modules/stats/top/top.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class StatsPage extends HookConsumerWidget { class StatsPage extends HookConsumerWidget {
static const name = "stats"; static const name = "stats";
@ -12,23 +15,30 @@ class StatsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
return SafeArea( return PopScope(
bottom: false, canPop: false,
child: Scaffold( onPopInvokedWithResult: (didPop, result) {
headers: [ context.navigateTo(const HomeRoute());
if (kTitlebarVisible) const TitleBar(), },
], child: SafeArea(
child: CustomScrollView( bottom: false,
slivers: [ child: Scaffold(
if (kIsMacOS) const SliverGap(20), headers: [
const StatsPageSummarySection(), if (kTitlebarVisible)
const StatsPageTopSection(), const TitleBar(automaticallyImplyLeading: false),
const SliverToBoxAdapter(
child: SafeArea(
child: SizedBox(),
),
)
], ],
child: CustomScrollView(
slivers: [
if (kIsMacOS) const SliverGap(20),
const StatsPageSummarySection(),
const StatsPageTopSection(),
const SliverToBoxAdapter(
child: SafeArea(
child: SizedBox(),
),
)
],
),
), ),
), ),
); );

View File

@ -10,7 +10,9 @@ import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/history/top/tracks.dart'; import 'package:spotube/provider/history/top/tracks.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class StatsStreamsPage extends HookConsumerWidget { class StatsStreamsPage extends HookConsumerWidget {
static const name = "stats_streams"; static const name = "stats_streams";
@ -26,34 +28,36 @@ class StatsStreamsPage extends HookConsumerWidget {
final tracksData = topTracks.asData?.value.items ?? []; final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
title: Text(context.l10n.streamed_songs), headers: [
automaticallyImplyLeading: true, TitleBar(
) title: Text(context.l10n.streamed_songs),
], )
child: Skeletonizer( ],
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, child: Skeletonizer(
child: InfiniteList( enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
separatorBuilder: (context, index) => const Gap(8), child: InfiniteList(
onFetchData: () async { separatorBuilder: (context, index) => const Gap(8),
await topTracksNotifier.fetchMore(); onFetchData: () async {
}, await topTracksNotifier.fetchMore();
hasError: topTracks.hasError, },
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, hasError: topTracks.hasError,
hasReachedMax: topTracks.asData?.value.hasMore ?? true, isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
itemCount: tracksData.length, hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemBuilder: (context, index) { itemCount: tracksData.length,
final track = tracksData[index]; itemBuilder: (context, index) {
return StatsTrackItem( final track = tracksData[index];
track: track.track, return StatsTrackItem(
info: Text( track: track.track,
context.l10n info: Text(
.count_plays(compactNumberFormatter.format(track.count)), context.l10n
), .count_plays(compactNumberFormatter.format(track.count)),
); ),
}, );
},
),
), ),
), ),
); );

View File

@ -1,10 +1,10 @@
import 'dart:ui'; import 'dart:ui';
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:shadcn_flutter/shadcn_flutter.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/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/heart_button/heart_button.dart'; import 'package:spotube/components/heart_button/heart_button.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
@ -20,14 +20,16 @@ import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:auto_route/auto_route.dart';
@RoutePage()
class TrackPage extends HookConsumerWidget { class TrackPage extends HookConsumerWidget {
static const name = "track"; static const name = "track";
final String trackId; final String trackId;
const TrackPage({ const TrackPage({
super.key, super.key,
required this.trackId, @PathParam("id") required this.trackId,
}); });
@override @override
@ -52,195 +54,200 @@ class TrackPage extends HookConsumerWidget {
} }
} }
return Scaffold( return SafeArea(
headers: const [ bottom: false,
TitleBar( child: Scaffold(
automaticallyImplyLeading: true, headers: const [
backgroundColor: Colors.transparent, TitleBar(
surfaceBlur: 0, backgroundColor: Colors.transparent,
) surfaceBlur: 0,
], )
floatingHeader: true, ],
child: Stack( floatingHeader: true,
children: [ child: Stack(
Positioned.fill( children: [
child: Container( Positioned.fill(
decoration: BoxDecoration( child: Container(
image: DecorationImage( decoration: BoxDecoration(
image: UniversalImage.imageProvider( image: DecorationImage(
track.album!.images.asUrlString( image: UniversalImage.imageProvider(
placeholder: ImagePlaceholder.albumArt, track.album!.images.asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
), ),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
colorScheme.background.withOpacity(0.5),
BlendMode.srcOver,
),
alignment: Alignment.topCenter,
), ),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
colorScheme.background.withOpacity(0.5),
BlendMode.srcOver,
),
alignment: Alignment.topCenter,
), ),
), ),
), ),
), Positioned.fill(
Positioned.fill( child: BackdropFilter(
child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Skeletonizer(
child: Skeletonizer( enabled: trackQuery.isLoading,
enabled: trackQuery.isLoading, child: Container(
child: Container( alignment: Alignment.topCenter,
alignment: Alignment.topCenter, decoration: BoxDecoration(
decoration: BoxDecoration( gradient: LinearGradient(
gradient: LinearGradient( colors: [
colors: [ colorScheme.background,
colorScheme.background, Colors.transparent,
Colors.transparent, ],
], begin: Alignment.topCenter,
begin: Alignment.topCenter, end: Alignment.bottomCenter,
end: Alignment.bottomCenter, stops: const [0.2, 1],
stops: const [0.2, 1], ),
), ),
), child: SafeArea(
child: SafeArea( child: Wrap(
child: Wrap( spacing: 20,
spacing: 20, runSpacing: 20,
runSpacing: 20, alignment: WrapAlignment.center,
alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center, runAlignment: WrapAlignment.center,
runAlignment: WrapAlignment.center, children: [
children: [ ClipRRect(
ClipRRect( borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10), child: UniversalImage(
child: UniversalImage( path: track.album!.images.asUrlString(
path: track.album!.images.asUrlString( placeholder: ImagePlaceholder.albumArt,
placeholder: ImagePlaceholder.albumArt, ),
height: 200,
width: 200,
), ),
height: 200,
width: 200,
), ),
), Padding(
Padding( padding:
padding: const EdgeInsets.symmetric(horizontal: 16.0), const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
crossAxisAlignment: mediaQuery.smAndDown crossAxisAlignment: mediaQuery.smAndDown
? CrossAxisAlignment.center ? CrossAxisAlignment.center
: CrossAxisAlignment.start, : CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
track.name!, track.name!,
).large().semiBold(), ).large().semiBold(),
const Gap(10), const Gap(10),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [
const Icon(SpotubeIcons.album),
const Gap(5),
Flexible(
child: LinkText(
track.album!.name!,
'/album/${track.album!.id}',
push: true,
extra: track.album,
),
),
],
),
const Gap(10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(SpotubeIcons.artist),
const Gap(5),
Flexible(
child: ArtistLink(
artists: track.artists!,
hideOverflowArtist: false,
),
),
],
),
const Gap(10),
ConstrainedBox(
constraints:
const BoxConstraints(maxWidth: 350),
child: Row(
mainAxisSize: mediaQuery.smAndDown
? MainAxisSize.max
: MainAxisSize.min,
children: [ children: [
const Icon(SpotubeIcons.album),
const Gap(5), const Gap(5),
if (!isActive && Flexible(
!playlist.tracks child: LinkText(
.containsBy(track, (t) => t.id)) track.album!.name!,
Button.outline( AlbumRoute(
leading: id: track.album!.id!,
const Icon(SpotubeIcons.queueAdd), album: track.album!,
child: Text(context.l10n.queue),
onPressed: () {
playlistNotifier.addTrack(track);
},
),
const Gap(5),
if (!isActive &&
!playlist.tracks
.containsBy(track, (t) => t.id))
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.play_next),
), ),
child: IconButton.outline( push: true,
icon: const Icon(
SpotubeIcons.lightning),
onPressed: () {
playlistNotifier
.addTracksAtFirst([track]);
},
),
),
const Gap(5),
Tooltip(
tooltip: TooltipContainer(
child: Text(
isActive
? context.l10n.pause_playback
: context.l10n.play,
),
),
child: IconButton.primary(
shape: ButtonShape.circle,
icon: Icon(
isActive
? SpotubeIcons.pause
: SpotubeIcons.play,
),
onPressed: onPlay,
), ),
), ),
const Gap(5),
if (mediaQuery.smAndDown)
const Spacer()
else
const Gap(20),
TrackHeartButton(track: track),
TrackOptions(
track: track,
userPlaylist: false,
),
const Gap(5),
], ],
), ),
), const Gap(10),
], Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(SpotubeIcons.artist),
const Gap(5),
Flexible(
child: ArtistLink(
artists: track.artists!,
hideOverflowArtist: false,
),
),
],
),
const Gap(10),
ConstrainedBox(
constraints:
const BoxConstraints(maxWidth: 350),
child: Row(
mainAxisSize: mediaQuery.smAndDown
? MainAxisSize.max
: MainAxisSize.min,
children: [
const Gap(5),
if (!isActive &&
!playlist.tracks
.containsBy(track, (t) => t.id))
Button.outline(
leading:
const Icon(SpotubeIcons.queueAdd),
child: Text(context.l10n.queue),
onPressed: () {
playlistNotifier.addTrack(track);
},
),
const Gap(5),
if (!isActive &&
!playlist.tracks
.containsBy(track, (t) => t.id))
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.play_next),
),
child: IconButton.outline(
icon: const Icon(
SpotubeIcons.lightning),
onPressed: () {
playlistNotifier
.addTracksAtFirst([track]);
},
),
),
const Gap(5),
Tooltip(
tooltip: TooltipContainer(
child: Text(
isActive
? context.l10n.pause_playback
: context.l10n.play,
),
),
child: IconButton.primary(
shape: ButtonShape.circle,
icon: Icon(
isActive
? SpotubeIcons.pause
: SpotubeIcons.play,
),
onPressed: onPlay,
),
),
const Gap(5),
if (mediaQuery.smAndDown)
const Spacer()
else
const Gap(20),
TrackHeartButton(track: track),
TrackOptions(
track: track,
userPlaylist: false,
),
const Gap(5),
],
),
),
],
),
), ),
), ],
], ),
), ),
), ),
), ),
), ),
), ),
), ],
], ),
), ),
); );
} }

View File

@ -1,8 +1,9 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
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';
@ -276,70 +277,6 @@ abstract class ServiceUtils {
return subtitle; return subtitle;
} }
static void navigate(BuildContext context, String location, {Object? extra}) {
if (GoRouterState.of(context).matchedLocation == location) return;
GoRouter.of(context).go(location, extra: extra);
}
static void navigateNamed(
BuildContext context,
String name, {
Object? extra,
Map<String, String>? pathParameters,
Map<String, dynamic>? queryParameters,
}) {
if (GoRouterState.of(context).matchedLocation == name) return;
GoRouter.of(context).goNamed(
name,
pathParameters: pathParameters ?? const {},
queryParameters: queryParameters ?? const {},
extra: extra,
);
}
static void push(BuildContext context, String location, {Object? extra}) {
final router = GoRouter.of(context);
final routerState = GoRouterState.of(context);
final routerStack = router.routerDelegate.currentConfiguration.matches
.map((e) => e.matchedLocation);
if (routerState.matchedLocation == location ||
routerStack.contains(location)) {
return;
}
router.push(location, extra: extra);
}
static void pushNamed(
BuildContext context,
String name, {
Object? extra,
Map<String, String> pathParameters = const {},
Map<String, String> queryParameters = const {},
}) {
final router = GoRouter.of(context);
final routerState = GoRouterState.of(context);
final routerStack = router.routerDelegate.currentConfiguration.matches
.map((e) => e.matchedLocation);
final nameLocation = routerState.namedLocation(
name,
pathParameters: pathParameters,
queryParameters: queryParameters,
);
if (routerState.matchedLocation == nameLocation ||
routerStack.contains(nameLocation)) {
return;
}
router.pushNamed(
name,
pathParameters: pathParameters,
queryParameters: queryParameters,
extra: extra,
);
}
static DateTime parseSpotifyAlbumDate(AlbumSimple? album) { static DateTime parseSpotifyAlbumDate(AlbumSimple? album) {
if (album == null || album.releaseDate == null) { if (album == null || album.releaseDate == null) {
return DateTime.parse("1975-01-01"); return DateTime.parse("1975-01-01");

View File

@ -142,6 +142,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.21" version: "0.1.21"
auto_route:
dependency: "direct main"
description:
name: auto_route
sha256: "1d1bd908a1fec327719326d5d0791edd37f16caff6493c01003689fb03315ad7"
url: "https://pub.dev"
source: hosted
version: "9.3.0+1"
auto_route_generator:
dependency: "direct dev"
description:
name: auto_route_generator
sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6
url: "https://pub.dev"
source: hosted
version: "9.0.0"
auto_size_text: auto_size_text:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1073,14 +1089,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539"
url: "https://pub.dev"
source: hosted
version: "14.6.2"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -19,6 +19,7 @@ dependencies:
audio_service: ^0.18.13 audio_service: ^0.18.13
audio_service_mpris: ^0.2.0 audio_service_mpris: ^0.2.0
audio_session: ^0.1.19 audio_session: ^0.1.19
auto_route: ^9.3.0+1
auto_size_text: ^3.0.0 auto_size_text: ^3.0.0
bonsoir: ^5.1.10 bonsoir: ^5.1.10
cached_network_image: ^3.3.1 cached_network_image: ^3.3.1
@ -70,7 +71,6 @@ dependencies:
freezed_annotation: ^2.4.1 freezed_annotation: ^2.4.1
fuzzywuzzy: ^1.1.6 fuzzywuzzy: ^1.1.6
gap: ^3.0.1 gap: ^3.0.1
go_router: ^14.2.7
google_fonts: ^6.2.1 google_fonts: ^6.2.1
hive: ^2.2.3 hive: ^2.2.3
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
@ -161,6 +161,7 @@ dev_dependencies:
xml: ^6.5.0 xml: ^6.5.0
io: ^1.0.4 io: ^1.0.4
drift_dev: ^2.21.0 drift_dev: ^2.21.0
auto_route_generator: ^9.0.0
dependency_overrides: dependency_overrides:
bonsoir_android: bonsoir_android: