mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-09 08:47:31 +00:00
feat(stats): add individual minutes and streams page
This commit is contained in:
parent
f0b6d660e2
commit
9393ed75d7
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
},
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
@ -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"),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user