diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 702aca9f..42e31e95 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -24,7 +24,11 @@ import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/about.dart'; import 'package:spotube/pages/settings/logs.dart'; +import 'package:spotube/pages/stats/albums/albums.dart'; +import 'package:spotube/pages/stats/artists/artists.dart'; +import 'package:spotube/pages/stats/fees/fees.dart'; import 'package:spotube/pages/stats/minutes/minutes.dart'; +import 'package:spotube/pages/stats/playlists/playlists.dart'; import 'package:spotube/pages/stats/stats.dart'; import 'package:spotube/pages/stats/streams/streams.dart'; import 'package:spotube/pages/track/track.dart'; @@ -246,7 +250,35 @@ final routerProvider = Provider((ref) { 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(), + ), + ), ], ) ], diff --git a/lib/components/stats/common/playlist_item.dart b/lib/components/stats/common/playlist_item.dart new file mode 100644 index 00000000..b07311ab --- /dev/null +++ b/lib/components/stats/common/playlist_item.dart @@ -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, + ); + }, + ); + } +} diff --git a/lib/components/stats/summary/summary.dart b/lib/components/stats/summary/summary.dart index 69524376..61f3bd6c 100644 --- a/lib/components/stats/summary/summary.dart +++ b/lib/components/stats/summary/summary.dart @@ -3,7 +3,11 @@ 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'; @@ -26,7 +30,9 @@ class StatsPageSummarySection extends HookConsumerWidget { ? 3 : constrains.mdAndDown ? 4 - : 5, + : constrains.lgAndDown + ? 5 + : 6, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: constrains.isXs ? 1.3 : 1.5, @@ -55,24 +61,36 @@ class StatsPageSummarySection extends HookConsumerWidget { 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); + }, ), ]), ); diff --git a/lib/pages/stats/albums/albums.dart b/lib/pages/stats/albums/albums.dart index e69de29b..83867f93 100644 --- a/lib/pages/stats/albums/albums.dart +++ b/lib/pages/stats/albums/albums.dart @@ -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"), + ); + }, + ), + ); + } +} diff --git a/lib/pages/stats/artists/artists.dart b/lib/pages/stats/artists/artists.dart index e69de29b..755475ae 100644 --- a/lib/pages/stats/artists/artists.dart +++ b/lib/pages/stats/artists/artists.dart @@ -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"), + ); + }, + ), + ); + } +} diff --git a/lib/pages/stats/fees/fees.dart b/lib/pages/stats/fees/fees.dart index e69de29b..e5bb9330 100644 --- a/lib/pages/stats/fees/fees.dart +++ b/lib/pages/stats/fees/fees.dart @@ -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/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 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: ListView.builder( + itemCount: artists.length, + itemBuilder: (context, index) { + final artist = artists[index]; + return StatsArtistItem( + artist: artist.artist, + info: Text(usdFormatter.format(artist.count * 0.005)), + ); + }, + ), + ); + } +} diff --git a/lib/pages/stats/playlists/playlists.dart b/lib/pages/stats/playlists/playlists.dart index e69de29b..cca7febb 100644 --- a/lib/pages/stats/playlists/playlists.dart +++ b/lib/pages/stats/playlists/playlists.dart @@ -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"), + ); + }, + ), + ); + } +} diff --git a/lib/provider/history/top.dart b/lib/provider/history/top.dart index bee2bf29..0188707e 100644 --- a/lib/provider/history/top.dart +++ b/lib/provider/history/top.dart @@ -34,6 +34,14 @@ final playbackHistoryTopProvider = ) .toList(); + final playlists = grouped.playlists + .where( + (item) => item.date.isAfter( + DateTime.now().subtract(duration), + ), + ) + .toList(); + final tracksWithCount = groupBy( tracks, (track) => track.track.id!, @@ -69,9 +77,19 @@ final playbackHistoryTopProvider = .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 + artists: artistsWithCount, + playlists: playlistsWithCount, ); });