mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge pull request #2291 from KRTirtho/migrate/auto-routes
refactor: migrate to auto_route from go_router
This commit is contained in:
commit
cd39bbf87c
10
build.yaml
10
build.yaml
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
1143
lib/collections/routes.gr.dart
Normal file
1143
lib/collections/routes.gr.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
|
@ -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()),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
@ -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(
|
||||||
|
@ -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!,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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!,
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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:
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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: [
|
||||||
|
@ -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());
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
@ -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!,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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,
|
||||||
|
@ -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!,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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;
|
|
||||||
}
|
|
132
lib/modules/root/sidebar/sidebar.dart
Normal file
132
lib/modules/root/sidebar/sidebar.dart
Normal 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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
140
lib/modules/root/sidebar/sidebar_footer.dart
Normal file
140
lib/modules/root/sidebar/sidebar_footer.dart
Normal 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;
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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!},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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!,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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!,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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) {
|
||||||
|
@ -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),
|
||||||
|
@ -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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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(
|
||||||
|
@ -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),
|
||||||
|
@ -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),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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,
|
);
|
||||||
);
|
}),
|
||||||
}),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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});
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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});
|
||||||
|
@ -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),
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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());
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
|
@ -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());
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
|
@ -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(
|
||||||
|
@ -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))),
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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))),
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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)),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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)),
|
||||||
},
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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)),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
24
pubspec.lock
24
pubspec.lock
@ -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:
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user