mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-08 16:27:31 +00:00
feat: add stats summary and top tracks/artists/albums
This commit is contained in:
parent
5f442a1ff7
commit
c9bd42c847
3
lib/collections/formatters.dart
Normal file
3
lib/collections/formatters.dart
Normal file
@ -0,0 +1,3 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
final compactNumberFormatter = NumberFormat.compact();
|
||||
@ -5,7 +5,8 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||
|
||||
class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
|
||||
final List<Widget> tabs;
|
||||
const ThemedButtonsTabBar({super.key, required this.tabs});
|
||||
final TabController? controller;
|
||||
const ThemedButtonsTabBar({super.key, required this.tabs, this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -21,6 +22,7 @@ class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
|
||||
bottom: 8,
|
||||
),
|
||||
child: ButtonsTabBar(
|
||||
controller: controller,
|
||||
radius: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
|
||||
74
lib/components/stats/summary/summary.dart
Normal file
74
lib/components/stats/summary/summary.dart
Normal file
@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/stats/summary/summary_card.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/provider/history/summary.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
|
||||
: 5,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: constrains.isXs ? 1.3 : 1.5,
|
||||
),
|
||||
delegate: SliverChildListDelegate([
|
||||
switch (summary.duration) {
|
||||
>= const Duration(hours: 1) => SummaryCard(
|
||||
title: summary.duration.inHours.toDouble(),
|
||||
unit: "hours",
|
||||
description: 'Listened to music',
|
||||
color: Colors.green,
|
||||
),
|
||||
_ => SummaryCard(
|
||||
title: summary.duration.inMinutes.toDouble(),
|
||||
unit: "minutes",
|
||||
description: 'Listened to music',
|
||||
color: Colors.green,
|
||||
),
|
||||
},
|
||||
SummaryCard(
|
||||
title: summary.tracks.toDouble(),
|
||||
unit: "songs",
|
||||
description: 'Streamed overall',
|
||||
color: Colors.lightBlue,
|
||||
),
|
||||
SummaryCard(
|
||||
title: summary.artists.toDouble(),
|
||||
unit: "artist's",
|
||||
description: 'Music reached you',
|
||||
color: Colors.yellow,
|
||||
),
|
||||
SummaryCard(
|
||||
title: summary.albums.toDouble(),
|
||||
unit: "full albums",
|
||||
description: 'Got your love',
|
||||
color: Colors.pink,
|
||||
),
|
||||
SummaryCard(
|
||||
title: summary.playlists.toDouble(),
|
||||
unit: "playlists",
|
||||
description: 'Were on repeat',
|
||||
color: Colors.teal,
|
||||
),
|
||||
]),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/components/stats/summary/summary_card.dart
Normal file
67
lib/components/stats/summary/summary_card.dart
Normal file
@ -0,0 +1,67 @@
|
||||
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 double title;
|
||||
final String unit;
|
||||
final String description;
|
||||
|
||||
final MaterialColor color;
|
||||
|
||||
const SummaryCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.unit,
|
||||
required this.description,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData(:textTheme, :brightness) = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
color: brightness == Brightness.dark ? color.shade100 : color.shade50,
|
||||
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: compactNumberFormatter.format(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: 1,
|
||||
minFontSize: 9,
|
||||
style: textTheme.labelMedium!.copyWith(
|
||||
color: color.shade900,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/components/stats/top/albums.dart
Normal file
67
lib/components/stats/top/albums.dart
Normal file
@ -0,0 +1,67 @@
|
||||
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/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/provider/history/top.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
class TopAlbums extends HookConsumerWidget {
|
||||
const TopAlbums({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final albums =
|
||||
ref.watch(playbackHistoryTopProvider.select((value) => value.albums));
|
||||
|
||||
return SliverList.builder(
|
||||
itemCount: albums.length,
|
||||
itemBuilder: (context, index) {
|
||||
final album = albums[index];
|
||||
return ListTile(
|
||||
horizontalTitleGap: 8,
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: UniversalImage(
|
||||
path: (album.album.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
title: Text(album.album.name!),
|
||||
subtitle: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("${album.album.albumType?.formatted} • "),
|
||||
Flexible(
|
||||
child: ArtistLink(
|
||||
artists: album.album.artists!,
|
||||
mainAxisAlignment: WrapAlignment.start,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Text(
|
||||
"${compactNumberFormatter.format(album.count)} plays",
|
||||
),
|
||||
onTap: () {
|
||||
ServiceUtils.pushNamed(
|
||||
context,
|
||||
AlbumPage.name,
|
||||
pathParameters: {
|
||||
"id": album.album.id!,
|
||||
},
|
||||
extra: album.album,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/components/stats/top/artists.dart
Normal file
48
lib/components/stats/top/artists.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/formatters.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/provider/history/top.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
class TopArtists extends HookConsumerWidget {
|
||||
const TopArtists({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final artists =
|
||||
ref.watch(playbackHistoryTopProvider.select((value) => value.artists));
|
||||
|
||||
return SliverList.builder(
|
||||
itemCount: artists.length,
|
||||
itemBuilder: (context, index) {
|
||||
final artist = artists[index];
|
||||
return ListTile(
|
||||
title: Text(artist.artist.name!),
|
||||
horizontalTitleGap: 8,
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: UniversalImage.imageProvider(
|
||||
(artist.artist.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.artist,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: Text(
|
||||
"${compactNumberFormatter.format(artist.count)} plays",
|
||||
),
|
||||
onTap: () {
|
||||
ServiceUtils.pushNamed(
|
||||
context,
|
||||
ArtistPage.name,
|
||||
pathParameters: {
|
||||
"id": artist.artist.id!,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
57
lib/components/stats/top/top.dart
Normal file
57
lib/components/stats/top/top.dart
Normal file
@ -0,0 +1,57 @@
|
||||
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';
|
||||
|
||||
class StatsPageTopSection extends HookConsumerWidget {
|
||||
const StatsPageTopSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final tabController = useTabController(initialLength: 3);
|
||||
|
||||
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"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListenableBuilder(
|
||||
listenable: tabController,
|
||||
builder: (context, _) {
|
||||
return switch (tabController.index) {
|
||||
1 => const TopArtists(),
|
||||
2 => const TopAlbums(),
|
||||
_ => const TopTracks(),
|
||||
};
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/components/stats/top/tracks.dart
Normal file
56
lib/components/stats/top/tracks.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/formatters.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/provider/history/top.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
class TopTracks extends HookConsumerWidget {
|
||||
const TopTracks({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final tracks =
|
||||
ref.watch(playbackHistoryTopProvider.select((value) => value.tracks));
|
||||
|
||||
return SliverList.builder(
|
||||
itemCount: tracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final track = tracks[index];
|
||||
return ListTile(
|
||||
horizontalTitleGap: 8,
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: UniversalImage(
|
||||
path: (track.track.album?.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
title: Text(track.track.name!),
|
||||
subtitle: ArtistLink(
|
||||
artists: track.track.artists!,
|
||||
mainAxisAlignment: WrapAlignment.start,
|
||||
),
|
||||
trailing: Text(
|
||||
"${compactNumberFormatter.format(track.count)} plays",
|
||||
),
|
||||
onTap: () {
|
||||
ServiceUtils.pushNamed(
|
||||
context,
|
||||
TrackPage.name,
|
||||
pathParameters: {
|
||||
"id": track.track.id!,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ import 'package:spotube/components/root/spotube_navigation_bar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
||||
import 'package:spotube/hooks/configurators/use_update_checker.dart';
|
||||
import 'package:spotube/pages/home/home.dart';
|
||||
import 'package:spotube/provider/connect/server.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
@ -173,10 +174,11 @@ class RootApp extends HookConsumerWidget {
|
||||
// ignore: deprecated_member_use
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
// if (rootPaths[location] != 0) {
|
||||
// onSelectIndexChanged(0);
|
||||
// return false;
|
||||
// }
|
||||
final routerState = GoRouterState.of(context);
|
||||
if (routerState.matchedLocation != "/") {
|
||||
context.goNamed(HomePage.name);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
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";
|
||||
@ -8,6 +13,23 @@ class StatsPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
return Container();
|
||||
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(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
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 {
|
||||
@ -36,9 +38,12 @@ class PlaybackHistoryState {
|
||||
|
||||
class PlaybackHistoryNotifier
|
||||
extends PersistedStateNotifier<PlaybackHistoryState> {
|
||||
PlaybackHistoryNotifier()
|
||||
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);
|
||||
@ -69,12 +74,18 @@ class PlaybackHistoryNotifier
|
||||
);
|
||||
}
|
||||
|
||||
void addTracks(List<TrackSimple> tracks) {
|
||||
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,
|
||||
for (final track in tracks)
|
||||
PlaybackHistoryItem.track(date: DateTime.now(), track: track),
|
||||
PlaybackHistoryItem.track(date: DateTime.now(), track: track),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -86,5 +97,33 @@ class PlaybackHistoryNotifier
|
||||
|
||||
final playbackHistoryProvider =
|
||||
StateNotifierProvider<PlaybackHistoryNotifier, PlaybackHistoryState>(
|
||||
(ref) => PlaybackHistoryNotifier(),
|
||||
(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,
|
||||
);
|
||||
});
|
||||
|
||||
@ -18,7 +18,7 @@ class PlaybackHistoryItem with _$PlaybackHistoryItem {
|
||||
|
||||
factory PlaybackHistoryItem.track({
|
||||
required DateTime date,
|
||||
required TrackSimple track,
|
||||
required Track track,
|
||||
}) = PlaybackHistoryTrack;
|
||||
|
||||
factory PlaybackHistoryItem.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@ -36,21 +36,21 @@ mixin _$PlaybackHistoryItem {
|
||||
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, TrackSimple track) track,
|
||||
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, TrackSimple track)? track,
|
||||
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, TrackSimple track)? track,
|
||||
TResult Function(DateTime date, Track track)? track,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@ -205,7 +205,7 @@ class _$PlaybackHistoryPlaylistImpl implements PlaybackHistoryPlaylist {
|
||||
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, TrackSimple track) track,
|
||||
required TResult Function(DateTime date, Track track) track,
|
||||
}) {
|
||||
return playlist(date, this.playlist);
|
||||
}
|
||||
@ -215,7 +215,7 @@ class _$PlaybackHistoryPlaylistImpl implements PlaybackHistoryPlaylist {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||
TResult? Function(DateTime date, AlbumSimple album)? album,
|
||||
TResult? Function(DateTime date, TrackSimple track)? track,
|
||||
TResult? Function(DateTime date, Track track)? track,
|
||||
}) {
|
||||
return playlist?.call(date, this.playlist);
|
||||
}
|
||||
@ -225,7 +225,7 @@ class _$PlaybackHistoryPlaylistImpl implements PlaybackHistoryPlaylist {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||
TResult Function(DateTime date, AlbumSimple album)? album,
|
||||
TResult Function(DateTime date, TrackSimple track)? track,
|
||||
TResult Function(DateTime date, Track track)? track,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (playlist != null) {
|
||||
@ -380,7 +380,7 @@ class _$PlaybackHistoryAlbumImpl implements PlaybackHistoryAlbum {
|
||||
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, TrackSimple track) track,
|
||||
required TResult Function(DateTime date, Track track) track,
|
||||
}) {
|
||||
return album(date, this.album);
|
||||
}
|
||||
@ -390,7 +390,7 @@ class _$PlaybackHistoryAlbumImpl implements PlaybackHistoryAlbum {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||
TResult? Function(DateTime date, AlbumSimple album)? album,
|
||||
TResult? Function(DateTime date, TrackSimple track)? track,
|
||||
TResult? Function(DateTime date, Track track)? track,
|
||||
}) {
|
||||
return album?.call(date, this.album);
|
||||
}
|
||||
@ -400,7 +400,7 @@ class _$PlaybackHistoryAlbumImpl implements PlaybackHistoryAlbum {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||
TResult Function(DateTime date, AlbumSimple album)? album,
|
||||
TResult Function(DateTime date, TrackSimple track)? track,
|
||||
TResult Function(DateTime date, Track track)? track,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (album != null) {
|
||||
@ -476,7 +476,7 @@ abstract class _$$PlaybackHistoryTrackImplCopyWith<$Res>
|
||||
__$$PlaybackHistoryTrackImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({DateTime date, TrackSimple track});
|
||||
$Res call({DateTime date, Track track});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -501,7 +501,7 @@ class __$$PlaybackHistoryTrackImplCopyWithImpl<$Res>
|
||||
track: null == track
|
||||
? _value.track
|
||||
: track // ignore: cast_nullable_to_non_nullable
|
||||
as TrackSimple,
|
||||
as Track,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -519,7 +519,7 @@ class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack {
|
||||
@override
|
||||
final DateTime date;
|
||||
@override
|
||||
final TrackSimple track;
|
||||
final Track track;
|
||||
|
||||
@JsonKey(name: 'runtimeType')
|
||||
final String $type;
|
||||
@ -555,7 +555,7 @@ class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack {
|
||||
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, TrackSimple track) track,
|
||||
required TResult Function(DateTime date, Track track) track,
|
||||
}) {
|
||||
return track(date, this.track);
|
||||
}
|
||||
@ -565,7 +565,7 @@ class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||
TResult? Function(DateTime date, AlbumSimple album)? album,
|
||||
TResult? Function(DateTime date, TrackSimple track)? track,
|
||||
TResult? Function(DateTime date, Track track)? track,
|
||||
}) {
|
||||
return track?.call(date, this.track);
|
||||
}
|
||||
@ -575,7 +575,7 @@ class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(DateTime date, PlaylistSimple playlist)? playlist,
|
||||
TResult Function(DateTime date, AlbumSimple album)? album,
|
||||
TResult Function(DateTime date, TrackSimple track)? track,
|
||||
TResult Function(DateTime date, Track track)? track,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (track != null) {
|
||||
@ -629,14 +629,14 @@ class _$PlaybackHistoryTrackImpl implements PlaybackHistoryTrack {
|
||||
abstract class PlaybackHistoryTrack implements PlaybackHistoryItem {
|
||||
factory PlaybackHistoryTrack(
|
||||
{required final DateTime date,
|
||||
required final TrackSimple track}) = _$PlaybackHistoryTrackImpl;
|
||||
required final Track track}) = _$PlaybackHistoryTrackImpl;
|
||||
|
||||
factory PlaybackHistoryTrack.fromJson(Map<String, dynamic> json) =
|
||||
_$PlaybackHistoryTrackImpl.fromJson;
|
||||
|
||||
@override
|
||||
DateTime get date;
|
||||
TrackSimple get track;
|
||||
Track get track;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$PlaybackHistoryTrackImplCopyWith<_$PlaybackHistoryTrackImpl>
|
||||
|
||||
@ -42,8 +42,7 @@ Map<String, dynamic> _$$PlaybackHistoryAlbumImplToJson(
|
||||
_$PlaybackHistoryTrackImpl _$$PlaybackHistoryTrackImplFromJson(Map json) =>
|
||||
_$PlaybackHistoryTrackImpl(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
track:
|
||||
TrackSimple.fromJson(Map<String, dynamic>.from(json['track'] as Map)),
|
||||
track: Track.fromJson(Map<String, dynamic>.from(json['track'] as Map)),
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
|
||||
53
lib/provider/history/summary.dart
Normal file
53
lib/provider/history/summary.dart
Normal file
@ -0,0 +1,53 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/provider/history/history.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;
|
||||
|
||||
return (
|
||||
duration: totalDurationListened,
|
||||
tracks: totalTracksListened,
|
||||
artists: totalArtistsListened,
|
||||
albums: totalAlbumsListened,
|
||||
playlists: totalPlaylistsListened,
|
||||
);
|
||||
});
|
||||
47
lib/provider/history/top.dart
Normal file
47
lib/provider/history/top.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/history/history.dart';
|
||||
|
||||
final playbackHistoryTopProvider = Provider((ref) {
|
||||
final (:tracks, :albums, playlists: _) =
|
||||
ref.watch(playbackHistoryGroupedProvider);
|
||||
|
||||
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();
|
||||
|
||||
return (
|
||||
tracks: tracksWithCount,
|
||||
albums: albumsWithCount,
|
||||
artists: artistsWithCount
|
||||
);
|
||||
});
|
||||
@ -83,7 +83,7 @@ extension ProxyPlaylistListeners on ProxyPlaylistNotifier {
|
||||
}
|
||||
|
||||
scrobbler.scrobble(playlist.activeTrack!);
|
||||
history.addTracks([playlist.activeTrack!]);
|
||||
history.addTrack(playlist.activeTrack!);
|
||||
lastScrobbled = uid;
|
||||
} catch (e, stack) {
|
||||
Catcher2.reportCheckedError(e, stack);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/track.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user