mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: personalized stats based on local music history (#1522)
* feat: add playback history provider * feat: implement recently played section * refactor: use route names * feat: add stats summary and top tracks/artists/albums * feat: add top date based filtering * feat: add stream money calculation * refactor: place search in mobile navbar and settings in home appbar * feat: add individual minutes and streams page * feat(stats): add individual minutes and streams page * chore: default period to 1 month * feat: add text to explain user how hypothetical fees are calculated * chore: ensure usage of route names instead of direct paths * cd: add cache key * cd: remove media_kit_event_loop from git
This commit is contained in:
parent
fc5bfa089c
commit
82307bc030
1
.github/workflows/spotube-release-binary.yml
vendored
1
.github/workflows/spotube-release-binary.yml
vendored
@ -66,6 +66,7 @@ jobs:
|
|||||||
- uses: subosito/flutter-action@v2.12.0
|
- uses: subosito/flutter-action@v2.12.0
|
||||||
with:
|
with:
|
||||||
cache: true
|
cache: true
|
||||||
|
cache-key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.yaml') }}
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
if: ${{matrix.platform == 'android'}}
|
if: ${{matrix.platform == 'android'}}
|
||||||
|
@ -3,3 +3,8 @@ targets:
|
|||||||
sources:
|
sources:
|
||||||
exclude:
|
exclude:
|
||||||
- bin/*.dart
|
- bin/*.dart
|
||||||
|
builders:
|
||||||
|
json_serializable:
|
||||||
|
options:
|
||||||
|
any_map: true
|
||||||
|
explicit_to_json: true
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
|
||||||
import 'package:spotube/models/spotify/home_feed.dart';
|
import 'package:spotube/models/spotify/home_feed.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
|
|
||||||
|
8
lib/collections/formatters.dart
Normal file
8
lib/collections/formatters.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
final compactNumberFormatter = NumberFormat.compact();
|
||||||
|
final usdFormatter = NumberFormat.compactCurrency(
|
||||||
|
locale: 'en-US',
|
||||||
|
symbol: r"$",
|
||||||
|
decimalDigits: 2,
|
||||||
|
);
|
@ -7,6 +7,10 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:spotube/collections/routes.dart';
|
import 'package:spotube/collections/routes.dart';
|
||||||
import 'package:spotube/components/player/player_controls.dart';
|
import 'package:spotube/components/player/player_controls.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
|
import 'package:spotube/pages/home/home.dart';
|
||||||
|
import 'package:spotube/pages/library/library.dart';
|
||||||
|
import 'package:spotube/pages/lyrics/lyrics.dart';
|
||||||
|
import 'package:spotube/pages/search/search.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.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';
|
||||||
@ -67,16 +71,16 @@ class HomeTabAction extends Action<HomeTabIntent> {
|
|||||||
final router = intent.ref.read(routerProvider);
|
final router = intent.ref.read(routerProvider);
|
||||||
switch (intent.tab) {
|
switch (intent.tab) {
|
||||||
case HomeTabs.browse:
|
case HomeTabs.browse:
|
||||||
router.go("/");
|
router.goNamed(HomePage.name);
|
||||||
break;
|
break;
|
||||||
case HomeTabs.search:
|
case HomeTabs.search:
|
||||||
router.go("/search");
|
router.goNamed(SearchPage.name);
|
||||||
break;
|
break;
|
||||||
case HomeTabs.library:
|
case HomeTabs.library:
|
||||||
router.go("/library");
|
router.goNamed(LibraryPage.name);
|
||||||
break;
|
break;
|
||||||
case HomeTabs.lyrics:
|
case HomeTabs.lyrics:
|
||||||
router.go("/lyrics");
|
router.goNamed(LyricsPage.name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -25,6 +25,13 @@ import 'package:spotube/pages/search/search.dart';
|
|||||||
import 'package:spotube/pages/settings/blacklist.dart';
|
import 'package:spotube/pages/settings/blacklist.dart';
|
||||||
import 'package:spotube/pages/settings/about.dart';
|
import 'package:spotube/pages/settings/about.dart';
|
||||||
import 'package:spotube/pages/settings/logs.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/pages/track/track.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
@ -51,6 +58,7 @@ final routerProvider = Provider((ref) {
|
|||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/",
|
path: "/",
|
||||||
|
name: HomePage.name,
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
final authNotifier = ref.read(authenticationProvider.notifier);
|
final authNotifier = ref.read(authenticationProvider.notifier);
|
||||||
final json = await authNotifier.box.get(authNotifier.cacheKey);
|
final json = await authNotifier.box.get(authNotifier.cacheKey);
|
||||||
@ -67,11 +75,13 @@ final routerProvider = Provider((ref) {
|
|||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "genres",
|
path: "genres",
|
||||||
|
name: GenrePage.name,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: GenrePage()),
|
const SpotubePage(child: GenrePage()),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "genre/:categoryId",
|
path: "genre/:categoryId",
|
||||||
|
name: GenrePlaylistsPage.name,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: GenrePlaylistsPage(
|
child: GenrePlaylistsPage(
|
||||||
category: state.extra as Category,
|
category: state.extra as Category,
|
||||||
@ -80,6 +90,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "feeds/:feedId",
|
path: "feeds/:feedId",
|
||||||
|
name: HomeFeedSectionPage.name,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: HomeFeedSectionPage(
|
child: HomeFeedSectionPage(
|
||||||
sectionUri: state.pathParameters["feedId"] as String,
|
sectionUri: state.pathParameters["feedId"] as String,
|
||||||
@ -90,56 +101,62 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/search",
|
path: "/search",
|
||||||
name: "Search",
|
name: SearchPage.name,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: SearchPage()),
|
const SpotubePage(child: SearchPage()),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/library",
|
path: "/library",
|
||||||
name: "Library",
|
name: LibraryPage.name,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: LibraryPage()),
|
const SpotubePage(child: LibraryPage()),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "generate",
|
path: "generate",
|
||||||
pageBuilder: (context, state) =>
|
name: PlaylistGeneratorPage.name,
|
||||||
const SpotubePage(child: PlaylistGeneratorPage()),
|
pageBuilder: (context, state) =>
|
||||||
routes: [
|
const SpotubePage(child: PlaylistGeneratorPage()),
|
||||||
GoRoute(
|
routes: [
|
||||||
path: "result",
|
GoRoute(
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
path: "result",
|
||||||
child: PlaylistGenerateResultPage(
|
name: PlaylistGenerateResultPage.name,
|
||||||
state: state.extra as GeneratePlaylistProviderInput,
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
),
|
child: PlaylistGenerateResultPage(
|
||||||
|
state: state.extra as GeneratePlaylistProviderInput,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "local",
|
path: "local",
|
||||||
|
name: LocalLibraryPage.name,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
assert(state.extra is String);
|
assert(state.extra is String);
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
child: LocalLibraryPage(state.extra as String,
|
child: LocalLibraryPage(state.extra as String,
|
||||||
isDownloads: state.uri.queryParameters["downloads"] != null
|
isDownloads:
|
||||||
),
|
state.uri.queryParameters["downloads"] != null),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/lyrics",
|
path: "/lyrics",
|
||||||
name: "Lyrics",
|
name: LyricsPage.name,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: LyricsPage()),
|
const SpotubePage(child: LyricsPage()),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
name: SettingsPage.name,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: SettingsPage(),
|
child: SettingsPage(),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "blacklist",
|
path: "blacklist",
|
||||||
|
name: BlackListPage.name,
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
child: const BlackListPage(),
|
child: const BlackListPage(),
|
||||||
),
|
),
|
||||||
@ -147,12 +164,14 @@ final routerProvider = Provider((ref) {
|
|||||||
if (!kIsWeb)
|
if (!kIsWeb)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "logs",
|
path: "logs",
|
||||||
|
name: LogsPage.name,
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
child: const LogsPage(),
|
child: const LogsPage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "about",
|
path: "about",
|
||||||
|
name: AboutSpotube.name,
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
child: const AboutSpotube(),
|
child: const AboutSpotube(),
|
||||||
),
|
),
|
||||||
@ -161,6 +180,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/album/:id",
|
path: "/album/:id",
|
||||||
|
name: AlbumPage.name,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
assert(state.extra is AlbumSimple);
|
assert(state.extra is AlbumSimple);
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -170,6 +190,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/artist/:id",
|
path: "/artist/:id",
|
||||||
|
name: ArtistPage.name,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
assert(state.pathParameters["id"] != null);
|
assert(state.pathParameters["id"] != null);
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -178,6 +199,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/playlist/:id",
|
path: "/playlist/:id",
|
||||||
|
name: PlaylistPage.name,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
assert(state.extra is PlaylistSimple);
|
assert(state.extra is PlaylistSimple);
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -189,6 +211,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/track/:id",
|
path: "/track/:id",
|
||||||
|
name: TrackPage.name,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
final id = state.pathParameters["id"]!;
|
final id = state.pathParameters["id"]!;
|
||||||
return SpotubePage(
|
return SpotubePage(
|
||||||
@ -198,12 +221,14 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/connect",
|
path: "/connect",
|
||||||
|
name: ConnectPage.name,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: ConnectPage(),
|
child: ConnectPage(),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "control",
|
path: "control",
|
||||||
|
name: ConnectControlPage.name,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
return const SpotubePage(
|
return const SpotubePage(
|
||||||
child: ConnectControlPage(),
|
child: ConnectControlPage(),
|
||||||
@ -214,13 +239,66 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/profile",
|
path: "/profile",
|
||||||
|
name: ProfilePage.name,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: ProfilePage()),
|
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(
|
GoRoute(
|
||||||
path: "/mini-player",
|
path: "/mini-player",
|
||||||
|
name: MiniLyricsPage.name,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
child: MiniLyricsPage(prevSize: state.extra as Size),
|
||||||
@ -228,6 +306,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/getting-started",
|
path: "/getting-started",
|
||||||
|
name: GettingStarting.name,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: GettingStarting(),
|
child: GettingStarting(),
|
||||||
@ -235,6 +314,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/login",
|
path: "/login",
|
||||||
|
name: WebViewLogin.name,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(),
|
child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(),
|
||||||
@ -242,6 +322,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/login-tutorial",
|
path: "/login-tutorial",
|
||||||
|
name: LoginTutorial.name,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
child: LoginTutorial(),
|
child: LoginTutorial(),
|
||||||
@ -249,6 +330,7 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/lastfm-login",
|
path: "/lastfm-login",
|
||||||
|
name: LastFMLoginPage.name,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: LastFMLoginPage()),
|
const SpotubePage(child: LastFMLoginPage()),
|
||||||
|
@ -1,33 +1,82 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:spotube/pages/home/home.dart';
|
||||||
|
import 'package:spotube/pages/library/library.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;
|
||||||
SideBarTiles({required this.icon, required this.title, required this.id});
|
final String name;
|
||||||
|
|
||||||
|
SideBarTiles({
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
|
List<SideBarTiles> getSidebarTileList(AppLocalizations l10n) => [
|
||||||
SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse),
|
|
||||||
SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search),
|
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "library", icon: SpotubeIcons.library, title: l10n.library),
|
id: "browse",
|
||||||
SideBarTiles(id: "lyrics", icon: SpotubeIcons.music, title: l10n.lyrics),
|
name: HomePage.name,
|
||||||
];
|
icon: SpotubeIcons.home,
|
||||||
|
title: l10n.browse,
|
||||||
List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
|
),
|
||||||
SideBarTiles(id: "browse", icon: SpotubeIcons.home, title: l10n.browse),
|
SideBarTiles(
|
||||||
SideBarTiles(id: "search", icon: SpotubeIcons.search, title: l10n.search),
|
id: "search",
|
||||||
|
name: SearchPage.name,
|
||||||
|
icon: SpotubeIcons.search,
|
||||||
|
title: l10n.search,
|
||||||
|
),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "library",
|
id: "library",
|
||||||
|
name: LibraryPage.name,
|
||||||
icon: SpotubeIcons.library,
|
icon: SpotubeIcons.library,
|
||||||
title: l10n.library,
|
title: l10n.library,
|
||||||
),
|
),
|
||||||
SideBarTiles(
|
SideBarTiles(
|
||||||
id: "settings",
|
id: "lyrics",
|
||||||
icon: SpotubeIcons.settings,
|
name: LyricsPage.name,
|
||||||
title: l10n.settings,
|
icon: SpotubeIcons.music,
|
||||||
)
|
title: l10n.lyrics,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "stats",
|
||||||
|
name: StatsPage.name,
|
||||||
|
icon: SpotubeIcons.chart,
|
||||||
|
title: l10n.stats,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
List<SideBarTiles> getNavbarTileList(AppLocalizations l10n) => [
|
||||||
|
SideBarTiles(
|
||||||
|
id: "browse",
|
||||||
|
name: HomePage.name,
|
||||||
|
icon: SpotubeIcons.home,
|
||||||
|
title: l10n.browse,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "search",
|
||||||
|
name: SearchPage.name,
|
||||||
|
icon: SpotubeIcons.search,
|
||||||
|
title: l10n.search,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "library",
|
||||||
|
name: LibraryPage.name,
|
||||||
|
icon: SpotubeIcons.library,
|
||||||
|
title: l10n.library,
|
||||||
|
),
|
||||||
|
SideBarTiles(
|
||||||
|
id: "stats",
|
||||||
|
name: StatsPage.name,
|
||||||
|
icon: SpotubeIcons.chart,
|
||||||
|
title: l10n.stats,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
@ -121,6 +121,7 @@ abstract class SpotubeIcons {
|
|||||||
static const monitor = FeatherIcons.monitor;
|
static const monitor = FeatherIcons.monitor;
|
||||||
static const power = FeatherIcons.power;
|
static const power = FeatherIcons.power;
|
||||||
static const bluetooth = FeatherIcons.bluetooth;
|
static const bluetooth = FeatherIcons.bluetooth;
|
||||||
|
static const chart = FeatherIcons.barChart2;
|
||||||
static const folderAdd = FeatherIcons.folderPlus;
|
static const folderAdd = FeatherIcons.folderPlus;
|
||||||
static const folderRemove = FeatherIcons.folderMinus;
|
static const folderRemove = FeatherIcons.folderMinus;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,9 @@ 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/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.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';
|
||||||
@ -32,6 +34,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
|
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
|
||||||
|
|
||||||
bool isPlaylistPlaying = useMemoized(
|
bool isPlaylistPlaying = useMemoized(
|
||||||
() => playlist.containsCollection(album.id!),
|
() => playlist.containsCollection(album.id!),
|
||||||
@ -62,7 +65,14 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
description:
|
description:
|
||||||
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}",
|
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}",
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.push(context, "/album/${album.id}", extra: album);
|
ServiceUtils.pushNamed(
|
||||||
|
context,
|
||||||
|
AlbumPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"id": album.id!,
|
||||||
|
},
|
||||||
|
extra: album,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onPlaybuttonPressed: () async {
|
onPlaybuttonPressed: () async {
|
||||||
updating.value = true;
|
updating.value = true;
|
||||||
@ -79,14 +89,15 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
if (isRemoteDevice) {
|
if (isRemoteDevice) {
|
||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData(
|
WebSocketLoadEventData.album(
|
||||||
tracks: fetchedTracks,
|
tracks: fetchedTracks,
|
||||||
collectionId: album.id!,
|
collection: album,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
||||||
playlistNotifier.addCollection(album.id!);
|
playlistNotifier.addCollection(album.id!);
|
||||||
|
historyNotifier.addAlbums([album]);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
@ -104,6 +115,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
if (fetchedTracks.isEmpty) return;
|
if (fetchedTracks.isEmpty) return;
|
||||||
playlistNotifier.addTracks(fetchedTracks);
|
playlistNotifier.addTracks(fetchedTracks);
|
||||||
playlistNotifier.addCollection(album.id!);
|
playlistNotifier.addCollection(album.id!);
|
||||||
|
historyNotifier.addAlbums([album]);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final snackbar = SnackBar(
|
final snackbar = SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
|
@ -9,6 +9,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.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';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -63,7 +64,13 @@ class ArtistCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.push(context, "/artist/${artist.id}");
|
ServiceUtils.pushNamed(
|
||||||
|
context,
|
||||||
|
ArtistPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"id": artist.id!,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
@ -3,6 +3,7 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.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';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ class ConnectDeviceButton extends HookConsumerWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ServiceUtils.push(context, "/connect");
|
ServiceUtils.pushNamed(context, ConnectPage.name);
|
||||||
},
|
},
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@ -59,7 +60,7 @@ class ConnectDeviceButton extends HookConsumerWidget {
|
|||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.push(context, "/connect");
|
ServiceUtils.pushNamed(context, ConnectPage.name);
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(50),
|
borderRadius: BorderRadius.circular(50),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
@ -111,7 +112,7 @@ class ConnectDeviceButton extends HookConsumerWidget {
|
|||||||
foregroundColor: colorScheme.onPrimary,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ServiceUtils.push(context, "/connect");
|
ServiceUtils.pushNamed(context, ConnectPage.name);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.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';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -41,8 +42,13 @@ class HomePageFeedSection extends HookConsumerWidget {
|
|||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
label: const Text("Browse More"),
|
label: const Text("Browse More"),
|
||||||
icon: const Icon(SpotubeIcons.angleRight),
|
icon: const Icon(SpotubeIcons.angleRight),
|
||||||
onPressed: () =>
|
onPressed: () => ServiceUtils.pushNamed(
|
||||||
ServiceUtils.push(context, "/feeds/${section.uri}"),
|
context,
|
||||||
|
HomeFeedSectionPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"feedId": section.uri,
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/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 {
|
||||||
@ -57,7 +60,9 @@ class FriendItem extends HookConsumerWidget {
|
|||||||
text: friend.track.name,
|
text: friend.track.name,
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () {
|
||||||
context.push("/track/${friend.track.id}");
|
context.pushNamed(TrackPage.name, pathParameters: {
|
||||||
|
"id": friend.track.id,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const TextSpan(text: " • "),
|
const TextSpan(text: " • "),
|
||||||
@ -71,8 +76,12 @@ class FriendItem extends HookConsumerWidget {
|
|||||||
text: " ${friend.track.artist.name}",
|
text: " ${friend.track.artist.name}",
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () {
|
||||||
context.push(
|
context.pushNamed(
|
||||||
"/artist/${friend.track.artist.id}",
|
ArtistPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"id": friend.track.artist.id,
|
||||||
|
},
|
||||||
|
extra: friend.track.artist,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -105,8 +114,11 @@ 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.push(
|
context.pushNamed(
|
||||||
"/album/${friend.track.album.id}",
|
AlbumPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"id": friend.track.album.id,
|
||||||
|
},
|
||||||
extra: album,
|
extra: album,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/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/pages/home/genres/genre_playlists.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 {
|
||||||
@ -50,7 +52,7 @@ class HomeGenresSection extends HookConsumerWidget {
|
|||||||
textDirection: TextDirection.rtl,
|
textDirection: TextDirection.rtl,
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.push('/genres');
|
context.pushNamed(GenrePage.name);
|
||||||
},
|
},
|
||||||
icon: const Icon(SpotubeIcons.angleRight),
|
icon: const Icon(SpotubeIcons.angleRight),
|
||||||
label: Text(
|
label: Text(
|
||||||
@ -110,7 +112,13 @@ class HomeGenresSection extends HookConsumerWidget {
|
|||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.push('/genre/${category.id}', extra: category);
|
context.pushNamed(
|
||||||
|
GenrePlaylistsPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"categoryId": category.id!,
|
||||||
|
},
|
||||||
|
extra: category,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
|
32
lib/components/home/sections/recent.dart
Normal file
32
lib/components/home/sections/recent.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
|
import 'package:spotube/provider/history/recent.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
|
||||||
|
class HomeRecentlyPlayedSection extends HookConsumerWidget {
|
||||||
|
const HomeRecentlyPlayedSection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final history = ref.watch(recentlyPlayedItems);
|
||||||
|
|
||||||
|
if (history.isEmpty) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return HorizontalPlaybuttonCardView(
|
||||||
|
title: const Text('Recently Played'),
|
||||||
|
items: [
|
||||||
|
for (final item in history)
|
||||||
|
if (item is PlaybackHistoryPlaylist)
|
||||||
|
item.playlist
|
||||||
|
else if (item is PlaybackHistoryAlbum)
|
||||||
|
item.album
|
||||||
|
],
|
||||||
|
hasNextPage: false,
|
||||||
|
isLoadingNextPage: false,
|
||||||
|
onFetchMore: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ 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/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
|
import 'package:spotube/pages/library/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';
|
||||||
|
|
||||||
@ -57,14 +58,13 @@ class LocalFolderItem extends HookConsumerWidget {
|
|||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (isDownloadFolder) {
|
context.goNamed(
|
||||||
context.go("/library/local?downloads=1", extra: folder);
|
LocalLibraryPage.name,
|
||||||
} else {
|
queryParameters: {
|
||||||
context.go(
|
if (isDownloadFolder) "downloads": 1,
|
||||||
"/library/local",
|
},
|
||||||
extra: folder,
|
extra: folder,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
|
@ -6,7 +6,9 @@ import 'package:spotube/components/shared/dialogs/select_device_dialog.dart';
|
|||||||
import 'package:spotube/components/shared/playbutton_card.dart';
|
import 'package:spotube/components/shared/playbutton_card.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/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.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';
|
||||||
@ -22,6 +24,8 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlistQueue = ref.watch(proxyPlaylistProvider);
|
final playlistQueue = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
|
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
|
||||||
|
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
bool isPlaylistPlaying = useMemoized(
|
bool isPlaylistPlaying = useMemoized(
|
||||||
@ -55,9 +59,12 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
isOwner: playlist.owner?.id == me.asData?.value.id &&
|
isOwner: playlist.owner?.id == me.asData?.value.id &&
|
||||||
me.asData?.value.id != null,
|
me.asData?.value.id != null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.push(
|
ServiceUtils.pushNamed(
|
||||||
context,
|
context,
|
||||||
"/playlist/${playlist.id}",
|
PlaylistPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"id": playlist.id!,
|
||||||
|
},
|
||||||
extra: playlist,
|
extra: playlist,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -78,14 +85,15 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
if (isRemoteDevice) {
|
if (isRemoteDevice) {
|
||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData(
|
WebSocketLoadEventData.playlist(
|
||||||
tracks: fetchedTracks,
|
tracks: fetchedTracks,
|
||||||
collectionId: playlist.id!,
|
collection: playlist,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
||||||
playlistNotifier.addCollection(playlist.id!);
|
playlistNotifier.addCollection(playlist.id!);
|
||||||
|
historyNotifier.addPlaylists([playlist]);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@ -104,6 +112,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
playlistNotifier.addTracks(fetchedTracks);
|
playlistNotifier.addTracks(fetchedTracks);
|
||||||
playlistNotifier.addCollection(playlist.id!);
|
playlistNotifier.addCollection(playlist.id!);
|
||||||
|
historyNotifier.addPlaylists([playlist]);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final snackbar = SnackBar(
|
final snackbar = SnackBar(
|
||||||
content: Text("Added ${fetchedTracks.length} tracks to queue"),
|
content: Text("Added ${fetchedTracks.length} tracks to queue"),
|
||||||
|
@ -16,6 +16,8 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
|
import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
|
||||||
|
import 'package:spotube/pages/profile/profile.dart';
|
||||||
|
import 'package:spotube/pages/settings/settings.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
@ -26,13 +28,9 @@ import 'package:spotube/utils/platform.dart';
|
|||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class Sidebar extends HookConsumerWidget {
|
class Sidebar extends HookConsumerWidget {
|
||||||
final int? selectedIndex;
|
|
||||||
final void Function(int) onSelectedIndexChanged;
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const Sidebar({
|
const Sidebar({
|
||||||
required this.selectedIndex,
|
|
||||||
required this.onSelectedIndexChanged,
|
|
||||||
required this.child,
|
required this.child,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@ -47,12 +45,9 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void goToSettings(BuildContext context) {
|
|
||||||
GoRouter.of(context).go("/settings");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final routerState = GoRouterState.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
|
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
|
||||||
@ -60,8 +55,17 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
final layoutMode =
|
final layoutMode =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
|
||||||
|
final sidebarTileList = useMemoized(
|
||||||
|
() => getSidebarTileList(context.l10n),
|
||||||
|
[context.l10n],
|
||||||
|
);
|
||||||
|
|
||||||
|
final selectedIndex = sidebarTileList.indexWhere(
|
||||||
|
(e) => routerState.namedLocation(e.name) == routerState.matchedLocation,
|
||||||
|
);
|
||||||
|
|
||||||
final controller = useSidebarXController(
|
final controller = useSidebarXController(
|
||||||
selectedIndex: selectedIndex ?? 0,
|
selectedIndex: selectedIndex,
|
||||||
extended: mediaQuery.lgAndUp,
|
extended: mediaQuery.lgAndUp,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -73,29 +77,6 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
Color.lerp(bg, Colors.black, 0.45)!,
|
Color.lerp(bg, Colors.black, 0.45)!,
|
||||||
);
|
);
|
||||||
|
|
||||||
final sidebarTileList = useMemoized(
|
|
||||||
() => getSidebarTileList(context.l10n),
|
|
||||||
[context.l10n],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
if (controller.selectedIndex != selectedIndex && selectedIndex != null) {
|
|
||||||
controller.selectIndex(selectedIndex!);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [selectedIndex]);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
void listener() {
|
|
||||||
onSelectedIndexChanged(controller.selectedIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.addListener(listener);
|
|
||||||
return () {
|
|
||||||
controller.removeListener(listener);
|
|
||||||
};
|
|
||||||
}, [controller]);
|
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (mediaQuery.lgAndUp && !controller.extended) {
|
if (mediaQuery.lgAndUp && !controller.extended) {
|
||||||
@ -106,6 +87,13 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [mediaQuery, controller]);
|
}, [mediaQuery, controller]);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (controller.selectedIndex != selectedIndex) {
|
||||||
|
controller.selectIndex(selectedIndex);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [selectedIndex]);
|
||||||
|
|
||||||
if (layoutMode == LayoutMode.compact ||
|
if (layoutMode == LayoutMode.compact ||
|
||||||
(mediaQuery.smAndDown && layoutMode == LayoutMode.adaptive)) {
|
(mediaQuery.smAndDown && layoutMode == LayoutMode.adaptive)) {
|
||||||
return Scaffold(body: child);
|
return Scaffold(body: child);
|
||||||
@ -119,23 +107,28 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
items: sidebarTileList.mapIndexed(
|
items: sidebarTileList.mapIndexed(
|
||||||
(index, e) {
|
(index, e) {
|
||||||
return SidebarXItem(
|
return SidebarXItem(
|
||||||
iconWidget: Badge(
|
onTap: () {
|
||||||
backgroundColor: theme.colorScheme.primary,
|
context.goNamed(e.name);
|
||||||
isLabelVisible: e.title == "Library" && downloadCount > 0,
|
},
|
||||||
label: Text(
|
iconBuilder: (selected, hovered) {
|
||||||
downloadCount.toString(),
|
return Badge(
|
||||||
style: const TextStyle(
|
backgroundColor: theme.colorScheme.primary,
|
||||||
color: Colors.white,
|
isLabelVisible: e.title == "Library" && downloadCount > 0,
|
||||||
fontSize: 10,
|
label: Text(
|
||||||
|
downloadCount.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: Icon(
|
||||||
child: Icon(
|
e.icon,
|
||||||
e.icon,
|
color: selected || hovered
|
||||||
color: selectedIndex == index
|
? theme.colorScheme.primary
|
||||||
? theme.colorScheme.primary
|
: null,
|
||||||
: null,
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
label: e.title,
|
label: e.title,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -257,7 +250,7 @@ class SidebarFooter extends HookConsumerWidget {
|
|||||||
if (mediaQuery.mdAndDown) {
|
if (mediaQuery.mdAndDown) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(SpotubeIcons.settings),
|
icon: const Icon(SpotubeIcons.settings),
|
||||||
onPressed: () => Sidebar.goToSettings(context),
|
onPressed: () => ServiceUtils.navigateNamed(context, SettingsPage.name),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +271,7 @@ class SidebarFooter extends HookConsumerWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.push(context, "/profile");
|
ServiceUtils.pushNamed(context, ProfilePage.name);
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -310,7 +303,7 @@ class SidebarFooter extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(SpotubeIcons.settings),
|
icon: const Icon(SpotubeIcons.settings),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Sidebar.goToSettings(context);
|
ServiceUtils.pushNamed(context, SettingsPage.name);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -3,55 +3,54 @@ import 'dart:ui';
|
|||||||
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
|
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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:spotube/collections/side_bar_tiles.dart';
|
import 'package:spotube/collections/side_bar_tiles.dart';
|
||||||
import 'package:spotube/components/root/sidebar.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/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.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/provider/user_preferences/user_preferences_state.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_state.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 {
|
||||||
final int? selectedIndex;
|
|
||||||
final void Function(int) onSelectedIndexChanged;
|
|
||||||
|
|
||||||
const SpotubeNavigationBar({
|
const SpotubeNavigationBar({
|
||||||
required this.selectedIndex,
|
|
||||||
required this.onSelectedIndexChanged,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final routerState = GoRouterState.of(context);
|
||||||
|
|
||||||
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
|
final downloadCount = ref.watch(downloadManagerProvider).$downloadCount;
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final layoutMode =
|
final layoutMode =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
|
||||||
final insideSelectedIndex = useState<int>(selectedIndex ?? 0);
|
|
||||||
|
|
||||||
final buttonColor = useBrightnessValue(
|
final buttonColor = useBrightnessValue(
|
||||||
theme.colorScheme.inversePrimary,
|
theme.colorScheme.inversePrimary,
|
||||||
theme.colorScheme.primary.withOpacity(0.2),
|
theme.colorScheme.primary.withOpacity(0.2),
|
||||||
);
|
);
|
||||||
|
|
||||||
final navbarTileList =
|
final navbarTileList = useMemoized(
|
||||||
useMemoized(() => getNavbarTileList(context.l10n), [context.l10n]);
|
() => getNavbarTileList(context.l10n),
|
||||||
|
[context.l10n],
|
||||||
|
);
|
||||||
|
|
||||||
final panelHeight = ref.watch(navigationPanelHeight);
|
final panelHeight = ref.watch(navigationPanelHeight);
|
||||||
|
|
||||||
useEffect(() {
|
final selectedIndex = useMemoized(() {
|
||||||
if (selectedIndex != null) {
|
final index = navbarTileList.indexWhere(
|
||||||
insideSelectedIndex.value = selectedIndex!;
|
(e) => routerState.namedLocation(e.name) == routerState.matchedLocation,
|
||||||
}
|
);
|
||||||
return null;
|
|
||||||
}, [selectedIndex]);
|
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) ||
|
||||||
@ -91,14 +90,9 @@ class SpotubeNavigationBar extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
index: insideSelectedIndex.value,
|
index: selectedIndex,
|
||||||
onTap: (i) {
|
onTap: (i) {
|
||||||
insideSelectedIndex.value = i;
|
ServiceUtils.navigateNamed(context, navbarTileList[i].name);
|
||||||
if (navbarTileList[i].id == "settings") {
|
|
||||||
Sidebar.goToSettings(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSelectedIndexChanged(i);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.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_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -25,7 +26,7 @@ class AnonymousFallback extends ConsumerWidget {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
child: Text(context.l10n.login_with_spotify),
|
child: Text(context.l10n.login_with_spotify),
|
||||||
onPressed: () => ServiceUtils.push(context, "/settings"),
|
onPressed: () => ServiceUtils.pushNamed(context, SettingsPage.name),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -96,7 +96,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
return switch (item) {
|
return switch (item) {
|
||||||
PlaylistSimple() =>
|
PlaylistSimple() =>
|
||||||
PlaylistCard(item as PlaylistSimple),
|
PlaylistCard(item as PlaylistSimple),
|
||||||
AlbumSimple() => AlbumCard(item as Album),
|
AlbumSimple() => AlbumCard(item as AlbumSimple),
|
||||||
Artist() => Padding(
|
Artist() => Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12.0),
|
horizontal: 12.0),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/links/anchor_button.dart';
|
import 'package:spotube/components/shared/links/anchor_button.dart';
|
||||||
|
import 'package:spotube/pages/artist/artist.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class ArtistLink extends StatelessWidget {
|
class ArtistLink extends StatelessWidget {
|
||||||
@ -40,9 +41,12 @@ 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.push(
|
ServiceUtils.pushNamed(
|
||||||
context,
|
context,
|
||||||
"/artist/${artist.value.id}",
|
ArtistPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"id": artist.value.id!,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5,7 +5,8 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
|||||||
|
|
||||||
class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
|
class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
|
||||||
final List<Widget> tabs;
|
final List<Widget> tabs;
|
||||||
const ThemedButtonsTabBar({super.key, required this.tabs});
|
final TabController? controller;
|
||||||
|
const ThemedButtonsTabBar({super.key, required this.tabs, this.controller});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -21,6 +22,7 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
|
|||||||
bottom: 8,
|
bottom: 8,
|
||||||
),
|
),
|
||||||
child: ButtonsTabBar(
|
child: ButtonsTabBar(
|
||||||
|
controller: controller,
|
||||||
radius: 100,
|
radius: 100,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: bgColor,
|
color: bgColor,
|
||||||
|
@ -17,6 +17,7 @@ import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
|||||||
import 'package:spotube/components/shared/tracks_view/track_view_provider.dart';
|
import 'package:spotube/components/shared/tracks_view/track_view_provider.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.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/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
@ -28,6 +29,7 @@ class TrackViewBodySection extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
|
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
||||||
final props = InheritedTrackView.of(context);
|
final props = InheritedTrackView.of(context);
|
||||||
final trackViewState = ref.watch(trackViewProvider(props.tracks));
|
final trackViewState = ref.watch(trackViewProvider(props.tracks));
|
||||||
|
|
||||||
@ -146,11 +148,17 @@ class TrackViewBodySection extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
final tracks = await props.pagination.onFetchAll();
|
final tracks = await props.pagination.onFetchAll();
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData(
|
props.collection is AlbumSimple
|
||||||
tracks: tracks,
|
? WebSocketLoadEventData.album(
|
||||||
collectionId: props.collectionId,
|
tracks: tracks,
|
||||||
initialIndex: index,
|
collection: props.collection as AlbumSimple,
|
||||||
),
|
initialIndex: index,
|
||||||
|
)
|
||||||
|
: WebSocketLoadEventData.playlist(
|
||||||
|
tracks: tracks,
|
||||||
|
collection: props.collection as PlaylistSimple,
|
||||||
|
initialIndex: index,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -164,6 +172,13 @@ class TrackViewBodySection extends HookConsumerWidget {
|
|||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
);
|
);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
playlistNotifier.addCollection(props.collectionId);
|
||||||
|
if (props.collection is AlbumSimple) {
|
||||||
|
historyNotifier
|
||||||
|
.addAlbums([props.collection as AlbumSimple]);
|
||||||
|
} else {
|
||||||
|
historyNotifier
|
||||||
|
.addPlaylists([props.collection as PlaylistSimple]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.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';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
||||||
import 'package:spotube/components/shared/dialogs/confirm_download_dialog.dart';
|
import 'package:spotube/components/shared/dialogs/confirm_download_dialog.dart';
|
||||||
@ -8,6 +9,7 @@ import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
|||||||
import 'package:spotube/components/shared/tracks_view/track_view_provider.dart';
|
import 'package:spotube/components/shared/tracks_view/track_view_provider.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:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_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/provider/user_preferences/user_preferences_state.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||||
@ -23,6 +25,7 @@ class TrackViewBodyOptions extends HookConsumerWidget {
|
|||||||
ref.watch(downloadManagerProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
|
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
||||||
final audioSource =
|
final audioSource =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
|
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
|
||||||
|
|
||||||
@ -72,6 +75,12 @@ class TrackViewBodyOptions extends HookConsumerWidget {
|
|||||||
{
|
{
|
||||||
playlistNotifier.addTracksAtFirst(selectedTracks);
|
playlistNotifier.addTracksAtFirst(selectedTracks);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
playlistNotifier.addCollection(props.collectionId);
|
||||||
|
if (props.collection is AlbumSimple) {
|
||||||
|
historyNotifier.addAlbums([props.collection as AlbumSimple]);
|
||||||
|
} else {
|
||||||
|
historyNotifier
|
||||||
|
.addPlaylists([props.collection as PlaylistSimple]);
|
||||||
|
}
|
||||||
trackViewState.deselectAll();
|
trackViewState.deselectAll();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -79,6 +88,12 @@ class TrackViewBodyOptions extends HookConsumerWidget {
|
|||||||
{
|
{
|
||||||
playlistNotifier.addTracks(selectedTracks);
|
playlistNotifier.addTracks(selectedTracks);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
playlistNotifier.addCollection(props.collectionId);
|
||||||
|
if (props.collection is AlbumSimple) {
|
||||||
|
historyNotifier.addAlbums([props.collection as AlbumSimple]);
|
||||||
|
} else {
|
||||||
|
historyNotifier
|
||||||
|
.addPlaylists([props.collection as PlaylistSimple]);
|
||||||
|
}
|
||||||
trackViewState.deselectAll();
|
trackViewState.deselectAll();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
||||||
import 'package:spotube/components/shared/heart_button.dart';
|
import 'package:spotube/components/shared/heart_button.dart';
|
||||||
@ -9,6 +10,7 @@ import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_
|
|||||||
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
|
||||||
class TrackViewHeaderActions extends HookConsumerWidget {
|
class TrackViewHeaderActions extends HookConsumerWidget {
|
||||||
@ -20,6 +22,7 @@ class TrackViewHeaderActions extends HookConsumerWidget {
|
|||||||
|
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
|
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
||||||
|
|
||||||
final isActive = playlist.collections.contains(props.collectionId);
|
final isActive = playlist.collections.contains(props.collectionId);
|
||||||
|
|
||||||
@ -61,6 +64,13 @@ class TrackViewHeaderActions extends HookConsumerWidget {
|
|||||||
final tracks = await props.pagination.onFetchAll();
|
final tracks = await props.pagination.onFetchAll();
|
||||||
await playlistNotifier.addTracks(tracks);
|
await playlistNotifier.addTracks(tracks);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
playlistNotifier.addCollection(props.collectionId);
|
||||||
|
if (props.collection is AlbumSimple) {
|
||||||
|
historyNotifier
|
||||||
|
.addAlbums([props.collection as AlbumSimple]);
|
||||||
|
} else {
|
||||||
|
historyNotifier
|
||||||
|
.addPlaylists([props.collection as PlaylistSimple]);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (props.onHeart != null && auth != null)
|
if (props.onHeart != null && auth != null)
|
||||||
|
@ -5,12 +5,14 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.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:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/dialogs/select_device_dialog.dart';
|
import 'package:spotube/components/shared/dialogs/select_device_dialog.dart';
|
||||||
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.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/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
@ -28,6 +30,7 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
|||||||
final props = InheritedTrackView.of(context);
|
final props = InheritedTrackView.of(context);
|
||||||
final playlist = ref.watch(proxyPlaylistProvider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
|
final historyNotifier = ref.watch(playbackHistoryProvider.notifier);
|
||||||
|
|
||||||
final isActive = playlist.collections.contains(props.collectionId);
|
final isActive = playlist.collections.contains(props.collectionId);
|
||||||
|
|
||||||
@ -52,10 +55,16 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
|||||||
if (isRemoteDevice) {
|
if (isRemoteDevice) {
|
||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData(
|
props.collection is AlbumSimple
|
||||||
tracks: allTracks,
|
? WebSocketLoadEventData.album(
|
||||||
collectionId: props.collectionId,
|
tracks: allTracks,
|
||||||
initialIndex: Random().nextInt(allTracks.length)),
|
collection: props.collection as AlbumSimple,
|
||||||
|
initialIndex: Random().nextInt(allTracks.length))
|
||||||
|
: WebSocketLoadEventData.playlist(
|
||||||
|
tracks: allTracks,
|
||||||
|
collection: props.collection as PlaylistSimple,
|
||||||
|
initialIndex: Random().nextInt(allTracks.length),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await remotePlayback.setShuffle(true);
|
await remotePlayback.setShuffle(true);
|
||||||
} else {
|
} else {
|
||||||
@ -66,6 +75,11 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
await audioPlayer.setShuffle(true);
|
await audioPlayer.setShuffle(true);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
playlistNotifier.addCollection(props.collectionId);
|
||||||
|
if (props.collection is AlbumSimple) {
|
||||||
|
historyNotifier.addAlbums([props.collection as AlbumSimple]);
|
||||||
|
} else {
|
||||||
|
historyNotifier.addPlaylists([props.collection as PlaylistSimple]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
@ -84,14 +98,24 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
|
|||||||
if (isRemoteDevice) {
|
if (isRemoteDevice) {
|
||||||
final remotePlayback = ref.read(connectProvider.notifier);
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData(
|
props.collection is AlbumSimple
|
||||||
tracks: allTracks,
|
? WebSocketLoadEventData.album(
|
||||||
collectionId: props.collectionId,
|
tracks: allTracks,
|
||||||
),
|
collection: props.collection as AlbumSimple,
|
||||||
|
)
|
||||||
|
: WebSocketLoadEventData.playlist(
|
||||||
|
tracks: allTracks,
|
||||||
|
collection: props.collection as PlaylistSimple,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.load(allTracks, autoPlay: true);
|
await playlistNotifier.load(allTracks, autoPlay: true);
|
||||||
playlistNotifier.addCollection(props.collectionId);
|
playlistNotifier.addCollection(props.collectionId);
|
||||||
|
if (props.collection is AlbumSimple) {
|
||||||
|
historyNotifier.addAlbums([props.collection as AlbumSimple]);
|
||||||
|
} else {
|
||||||
|
historyNotifier.addPlaylists([props.collection as PlaylistSimple]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
@ -39,7 +39,7 @@ class PaginationProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class InheritedTrackView extends InheritedWidget {
|
class InheritedTrackView extends InheritedWidget {
|
||||||
final String collectionId;
|
final Object collection;
|
||||||
final String title;
|
final String title;
|
||||||
final String? description;
|
final String? description;
|
||||||
final String image;
|
final String image;
|
||||||
@ -55,7 +55,7 @@ class InheritedTrackView extends InheritedWidget {
|
|||||||
const InheritedTrackView({
|
const InheritedTrackView({
|
||||||
super.key,
|
super.key,
|
||||||
required super.child,
|
required super.child,
|
||||||
required this.collectionId,
|
required this.collection,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.description,
|
this.description,
|
||||||
required this.image,
|
required this.image,
|
||||||
@ -65,7 +65,11 @@ class InheritedTrackView extends InheritedWidget {
|
|||||||
required this.shareUrl,
|
required this.shareUrl,
|
||||||
this.isLiked = false,
|
this.isLiked = false,
|
||||||
this.onHeart,
|
this.onHeart,
|
||||||
});
|
}) : assert(collection is AlbumSimple || collection is PlaylistSimple);
|
||||||
|
|
||||||
|
String get collectionId => collection is AlbumSimple
|
||||||
|
? (collection as AlbumSimple).id!
|
||||||
|
: (collection as PlaylistSimple).id!;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(InheritedTrackView oldWidget) {
|
bool updateShouldNotify(InheritedTrackView oldWidget) {
|
||||||
@ -78,7 +82,7 @@ class InheritedTrackView extends InheritedWidget {
|
|||||||
oldWidget.onHeart != onHeart ||
|
oldWidget.onHeart != onHeart ||
|
||||||
oldWidget.shareUrl != shareUrl ||
|
oldWidget.shareUrl != shareUrl ||
|
||||||
oldWidget.routePath != routePath ||
|
oldWidget.routePath != routePath ||
|
||||||
oldWidget.collectionId != collectionId ||
|
oldWidget.collection != collection ||
|
||||||
oldWidget.child != child;
|
oldWidget.child != child;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
53
lib/components/stats/common/album_item.dart
Normal file
53
lib/components/stats/common/album_item.dart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/album/album_card.dart';
|
||||||
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
|
import 'package:spotube/components/shared/links/artist_link.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 {
|
||||||
|
final AlbumSimple album;
|
||||||
|
final Widget info;
|
||||||
|
const StatsAlbumItem({super.key, required this.album, required this.info});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
horizontalTitleGap: 8,
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: UniversalImage(
|
||||||
|
path: (album.images).asUrlString(
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
),
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(album.name!),
|
||||||
|
subtitle: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text("${album.albumType?.formatted} • "),
|
||||||
|
Flexible(
|
||||||
|
child: ArtistLink(
|
||||||
|
artists: album.artists!,
|
||||||
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: info,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(
|
||||||
|
context,
|
||||||
|
AlbumPage.name,
|
||||||
|
pathParameters: {"id": album.id!},
|
||||||
|
extra: album,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
39
lib/components/stats/common/artist_item.dart
Normal file
39
lib/components/stats/common/artist_item.dart
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/shared/image/universal_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 {
|
||||||
|
final Artist artist;
|
||||||
|
final Widget info;
|
||||||
|
const StatsArtistItem({
|
||||||
|
super.key,
|
||||||
|
required this.artist,
|
||||||
|
required this.info,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(artist.name!),
|
||||||
|
horizontalTitleGap: 8,
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundImage: UniversalImage.imageProvider(
|
||||||
|
(artist.images).asUrlString(
|
||||||
|
placeholder: ImagePlaceholder.artist,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: info,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(
|
||||||
|
context,
|
||||||
|
ArtistPage.name,
|
||||||
|
pathParameters: {"id": artist.id!},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
lib/components/stats/common/playlist_item.dart
Normal file
46
lib/components/stats/common/playlist_item.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
|
import 'package:spotube/components/shared/playbutton_card.dart';
|
||||||
|
import 'package:spotube/extensions/image.dart';
|
||||||
|
import 'package:spotube/pages/playlist/playlist.dart';
|
||||||
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
|
class StatsPlaylistItem extends StatelessWidget {
|
||||||
|
final PlaylistSimple playlist;
|
||||||
|
final Widget info;
|
||||||
|
const StatsPlaylistItem(
|
||||||
|
{super.key, required this.playlist, required this.info});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
horizontalTitleGap: 8,
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: UniversalImage(
|
||||||
|
path: (playlist.images).asUrlString(
|
||||||
|
placeholder: ImagePlaceholder.collection,
|
||||||
|
),
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(playlist.name!),
|
||||||
|
subtitle: Text(
|
||||||
|
playlist.description!.replaceAll(htmlTagRegexp, ''),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
trailing: info,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(
|
||||||
|
context,
|
||||||
|
PlaylistPage.name,
|
||||||
|
pathParameters: {"id": playlist.id!},
|
||||||
|
extra: playlist,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
49
lib/components/stats/common/track_item.dart
Normal file
49
lib/components/stats/common/track_item.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
|
import 'package:spotube/components/shared/links/artist_link.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 {
|
||||||
|
final Track track;
|
||||||
|
final Widget info;
|
||||||
|
const StatsTrackItem({
|
||||||
|
super.key,
|
||||||
|
required this.track,
|
||||||
|
required this.info,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
horizontalTitleGap: 8,
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: UniversalImage(
|
||||||
|
path: (track.album?.images).asUrlString(
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
),
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(track.name!),
|
||||||
|
subtitle: ArtistLink(
|
||||||
|
artists: track.artists!,
|
||||||
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
|
),
|
||||||
|
trailing: info,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(
|
||||||
|
context,
|
||||||
|
TrackPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"id": track.id!,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
100
lib/components/stats/summary/summary.dart
Normal file
100
lib/components/stats/summary/summary.dart
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
import 'package:spotube/components/stats/summary/summary_card.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.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/utils/service_utils.dart';
|
||||||
|
|
||||||
|
class StatsPageSummarySection extends HookConsumerWidget {
|
||||||
|
const StatsPageSummarySection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final summary = ref.watch(playbackHistorySummaryProvider);
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
sliver: SliverLayoutBuilder(builder: (context, constrains) {
|
||||||
|
return SliverGrid(
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: constrains.isXs
|
||||||
|
? 2
|
||||||
|
: constrains.smAndDown
|
||||||
|
? 3
|
||||||
|
: constrains.mdAndDown
|
||||||
|
? 4
|
||||||
|
: constrains.lgAndDown
|
||||||
|
? 5
|
||||||
|
: 6,
|
||||||
|
mainAxisSpacing: 10,
|
||||||
|
crossAxisSpacing: 10,
|
||||||
|
childAspectRatio: constrains.isXs ? 1.3 : 1.5,
|
||||||
|
),
|
||||||
|
delegate: SliverChildListDelegate([
|
||||||
|
SummaryCard(
|
||||||
|
title: summary.duration.inMinutes.toDouble(),
|
||||||
|
unit: "minutes",
|
||||||
|
description: 'Listened to music',
|
||||||
|
color: Colors.purple,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(context, StatsMinutesPage.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SummaryCard(
|
||||||
|
title: summary.tracks.toDouble(),
|
||||||
|
unit: "songs",
|
||||||
|
description: 'Streamed overall',
|
||||||
|
color: Colors.lightBlue,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(context, StatsStreamsPage.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SummaryCard.unformatted(
|
||||||
|
title: usdFormatter.format(summary.fees.toDouble()),
|
||||||
|
unit: "",
|
||||||
|
description: 'Owed to artists\nthis month',
|
||||||
|
color: Colors.green,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(context, StatsStreamFeesPage.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SummaryCard(
|
||||||
|
title: summary.artists.toDouble(),
|
||||||
|
unit: "artist's",
|
||||||
|
description: 'Music reached you',
|
||||||
|
color: Colors.yellow,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(context, StatsArtistsPage.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SummaryCard(
|
||||||
|
title: summary.albums.toDouble(),
|
||||||
|
unit: "full albums",
|
||||||
|
description: 'Got your love',
|
||||||
|
color: Colors.pink,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(context, StatsAlbumsPage.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SummaryCard(
|
||||||
|
title: summary.playlists.toDouble(),
|
||||||
|
unit: "playlists",
|
||||||
|
description: 'Were on repeat',
|
||||||
|
color: Colors.teal,
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(context, StatsPlaylistsPage.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
86
lib/components/stats/summary/summary_card.dart
Normal file
86
lib/components/stats/summary/summary_card.dart
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
|
||||||
|
class SummaryCard extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String unit;
|
||||||
|
final String description;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
final MaterialColor color;
|
||||||
|
|
||||||
|
SummaryCard({
|
||||||
|
super.key,
|
||||||
|
required double title,
|
||||||
|
required this.unit,
|
||||||
|
required this.description,
|
||||||
|
required this.color,
|
||||||
|
this.onTap,
|
||||||
|
}) : title = compactNumberFormatter.format(title);
|
||||||
|
|
||||||
|
const SummaryCard.unformatted({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.unit,
|
||||||
|
required this.description,
|
||||||
|
required this.color,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData(:textTheme, :brightness) = Theme.of(context);
|
||||||
|
|
||||||
|
final descriptionNewLines = description.split("").where((s) => s == "\n");
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
color: brightness == Brightness.dark ? color.shade100 : color.shade50,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
AutoSizeText.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: title,
|
||||||
|
style: textTheme.headlineLarge?.copyWith(
|
||||||
|
color: color.shade900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: " $unit",
|
||||||
|
style: textTheme.titleMedium?.copyWith(
|
||||||
|
color: color.shade900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
const Gap(5),
|
||||||
|
AutoSizeText(
|
||||||
|
description,
|
||||||
|
maxLines: description.contains("\n")
|
||||||
|
? descriptionNewLines.length + 1
|
||||||
|
: 1,
|
||||||
|
minFontSize: 9,
|
||||||
|
style: textTheme.labelMedium!.copyWith(
|
||||||
|
color: color.shade900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
29
lib/components/stats/top/albums.dart
Normal file
29
lib/components/stats/top/albums.dart
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
import 'package:spotube/components/stats/common/album_item.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
class TopAlbums extends HookConsumerWidget {
|
||||||
|
const TopAlbums({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||||
|
final albums = ref.watch(playbackHistoryTopProvider(historyDuration)
|
||||||
|
.select((value) => value.albums));
|
||||||
|
|
||||||
|
return SliverList.builder(
|
||||||
|
itemCount: albums.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final album = albums[index];
|
||||||
|
return StatsAlbumItem(
|
||||||
|
album: album.album,
|
||||||
|
info: Text(
|
||||||
|
"${compactNumberFormatter.format(album.count)} plays",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
27
lib/components/stats/top/artists.dart
Normal file
27
lib/components/stats/top/artists.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
import 'package:spotube/components/stats/common/artist_item.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
class TopArtists extends HookConsumerWidget {
|
||||||
|
const TopArtists({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||||
|
final artists = ref.watch(playbackHistoryTopProvider(historyDuration)
|
||||||
|
.select((value) => value.artists));
|
||||||
|
|
||||||
|
return SliverList.builder(
|
||||||
|
itemCount: artists.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final artist = artists[index];
|
||||||
|
return StatsArtistItem(
|
||||||
|
artist: artist.artist,
|
||||||
|
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
106
lib/components/stats/top/top.dart
Normal file
106
lib/components/stats/top/top.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/components/shared/themed_button_tab_bar.dart';
|
||||||
|
import 'package:spotube/components/stats/top/albums.dart';
|
||||||
|
import 'package:spotube/components/stats/top/artists.dart';
|
||||||
|
import 'package:spotube/components/stats/top/tracks.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
class StatsPageTopSection extends HookConsumerWidget {
|
||||||
|
const StatsPageTopSection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final tabController = useTabController(initialLength: 3);
|
||||||
|
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||||
|
final historyDurationNotifier =
|
||||||
|
ref.watch(playbackHistoryTopDurationProvider.notifier);
|
||||||
|
|
||||||
|
return SliverMainAxisGroup(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
flexibleSpace: ThemedButtonsTabBar(
|
||||||
|
controller: tabController,
|
||||||
|
tabs: const [
|
||||||
|
Tab(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(5),
|
||||||
|
child: Text("Top Tracks"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(5),
|
||||||
|
child: Text("Top Artists"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(5),
|
||||||
|
child: Text("Top Albums"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: DropdownButton(
|
||||||
|
style: Theme.of(context).textTheme.bodySmall!,
|
||||||
|
isDense: true,
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
underline: const SizedBox(),
|
||||||
|
value: historyDuration,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
historyDurationNotifier.update((_) => value);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.arrow_drop_down),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: HistoryDuration.days7,
|
||||||
|
child: Text("This week"),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: HistoryDuration.days30,
|
||||||
|
child: Text("This month"),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: HistoryDuration.months6,
|
||||||
|
child: Text("Last 6 months"),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: HistoryDuration.year,
|
||||||
|
child: Text("This year"),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: HistoryDuration.years2,
|
||||||
|
child: Text("Last 2 years"),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: HistoryDuration.allTime,
|
||||||
|
child: Text("All time"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: tabController,
|
||||||
|
builder: (context, _) {
|
||||||
|
return switch (tabController.index) {
|
||||||
|
1 => const TopArtists(),
|
||||||
|
2 => const TopAlbums(),
|
||||||
|
_ => const TopTracks(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
31
lib/components/stats/top/tracks.dart
Normal file
31
lib/components/stats/top/tracks.dart
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
import 'package:spotube/components/stats/common/track_item.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
class TopTracks extends HookConsumerWidget {
|
||||||
|
const TopTracks({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||||
|
final tracks = ref.watch(
|
||||||
|
playbackHistoryTopProvider(historyDuration)
|
||||||
|
.select((value) => value.tracks),
|
||||||
|
);
|
||||||
|
|
||||||
|
return SliverList.builder(
|
||||||
|
itemCount: tracks.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final track = tracks[index];
|
||||||
|
return StatsTrackItem(
|
||||||
|
track: track.track,
|
||||||
|
info: Text(
|
||||||
|
"${compactNumberFormatter.format(track.count)} plays",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,6 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
extension AlbumExtensions on AlbumSimple {
|
extension AlbumExtensions on AlbumSimple {
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
"albumType": albumType?.name,
|
|
||||||
"id": id,
|
|
||||||
"name": name,
|
|
||||||
"images": images
|
|
||||||
?.map((image) => {
|
|
||||||
"height": image.height,
|
|
||||||
"url": image.url,
|
|
||||||
"width": image.width,
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Album toAlbum() {
|
Album toAlbum() {
|
||||||
Album album = Album();
|
Album album = Album();
|
||||||
album.albumType = albumType;
|
album.albumType = albumType;
|
||||||
|
@ -1,17 +1,5 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
extension ArtistJson on ArtistSimple {
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
"href": href,
|
|
||||||
"id": id,
|
|
||||||
"name": name,
|
|
||||||
"type": type,
|
|
||||||
"uri": uri,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ArtistExtension on List<ArtistSimple> {
|
extension ArtistExtension on List<ArtistSimple> {
|
||||||
String asString() {
|
String asString() {
|
||||||
return map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
return map((e) => e.name?.replaceAll(",", " ")).join(", ");
|
||||||
|
@ -3,8 +3,6 @@ import 'dart:io';
|
|||||||
import 'package:metadata_god/metadata_god.dart';
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/album_simple.dart';
|
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
extension TrackExtensions on Track {
|
extension TrackExtensions on Track {
|
||||||
@ -39,33 +37,6 @@ extension TrackExtensions on Track {
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return TrackExtensions.trackToJson(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, dynamic> trackToJson(Track track) {
|
|
||||||
return {
|
|
||||||
"album": track.album?.toJson(),
|
|
||||||
"artists": track.artists?.map((artist) => artist.toJson()).toList(),
|
|
||||||
"available_markets": track.availableMarkets?.map((e) => e.name).toList(),
|
|
||||||
"disc_number": track.discNumber,
|
|
||||||
"duration_ms": track.durationMs,
|
|
||||||
"explicit": track.explicit,
|
|
||||||
// "external_ids"track.: externalIds,
|
|
||||||
// "external_urls"track.: externalUrls,
|
|
||||||
"href": track.href,
|
|
||||||
"id": track.id,
|
|
||||||
"is_playable": track.isPlayable,
|
|
||||||
// "linked_from"track.: linkedFrom,
|
|
||||||
"name": track.name,
|
|
||||||
"popularity": track.popularity,
|
|
||||||
"preview_rrl": track.previewUrl,
|
|
||||||
"track_number": track.trackNumber,
|
|
||||||
"type": track.type,
|
|
||||||
"uri": track.uri,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrackSimpleExtensions on TrackSimple {
|
extension TrackSimpleExtensions on TrackSimple {
|
||||||
|
@ -324,5 +324,6 @@
|
|||||||
"select": "Select",
|
"select": "Select",
|
||||||
"connect_client_alert": "You're being controlled by {client}",
|
"connect_client_alert": "You're being controlled by {client}",
|
||||||
"this_device": "This Device",
|
"this_device": "This Device",
|
||||||
"remote": "Remote"
|
"remote": "Remote",
|
||||||
|
"stats": "Stats"
|
||||||
}
|
}
|
@ -5,7 +5,6 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||||
|
|
||||||
|
@ -16,16 +16,89 @@ final _privateConstructorUsedError = UnsupportedError(
|
|||||||
|
|
||||||
WebSocketLoadEventData _$WebSocketLoadEventDataFromJson(
|
WebSocketLoadEventData _$WebSocketLoadEventDataFromJson(
|
||||||
Map<String, dynamic> json) {
|
Map<String, dynamic> json) {
|
||||||
return _WebSocketLoadEventData.fromJson(json);
|
switch (json['runtimeType']) {
|
||||||
|
case 'playlist':
|
||||||
|
return WebSocketLoadEventDataPlaylist.fromJson(json);
|
||||||
|
case 'album':
|
||||||
|
return WebSocketLoadEventDataAlbum.fromJson(json);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw CheckedFromJsonException(
|
||||||
|
json,
|
||||||
|
'runtimeType',
|
||||||
|
'WebSocketLoadEventData',
|
||||||
|
'Invalid union type "${json['runtimeType']}"!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$WebSocketLoadEventData {
|
mixin _$WebSocketLoadEventData {
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||||
List<Track> get tracks => throw _privateConstructorUsedError;
|
List<Track> get tracks => throw _privateConstructorUsedError;
|
||||||
String? get collectionId => throw _privateConstructorUsedError;
|
Object? get collection => throw _privateConstructorUsedError;
|
||||||
int? get initialIndex => throw _privateConstructorUsedError;
|
int? get initialIndex => throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
PlaylistSimple? collection,
|
||||||
|
int? initialIndex)
|
||||||
|
playlist,
|
||||||
|
required TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex)
|
||||||
|
album,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
PlaylistSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
playlist,
|
||||||
|
TResult? Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
album,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
PlaylistSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
playlist,
|
||||||
|
TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
album,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(WebSocketLoadEventDataPlaylist value) playlist,
|
||||||
|
required TResult Function(WebSocketLoadEventDataAlbum value) album,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(WebSocketLoadEventDataPlaylist value)? playlist,
|
||||||
|
TResult? Function(WebSocketLoadEventDataAlbum value)? album,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(WebSocketLoadEventDataPlaylist value)? playlist,
|
||||||
|
TResult Function(WebSocketLoadEventDataAlbum value)? album,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
$WebSocketLoadEventDataCopyWith<WebSocketLoadEventData> get copyWith =>
|
$WebSocketLoadEventDataCopyWith<WebSocketLoadEventData> get copyWith =>
|
||||||
@ -40,7 +113,6 @@ abstract class $WebSocketLoadEventDataCopyWith<$Res> {
|
|||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
{@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
String? collectionId,
|
|
||||||
int? initialIndex});
|
int? initialIndex});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +131,6 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res,
|
|||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? tracks = null,
|
Object? tracks = null,
|
||||||
Object? collectionId = freezed,
|
|
||||||
Object? initialIndex = freezed,
|
Object? initialIndex = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
@ -67,10 +138,6 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res,
|
|||||||
? _value.tracks
|
? _value.tracks
|
||||||
: tracks // ignore: cast_nullable_to_non_nullable
|
: tracks // ignore: cast_nullable_to_non_nullable
|
||||||
as List<Track>,
|
as List<Track>,
|
||||||
collectionId: freezed == collectionId
|
|
||||||
? _value.collectionId
|
|
||||||
: collectionId // ignore: cast_nullable_to_non_nullable
|
|
||||||
as String?,
|
|
||||||
initialIndex: freezed == initialIndex
|
initialIndex: freezed == initialIndex
|
||||||
? _value.initialIndex
|
? _value.initialIndex
|
||||||
: initialIndex // ignore: cast_nullable_to_non_nullable
|
: initialIndex // ignore: cast_nullable_to_non_nullable
|
||||||
@ -80,46 +147,46 @@ class _$WebSocketLoadEventDataCopyWithImpl<$Res,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
abstract class _$$WebSocketLoadEventDataImplCopyWith<$Res>
|
abstract class _$$WebSocketLoadEventDataPlaylistImplCopyWith<$Res>
|
||||||
implements $WebSocketLoadEventDataCopyWith<$Res> {
|
implements $WebSocketLoadEventDataCopyWith<$Res> {
|
||||||
factory _$$WebSocketLoadEventDataImplCopyWith(
|
factory _$$WebSocketLoadEventDataPlaylistImplCopyWith(
|
||||||
_$WebSocketLoadEventDataImpl value,
|
_$WebSocketLoadEventDataPlaylistImpl value,
|
||||||
$Res Function(_$WebSocketLoadEventDataImpl) then) =
|
$Res Function(_$WebSocketLoadEventDataPlaylistImpl) then) =
|
||||||
__$$WebSocketLoadEventDataImplCopyWithImpl<$Res>;
|
__$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>;
|
||||||
@override
|
@override
|
||||||
@useResult
|
@useResult
|
||||||
$Res call(
|
$Res call(
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
{@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
String? collectionId,
|
PlaylistSimple? collection,
|
||||||
int? initialIndex});
|
int? initialIndex});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
class __$$WebSocketLoadEventDataImplCopyWithImpl<$Res>
|
class __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<$Res>
|
||||||
extends _$WebSocketLoadEventDataCopyWithImpl<$Res,
|
extends _$WebSocketLoadEventDataCopyWithImpl<$Res,
|
||||||
_$WebSocketLoadEventDataImpl>
|
_$WebSocketLoadEventDataPlaylistImpl>
|
||||||
implements _$$WebSocketLoadEventDataImplCopyWith<$Res> {
|
implements _$$WebSocketLoadEventDataPlaylistImplCopyWith<$Res> {
|
||||||
__$$WebSocketLoadEventDataImplCopyWithImpl(
|
__$$WebSocketLoadEventDataPlaylistImplCopyWithImpl(
|
||||||
_$WebSocketLoadEventDataImpl _value,
|
_$WebSocketLoadEventDataPlaylistImpl _value,
|
||||||
$Res Function(_$WebSocketLoadEventDataImpl) _then)
|
$Res Function(_$WebSocketLoadEventDataPlaylistImpl) _then)
|
||||||
: super(_value, _then);
|
: super(_value, _then);
|
||||||
|
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? tracks = null,
|
Object? tracks = null,
|
||||||
Object? collectionId = freezed,
|
Object? collection = freezed,
|
||||||
Object? initialIndex = freezed,
|
Object? initialIndex = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$WebSocketLoadEventDataImpl(
|
return _then(_$WebSocketLoadEventDataPlaylistImpl(
|
||||||
tracks: null == tracks
|
tracks: null == tracks
|
||||||
? _value._tracks
|
? _value._tracks
|
||||||
: tracks // ignore: cast_nullable_to_non_nullable
|
: tracks // ignore: cast_nullable_to_non_nullable
|
||||||
as List<Track>,
|
as List<Track>,
|
||||||
collectionId: freezed == collectionId
|
collection: freezed == collection
|
||||||
? _value.collectionId
|
? _value.collection
|
||||||
: collectionId // ignore: cast_nullable_to_non_nullable
|
: collection // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,
|
as PlaylistSimple?,
|
||||||
initialIndex: freezed == initialIndex
|
initialIndex: freezed == initialIndex
|
||||||
? _value.initialIndex
|
? _value.initialIndex
|
||||||
: initialIndex // ignore: cast_nullable_to_non_nullable
|
: initialIndex // ignore: cast_nullable_to_non_nullable
|
||||||
@ -130,16 +197,21 @@ class __$$WebSocketLoadEventDataImplCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$WebSocketLoadEventDataImpl implements _WebSocketLoadEventData {
|
class _$WebSocketLoadEventDataPlaylistImpl
|
||||||
_$WebSocketLoadEventDataImpl(
|
extends WebSocketLoadEventDataPlaylist {
|
||||||
|
_$WebSocketLoadEventDataPlaylistImpl(
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||||
required final List<Track> tracks,
|
required final List<Track> tracks,
|
||||||
this.collectionId,
|
this.collection,
|
||||||
this.initialIndex})
|
this.initialIndex,
|
||||||
: _tracks = tracks;
|
final String? $type})
|
||||||
|
: _tracks = tracks,
|
||||||
|
$type = $type ?? 'playlist',
|
||||||
|
super._();
|
||||||
|
|
||||||
factory _$WebSocketLoadEventDataImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$WebSocketLoadEventDataPlaylistImpl.fromJson(
|
||||||
_$$WebSocketLoadEventDataImplFromJson(json);
|
Map<String, dynamic> json) =>
|
||||||
|
_$$WebSocketLoadEventDataPlaylistImplFromJson(json);
|
||||||
|
|
||||||
final List<Track> _tracks;
|
final List<Track> _tracks;
|
||||||
@override
|
@override
|
||||||
@ -151,23 +223,26 @@ class _$WebSocketLoadEventDataImpl implements _WebSocketLoadEventData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final String? collectionId;
|
final PlaylistSimple? collection;
|
||||||
@override
|
@override
|
||||||
final int? initialIndex;
|
final int? initialIndex;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'WebSocketLoadEventData(tracks: $tracks, collectionId: $collectionId, initialIndex: $initialIndex)';
|
return 'WebSocketLoadEventData.playlist(tracks: $tracks, collection: $collection, initialIndex: $initialIndex)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) ||
|
return identical(this, other) ||
|
||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is _$WebSocketLoadEventDataImpl &&
|
other is _$WebSocketLoadEventDataPlaylistImpl &&
|
||||||
const DeepCollectionEquality().equals(other._tracks, _tracks) &&
|
const DeepCollectionEquality().equals(other._tracks, _tracks) &&
|
||||||
(identical(other.collectionId, collectionId) ||
|
(identical(other.collection, collection) ||
|
||||||
other.collectionId == collectionId) &&
|
other.collection == collection) &&
|
||||||
(identical(other.initialIndex, initialIndex) ||
|
(identical(other.initialIndex, initialIndex) ||
|
||||||
other.initialIndex == initialIndex));
|
other.initialIndex == initialIndex));
|
||||||
}
|
}
|
||||||
@ -175,42 +250,361 @@ class _$WebSocketLoadEventDataImpl implements _WebSocketLoadEventData {
|
|||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,
|
int get hashCode => Object.hash(runtimeType,
|
||||||
const DeepCollectionEquality().hash(_tracks), collectionId, initialIndex);
|
const DeepCollectionEquality().hash(_tracks), collection, initialIndex);
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
_$$WebSocketLoadEventDataImplCopyWith<_$WebSocketLoadEventDataImpl>
|
_$$WebSocketLoadEventDataPlaylistImplCopyWith<
|
||||||
get copyWith => __$$WebSocketLoadEventDataImplCopyWithImpl<
|
_$WebSocketLoadEventDataPlaylistImpl>
|
||||||
_$WebSocketLoadEventDataImpl>(this, _$identity);
|
get copyWith => __$$WebSocketLoadEventDataPlaylistImplCopyWithImpl<
|
||||||
|
_$WebSocketLoadEventDataPlaylistImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
PlaylistSimple? collection,
|
||||||
|
int? initialIndex)
|
||||||
|
playlist,
|
||||||
|
required TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex)
|
||||||
|
album,
|
||||||
|
}) {
|
||||||
|
return playlist(tracks, collection, initialIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
PlaylistSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
playlist,
|
||||||
|
TResult? Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
album,
|
||||||
|
}) {
|
||||||
|
return playlist?.call(tracks, collection, initialIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
PlaylistSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
playlist,
|
||||||
|
TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
album,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (playlist != null) {
|
||||||
|
return playlist(tracks, collection, initialIndex);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(WebSocketLoadEventDataPlaylist value) playlist,
|
||||||
|
required TResult Function(WebSocketLoadEventDataAlbum value) album,
|
||||||
|
}) {
|
||||||
|
return playlist(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(WebSocketLoadEventDataPlaylist value)? playlist,
|
||||||
|
TResult? Function(WebSocketLoadEventDataAlbum value)? album,
|
||||||
|
}) {
|
||||||
|
return playlist?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(WebSocketLoadEventDataPlaylist value)? playlist,
|
||||||
|
TResult Function(WebSocketLoadEventDataAlbum value)? album,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (playlist != null) {
|
||||||
|
return playlist(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return _$$WebSocketLoadEventDataImplToJson(
|
return _$$WebSocketLoadEventDataPlaylistImplToJson(
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _WebSocketLoadEventData implements WebSocketLoadEventData {
|
abstract class WebSocketLoadEventDataPlaylist extends WebSocketLoadEventData {
|
||||||
factory _WebSocketLoadEventData(
|
factory WebSocketLoadEventDataPlaylist(
|
||||||
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||||
required final List<Track> tracks,
|
required final List<Track> tracks,
|
||||||
final String? collectionId,
|
final PlaylistSimple? collection,
|
||||||
final int? initialIndex}) = _$WebSocketLoadEventDataImpl;
|
final int? initialIndex}) = _$WebSocketLoadEventDataPlaylistImpl;
|
||||||
|
WebSocketLoadEventDataPlaylist._() : super._();
|
||||||
|
|
||||||
factory _WebSocketLoadEventData.fromJson(Map<String, dynamic> json) =
|
factory WebSocketLoadEventDataPlaylist.fromJson(Map<String, dynamic> json) =
|
||||||
_$WebSocketLoadEventDataImpl.fromJson;
|
_$WebSocketLoadEventDataPlaylistImpl.fromJson;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||||
List<Track> get tracks;
|
List<Track> get tracks;
|
||||||
@override
|
@override
|
||||||
String? get collectionId;
|
PlaylistSimple? get collection;
|
||||||
@override
|
@override
|
||||||
int? get initialIndex;
|
int? get initialIndex;
|
||||||
@override
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
_$$WebSocketLoadEventDataImplCopyWith<_$WebSocketLoadEventDataImpl>
|
_$$WebSocketLoadEventDataPlaylistImplCopyWith<
|
||||||
|
_$WebSocketLoadEventDataPlaylistImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$WebSocketLoadEventDataAlbumImplCopyWith<$Res>
|
||||||
|
implements $WebSocketLoadEventDataCopyWith<$Res> {
|
||||||
|
factory _$$WebSocketLoadEventDataAlbumImplCopyWith(
|
||||||
|
_$WebSocketLoadEventDataAlbumImpl value,
|
||||||
|
$Res Function(_$WebSocketLoadEventDataAlbumImpl) then) =
|
||||||
|
__$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<$Res>
|
||||||
|
extends _$WebSocketLoadEventDataCopyWithImpl<$Res,
|
||||||
|
_$WebSocketLoadEventDataAlbumImpl>
|
||||||
|
implements _$$WebSocketLoadEventDataAlbumImplCopyWith<$Res> {
|
||||||
|
__$$WebSocketLoadEventDataAlbumImplCopyWithImpl(
|
||||||
|
_$WebSocketLoadEventDataAlbumImpl _value,
|
||||||
|
$Res Function(_$WebSocketLoadEventDataAlbumImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? tracks = null,
|
||||||
|
Object? collection = freezed,
|
||||||
|
Object? initialIndex = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$WebSocketLoadEventDataAlbumImpl(
|
||||||
|
tracks: null == tracks
|
||||||
|
? _value._tracks
|
||||||
|
: tracks // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<Track>,
|
||||||
|
collection: freezed == collection
|
||||||
|
? _value.collection
|
||||||
|
: collection // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AlbumSimple?,
|
||||||
|
initialIndex: freezed == initialIndex
|
||||||
|
? _value.initialIndex
|
||||||
|
: initialIndex // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$WebSocketLoadEventDataAlbumImpl extends WebSocketLoadEventDataAlbum {
|
||||||
|
_$WebSocketLoadEventDataAlbumImpl(
|
||||||
|
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||||
|
required final List<Track> tracks,
|
||||||
|
this.collection,
|
||||||
|
this.initialIndex,
|
||||||
|
final String? $type})
|
||||||
|
: _tracks = tracks,
|
||||||
|
$type = $type ?? 'album',
|
||||||
|
super._();
|
||||||
|
|
||||||
|
factory _$WebSocketLoadEventDataAlbumImpl.fromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$$WebSocketLoadEventDataAlbumImplFromJson(json);
|
||||||
|
|
||||||
|
final List<Track> _tracks;
|
||||||
|
@override
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||||
|
List<Track> get tracks {
|
||||||
|
if (_tracks is EqualUnmodifiableListView) return _tracks;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(_tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final AlbumSimple? collection;
|
||||||
|
@override
|
||||||
|
final int? initialIndex;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'WebSocketLoadEventData.album(tracks: $tracks, collection: $collection, initialIndex: $initialIndex)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$WebSocketLoadEventDataAlbumImpl &&
|
||||||
|
const DeepCollectionEquality().equals(other._tracks, _tracks) &&
|
||||||
|
(identical(other.collection, collection) ||
|
||||||
|
other.collection == collection) &&
|
||||||
|
(identical(other.initialIndex, initialIndex) ||
|
||||||
|
other.initialIndex == initialIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,
|
||||||
|
const DeepCollectionEquality().hash(_tracks), collection, initialIndex);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl>
|
||||||
|
get copyWith => __$$WebSocketLoadEventDataAlbumImplCopyWithImpl<
|
||||||
|
_$WebSocketLoadEventDataAlbumImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
PlaylistSimple? collection,
|
||||||
|
int? initialIndex)
|
||||||
|
playlist,
|
||||||
|
required TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex)
|
||||||
|
album,
|
||||||
|
}) {
|
||||||
|
return album(tracks, collection, initialIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
PlaylistSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
playlist,
|
||||||
|
TResult? Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
album,
|
||||||
|
}) {
|
||||||
|
return album?.call(tracks, collection, initialIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
PlaylistSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
playlist,
|
||||||
|
TResult Function(
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex)?
|
||||||
|
album,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (album != null) {
|
||||||
|
return album(tracks, collection, initialIndex);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(WebSocketLoadEventDataPlaylist value) playlist,
|
||||||
|
required TResult Function(WebSocketLoadEventDataAlbum value) album,
|
||||||
|
}) {
|
||||||
|
return album(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(WebSocketLoadEventDataPlaylist value)? playlist,
|
||||||
|
TResult? Function(WebSocketLoadEventDataAlbum value)? album,
|
||||||
|
}) {
|
||||||
|
return album?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(WebSocketLoadEventDataPlaylist value)? playlist,
|
||||||
|
TResult Function(WebSocketLoadEventDataAlbum value)? album,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (album != null) {
|
||||||
|
return album(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$WebSocketLoadEventDataAlbumImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class WebSocketLoadEventDataAlbum extends WebSocketLoadEventData {
|
||||||
|
factory WebSocketLoadEventDataAlbum(
|
||||||
|
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||||
|
required final List<Track> tracks,
|
||||||
|
final AlbumSimple? collection,
|
||||||
|
final int? initialIndex}) = _$WebSocketLoadEventDataAlbumImpl;
|
||||||
|
WebSocketLoadEventDataAlbum._() : super._();
|
||||||
|
|
||||||
|
factory WebSocketLoadEventDataAlbum.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$WebSocketLoadEventDataAlbumImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||||
|
List<Track> get tracks;
|
||||||
|
@override
|
||||||
|
AlbumSimple? get collection;
|
||||||
|
@override
|
||||||
|
int? get initialIndex;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$WebSocketLoadEventDataAlbumImplCopyWith<_$WebSocketLoadEventDataAlbumImpl>
|
||||||
get copyWith => throw _privateConstructorUsedError;
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
@ -6,20 +6,48 @@ part of 'connect.dart';
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
_$WebSocketLoadEventDataImpl _$$WebSocketLoadEventDataImplFromJson(
|
_$WebSocketLoadEventDataPlaylistImpl
|
||||||
Map<String, dynamic> json) =>
|
_$$WebSocketLoadEventDataPlaylistImplFromJson(Map json) =>
|
||||||
_$WebSocketLoadEventDataImpl(
|
_$WebSocketLoadEventDataPlaylistImpl(
|
||||||
tracks: (json['tracks'] as List<dynamic>)
|
tracks: (json['tracks'] as List<dynamic>)
|
||||||
.map((e) => Track.fromJson(e as Map<String, dynamic>))
|
.map((e) => Track.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
collectionId: json['collectionId'] as String?,
|
collection: json['collection'] == null
|
||||||
initialIndex: json['initialIndex'] as int?,
|
? null
|
||||||
);
|
: PlaylistSimple.fromJson(
|
||||||
|
Map<String, dynamic>.from(json['collection'] as Map)),
|
||||||
|
initialIndex: json['initialIndex'] as int?,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$WebSocketLoadEventDataImplToJson(
|
Map<String, dynamic> _$$WebSocketLoadEventDataPlaylistImplToJson(
|
||||||
_$WebSocketLoadEventDataImpl instance) =>
|
_$WebSocketLoadEventDataPlaylistImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'tracks': _tracksJson(instance.tracks),
|
'tracks': _tracksJson(instance.tracks),
|
||||||
'collectionId': instance.collectionId,
|
'collection': instance.collection?.toJson(),
|
||||||
'initialIndex': instance.initialIndex,
|
'initialIndex': instance.initialIndex,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$WebSocketLoadEventDataAlbumImpl _$$WebSocketLoadEventDataAlbumImplFromJson(
|
||||||
|
Map json) =>
|
||||||
|
_$WebSocketLoadEventDataAlbumImpl(
|
||||||
|
tracks: (json['tracks'] as List<dynamic>)
|
||||||
|
.map((e) => Track.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||||
|
.toList(),
|
||||||
|
collection: json['collection'] == null
|
||||||
|
? null
|
||||||
|
: AlbumSimple.fromJson(
|
||||||
|
Map<String, dynamic>.from(json['collection'] as Map)),
|
||||||
|
initialIndex: json['initialIndex'] as int?,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$WebSocketLoadEventDataAlbumImplToJson(
|
||||||
|
_$WebSocketLoadEventDataAlbumImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'tracks': _tracksJson(instance.tracks),
|
||||||
|
'collection': instance.collection?.toJson(),
|
||||||
|
'initialIndex': instance.initialIndex,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
};
|
};
|
||||||
|
@ -6,14 +6,27 @@ List<Map<String, dynamic>> _tracksJson(List<Track> tracks) {
|
|||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class WebSocketLoadEventData with _$WebSocketLoadEventData {
|
class WebSocketLoadEventData with _$WebSocketLoadEventData {
|
||||||
factory WebSocketLoadEventData({
|
const WebSocketLoadEventData._();
|
||||||
|
|
||||||
|
factory WebSocketLoadEventData.playlist({
|
||||||
@JsonKey(name: 'tracks', toJson: _tracksJson) required List<Track> tracks,
|
@JsonKey(name: 'tracks', toJson: _tracksJson) required List<Track> tracks,
|
||||||
String? collectionId,
|
PlaylistSimple? collection,
|
||||||
int? initialIndex,
|
int? initialIndex,
|
||||||
}) = _WebSocketLoadEventData;
|
}) = WebSocketLoadEventDataPlaylist;
|
||||||
|
|
||||||
|
factory WebSocketLoadEventData.album({
|
||||||
|
@JsonKey(name: 'tracks', toJson: _tracksJson) required List<Track> tracks,
|
||||||
|
AlbumSimple? collection,
|
||||||
|
int? initialIndex,
|
||||||
|
}) = WebSocketLoadEventDataAlbum;
|
||||||
|
|
||||||
factory WebSocketLoadEventData.fromJson(Map<String, dynamic> json) =>
|
factory WebSocketLoadEventData.fromJson(Map<String, dynamic> json) =>
|
||||||
_$WebSocketLoadEventDataFromJson(json);
|
_$WebSocketLoadEventDataFromJson(json);
|
||||||
|
|
||||||
|
String? get collectionId => when(
|
||||||
|
playlist: (tracks, collection, _) => collection?.id,
|
||||||
|
album: (tracks, collection, _) => collection?.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSocketLoadEvent extends WebSocketEvent<WebSocketLoadEventData> {
|
class WebSocketLoadEvent extends WebSocketEvent<WebSocketLoadEventData> {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
|
|
||||||
class CurrentPlaylist {
|
class CurrentPlaylist {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
|
||||||
|
|
||||||
class LocalTrack extends Track {
|
class LocalTrack extends Track {
|
||||||
final String path;
|
final String path;
|
||||||
@ -35,9 +34,10 @@ class LocalTrack extends Track {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
...TrackExtensions.trackToJson(this),
|
...super.toJson(),
|
||||||
'path': path,
|
'path': path,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ class SourceTypeAdapter extends TypeAdapter<SourceType> {
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
SourceMatch _$SourceMatchFromJson(Map<String, dynamic> json) => SourceMatch(
|
SourceMatch _$SourceMatchFromJson(Map json) => SourceMatch(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
sourceId: json['sourceId'] as String,
|
sourceId: json['sourceId'] as String,
|
||||||
sourceType: $enumDecode(_$SourceTypeEnumMap, json['sourceType']),
|
sourceType: $enumDecode(_$SourceTypeEnumMap, json['sourceType']),
|
||||||
|
@ -6,14 +6,13 @@ part of 'home_feed.dart';
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
_$SpotifySectionPlaylistImpl _$$SpotifySectionPlaylistImplFromJson(
|
_$SpotifySectionPlaylistImpl _$$SpotifySectionPlaylistImplFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
_$SpotifySectionPlaylistImpl(
|
_$SpotifySectionPlaylistImpl(
|
||||||
description: json['description'] as String,
|
description: json['description'] as String,
|
||||||
format: json['format'] as String,
|
format: json['format'] as String,
|
||||||
images: (json['images'] as List<dynamic>)
|
images: (json['images'] as List<dynamic>)
|
||||||
.map((e) =>
|
.map((e) => SpotifySectionItemImage.fromJson(
|
||||||
SpotifySectionItemImage.fromJson(e as Map<String, dynamic>))
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
owner: json['owner'] as String,
|
owner: json['owner'] as String,
|
||||||
@ -25,20 +24,19 @@ Map<String, dynamic> _$$SpotifySectionPlaylistImplToJson(
|
|||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'description': instance.description,
|
'description': instance.description,
|
||||||
'format': instance.format,
|
'format': instance.format,
|
||||||
'images': instance.images,
|
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'owner': instance.owner,
|
'owner': instance.owner,
|
||||||
'uri': instance.uri,
|
'uri': instance.uri,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SpotifySectionArtistImpl _$$SpotifySectionArtistImplFromJson(
|
_$SpotifySectionArtistImpl _$$SpotifySectionArtistImplFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
_$SpotifySectionArtistImpl(
|
_$SpotifySectionArtistImpl(
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
uri: json['uri'] as String,
|
uri: json['uri'] as String,
|
||||||
images: (json['images'] as List<dynamic>)
|
images: (json['images'] as List<dynamic>)
|
||||||
.map((e) =>
|
.map((e) => SpotifySectionItemImage.fromJson(
|
||||||
SpotifySectionItemImage.fromJson(e as Map<String, dynamic>))
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -47,19 +45,18 @@ Map<String, dynamic> _$$SpotifySectionArtistImplToJson(
|
|||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'uri': instance.uri,
|
'uri': instance.uri,
|
||||||
'images': instance.images,
|
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SpotifySectionAlbumImpl _$$SpotifySectionAlbumImplFromJson(
|
_$SpotifySectionAlbumImpl _$$SpotifySectionAlbumImplFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
_$SpotifySectionAlbumImpl(
|
_$SpotifySectionAlbumImpl(
|
||||||
artists: (json['artists'] as List<dynamic>)
|
artists: (json['artists'] as List<dynamic>)
|
||||||
.map((e) =>
|
.map((e) => SpotifySectionAlbumArtist.fromJson(
|
||||||
SpotifySectionAlbumArtist.fromJson(e as Map<String, dynamic>))
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
images: (json['images'] as List<dynamic>)
|
images: (json['images'] as List<dynamic>)
|
||||||
.map((e) =>
|
.map((e) => SpotifySectionItemImage.fromJson(
|
||||||
SpotifySectionItemImage.fromJson(e as Map<String, dynamic>))
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
uri: json['uri'] as String,
|
uri: json['uri'] as String,
|
||||||
@ -68,14 +65,14 @@ _$SpotifySectionAlbumImpl _$$SpotifySectionAlbumImplFromJson(
|
|||||||
Map<String, dynamic> _$$SpotifySectionAlbumImplToJson(
|
Map<String, dynamic> _$$SpotifySectionAlbumImplToJson(
|
||||||
_$SpotifySectionAlbumImpl instance) =>
|
_$SpotifySectionAlbumImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'artists': instance.artists,
|
'artists': instance.artists.map((e) => e.toJson()).toList(),
|
||||||
'images': instance.images,
|
'images': instance.images.map((e) => e.toJson()).toList(),
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'uri': instance.uri,
|
'uri': instance.uri,
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SpotifySectionAlbumArtistImpl _$$SpotifySectionAlbumArtistImplFromJson(
|
_$SpotifySectionAlbumArtistImpl _$$SpotifySectionAlbumArtistImplFromJson(
|
||||||
Map<String, dynamic> json) =>
|
Map json) =>
|
||||||
_$SpotifySectionAlbumArtistImpl(
|
_$SpotifySectionAlbumArtistImpl(
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
uri: json['uri'] as String,
|
uri: json['uri'] as String,
|
||||||
@ -89,7 +86,7 @@ Map<String, dynamic> _$$SpotifySectionAlbumArtistImplToJson(
|
|||||||
};
|
};
|
||||||
|
|
||||||
_$SpotifySectionItemImageImpl _$$SpotifySectionItemImageImplFromJson(
|
_$SpotifySectionItemImageImpl _$$SpotifySectionItemImageImplFromJson(
|
||||||
Map<String, dynamic> json) =>
|
Map json) =>
|
||||||
_$SpotifySectionItemImageImpl(
|
_$SpotifySectionItemImageImpl(
|
||||||
height: json['height'] as num?,
|
height: json['height'] as num?,
|
||||||
url: json['url'] as String,
|
url: json['url'] as String,
|
||||||
@ -105,40 +102,40 @@ Map<String, dynamic> _$$SpotifySectionItemImageImplToJson(
|
|||||||
};
|
};
|
||||||
|
|
||||||
_$SpotifyHomeFeedSectionItemImpl _$$SpotifyHomeFeedSectionItemImplFromJson(
|
_$SpotifyHomeFeedSectionItemImpl _$$SpotifyHomeFeedSectionItemImplFromJson(
|
||||||
Map<String, dynamic> json) =>
|
Map json) =>
|
||||||
_$SpotifyHomeFeedSectionItemImpl(
|
_$SpotifyHomeFeedSectionItemImpl(
|
||||||
typename: json['typename'] as String,
|
typename: json['typename'] as String,
|
||||||
playlist: json['playlist'] == null
|
playlist: json['playlist'] == null
|
||||||
? null
|
? null
|
||||||
: SpotifySectionPlaylist.fromJson(
|
: SpotifySectionPlaylist.fromJson(
|
||||||
json['playlist'] as Map<String, dynamic>),
|
Map<String, dynamic>.from(json['playlist'] as Map)),
|
||||||
artist: json['artist'] == null
|
artist: json['artist'] == null
|
||||||
? null
|
? null
|
||||||
: SpotifySectionArtist.fromJson(
|
: SpotifySectionArtist.fromJson(
|
||||||
json['artist'] as Map<String, dynamic>),
|
Map<String, dynamic>.from(json['artist'] as Map)),
|
||||||
album: json['album'] == null
|
album: json['album'] == null
|
||||||
? null
|
? null
|
||||||
: SpotifySectionAlbum.fromJson(json['album'] as Map<String, dynamic>),
|
: SpotifySectionAlbum.fromJson(
|
||||||
|
Map<String, dynamic>.from(json['album'] as Map)),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$SpotifyHomeFeedSectionItemImplToJson(
|
Map<String, dynamic> _$$SpotifyHomeFeedSectionItemImplToJson(
|
||||||
_$SpotifyHomeFeedSectionItemImpl instance) =>
|
_$SpotifyHomeFeedSectionItemImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'typename': instance.typename,
|
'typename': instance.typename,
|
||||||
'playlist': instance.playlist,
|
'playlist': instance.playlist?.toJson(),
|
||||||
'artist': instance.artist,
|
'artist': instance.artist?.toJson(),
|
||||||
'album': instance.album,
|
'album': instance.album?.toJson(),
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SpotifyHomeFeedSectionImpl _$$SpotifyHomeFeedSectionImplFromJson(
|
_$SpotifyHomeFeedSectionImpl _$$SpotifyHomeFeedSectionImplFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
_$SpotifyHomeFeedSectionImpl(
|
_$SpotifyHomeFeedSectionImpl(
|
||||||
typename: json['typename'] as String,
|
typename: json['typename'] as String,
|
||||||
title: json['title'] as String?,
|
title: json['title'] as String?,
|
||||||
uri: json['uri'] as String,
|
uri: json['uri'] as String,
|
||||||
items: (json['items'] as List<dynamic>)
|
items: (json['items'] as List<dynamic>)
|
||||||
.map((e) =>
|
.map((e) => SpotifyHomeFeedSectionItem.fromJson(
|
||||||
SpotifyHomeFeedSectionItem.fromJson(e as Map<String, dynamic>))
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -148,16 +145,15 @@ Map<String, dynamic> _$$SpotifyHomeFeedSectionImplToJson(
|
|||||||
'typename': instance.typename,
|
'typename': instance.typename,
|
||||||
'title': instance.title,
|
'title': instance.title,
|
||||||
'uri': instance.uri,
|
'uri': instance.uri,
|
||||||
'items': instance.items,
|
'items': instance.items.map((e) => e.toJson()).toList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
_$SpotifyHomeFeedImpl _$$SpotifyHomeFeedImplFromJson(
|
_$SpotifyHomeFeedImpl _$$SpotifyHomeFeedImplFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
_$SpotifyHomeFeedImpl(
|
_$SpotifyHomeFeedImpl(
|
||||||
greeting: json['greeting'] as String,
|
greeting: json['greeting'] as String,
|
||||||
sections: (json['sections'] as List<dynamic>)
|
sections: (json['sections'] as List<dynamic>)
|
||||||
.map(
|
.map((e) => SpotifyHomeFeedSection.fromJson(
|
||||||
(e) => SpotifyHomeFeedSection.fromJson(e as Map<String, dynamic>))
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -165,5 +161,5 @@ Map<String, dynamic> _$$SpotifyHomeFeedImplToJson(
|
|||||||
_$SpotifyHomeFeedImpl instance) =>
|
_$SpotifyHomeFeedImpl instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'greeting': instance.greeting,
|
'greeting': instance.greeting,
|
||||||
'sections': instance.sections,
|
'sections': instance.sections.map((e) => e.toJson()).toList(),
|
||||||
};
|
};
|
||||||
|
@ -6,8 +6,7 @@ part of 'recommendation_seeds.dart';
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
_$RecommendationSeedsImpl _$$RecommendationSeedsImplFromJson(
|
_$RecommendationSeedsImpl _$$RecommendationSeedsImplFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
_$RecommendationSeedsImpl(
|
_$RecommendationSeedsImpl(
|
||||||
acousticness: json['acousticness'] as num?,
|
acousticness: json['acousticness'] as num?,
|
||||||
danceability: json['danceability'] as num?,
|
danceability: json['danceability'] as num?,
|
||||||
|
@ -6,60 +6,55 @@ part of 'spotify_friends.dart';
|
|||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
SpotifyFriend _$SpotifyFriendFromJson(Map<String, dynamic> json) =>
|
SpotifyFriend _$SpotifyFriendFromJson(Map json) => SpotifyFriend(
|
||||||
SpotifyFriend(
|
|
||||||
uri: json['uri'] as String,
|
uri: json['uri'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
imageUrl: json['imageUrl'] as String,
|
imageUrl: json['imageUrl'] as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
SpotifyActivityArtist _$SpotifyActivityArtistFromJson(
|
SpotifyActivityArtist _$SpotifyActivityArtistFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
SpotifyActivityArtist(
|
SpotifyActivityArtist(
|
||||||
uri: json['uri'] as String,
|
uri: json['uri'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
SpotifyActivityAlbum _$SpotifyActivityAlbumFromJson(
|
SpotifyActivityAlbum _$SpotifyActivityAlbumFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
SpotifyActivityAlbum(
|
SpotifyActivityAlbum(
|
||||||
uri: json['uri'] as String,
|
uri: json['uri'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
SpotifyActivityContext _$SpotifyActivityContextFromJson(
|
SpotifyActivityContext _$SpotifyActivityContextFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
SpotifyActivityContext(
|
SpotifyActivityContext(
|
||||||
uri: json['uri'] as String,
|
uri: json['uri'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
index: json['index'] as num,
|
index: json['index'] as num,
|
||||||
);
|
);
|
||||||
|
|
||||||
SpotifyActivityTrack _$SpotifyActivityTrackFromJson(
|
SpotifyActivityTrack _$SpotifyActivityTrackFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
SpotifyActivityTrack(
|
SpotifyActivityTrack(
|
||||||
uri: json['uri'] as String,
|
uri: json['uri'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
imageUrl: json['imageUrl'] as String,
|
imageUrl: json['imageUrl'] as String,
|
||||||
artist: SpotifyActivityArtist.fromJson(
|
artist: SpotifyActivityArtist.fromJson(
|
||||||
json['artist'] as Map<String, dynamic>),
|
Map<String, dynamic>.from(json['artist'] as Map)),
|
||||||
album:
|
album: SpotifyActivityAlbum.fromJson(
|
||||||
SpotifyActivityAlbum.fromJson(json['album'] as Map<String, dynamic>),
|
Map<String, dynamic>.from(json['album'] as Map)),
|
||||||
context: SpotifyActivityContext.fromJson(
|
context: SpotifyActivityContext.fromJson(
|
||||||
json['context'] as Map<String, dynamic>),
|
Map<String, dynamic>.from(json['context'] as Map)),
|
||||||
);
|
);
|
||||||
|
|
||||||
SpotifyFriendActivity _$SpotifyFriendActivityFromJson(
|
SpotifyFriendActivity _$SpotifyFriendActivityFromJson(Map json) =>
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
SpotifyFriendActivity(
|
SpotifyFriendActivity(
|
||||||
user: SpotifyFriend.fromJson(json['user'] as Map<String, dynamic>),
|
user: SpotifyFriend.fromJson(
|
||||||
track:
|
Map<String, dynamic>.from(json['user'] as Map)),
|
||||||
SpotifyActivityTrack.fromJson(json['track'] as Map<String, dynamic>),
|
track: SpotifyActivityTrack.fromJson(
|
||||||
|
Map<String, dynamic>.from(json['track'] as Map)),
|
||||||
);
|
);
|
||||||
|
|
||||||
SpotifyFriends _$SpotifyFriendsFromJson(Map<String, dynamic> json) =>
|
SpotifyFriends _$SpotifyFriendsFromJson(Map json) => SpotifyFriends(
|
||||||
SpotifyFriends(
|
|
||||||
friends: (json['friends'] as List<dynamic>)
|
friends: (json['friends'] as List<dynamic>)
|
||||||
.map((e) => SpotifyFriendActivity.fromJson(e as Map<String, dynamic>))
|
.map((e) => SpotifyFriendActivity.fromJson(
|
||||||
|
Map<String, dynamic>.from(e as Map)))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
@ -8,6 +8,8 @@ import 'package:spotube/extensions/image.dart';
|
|||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class AlbumPage extends HookConsumerWidget {
|
class AlbumPage extends HookConsumerWidget {
|
||||||
|
static const name = "album";
|
||||||
|
|
||||||
final AlbumSimple album;
|
final AlbumSimple album;
|
||||||
const AlbumPage({
|
const AlbumPage({
|
||||||
super.key,
|
super.key,
|
||||||
@ -22,7 +24,7 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!));
|
final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!));
|
||||||
|
|
||||||
return InheritedTrackView(
|
return InheritedTrackView(
|
||||||
collectionId: album.id!,
|
collection: album,
|
||||||
image: album.images.asUrlString(
|
image: album.images.asUrlString(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
|
@ -15,6 +15,8 @@ import 'package:spotube/pages/artist/section/top_tracks.dart';
|
|||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class ArtistPage extends HookConsumerWidget {
|
class ArtistPage extends HookConsumerWidget {
|
||||||
|
static const name = "artist";
|
||||||
|
|
||||||
final String artistId;
|
final String artistId;
|
||||||
final logger = getLogger(ArtistPage);
|
final logger = getLogger(ArtistPage);
|
||||||
ArtistPage(this.artistId, {super.key});
|
ArtistPage(this.artistId, {super.key});
|
||||||
|
@ -52,8 +52,9 @@ class ArtistPageTopTracks extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData(
|
WebSocketLoadEventData.playlist(
|
||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
|
collection: null,
|
||||||
initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id),
|
initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -5,10 +5,13 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/connect/local_devices.dart';
|
import 'package:spotube/components/connect/local_devices.dart';
|
||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.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:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class ConnectPage extends HookConsumerWidget {
|
class ConnectPage extends HookConsumerWidget {
|
||||||
|
static const name = "connect";
|
||||||
|
|
||||||
const ConnectPage({super.key});
|
const ConnectPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -65,9 +68,9 @@ class ConnectPage extends HookConsumerWidget {
|
|||||||
selected: selected,
|
selected: selected,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
ServiceUtils.push(
|
ServiceUtils.pushNamed(
|
||||||
context,
|
context,
|
||||||
"/connect/control",
|
ConnectControlPage.name,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
connectClientsNotifier.resolveService(device);
|
connectClientsNotifier.resolveService(device);
|
||||||
|
@ -13,6 +13,7 @@ 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:spotube/services/audio_player/loop_mode.dart';
|
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||||
@ -46,6 +47,8 @@ class RemotePlayerQueue extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ConnectControlPage extends HookConsumerWidget {
|
class ConnectControlPage extends HookConsumerWidget {
|
||||||
|
static const name = "connect_control";
|
||||||
|
|
||||||
const ConnectControlPage({super.key});
|
const ConnectControlPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -125,9 +128,13 @@ class ConnectControlPage extends HookConsumerWidget {
|
|||||||
playlist.activeTrack?.name ?? "",
|
playlist.activeTrack?.name ?? "",
|
||||||
style: textTheme.titleLarge!,
|
style: textTheme.titleLarge!,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.push(
|
if (playlist.activeTrack == null) return;
|
||||||
|
ServiceUtils.pushNamed(
|
||||||
context,
|
context,
|
||||||
"/track/${playlist.activeTrack?.id}",
|
TrackPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"id": playlist.activeTrack!.id!,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -7,8 +7,10 @@ import 'package:spotube/components/desktop_login/login_form.dart';
|
|||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.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/mobile_login/mobile_login.dart';
|
||||||
|
|
||||||
class DesktopLoginPage extends HookConsumerWidget {
|
class DesktopLoginPage extends HookConsumerWidget {
|
||||||
|
static const name = WebViewLogin.name;
|
||||||
const DesktopLoginPage({super.key});
|
const DesktopLoginPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -8,10 +8,12 @@ import 'package:spotube/components/desktop_login/login_form.dart';
|
|||||||
import 'package:spotube/components/shared/links/hyper_link.dart';
|
import 'package:spotube/components/shared/links/hyper_link.dart';
|
||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/pages/home/home.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class LoginTutorial extends ConsumerWidget {
|
class LoginTutorial extends ConsumerWidget {
|
||||||
|
static const name = "login_tutorial";
|
||||||
const LoginTutorial({super.key});
|
const LoginTutorial({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -53,7 +55,7 @@ class LoginTutorial extends ConsumerWidget {
|
|||||||
overrideDone: FilledButton(
|
overrideDone: FilledButton(
|
||||||
onPressed: authenticationNotifier.isLoggedIn
|
onPressed: authenticationNotifier.isLoggedIn
|
||||||
? () {
|
? () {
|
||||||
ServiceUtils.push(context, "/");
|
ServiceUtils.pushNamed(context, HomePage.name);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Center(child: Text(context.l10n.done)),
|
child: Center(child: Text(context.l10n.done)),
|
||||||
|
@ -12,6 +12,8 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
|||||||
import 'package:spotube/themes/theme.dart';
|
import 'package:spotube/themes/theme.dart';
|
||||||
|
|
||||||
class GettingStarting extends HookConsumerWidget {
|
class GettingStarting extends HookConsumerWidget {
|
||||||
|
static const name = "getting_started";
|
||||||
|
|
||||||
const GettingStarting({super.key});
|
const GettingStarting({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -5,6 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/getting_started/blur_card.dart';
|
import 'package:spotube/components/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/mobile_login.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';
|
||||||
|
|
||||||
@ -104,7 +106,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await KVStoreService.setDoneGettingStarted(true);
|
await KVStoreService.setDoneGettingStarted(true);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.go("/");
|
context.go(HomePage.name);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -120,7 +122,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await KVStoreService.setDoneGettingStarted(true);
|
await KVStoreService.setDoneGettingStarted(true);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.push("/login");
|
context.pushNamed(WebViewLogin.name);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -10,6 +10,8 @@ import 'package:spotube/extensions/constrains.dart';
|
|||||||
import 'package:spotube/provider/spotify/views/home_section.dart';
|
import 'package:spotube/provider/spotify/views/home_section.dart';
|
||||||
|
|
||||||
class HomeFeedSectionPage extends HookConsumerWidget {
|
class HomeFeedSectionPage extends HookConsumerWidget {
|
||||||
|
static const name = "home_feed_section";
|
||||||
|
|
||||||
final String sectionUri;
|
final String sectionUri;
|
||||||
const HomeFeedSectionPage({super.key, required this.sectionUri});
|
const HomeFeedSectionPage({super.key, required this.sectionUri});
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class GenrePlaylistsPage extends HookConsumerWidget {
|
class GenrePlaylistsPage extends HookConsumerWidget {
|
||||||
|
static const name = "genre_playlists";
|
||||||
|
|
||||||
final Category category;
|
final Category category;
|
||||||
const GenrePlaylistsPage({super.key, required this.category});
|
const GenrePlaylistsPage({super.key, required this.category});
|
||||||
|
|
||||||
|
@ -9,9 +9,11 @@ import 'package:spotube/collections/gradients.dart';
|
|||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.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';
|
||||||
|
|
||||||
class GenrePage extends HookConsumerWidget {
|
class GenrePage extends HookConsumerWidget {
|
||||||
|
static const name = "genre";
|
||||||
const GenrePage({super.key});
|
const GenrePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -47,7 +49,13 @@ class GenrePage extends HookConsumerWidget {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.push("/genre/${category.id}", extra: category);
|
context.pushNamed(
|
||||||
|
GenrePlaylistsPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"categoryId": category.id!,
|
||||||
|
},
|
||||||
|
extra: category,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Ink(
|
child: Ink(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
|
@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/connect/connect_device.dart';
|
import 'package:spotube/components/connect/connect_device.dart';
|
||||||
import 'package:spotube/components/home/sections/featured.dart';
|
import 'package:spotube/components/home/sections/featured.dart';
|
||||||
import 'package:spotube/components/home/sections/feed.dart';
|
import 'package:spotube/components/home/sections/feed.dart';
|
||||||
@ -10,16 +11,15 @@ import 'package:spotube/components/home/sections/friends.dart';
|
|||||||
import 'package:spotube/components/home/sections/genres.dart';
|
import 'package:spotube/components/home/sections/genres.dart';
|
||||||
import 'package:spotube/components/home/sections/made_for_user.dart';
|
import 'package:spotube/components/home/sections/made_for_user.dart';
|
||||||
import 'package:spotube/components/home/sections/new_releases.dart';
|
import 'package:spotube/components/home/sections/new_releases.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/home/sections/recent.dart';
|
||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/pages/settings/settings.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.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:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class HomePage extends HookConsumerWidget {
|
class HomePage extends HookConsumerWidget {
|
||||||
|
static const name = "home";
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -34,44 +34,27 @@ class HomePage extends HookConsumerWidget {
|
|||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
slivers: [
|
slivers: [
|
||||||
if (mediaQuery.mdAndDown)
|
if (mediaQuery.smAndDown)
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
floating: true,
|
floating: true,
|
||||||
title: Assets.spotubeLogoPng.image(height: 45),
|
title: Assets.spotubeLogoPng.image(height: 45),
|
||||||
actions: [
|
actions: [
|
||||||
const ConnectDeviceButton(),
|
const ConnectDeviceButton(),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Consumer(builder: (context, ref, _) {
|
IconButton(
|
||||||
final auth = ref.watch(authenticationProvider);
|
icon: const Icon(SpotubeIcons.settings, size: 20),
|
||||||
final me = ref.watch(meProvider);
|
onPressed: () {
|
||||||
final meData = me.asData?.value;
|
ServiceUtils.pushNamed(context, SettingsPage.name);
|
||||||
|
},
|
||||||
if (auth == null) {
|
),
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
return IconButton(
|
|
||||||
icon: CircleAvatar(
|
|
||||||
backgroundImage: UniversalImage.imageProvider(
|
|
||||||
(meData?.images).asUrlString(
|
|
||||||
placeholder: ImagePlaceholder.artist,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: IconButton.styleFrom(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
ServiceUtils.push(context, "/profile");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else if (kIsMacOS)
|
else if (kIsMacOS)
|
||||||
const SliverGap(10),
|
const SliverGap(10),
|
||||||
const HomeGenresSection(),
|
const HomeGenresSection(),
|
||||||
|
const SliverGap(10),
|
||||||
|
const SliverToBoxAdapter(child: HomeRecentlyPlayedSection()),
|
||||||
const SliverToBoxAdapter(child: HomeFeaturedSection()),
|
const SliverToBoxAdapter(child: HomeFeaturedSection()),
|
||||||
const HomePageFriendsSection(),
|
const HomePageFriendsSection(),
|
||||||
const SliverToBoxAdapter(child: HomeNewReleasesSection()),
|
const SliverToBoxAdapter(child: HomeNewReleasesSection()),
|
||||||
|
@ -10,6 +10,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/provider/scrobbler_provider.dart';
|
import 'package:spotube/provider/scrobbler_provider.dart';
|
||||||
|
|
||||||
class LastFMLoginPage extends HookConsumerWidget {
|
class LastFMLoginPage extends HookConsumerWidget {
|
||||||
|
static const name = "lastfm_login";
|
||||||
const LastFMLoginPage({super.key});
|
const LastFMLoginPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -12,6 +12,8 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
|
|
||||||
class LibraryPage extends HookConsumerWidget {
|
class LibraryPage extends HookConsumerWidget {
|
||||||
|
static const name = "library";
|
||||||
|
|
||||||
const LibraryPage({super.key});
|
const LibraryPage({super.key});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
@ -21,6 +21,8 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
|||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class LocalLibraryPage extends HookConsumerWidget {
|
class LocalLibraryPage extends HookConsumerWidget {
|
||||||
|
static const name = "local_library_page";
|
||||||
|
|
||||||
final String location;
|
final String location;
|
||||||
final bool isDownloads;
|
final bool isDownloads;
|
||||||
const LocalLibraryPage(this.location, {super.key, this.isDownloads = false});
|
const LocalLibraryPage(this.location, {super.key, this.isDownloads = false});
|
||||||
|
@ -24,6 +24,8 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
|||||||
const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0);
|
const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0);
|
||||||
|
|
||||||
class PlaylistGeneratorPage extends HookConsumerWidget {
|
class PlaylistGeneratorPage extends HookConsumerWidget {
|
||||||
|
static const name = "playlist_generator";
|
||||||
|
|
||||||
const PlaylistGeneratorPage({super.key});
|
const PlaylistGeneratorPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -10,10 +10,13 @@ import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart
|
|||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.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/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class PlaylistGenerateResultPage extends HookConsumerWidget {
|
class PlaylistGenerateResultPage extends HookConsumerWidget {
|
||||||
|
static const name = "playlist_generate_result";
|
||||||
|
|
||||||
final GeneratePlaylistProviderInput state;
|
final GeneratePlaylistProviderInput state;
|
||||||
|
|
||||||
const PlaylistGenerateResultPage({
|
const PlaylistGenerateResultPage({
|
||||||
@ -123,8 +126,11 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (playlist != null) {
|
if (playlist != null) {
|
||||||
router.go(
|
router.goNamed(
|
||||||
'/playlist/${playlist.id}',
|
PlaylistPage.name,
|
||||||
|
pathParameters: {
|
||||||
|
"id": playlist.id!,
|
||||||
|
},
|
||||||
extra: playlist,
|
extra: playlist,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ import 'package:spotube/utils/platform.dart';
|
|||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class LyricsPage extends HookConsumerWidget {
|
class LyricsPage extends HookConsumerWidget {
|
||||||
|
static const name = "lyrics";
|
||||||
|
|
||||||
final bool isModal;
|
final bool isModal;
|
||||||
const LyricsPage({super.key, this.isModal = false});
|
const LyricsPage({super.key, this.isModal = false});
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ import 'package:spotube/utils/platform.dart';
|
|||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
class MiniLyricsPage extends HookConsumerWidget {
|
class MiniLyricsPage extends HookConsumerWidget {
|
||||||
|
static const name = "mini_lyrics";
|
||||||
|
|
||||||
final Size prevSize;
|
final Size prevSize;
|
||||||
const MiniLyricsPage({super.key, required this.prevSize});
|
const MiniLyricsPage({super.key, required this.prevSize});
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import 'package:spotube/provider/authentication_provider.dart';
|
|||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class WebViewLogin extends HookConsumerWidget {
|
class WebViewLogin extends HookConsumerWidget {
|
||||||
|
static const name = "login";
|
||||||
const WebViewLogin({super.key});
|
const WebViewLogin({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -3,9 +3,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/tracks_view/track_view.dart';
|
import 'package:spotube/components/shared/tracks_view/track_view.dart';
|
||||||
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
||||||
|
import 'package:spotube/pages/playlist/playlist.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class LikedPlaylistPage extends HookConsumerWidget {
|
class LikedPlaylistPage extends HookConsumerWidget {
|
||||||
|
static const name = PlaylistPage.name;
|
||||||
|
|
||||||
final PlaylistSimple playlist;
|
final PlaylistSimple playlist;
|
||||||
const LikedPlaylistPage({
|
const LikedPlaylistPage({
|
||||||
super.key,
|
super.key,
|
||||||
@ -18,7 +21,7 @@ class LikedPlaylistPage extends HookConsumerWidget {
|
|||||||
final tracks = likedTracks.asData?.value ?? <Track>[];
|
final tracks = likedTracks.asData?.value ?? <Track>[];
|
||||||
|
|
||||||
return InheritedTrackView(
|
return InheritedTrackView(
|
||||||
collectionId: playlist.id!,
|
collection: playlist,
|
||||||
image: "assets/liked-tracks.jpg",
|
image: "assets/liked-tracks.jpg",
|
||||||
pagination: PaginationProps(
|
pagination: PaginationProps(
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
|
@ -10,6 +10,8 @@ import 'package:spotube/extensions/image.dart';
|
|||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class PlaylistPage extends HookConsumerWidget {
|
class PlaylistPage extends HookConsumerWidget {
|
||||||
|
static const name = "playlist";
|
||||||
|
|
||||||
final PlaylistSimple playlist;
|
final PlaylistSimple playlist;
|
||||||
const PlaylistPage({
|
const PlaylistPage({
|
||||||
super.key,
|
super.key,
|
||||||
@ -29,7 +31,7 @@ class PlaylistPage extends HookConsumerWidget {
|
|||||||
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!);
|
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!);
|
||||||
|
|
||||||
return InheritedTrackView(
|
return InheritedTrackView(
|
||||||
collectionId: playlist.id!,
|
collection: playlist,
|
||||||
image: playlist.images.asUrlString(
|
image: playlist.images.asUrlString(
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
|
@ -14,6 +14,8 @@ import 'package:spotube/provider/spotify/spotify.dart';
|
|||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class ProfilePage extends HookConsumerWidget {
|
class ProfilePage extends HookConsumerWidget {
|
||||||
|
static const name = "profile";
|
||||||
|
|
||||||
const ProfilePage({super.key});
|
const ProfilePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -14,6 +14,7 @@ import 'package:spotube/components/root/sidebar.dart';
|
|||||||
import 'package:spotube/components/root/spotube_navigation_bar.dart';
|
import 'package:spotube/components/root/spotube_navigation_bar.dart';
|
||||||
import 'package:spotube/extensions/context.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/pages/home/home.dart';
|
||||||
import 'package:spotube/provider/connect/server.dart';
|
import 'package:spotube/provider/connect/server.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
@ -22,13 +23,6 @@ import 'package:spotube/utils/persisted_state_notifier.dart';
|
|||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
const rootPaths = {
|
|
||||||
"/": 0,
|
|
||||||
"/search": 1,
|
|
||||||
"/library": 2,
|
|
||||||
"/lyrics": 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
class RootApp extends HookConsumerWidget {
|
class RootApp extends HookConsumerWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
const RootApp({
|
const RootApp({
|
||||||
@ -42,7 +36,6 @@ class RootApp extends HookConsumerWidget {
|
|||||||
final downloader = ref.watch(downloadManagerProvider);
|
final downloader = ref.watch(downloadManagerProvider);
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final location = GoRouterState.of(context).matchedLocation;
|
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
@ -179,32 +172,18 @@ class RootApp extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [backgroundColor]);
|
}, [backgroundColor]);
|
||||||
|
|
||||||
void onSelectIndexChanged(int d) {
|
|
||||||
final invertedRouteMap =
|
|
||||||
rootPaths.map((key, value) => MapEntry(value, key));
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
GoRouter.of(context).go(invertedRouteMap[d]!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore: deprecated_member_use
|
// ignore: deprecated_member_use
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
if (rootPaths[location] != 0) {
|
final routerState = GoRouterState.of(context);
|
||||||
onSelectIndexChanged(0);
|
if (routerState.matchedLocation != "/") {
|
||||||
|
context.goNamed(HomePage.name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Sidebar(
|
body: Sidebar(child: child),
|
||||||
selectedIndex: rootPaths[location],
|
|
||||||
onSelectedIndexChanged: onSelectIndexChanged,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
drawerScrimColor: Colors.transparent,
|
drawerScrimColor: Colors.transparent,
|
||||||
endDrawer: kIsDesktop
|
endDrawer: kIsDesktop
|
||||||
@ -238,10 +217,7 @@ class RootApp extends HookConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
BottomPlayer(),
|
BottomPlayer(),
|
||||||
SpotubeNavigationBar(
|
const SpotubeNavigationBar(),
|
||||||
selectedIndex: rootPaths[location],
|
|
||||||
onSelectedIndexChanged: onSelectIndexChanged,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart' hide Page;
|
|||||||
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:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.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';
|
||||||
@ -26,6 +27,8 @@ import 'package:spotube/services/kv_store/kv_store.dart';
|
|||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class SearchPage extends HookConsumerWidget {
|
class SearchPage extends HookConsumerWidget {
|
||||||
|
static const name = "search";
|
||||||
|
|
||||||
const SearchPage({super.key});
|
const SearchPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -85,99 +88,117 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: kIsDesktop && !kIsMacOS ? const PageWindowTitleBar() : null,
|
appBar: kIsDesktop && !kIsMacOS
|
||||||
|
? const PageWindowTitleBar(automaticallyImplyLeading: true)
|
||||||
|
: null,
|
||||||
body: !authenticationNotifier.isLoggedIn
|
body: !authenticationNotifier.isLoggedIn
|
||||||
? const AnonymousFallback()
|
? const AnonymousFallback()
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Row(
|
||||||
padding: const EdgeInsets.symmetric(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
horizontal: 20,
|
children: [
|
||||||
vertical: 10,
|
if ((kIsMobile || kIsMacOS) && context.canPop())
|
||||||
),
|
const BackButton()
|
||||||
color: theme.scaffoldBackgroundColor,
|
else
|
||||||
child: SearchAnchor(
|
const Gap(20),
|
||||||
searchController: controller,
|
Expanded(
|
||||||
viewBuilder: (_) => HookBuilder(builder: (context) {
|
child: Padding(
|
||||||
final searchController = useListenable(controller);
|
padding: const EdgeInsets.only(
|
||||||
final update = useForceUpdate();
|
right: 20,
|
||||||
final suggestions = searchController.text.isEmpty
|
top: 20,
|
||||||
? KVStoreService.recentSearches
|
bottom: 20,
|
||||||
: KVStoreService.recentSearches
|
),
|
||||||
.where(
|
child: SearchAnchor(
|
||||||
(s) =>
|
searchController: controller,
|
||||||
weightedRatio(
|
viewBuilder: (_) => HookBuilder(builder: (context) {
|
||||||
s.toLowerCase(),
|
final searchController =
|
||||||
searchController.text.toLowerCase(),
|
useListenable(controller);
|
||||||
) >
|
final update = useForceUpdate();
|
||||||
50,
|
final suggestions = searchController.text.isEmpty
|
||||||
)
|
? KVStoreService.recentSearches
|
||||||
.toList();
|
: KVStoreService.recentSearches
|
||||||
|
.where(
|
||||||
|
(s) =>
|
||||||
|
weightedRatio(
|
||||||
|
s.toLowerCase(),
|
||||||
|
searchController.text
|
||||||
|
.toLowerCase(),
|
||||||
|
) >
|
||||||
|
50,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: suggestions.length,
|
itemCount: suggestions.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final suggestion = suggestions[index];
|
final suggestion = suggestions[index];
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(SpotubeIcons.history),
|
leading: const Icon(SpotubeIcons.history),
|
||||||
title: Text(suggestion),
|
title: Text(suggestion),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(SpotubeIcons.trash),
|
icon: const Icon(SpotubeIcons.trash),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
KVStoreService.setRecentSearches(
|
KVStoreService.setRecentSearches(
|
||||||
KVStoreService.recentSearches
|
KVStoreService.recentSearches
|
||||||
.where((s) => s != suggestion)
|
.where((s) => s != suggestion)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
);
|
||||||
|
update();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
controller.closeView(suggestion);
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
searchTermStateProvider.notifier)
|
||||||
|
.state = suggestion;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
update();
|
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
onTap: () {
|
}),
|
||||||
controller.closeView(suggestion);
|
suggestionsBuilder: (context, controller) {
|
||||||
ref
|
return [];
|
||||||
.read(searchTermStateProvider.notifier)
|
},
|
||||||
.state = suggestion;
|
viewOnSubmitted: (value) async {
|
||||||
},
|
controller.closeView(value);
|
||||||
);
|
Timer(
|
||||||
},
|
const Duration(milliseconds: 50),
|
||||||
);
|
() {
|
||||||
}),
|
ref
|
||||||
suggestionsBuilder: (context, controller) {
|
.read(searchTermStateProvider.notifier)
|
||||||
return [];
|
.state = value;
|
||||||
},
|
if (value.trim().isEmpty) {
|
||||||
viewOnSubmitted: (value) async {
|
return;
|
||||||
controller.closeView(value);
|
}
|
||||||
Timer(
|
KVStoreService.setRecentSearches(
|
||||||
const Duration(milliseconds: 50),
|
{
|
||||||
() {
|
value,
|
||||||
ref.read(searchTermStateProvider.notifier).state =
|
...KVStoreService.recentSearches,
|
||||||
value;
|
}.toList(),
|
||||||
if (value.trim().isEmpty) {
|
);
|
||||||
return;
|
},
|
||||||
}
|
);
|
||||||
KVStoreService.setRecentSearches(
|
},
|
||||||
{
|
builder: (context, controller) {
|
||||||
value,
|
return SearchBar(
|
||||||
...KVStoreService.recentSearches,
|
autoFocus: queries.none((s) =>
|
||||||
}.toList(),
|
s.asData?.value != null &&
|
||||||
);
|
!s.hasError) &&
|
||||||
},
|
!kIsMobile,
|
||||||
);
|
controller: controller,
|
||||||
},
|
leading: const Icon(SpotubeIcons.search),
|
||||||
builder: (context, controller) {
|
hintText: "${context.l10n.search}...",
|
||||||
return SearchBar(
|
onTap: controller.openView,
|
||||||
autoFocus: queries.none((s) =>
|
onChanged: (_) => controller.openView(),
|
||||||
s.asData?.value != null && !s.hasError) &&
|
);
|
||||||
!kIsMobile,
|
},
|
||||||
controller: controller,
|
),
|
||||||
leading: const Icon(SpotubeIcons.search),
|
),
|
||||||
hintText: "${context.l10n.search}...",
|
),
|
||||||
onTap: controller.openView,
|
],
|
||||||
onChanged: (_) => controller.openView(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
|
@ -76,7 +76,7 @@ class SearchTracksSection extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (shouldPlay) {
|
if (shouldPlay) {
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData(
|
WebSocketLoadEventData.playlist(
|
||||||
tracks: [track],
|
tracks: [track],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -17,6 +17,8 @@ final _licenseProvider = FutureProvider<String>((ref) async {
|
|||||||
});
|
});
|
||||||
|
|
||||||
class AboutSpotube extends HookConsumerWidget {
|
class AboutSpotube extends HookConsumerWidget {
|
||||||
|
static const name = "about";
|
||||||
|
|
||||||
const AboutSpotube({super.key});
|
const AboutSpotube({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -11,6 +11,8 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
|
|
||||||
class BlackListPage extends HookConsumerWidget {
|
class BlackListPage extends HookConsumerWidget {
|
||||||
|
static const name = "blacklist";
|
||||||
|
|
||||||
const BlackListPage({super.key});
|
const BlackListPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -11,6 +11,8 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
|
|
||||||
class LogsPage extends HookWidget {
|
class LogsPage extends HookWidget {
|
||||||
|
static const name = "logs";
|
||||||
|
|
||||||
const LogsPage({super.key});
|
const LogsPage({super.key});
|
||||||
|
|
||||||
List<({DateTime? date, String body})> parseLogs(String raw) {
|
List<({DateTime? date, String body})> parseLogs(String raw) {
|
||||||
|
@ -4,10 +4,15 @@ 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/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/settings/section_card_with_heading.dart';
|
import 'package:spotube/components/settings/section_card_with_heading.dart';
|
||||||
|
import 'package:spotube/components/shared/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/pages/profile/profile.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/scrobbler_provider.dart';
|
import 'package:spotube/provider/scrobbler_provider.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});
|
||||||
@ -15,9 +20,12 @@ 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);
|
||||||
final router = GoRouter.of(context);
|
final me = ref.watch(meProvider);
|
||||||
|
final meData = me.asData?.value;
|
||||||
|
|
||||||
final logoutBtnStyle = FilledButton.styleFrom(
|
final logoutBtnStyle = FilledButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
@ -27,6 +35,24 @@ class SettingsAccountSection extends HookConsumerWidget {
|
|||||||
return SectionCardWithHeading(
|
return SectionCardWithHeading(
|
||||||
heading: context.l10n.account,
|
heading: context.l10n.account,
|
||||||
children: [
|
children: [
|
||||||
|
if (auth != null)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(SpotubeIcons.user),
|
||||||
|
title: const Text("User Profile"),
|
||||||
|
trailing: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: CircleAvatar(
|
||||||
|
backgroundImage: UniversalImage.imageProvider(
|
||||||
|
(meData?.images).asUrlString(
|
||||||
|
placeholder: ImagePlaceholder.artist,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.pushNamed(context, ProfilePage.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
if (auth == null)
|
if (auth == null)
|
||||||
LayoutBuilder(builder: (context, constrains) {
|
LayoutBuilder(builder: (context, constrains) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
@ -16,6 +16,8 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
|||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class SettingsPage extends HookConsumerWidget {
|
class SettingsPage extends HookConsumerWidget {
|
||||||
|
static const name = "settings";
|
||||||
|
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -29,6 +31,7 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
appBar: PageWindowTitleBar(
|
appBar: PageWindowTitleBar(
|
||||||
title: Text(context.l10n.settings),
|
title: Text(context.l10n.settings),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
),
|
),
|
||||||
body: Scrollbar(
|
body: Scrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
38
lib/pages/stats/albums/albums.dart
Normal file
38
lib/pages/stats/albums/albums.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/components/stats/common/album_item.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
class StatsAlbumsPage extends HookConsumerWidget {
|
||||||
|
static const name = "stats_albums";
|
||||||
|
const StatsAlbumsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final albums = ref.watch(
|
||||||
|
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||||
|
.select((s) => s.albums),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: const PageWindowTitleBar(
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
centerTitle: false,
|
||||||
|
title: Text("Albums"),
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
itemCount: albums.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final album = albums[index];
|
||||||
|
return StatsAlbumItem(
|
||||||
|
album: album.album,
|
||||||
|
info: Text("${compactNumberFormatter.format(album.count)} plays"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
38
lib/pages/stats/artists/artists.dart
Normal file
38
lib/pages/stats/artists/artists.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/components/stats/common/artist_item.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
class StatsArtistsPage extends HookConsumerWidget {
|
||||||
|
static const name = "stats_artists";
|
||||||
|
const StatsArtistsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final artists = ref.watch(
|
||||||
|
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||||
|
.select((s) => s.artists),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: const PageWindowTitleBar(
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
centerTitle: false,
|
||||||
|
title: Text("Artists"),
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
itemCount: artists.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final artist = artists[index];
|
||||||
|
return StatsArtistItem(
|
||||||
|
artist: artist.artist,
|
||||||
|
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
65
lib/pages/stats/fees/fees.dart
Normal file
65
lib/pages/stats/fees/fees.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/components/stats/common/artist_item.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
class StatsStreamFeesPage extends HookConsumerWidget {
|
||||||
|
static const name = "stats_stream_fees";
|
||||||
|
|
||||||
|
const StatsStreamFeesPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final ThemeData(:textTheme, :hintColor) = Theme.of(context);
|
||||||
|
|
||||||
|
final artists = ref.watch(
|
||||||
|
playbackHistoryTopProvider(HistoryDuration.days30)
|
||||||
|
.select((value) => value.artists),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: const PageWindowTitleBar(
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
centerTitle: false,
|
||||||
|
title: Text("Streaming fees (hypothetical)"),
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverCrossAxisConstrained(
|
||||||
|
maxCrossAxisExtent: 600,
|
||||||
|
alignment: -1,
|
||||||
|
child: SliverPadding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Text(
|
||||||
|
"*This is calculated based on Spotify's per stream "
|
||||||
|
"payout of \$0.003 to \$0.005. This is a hypothetical "
|
||||||
|
"calculation to give user insight about how much they "
|
||||||
|
"would have paid to the artists if they were to listen "
|
||||||
|
"their song in Spotify.",
|
||||||
|
style: textTheme.bodySmall?.copyWith(
|
||||||
|
color: hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList.builder(
|
||||||
|
itemCount: artists.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final artist = artists[index];
|
||||||
|
return StatsArtistItem(
|
||||||
|
artist: artist.artist,
|
||||||
|
info: Text(usdFormatter.format(artist.count * 0.005)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
44
lib/pages/stats/minutes/minutes.dart
Normal file
44
lib/pages/stats/minutes/minutes.dart
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/components/stats/common/track_item.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
class StatsMinutesPage extends HookConsumerWidget {
|
||||||
|
static const name = "stats_minutes";
|
||||||
|
|
||||||
|
const StatsMinutesPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final topTracks = ref.watch(
|
||||||
|
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||||
|
.select((s) => s.tracks),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: const PageWindowTitleBar(
|
||||||
|
title: Text("Minutes listened"),
|
||||||
|
centerTitle: false,
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
),
|
||||||
|
body: ListView.separated(
|
||||||
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
|
itemCount: topTracks.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final (:track, :count) = topTracks[index];
|
||||||
|
|
||||||
|
return StatsTrackItem(
|
||||||
|
track: track,
|
||||||
|
info: Text(
|
||||||
|
"${compactNumberFormatter.format(count * track.duration!.inMinutes)} mins",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
39
lib/pages/stats/playlists/playlists.dart
Normal file
39
lib/pages/stats/playlists/playlists.dart
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/components/stats/common/playlist_item.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
class StatsPlaylistsPage extends HookConsumerWidget {
|
||||||
|
static const name = "stats_playlists";
|
||||||
|
const StatsPlaylistsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final playlists = ref.watch(
|
||||||
|
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||||
|
.select((s) => s.playlists),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: const PageWindowTitleBar(
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
centerTitle: false,
|
||||||
|
title: Text("Playlists"),
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
itemCount: playlists.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final playlist = playlists[index];
|
||||||
|
return StatsPlaylistItem(
|
||||||
|
playlist: playlist.playlist.playlist,
|
||||||
|
info:
|
||||||
|
Text("${compactNumberFormatter.format(playlist.count)} plays"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
lib/pages/stats/stats.dart
Normal file
35
lib/pages/stats/stats.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/components/stats/summary/summary.dart';
|
||||||
|
import 'package:spotube/components/stats/top/top.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
|
class StatsPage extends HookConsumerWidget {
|
||||||
|
static const name = "stats";
|
||||||
|
|
||||||
|
const StatsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
return SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: kIsMacOS || kIsMobile ? null : const PageWindowTitleBar(),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
if (kIsMacOS) const SliverGap(20),
|
||||||
|
const StatsPageSummarySection(),
|
||||||
|
const StatsPageTopSection(),
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SafeArea(
|
||||||
|
child: SizedBox(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
44
lib/pages/stats/streams/streams.dart
Normal file
44
lib/pages/stats/streams/streams.dart
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/formatters.dart';
|
||||||
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/components/stats/common/track_item.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
class StatsStreamsPage extends HookConsumerWidget {
|
||||||
|
static const name = "stats_streams";
|
||||||
|
|
||||||
|
const StatsStreamsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final topTracks = ref.watch(
|
||||||
|
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||||
|
.select((s) => s.tracks),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: const PageWindowTitleBar(
|
||||||
|
title: Text("Streamed songs"),
|
||||||
|
centerTitle: false,
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
),
|
||||||
|
body: ListView.separated(
|
||||||
|
separatorBuilder: (context, index) => const Gap(8),
|
||||||
|
itemCount: topTracks.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final (:track, :count) = topTracks[index];
|
||||||
|
|
||||||
|
return StatsTrackItem(
|
||||||
|
track: track,
|
||||||
|
info: Text(
|
||||||
|
"${compactNumberFormatter.format(count)} streams",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,8 @@ import 'package:spotube/services/audio_player/audio_player.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
|
||||||
class TrackPage extends HookConsumerWidget {
|
class TrackPage extends HookConsumerWidget {
|
||||||
|
static const name = "track";
|
||||||
|
|
||||||
final String trackId;
|
final String trackId;
|
||||||
const TrackPage({
|
const TrackPage({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -9,9 +9,11 @@ import 'package:shelf/shelf_io.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shelf_router/shelf_router.dart';
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
|
import 'package:spotube/provider/history/history.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_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/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -32,6 +34,7 @@ final connectServerProvider = FutureProvider((ref) async {
|
|||||||
final resolvedService = await ref
|
final resolvedService = await ref
|
||||||
.watch(connectClientsProvider.selectAsync((s) => s.resolvedService));
|
.watch(connectClientsProvider.selectAsync((s) => s.resolvedService));
|
||||||
final playbackNotifier = ref.read(proxyPlaylistProvider.notifier);
|
final playbackNotifier = ref.read(proxyPlaylistProvider.notifier);
|
||||||
|
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
|
||||||
|
|
||||||
if (!enabled || resolvedService != null) {
|
if (!enabled || resolvedService != null) {
|
||||||
return null;
|
return null;
|
||||||
@ -79,7 +82,7 @@ final connectServerProvider = FutureProvider((ref) async {
|
|||||||
.toJson(),
|
.toJson(),
|
||||||
);
|
);
|
||||||
channel.sink.add(
|
channel.sink.add(
|
||||||
WebSocketShuffleEvent(await audioPlayer.isShuffled).toJson(),
|
WebSocketShuffleEvent(audioPlayer.isShuffled).toJson(),
|
||||||
);
|
);
|
||||||
channel.sink.add(
|
channel.sink.add(
|
||||||
WebSocketLoopEvent(audioPlayer.loopMode).toJson(),
|
WebSocketLoopEvent(audioPlayer.loopMode).toJson(),
|
||||||
@ -146,8 +149,14 @@ final connectServerProvider = FutureProvider((ref) async {
|
|||||||
initialIndex: event.data.initialIndex ?? 0,
|
initialIndex: event.data.initialIndex ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (event.data.collectionId != null) {
|
if (event.data.collectionId == null) return;
|
||||||
playbackNotifier.addCollection(event.data.collectionId!);
|
playbackNotifier.addCollection(event.data.collectionId!);
|
||||||
|
if (event.data.collection is AlbumSimple) {
|
||||||
|
historyNotifier
|
||||||
|
.addAlbums([event.data.collection as AlbumSimple]);
|
||||||
|
} else {
|
||||||
|
historyNotifier.addPlaylists(
|
||||||
|
[event.data.collection as PlaylistSimple]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
129
lib/provider/history/history.dart
Normal file
129
lib/provider/history/history.dart
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
|
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||||
|
|
||||||
|
class PlaybackHistoryState {
|
||||||
|
final List<PlaybackHistoryItem> items;
|
||||||
|
const PlaybackHistoryState({this.items = const []});
|
||||||
|
|
||||||
|
factory PlaybackHistoryState.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PlaybackHistoryState(
|
||||||
|
items: json["items"]
|
||||||
|
?.map(
|
||||||
|
(json) => PlaybackHistoryItem.fromJson(json),
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
.cast<PlaybackHistoryItem>() ??
|
||||||
|
<PlaybackHistoryItem>[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"items": items.map((s) => s.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaybackHistoryState copyWith({
|
||||||
|
List<PlaybackHistoryItem>? items,
|
||||||
|
}) {
|
||||||
|
return PlaybackHistoryState(items: items ?? this.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlaybackHistoryNotifier
|
||||||
|
extends PersistedStateNotifier<PlaybackHistoryState> {
|
||||||
|
final Ref ref;
|
||||||
|
PlaybackHistoryNotifier(this.ref)
|
||||||
|
: super(const PlaybackHistoryState(), "playback_history");
|
||||||
|
|
||||||
|
SpotifyApi get spotify => ref.read(spotifyProvider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<PlaybackHistoryState> fromJson(Map<String, dynamic> json) =>
|
||||||
|
PlaybackHistoryState.fromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return state.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addPlaylists(List<PlaylistSimple> playlists) {
|
||||||
|
state = state.copyWith(
|
||||||
|
items: [
|
||||||
|
...state.items,
|
||||||
|
for (final playlist in playlists)
|
||||||
|
PlaybackHistoryItem.playlist(
|
||||||
|
date: DateTime.now(), playlist: playlist),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addAlbums(List<AlbumSimple> albums) {
|
||||||
|
state = state.copyWith(
|
||||||
|
items: [
|
||||||
|
...state.items,
|
||||||
|
for (final album in albums)
|
||||||
|
PlaybackHistoryItem.album(date: DateTime.now(), album: album),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addTrack(Track track) async {
|
||||||
|
// For some reason Track's artists images are `null`
|
||||||
|
// so we need to fetch them from the API
|
||||||
|
final artists =
|
||||||
|
await spotify.artists.list(track.artists!.map((e) => e.id!).toList());
|
||||||
|
|
||||||
|
track.artists = artists.toList();
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
items: [
|
||||||
|
...state.items,
|
||||||
|
PlaybackHistoryItem.track(date: DateTime.now(), track: track),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
state = state.copyWith(items: []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final playbackHistoryProvider =
|
||||||
|
StateNotifierProvider<PlaybackHistoryNotifier, PlaybackHistoryState>(
|
||||||
|
(ref) => PlaybackHistoryNotifier(ref),
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef PlaybackHistoryGrouped = ({
|
||||||
|
List<PlaybackHistoryTrack> tracks,
|
||||||
|
List<PlaybackHistoryAlbum> albums,
|
||||||
|
List<PlaybackHistoryPlaylist> playlists,
|
||||||
|
});
|
||||||
|
|
||||||
|
final playbackHistoryGroupedProvider = Provider<PlaybackHistoryGrouped>((ref) {
|
||||||
|
final history = ref.watch(playbackHistoryProvider);
|
||||||
|
final tracks = history.items
|
||||||
|
.whereType<PlaybackHistoryTrack>()
|
||||||
|
.sorted((a, b) => b.date.compareTo(a.date))
|
||||||
|
.toList();
|
||||||
|
final albums = history.items
|
||||||
|
.whereType<PlaybackHistoryAlbum>()
|
||||||
|
.sorted((a, b) => b.date.compareTo(a.date))
|
||||||
|
.toList();
|
||||||
|
final playlists = history.items
|
||||||
|
.whereType<PlaybackHistoryPlaylist>()
|
||||||
|
.sorted((a, b) => b.date.compareTo(a.date))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
tracks: tracks,
|
||||||
|
albums: albums,
|
||||||
|
playlists: playlists,
|
||||||
|
);
|
||||||
|
});
|
40
lib/provider/history/recent.dart
Normal file
40
lib/provider/history/recent.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/provider/history/history.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
|
||||||
|
final recentlyPlayedItems = Provider((ref) {
|
||||||
|
return ref.watch(
|
||||||
|
playbackHistoryProvider.select(
|
||||||
|
(s) => s.items
|
||||||
|
.toSet()
|
||||||
|
// unique items
|
||||||
|
.whereIndexed(
|
||||||
|
(index, item) =>
|
||||||
|
index ==
|
||||||
|
s.items.lastIndexWhere(
|
||||||
|
(e) => switch ((e, item)) {
|
||||||
|
(
|
||||||
|
PlaybackHistoryPlaylist(:final playlist),
|
||||||
|
PlaybackHistoryPlaylist(playlist: final playlist2)
|
||||||
|
) =>
|
||||||
|
playlist.id == playlist2.id,
|
||||||
|
(
|
||||||
|
PlaybackHistoryAlbum(:final album),
|
||||||
|
PlaybackHistoryAlbum(album: final album2)
|
||||||
|
) =>
|
||||||
|
album.id == album2.id,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(s) => s is PlaybackHistoryPlaylist || s is PlaybackHistoryAlbum,
|
||||||
|
)
|
||||||
|
.take(10)
|
||||||
|
.sortedBy((s) => s.date)
|
||||||
|
.reversed
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
35
lib/provider/history/state.dart
Normal file
35
lib/provider/history/state.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
|
part 'state.freezed.dart';
|
||||||
|
part 'state.g.dart';
|
||||||
|
|
||||||
|
enum HistoryDuration {
|
||||||
|
allTime,
|
||||||
|
days7,
|
||||||
|
days30,
|
||||||
|
months6,
|
||||||
|
year,
|
||||||
|
years2,
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class PlaybackHistoryItem with _$PlaybackHistoryItem {
|
||||||
|
factory PlaybackHistoryItem.playlist({
|
||||||
|
required DateTime date,
|
||||||
|
required PlaylistSimple playlist,
|
||||||
|
}) = PlaybackHistoryPlaylist;
|
||||||
|
|
||||||
|
factory PlaybackHistoryItem.album({
|
||||||
|
required DateTime date,
|
||||||
|
required AlbumSimple album,
|
||||||
|
}) = PlaybackHistoryAlbum;
|
||||||
|
|
||||||
|
factory PlaybackHistoryItem.track({
|
||||||
|
required DateTime date,
|
||||||
|
required Track track,
|
||||||
|
}) = PlaybackHistoryTrack;
|
||||||
|
|
||||||
|
factory PlaybackHistoryItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$PlaybackHistoryItemFromJson(json);
|
||||||
|
}
|
644
lib/provider/history/state.freezed.dart
Normal file
644
lib/provider/history/state.freezed.dart
Normal file
@ -0,0 +1,644 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
PlaybackHistoryItem _$PlaybackHistoryItemFromJson(Map<String, dynamic> json) {
|
||||||
|
switch (json['runtimeType']) {
|
||||||
|
case 'playlist':
|
||||||
|
return PlaybackHistoryPlaylist.fromJson(json);
|
||||||
|
case 'album':
|
||||||
|
return PlaybackHistoryAlbum.fromJson(json);
|
||||||
|
case 'track':
|
||||||
|
return PlaybackHistoryTrack.fromJson(json);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw CheckedFromJsonException(json, 'runtimeType', 'PlaybackHistoryItem',
|
||||||
|
'Invalid union type "${json['runtimeType']}"!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$PlaybackHistoryItem {
|
||||||
|
DateTime get date => throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(DateTime date, PlaylistSimple playlist) playlist,
|
||||||
|
required TResult Function(DateTime date, AlbumSimple album) album,
|
||||||
|
required TResult Function(DateTime date, Track track) track,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||||
|
TResult? Function(DateTime date, AlbumSimple album)? album,
|
||||||
|
TResult? Function(DateTime date, Track track)? track,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||||
|
TResult Function(DateTime date, AlbumSimple album)? album,
|
||||||
|
TResult Function(DateTime date, Track track)? track,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(PlaybackHistoryPlaylist value) playlist,
|
||||||
|
required TResult Function(PlaybackHistoryAlbum value) album,
|
||||||
|
required TResult Function(PlaybackHistoryTrack value) track,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(PlaybackHistoryPlaylist value)? playlist,
|
||||||
|
TResult? Function(PlaybackHistoryAlbum value)? album,
|
||||||
|
TResult? Function(PlaybackHistoryTrack value)? track,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(PlaybackHistoryPlaylist value)? playlist,
|
||||||
|
TResult Function(PlaybackHistoryAlbum value)? album,
|
||||||
|
TResult Function(PlaybackHistoryTrack value)? track,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$PlaybackHistoryItemCopyWith<PlaybackHistoryItem> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $PlaybackHistoryItemCopyWith<$Res> {
|
||||||
|
factory $PlaybackHistoryItemCopyWith(
|
||||||
|
PlaybackHistoryItem value, $Res Function(PlaybackHistoryItem) then) =
|
||||||
|
_$PlaybackHistoryItemCopyWithImpl<$Res, PlaybackHistoryItem>;
|
||||||
|
@useResult
|
||||||
|
$Res call({DateTime date});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$PlaybackHistoryItemCopyWithImpl<$Res, $Val extends PlaybackHistoryItem>
|
||||||
|
implements $PlaybackHistoryItemCopyWith<$Res> {
|
||||||
|
_$PlaybackHistoryItemCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? date = null,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
date: null == date
|
||||||
|
? _value.date
|
||||||
|
: date // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$PlaybackHistoryPlaylistImplCopyWith<$Res>
|
||||||
|
implements $PlaybackHistoryItemCopyWith<$Res> {
|
||||||
|
factory _$$PlaybackHistoryPlaylistImplCopyWith(
|
||||||
|
_$PlaybackHistoryPlaylistImpl value,
|
||||||
|
$Res Function(_$PlaybackHistoryPlaylistImpl) then) =
|
||||||
|
__$$PlaybackHistoryPlaylistImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({DateTime date, PlaylistSimple playlist});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$PlaybackHistoryPlaylistImplCopyWithImpl<$Res>
|
||||||
|
extends _$PlaybackHistoryItemCopyWithImpl<$Res,
|
||||||
|
_$PlaybackHistoryPlaylistImpl>
|
||||||
|
implements _$$PlaybackHistoryPlaylistImplCopyWith<$Res> {
|
||||||
|
__$$PlaybackHistoryPlaylistImplCopyWithImpl(
|
||||||
|
_$PlaybackHistoryPlaylistImpl _value,
|
||||||
|
$Res Function(_$PlaybackHistoryPlaylistImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? date = null,
|
||||||
|
Object? playlist = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$PlaybackHistoryPlaylistImpl(
|
||||||
|
date: null == date
|
||||||
|
? _value.date
|
||||||
|
: date // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
playlist: null == playlist
|
||||||
|
? _value.playlist
|
||||||
|
: playlist // ignore: cast_nullable_to_non_nullable
|
||||||
|
as PlaylistSimple,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$PlaybackHistoryPlaylistImpl implements PlaybackHistoryPlaylist {
|
||||||
|
_$PlaybackHistoryPlaylistImpl(
|
||||||
|
{required this.date, required this.playlist, final String? $type})
|
||||||
|
: $type = $type ?? 'playlist';
|
||||||
|
|
||||||
|
factory _$PlaybackHistoryPlaylistImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$PlaybackHistoryPlaylistImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final DateTime date;
|
||||||
|
@override
|
||||||
|
final PlaylistSimple playlist;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PlaybackHistoryItem.playlist(date: $date, playlist: $playlist)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$PlaybackHistoryPlaylistImpl &&
|
||||||
|
(identical(other.date, date) || other.date == date) &&
|
||||||
|
(identical(other.playlist, playlist) ||
|
||||||
|
other.playlist == playlist));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, date, playlist);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl>
|
||||||
|
get copyWith => __$$PlaybackHistoryPlaylistImplCopyWithImpl<
|
||||||
|
_$PlaybackHistoryPlaylistImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(DateTime date, PlaylistSimple playlist) playlist,
|
||||||
|
required TResult Function(DateTime date, AlbumSimple album) album,
|
||||||
|
required TResult Function(DateTime date, Track track) track,
|
||||||
|
}) {
|
||||||
|
return playlist(date, this.playlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||||
|
TResult? Function(DateTime date, AlbumSimple album)? album,
|
||||||
|
TResult? Function(DateTime date, Track track)? track,
|
||||||
|
}) {
|
||||||
|
return playlist?.call(date, this.playlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||||
|
TResult Function(DateTime date, AlbumSimple album)? album,
|
||||||
|
TResult Function(DateTime date, Track track)? track,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (playlist != null) {
|
||||||
|
return playlist(date, this.playlist);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(PlaybackHistoryPlaylist value) playlist,
|
||||||
|
required TResult Function(PlaybackHistoryAlbum value) album,
|
||||||
|
required TResult Function(PlaybackHistoryTrack value) track,
|
||||||
|
}) {
|
||||||
|
return playlist(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(PlaybackHistoryPlaylist value)? playlist,
|
||||||
|
TResult? Function(PlaybackHistoryAlbum value)? album,
|
||||||
|
TResult? Function(PlaybackHistoryTrack value)? track,
|
||||||
|
}) {
|
||||||
|
return playlist?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(PlaybackHistoryPlaylist value)? playlist,
|
||||||
|
TResult Function(PlaybackHistoryAlbum value)? album,
|
||||||
|
TResult Function(PlaybackHistoryTrack value)? track,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (playlist != null) {
|
||||||
|
return playlist(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$PlaybackHistoryPlaylistImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PlaybackHistoryPlaylist implements PlaybackHistoryItem {
|
||||||
|
factory PlaybackHistoryPlaylist(
|
||||||
|
{required final DateTime date,
|
||||||
|
required final PlaylistSimple playlist}) = _$PlaybackHistoryPlaylistImpl;
|
||||||
|
|
||||||
|
factory PlaybackHistoryPlaylist.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$PlaybackHistoryPlaylistImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime get date;
|
||||||
|
PlaylistSimple get playlist;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$PlaybackHistoryPlaylistImplCopyWith<_$PlaybackHistoryPlaylistImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$PlaybackHistoryAlbumImplCopyWith<$Res>
|
||||||
|
implements $PlaybackHistoryItemCopyWith<$Res> {
|
||||||
|
factory _$$PlaybackHistoryAlbumImplCopyWith(_$PlaybackHistoryAlbumImpl value,
|
||||||
|
$Res Function(_$PlaybackHistoryAlbumImpl) then) =
|
||||||
|
__$$PlaybackHistoryAlbumImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({DateTime date, AlbumSimple album});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$PlaybackHistoryAlbumImplCopyWithImpl<$Res>
|
||||||
|
extends _$PlaybackHistoryItemCopyWithImpl<$Res, _$PlaybackHistoryAlbumImpl>
|
||||||
|
implements _$$PlaybackHistoryAlbumImplCopyWith<$Res> {
|
||||||
|
__$$PlaybackHistoryAlbumImplCopyWithImpl(_$PlaybackHistoryAlbumImpl _value,
|
||||||
|
$Res Function(_$PlaybackHistoryAlbumImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? date = null,
|
||||||
|
Object? album = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$PlaybackHistoryAlbumImpl(
|
||||||
|
date: null == date
|
||||||
|
? _value.date
|
||||||
|
: date // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
album: null == album
|
||||||
|
? _value.album
|
||||||
|
: album // ignore: cast_nullable_to_non_nullable
|
||||||
|
as AlbumSimple,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$PlaybackHistoryAlbumImpl implements PlaybackHistoryAlbum {
|
||||||
|
_$PlaybackHistoryAlbumImpl(
|
||||||
|
{required this.date, required this.album, final String? $type})
|
||||||
|
: $type = $type ?? 'album';
|
||||||
|
|
||||||
|
factory _$PlaybackHistoryAlbumImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$PlaybackHistoryAlbumImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final DateTime date;
|
||||||
|
@override
|
||||||
|
final AlbumSimple album;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PlaybackHistoryItem.album(date: $date, album: $album)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$PlaybackHistoryAlbumImpl &&
|
||||||
|
(identical(other.date, date) || other.date == date) &&
|
||||||
|
(identical(other.album, album) || other.album == album));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, date, album);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl>
|
||||||
|
get copyWith =>
|
||||||
|
__$$PlaybackHistoryAlbumImplCopyWithImpl<_$PlaybackHistoryAlbumImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(DateTime date, PlaylistSimple playlist) playlist,
|
||||||
|
required TResult Function(DateTime date, AlbumSimple album) album,
|
||||||
|
required TResult Function(DateTime date, Track track) track,
|
||||||
|
}) {
|
||||||
|
return album(date, this.album);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||||
|
TResult? Function(DateTime date, AlbumSimple album)? album,
|
||||||
|
TResult? Function(DateTime date, Track track)? track,
|
||||||
|
}) {
|
||||||
|
return album?.call(date, this.album);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||||
|
TResult Function(DateTime date, AlbumSimple album)? album,
|
||||||
|
TResult Function(DateTime date, Track track)? track,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (album != null) {
|
||||||
|
return album(date, this.album);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(PlaybackHistoryPlaylist value) playlist,
|
||||||
|
required TResult Function(PlaybackHistoryAlbum value) album,
|
||||||
|
required TResult Function(PlaybackHistoryTrack value) track,
|
||||||
|
}) {
|
||||||
|
return album(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(PlaybackHistoryPlaylist value)? playlist,
|
||||||
|
TResult? Function(PlaybackHistoryAlbum value)? album,
|
||||||
|
TResult? Function(PlaybackHistoryTrack value)? track,
|
||||||
|
}) {
|
||||||
|
return album?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(PlaybackHistoryPlaylist value)? playlist,
|
||||||
|
TResult Function(PlaybackHistoryAlbum value)? album,
|
||||||
|
TResult Function(PlaybackHistoryTrack value)? track,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (album != null) {
|
||||||
|
return album(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$PlaybackHistoryAlbumImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PlaybackHistoryAlbum implements PlaybackHistoryItem {
|
||||||
|
factory PlaybackHistoryAlbum(
|
||||||
|
{required final DateTime date,
|
||||||
|
required final AlbumSimple album}) = _$PlaybackHistoryAlbumImpl;
|
||||||
|
|
||||||
|
factory PlaybackHistoryAlbum.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$PlaybackHistoryAlbumImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime get date;
|
||||||
|
AlbumSimple get album;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$PlaybackHistoryAlbumImplCopyWith<_$PlaybackHistoryAlbumImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$PlaybackHistoryTrackImplCopyWith<$Res>
|
||||||
|
implements $PlaybackHistoryItemCopyWith<$Res> {
|
||||||
|
factory _$$PlaybackHistoryTrackImplCopyWith(_$PlaybackHistoryTrackImpl value,
|
||||||
|
$Res Function(_$PlaybackHistoryTrackImpl) then) =
|
||||||
|
__$$PlaybackHistoryTrackImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({DateTime date, Track track});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$PlaybackHistoryTrackImplCopyWithImpl<$Res>
|
||||||
|
extends _$PlaybackHistoryItemCopyWithImpl<$Res, _$PlaybackHistoryTrackImpl>
|
||||||
|
implements _$$PlaybackHistoryTrackImplCopyWith<$Res> {
|
||||||
|
__$$PlaybackHistoryTrackImplCopyWithImpl(_$PlaybackHistoryTrackImpl _value,
|
||||||
|
$Res Function(_$PlaybackHistoryTrackImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? date = null,
|
||||||
|
Object? track = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$PlaybackHistoryTrackImpl(
|
||||||
|
date: null == date
|
||||||
|
? _value.date
|
||||||
|
: date // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
track: null == track
|
||||||
|
? _value.track
|
||||||
|
: track // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Track,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack {
|
||||||
|
_$PlaybackHistoryTrackImpl(
|
||||||
|
{required this.date, required this.track, final String? $type})
|
||||||
|
: $type = $type ?? 'track';
|
||||||
|
|
||||||
|
factory _$PlaybackHistoryTrackImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$PlaybackHistoryTrackImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final DateTime date;
|
||||||
|
@override
|
||||||
|
final Track track;
|
||||||
|
|
||||||
|
@JsonKey(name: 'runtimeType')
|
||||||
|
final String $type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PlaybackHistoryItem.track(date: $date, track: $track)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$PlaybackHistoryTrackImpl &&
|
||||||
|
(identical(other.date, date) || other.date == date) &&
|
||||||
|
(identical(other.track, track) || other.track == track));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, date, track);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl>
|
||||||
|
get copyWith =>
|
||||||
|
__$$PlaybackHistoryTrackImplCopyWithImpl<_$PlaybackHistoryTrackImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function(DateTime date, PlaylistSimple playlist) playlist,
|
||||||
|
required TResult Function(DateTime date, AlbumSimple album) album,
|
||||||
|
required TResult Function(DateTime date, Track track) track,
|
||||||
|
}) {
|
||||||
|
return track(date, this.track);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||||
|
TResult? Function(DateTime date, AlbumSimple album)? album,
|
||||||
|
TResult? Function(DateTime date, Track track)? track,
|
||||||
|
}) {
|
||||||
|
return track?.call(date, this.track);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||||
|
TResult Function(DateTime date, AlbumSimple album)? album,
|
||||||
|
TResult Function(DateTime date, Track track)? track,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (track != null) {
|
||||||
|
return track(date, this.track);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(PlaybackHistoryPlaylist value) playlist,
|
||||||
|
required TResult Function(PlaybackHistoryAlbum value) album,
|
||||||
|
required TResult Function(PlaybackHistoryTrack value) track,
|
||||||
|
}) {
|
||||||
|
return track(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(PlaybackHistoryPlaylist value)? playlist,
|
||||||
|
TResult? Function(PlaybackHistoryAlbum value)? album,
|
||||||
|
TResult? Function(PlaybackHistoryTrack value)? track,
|
||||||
|
}) {
|
||||||
|
return track?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(PlaybackHistoryPlaylist value)? playlist,
|
||||||
|
TResult Function(PlaybackHistoryAlbum value)? album,
|
||||||
|
TResult Function(PlaybackHistoryTrack value)? track,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (track != null) {
|
||||||
|
return track(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$PlaybackHistoryTrackImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PlaybackHistoryTrack implements PlaybackHistoryItem {
|
||||||
|
factory PlaybackHistoryTrack(
|
||||||
|
{required final DateTime date,
|
||||||
|
required final Track track}) = _$PlaybackHistoryTrackImpl;
|
||||||
|
|
||||||
|
factory PlaybackHistoryTrack.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$PlaybackHistoryTrackImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime get date;
|
||||||
|
Track get track;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
55
lib/provider/history/state.g.dart
Normal file
55
lib/provider/history/state.g.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$PlaybackHistoryPlaylistImpl _$$PlaybackHistoryPlaylistImplFromJson(
|
||||||
|
Map json) =>
|
||||||
|
_$PlaybackHistoryPlaylistImpl(
|
||||||
|
date: DateTime.parse(json['date'] as String),
|
||||||
|
playlist: PlaylistSimple.fromJson(
|
||||||
|
Map<String, dynamic>.from(json['playlist'] as Map)),
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$PlaybackHistoryPlaylistImplToJson(
|
||||||
|
_$PlaybackHistoryPlaylistImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'date': instance.date.toIso8601String(),
|
||||||
|
'playlist': instance.playlist.toJson(),
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$PlaybackHistoryAlbumImpl _$$PlaybackHistoryAlbumImplFromJson(Map json) =>
|
||||||
|
_$PlaybackHistoryAlbumImpl(
|
||||||
|
date: DateTime.parse(json['date'] as String),
|
||||||
|
album:
|
||||||
|
AlbumSimple.fromJson(Map<String, dynamic>.from(json['album'] as Map)),
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$PlaybackHistoryAlbumImplToJson(
|
||||||
|
_$PlaybackHistoryAlbumImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'date': instance.date.toIso8601String(),
|
||||||
|
'album': instance.album.toJson(),
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$PlaybackHistoryTrackImpl _$$PlaybackHistoryTrackImplFromJson(Map json) =>
|
||||||
|
_$PlaybackHistoryTrackImpl(
|
||||||
|
date: DateTime.parse(json['date'] as String),
|
||||||
|
track: Track.fromJson(Map<String, dynamic>.from(json['track'] as Map)),
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$PlaybackHistoryTrackImplToJson(
|
||||||
|
_$PlaybackHistoryTrackImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'date': instance.date.toIso8601String(),
|
||||||
|
'track': instance.track.toJson(),
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
62
lib/provider/history/summary.dart
Normal file
62
lib/provider/history/summary.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/provider/history/history.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
import 'package:spotube/provider/history/top.dart';
|
||||||
|
|
||||||
|
final playbackHistorySummaryProvider = Provider((ref) {
|
||||||
|
final (:tracks, :albums, :playlists) =
|
||||||
|
ref.watch(playbackHistoryGroupedProvider);
|
||||||
|
|
||||||
|
final totalDurationListened = tracks.fold(
|
||||||
|
Duration.zero,
|
||||||
|
(previousValue, element) => previousValue + element.track.duration!,
|
||||||
|
);
|
||||||
|
|
||||||
|
final totalTracksListened = tracks
|
||||||
|
.whereIndexed(
|
||||||
|
(i, track) =>
|
||||||
|
i == tracks.lastIndexWhere((e) => e.track.id == track.track.id),
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
final artists =
|
||||||
|
tracks.map((e) => e.track.artists).expand((e) => e ?? []).toList();
|
||||||
|
|
||||||
|
final totalArtistsListened = artists
|
||||||
|
.whereIndexed(
|
||||||
|
(i, artist) => i == artists.lastIndexWhere((e) => e.id == artist.id),
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
final totalAlbumsListened = albums
|
||||||
|
.whereIndexed(
|
||||||
|
(i, album) =>
|
||||||
|
i == albums.lastIndexWhere((e) => e.album.id == album.album.id),
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
final totalPlaylistsListened = playlists
|
||||||
|
.whereIndexed(
|
||||||
|
(i, playlist) =>
|
||||||
|
i ==
|
||||||
|
playlists
|
||||||
|
.lastIndexWhere((e) => e.playlist.id == playlist.playlist.id),
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
final tracksThisMonth = ref.watch(
|
||||||
|
playbackHistoryTopProvider(HistoryDuration.days30).select((s) => s.tracks),
|
||||||
|
);
|
||||||
|
|
||||||
|
final streams = tracksThisMonth.fold(0, (acc, el) => acc + el.count);
|
||||||
|
|
||||||
|
return (
|
||||||
|
duration: totalDurationListened,
|
||||||
|
tracks: totalTracksListened,
|
||||||
|
artists: totalArtistsListened,
|
||||||
|
fees: streams * 0.005, // Spotify pays $0.003 to $0.005
|
||||||
|
albums: totalAlbumsListened,
|
||||||
|
playlists: totalPlaylistsListened,
|
||||||
|
);
|
||||||
|
});
|
95
lib/provider/history/top.dart
Normal file
95
lib/provider/history/top.dart
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/provider/history/history.dart';
|
||||||
|
import 'package:spotube/provider/history/state.dart';
|
||||||
|
|
||||||
|
final playbackHistoryTopDurationProvider =
|
||||||
|
StateProvider((ref) => HistoryDuration.days30);
|
||||||
|
|
||||||
|
final playbackHistoryTopProvider =
|
||||||
|
Provider.family((ref, HistoryDuration durationState) {
|
||||||
|
final grouped = ref.watch(playbackHistoryGroupedProvider);
|
||||||
|
|
||||||
|
final duration = switch (durationState) {
|
||||||
|
HistoryDuration.allTime => const Duration(days: 365 * 2003),
|
||||||
|
HistoryDuration.days7 => const Duration(days: 7),
|
||||||
|
HistoryDuration.days30 => const Duration(days: 30),
|
||||||
|
HistoryDuration.months6 => const Duration(days: 30 * 6),
|
||||||
|
HistoryDuration.year => const Duration(days: 365),
|
||||||
|
HistoryDuration.years2 => const Duration(days: 365 * 2),
|
||||||
|
};
|
||||||
|
final tracks = grouped.tracks
|
||||||
|
.where(
|
||||||
|
(item) => item.date.isAfter(
|
||||||
|
DateTime.now().subtract(duration),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
final albums = grouped.albums
|
||||||
|
.where(
|
||||||
|
(item) => item.date.isAfter(
|
||||||
|
DateTime.now().subtract(duration),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final playlists = grouped.playlists
|
||||||
|
.where(
|
||||||
|
(item) => item.date.isAfter(
|
||||||
|
DateTime.now().subtract(duration),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final tracksWithCount = groupBy(
|
||||||
|
tracks,
|
||||||
|
(track) => track.track.id!,
|
||||||
|
)
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
return (count: entry.value.length, track: entry.value.first.track);
|
||||||
|
})
|
||||||
|
.sorted((a, b) => b.count.compareTo(a.count))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final albumsWithTrackAlbums = [
|
||||||
|
for (final historicAlbum in albums) historicAlbum.album,
|
||||||
|
for (final track in tracks) track.track.album!
|
||||||
|
];
|
||||||
|
|
||||||
|
final albumsWithCount = groupBy(albumsWithTrackAlbums, (album) => album.id!)
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
return (count: entry.value.length, album: entry.value.first);
|
||||||
|
})
|
||||||
|
.sorted((a, b) => b.count.compareTo(a.count))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final artists =
|
||||||
|
tracks.map((track) => track.track.artists).expand((e) => e ?? <Artist>[]);
|
||||||
|
|
||||||
|
final artistsWithCount = groupBy(artists, (artist) => artist.id!)
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
return (count: entry.value.length, artist: entry.value.first);
|
||||||
|
})
|
||||||
|
.sorted((a, b) => b.count.compareTo(a.count))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final playlistsWithCount =
|
||||||
|
groupBy(playlists, (playlist) => playlist.playlist.id!)
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
return (count: entry.value.length, playlist: entry.value.first);
|
||||||
|
})
|
||||||
|
.sorted((a, b) => b.count.compareTo(a.count))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
tracks: tracksWithCount,
|
||||||
|
albums: albumsWithCount,
|
||||||
|
artists: artistsWithCount,
|
||||||
|
playlists: playlistsWithCount,
|
||||||
|
);
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user