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/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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
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/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);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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();
|
.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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user