feat(stats): add individual minutes and streams page

This commit is contained in:
Kingkor Roy Tirtho 2024-05-10 17:40:50 +06:00
parent f0b6d660e2
commit 9393ed75d7
8 changed files with 271 additions and 3 deletions

View File

@ -24,7 +24,11 @@ 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/minutes/minutes.dart';
import 'package:spotube/pages/stats/playlists/playlists.dart';
import 'package:spotube/pages/stats/stats.dart'; import 'package:spotube/pages/stats/stats.dart';
import 'package:spotube/pages/stats/streams/streams.dart'; import 'package:spotube/pages/stats/streams/streams.dart';
import 'package:spotube/pages/track/track.dart'; import 'package:spotube/pages/track/track.dart';
@ -246,7 +250,35 @@ final routerProvider = Provider((ref) {
pageBuilder: (context, state) => const SpotubePage( pageBuilder: (context, state) => const SpotubePage(
child: StatsStreamsPage(), 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(),
),
),
], ],
) )
], ],

View 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,
);
},
);
}
}

View File

@ -3,7 +3,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/stats/summary/summary_card.dart'; import 'package:spotube/components/stats/summary/summary_card.dart';
import 'package:spotube/extensions/constrains.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/minutes/minutes.dart';
import 'package:spotube/pages/stats/playlists/playlists.dart';
import 'package:spotube/pages/stats/streams/streams.dart'; import 'package:spotube/pages/stats/streams/streams.dart';
import 'package:spotube/provider/history/summary.dart'; import 'package:spotube/provider/history/summary.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -26,7 +30,9 @@ class StatsPageSummarySection extends HookConsumerWidget {
? 3 ? 3
: constrains.mdAndDown : constrains.mdAndDown
? 4 ? 4
: 5, : constrains.lgAndDown
? 5
: 6,
mainAxisSpacing: 10, mainAxisSpacing: 10,
crossAxisSpacing: 10, crossAxisSpacing: 10,
childAspectRatio: constrains.isXs ? 1.3 : 1.5, childAspectRatio: constrains.isXs ? 1.3 : 1.5,
@ -55,24 +61,36 @@ class StatsPageSummarySection extends HookConsumerWidget {
unit: "", unit: "",
description: 'Owed to artists\nthis month', description: 'Owed to artists\nthis month',
color: Colors.green, color: Colors.green,
onTap: () {
ServiceUtils.pushNamed(context, StatsStreamFeesPage.name);
},
), ),
SummaryCard( SummaryCard(
title: summary.artists.toDouble(), title: summary.artists.toDouble(),
unit: "artist's", unit: "artist's",
description: 'Music reached you', description: 'Music reached you',
color: Colors.yellow, color: Colors.yellow,
onTap: () {
ServiceUtils.pushNamed(context, StatsArtistsPage.name);
},
), ),
SummaryCard( SummaryCard(
title: summary.albums.toDouble(), title: summary.albums.toDouble(),
unit: "full albums", unit: "full albums",
description: 'Got your love', description: 'Got your love',
color: Colors.pink, color: Colors.pink,
onTap: () {
ServiceUtils.pushNamed(context, StatsAlbumsPage.name);
},
), ),
SummaryCard( SummaryCard(
title: summary.playlists.toDouble(), title: summary.playlists.toDouble(),
unit: "playlists", unit: "playlists",
description: 'Were on repeat', description: 'Were on repeat',
color: Colors.teal, color: Colors.teal,
onTap: () {
ServiceUtils.pushNamed(context, StatsPlaylistsPage.name);
},
), ),
]), ]),
); );

View 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"),
);
},
),
);
}
}

View 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"),
);
},
),
);
}
}

View 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/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)),
);
},
),
);
}
}

View 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"),
);
},
),
);
}
}

View File

@ -34,6 +34,14 @@ final playbackHistoryTopProvider =
) )
.toList(); .toList();
final playlists = grouped.playlists
.where(
(item) => item.date.isAfter(
DateTime.now().subtract(duration),
),
)
.toList();
final tracksWithCount = groupBy( final tracksWithCount = groupBy(
tracks, tracks,
(track) => track.track.id!, (track) => track.track.id!,
@ -69,9 +77,19 @@ final playbackHistoryTopProvider =
.sorted((a, b) => b.count.compareTo(a.count)) .sorted((a, b) => b.count.compareTo(a.count))
.toList(); .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 ( return (
tracks: tracksWithCount, tracks: tracksWithCount,
albums: albumsWithCount, albums: albumsWithCount,
artists: artistsWithCount artists: artistsWithCount,
playlists: playlistsWithCount,
); );
}); });