mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat(stats): add lazy loading support
This commit is contained in:
parent
4c5564fd2f
commit
3bdc46da4d
@ -4,6 +4,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/extensions/json1.dart';
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:media_kit/media_kit.dart' hide Track;
|
||||
import 'package:path/path.dart';
|
||||
@ -13,7 +14,7 @@ import 'package:spotube/models/lyrics.dart';
|
||||
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:flutter/material.dart' hide Table, Key;
|
||||
import 'package:flutter/material.dart' hide Table, Key, View;
|
||||
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
|
@ -4,6 +4,9 @@ import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/formatters.dart';
|
||||
import 'package:spotube/modules/stats/common/album_item.dart';
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/history/top/albums.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class TopAlbums extends HookConsumerWidget {
|
||||
const TopAlbums({super.key});
|
||||
@ -11,14 +14,21 @@ class TopAlbums extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||
final albums = ref.watch(playbackHistoryTopProvider(historyDuration)
|
||||
.select((value) => value.whenData((s) => s.albums)));
|
||||
final topAlbums = ref.watch(historyTopAlbumsProvider(historyDuration));
|
||||
final topAlbumsNotifier =
|
||||
ref.watch(historyTopAlbumsProvider(historyDuration).notifier);
|
||||
|
||||
final albumsData = albums.asData?.value ?? [];
|
||||
final albumsData = topAlbums.asData?.value.items ?? [];
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: albums.isLoading,
|
||||
child: SliverList.builder(
|
||||
return Skeletonizer.sliver(
|
||||
enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
|
||||
child: SliverInfiniteList(
|
||||
onFetchData: () async {
|
||||
await topAlbumsNotifier.fetchMore();
|
||||
},
|
||||
hasError: topAlbums.hasError,
|
||||
isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
|
||||
hasReachedMax: topAlbums.asData?.value.hasMore ?? true,
|
||||
itemCount: albumsData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final album = albumsData[index];
|
||||
|
@ -1,8 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/formatters.dart';
|
||||
import 'package:spotube/modules/stats/common/artist_item.dart';
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/history/top/tracks.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class TopArtists extends HookConsumerWidget {
|
||||
const TopArtists({super.key});
|
||||
@ -10,12 +15,24 @@ class TopArtists extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||
final artists = ref.watch(playbackHistoryTopProvider(historyDuration)
|
||||
.select((value) => value.whenData((s) => s.artists)));
|
||||
final topTracks = ref.watch(
|
||||
historyTopTracksProvider(historyDuration),
|
||||
);
|
||||
final topTracksNotifier =
|
||||
ref.watch(historyTopTracksProvider(historyDuration).notifier);
|
||||
|
||||
final artistsData = artists.asData?.value ?? [];
|
||||
final artistsData = useMemoized(
|
||||
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
|
||||
|
||||
return SliverList.builder(
|
||||
return Skeletonizer.sliver(
|
||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
child: SliverInfiniteList(
|
||||
onFetchData: () async {
|
||||
await topTracksNotifier.fetchMore();
|
||||
},
|
||||
hasError: topTracks.hasError,
|
||||
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
||||
itemCount: artistsData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final artist = artistsData[index];
|
||||
@ -24,6 +41,7 @@ class TopArtists extends HookConsumerWidget {
|
||||
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/formatters.dart';
|
||||
import 'package:spotube/modules/stats/common/track_item.dart';
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/history/top/tracks.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class TopTracks extends HookConsumerWidget {
|
||||
const TopTracks({super.key});
|
||||
@ -10,14 +14,23 @@ class TopTracks extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
|
||||
final tracks = ref.watch(
|
||||
playbackHistoryTopProvider(historyDuration)
|
||||
.select((value) => value.whenData((s) => s.tracks)),
|
||||
final topTracks = ref.watch(
|
||||
historyTopTracksProvider(historyDuration),
|
||||
);
|
||||
final topTracksNotifier =
|
||||
ref.watch(historyTopTracksProvider(historyDuration).notifier);
|
||||
|
||||
final tracksData = tracks.asData?.value ?? [];
|
||||
final tracksData = topTracks.asData?.value.items ?? [];
|
||||
|
||||
return SliverList.builder(
|
||||
return Skeletonizer.sliver(
|
||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
child: SliverInfiniteList(
|
||||
onFetchData: () async {
|
||||
await topTracksNotifier.fetchMore();
|
||||
},
|
||||
hasError: topTracks.hasError,
|
||||
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
||||
itemCount: tracksData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final track = tracksData[index];
|
||||
@ -28,6 +41,7 @@ class TopTracks extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/formatters.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/modules/stats/common/album_item.dart';
|
||||
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/history/top/albums.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class StatsAlbumsPage extends HookConsumerWidget {
|
||||
static const name = "stats_albums";
|
||||
@ -12,10 +16,12 @@ class StatsAlbumsPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final albums = ref.watch(playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||
.select((value) => value.whenData((s) => s.albums)));
|
||||
final topAlbums =
|
||||
ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime));
|
||||
final topAlbumsNotifier =
|
||||
ref.watch(historyTopAlbumsProvider(HistoryDuration.allTime).notifier);
|
||||
|
||||
final albumsData = albums.asData?.value ?? [];
|
||||
final albumsData = topAlbums.asData?.value.items ?? [];
|
||||
|
||||
return Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
@ -23,16 +29,27 @@ class StatsAlbumsPage extends HookConsumerWidget {
|
||||
centerTitle: false,
|
||||
title: Text("Albums"),
|
||||
),
|
||||
body: ListView.builder(
|
||||
body: Skeletonizer(
|
||||
enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
|
||||
child: InfiniteList(
|
||||
onFetchData: () async {
|
||||
await topAlbumsNotifier.fetchMore();
|
||||
},
|
||||
hasError: topAlbums.hasError,
|
||||
isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
|
||||
hasReachedMax: topAlbums.asData?.value.hasMore ?? true,
|
||||
itemCount: albumsData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final album = albumsData[index];
|
||||
return StatsAlbumItem(
|
||||
album: album.album,
|
||||
info: Text("${compactNumberFormatter.format(album.count)} plays"),
|
||||
info: Text(
|
||||
"${compactNumberFormatter.format(album.count)} plays",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/formatters.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/modules/stats/common/artist_item.dart';
|
||||
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/history/top/tracks.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class StatsArtistsPage extends HookConsumerWidget {
|
||||
static const name = "stats_artists";
|
||||
@ -12,12 +17,14 @@ class StatsArtistsPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final artists = ref.watch(
|
||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||
.select((s) => s.whenData((s) => s.artists)),
|
||||
final topTracks = ref.watch(
|
||||
historyTopTracksProvider(HistoryDuration.allTime),
|
||||
);
|
||||
final topTracksNotifier =
|
||||
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
|
||||
|
||||
final artistsData = artists.asData?.value ?? [];
|
||||
final artistsData = useMemoized(
|
||||
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
|
||||
|
||||
return Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
@ -25,16 +32,26 @@ class StatsArtistsPage extends HookConsumerWidget {
|
||||
centerTitle: false,
|
||||
title: Text("Artists"),
|
||||
),
|
||||
body: ListView.builder(
|
||||
body: Skeletonizer(
|
||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
child: InfiniteList(
|
||||
onFetchData: () async {
|
||||
await topTracksNotifier.fetchMore();
|
||||
},
|
||||
hasError: topTracks.hasError,
|
||||
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
||||
itemCount: artistsData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final artist = artistsData[index];
|
||||
return StatsArtistItem(
|
||||
artist: artist.artist,
|
||||
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
|
||||
info:
|
||||
Text("${compactNumberFormatter.format(artist.count)} plays"),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
import 'package:spotube/collections/formatters.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/modules/stats/common/artist_item.dart';
|
||||
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/history/top/tracks.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class StatsStreamFeesPage extends HookConsumerWidget {
|
||||
static const name = "stats_stream_fees";
|
||||
@ -16,12 +21,14 @@ class StatsStreamFeesPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme, :hintColor) = Theme.of(context);
|
||||
|
||||
final artists = ref.watch(
|
||||
playbackHistoryTopProvider(HistoryDuration.days30)
|
||||
.select((value) => value.whenData((s) => s.artists)),
|
||||
final topTracks = ref.watch(
|
||||
historyTopTracksProvider(HistoryDuration.allTime),
|
||||
);
|
||||
final topTracksNotifier =
|
||||
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
|
||||
|
||||
final artistsData = artists.asData?.value ?? [];
|
||||
final artistsData = useMemoized(
|
||||
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
|
||||
|
||||
return Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
@ -50,7 +57,15 @@ class StatsStreamFeesPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList.builder(
|
||||
Skeletonizer.sliver(
|
||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
child: SliverInfiniteList(
|
||||
onFetchData: () async {
|
||||
await topTracksNotifier.fetchMore();
|
||||
},
|
||||
hasError: topTracks.hasError,
|
||||
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
||||
itemCount: artistsData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final artist = artistsData[index];
|
||||
@ -60,6 +75,7 @@ class StatsStreamFeesPage extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -1,11 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/formatters.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/modules/stats/common/track_item.dart';
|
||||
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/history/top/tracks.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class StatsMinutesPage extends HookConsumerWidget {
|
||||
static const name = "stats_minutes";
|
||||
@ -15,11 +19,12 @@ class StatsMinutesPage extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final topTracks = ref.watch(
|
||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||
.select((s) => s.whenData((s) => s.tracks)),
|
||||
historyTopTracksProvider(HistoryDuration.allTime),
|
||||
);
|
||||
final topTracksNotifier =
|
||||
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
|
||||
|
||||
final topTracksData = topTracks.asData?.value ?? [];
|
||||
final tracksData = topTracks.asData?.value.items ?? [];
|
||||
|
||||
return Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
@ -27,20 +32,28 @@ class StatsMinutesPage extends HookConsumerWidget {
|
||||
centerTitle: false,
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: ListView.separated(
|
||||
body: Skeletonizer(
|
||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
child: InfiniteList(
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
itemCount: topTracksData.length,
|
||||
onFetchData: () async {
|
||||
await topTracksNotifier.fetchMore();
|
||||
},
|
||||
hasError: topTracks.hasError,
|
||||
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
||||
itemCount: tracksData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final (:track, :count) = topTracksData[index];
|
||||
|
||||
final track = tracksData[index];
|
||||
return StatsTrackItem(
|
||||
track: track,
|
||||
track: track.track,
|
||||
info: Text(
|
||||
"${compactNumberFormatter.format(count * track.duration!.inMinutes)} mins",
|
||||
"${compactNumberFormatter.format(track.count)} plays",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/formatters.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/modules/stats/common/playlist_item.dart';
|
||||
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/history/top/playlists.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class StatsPlaylistsPage extends HookConsumerWidget {
|
||||
static const name = "stats_playlists";
|
||||
@ -12,12 +16,13 @@ class StatsPlaylistsPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final playlists = ref.watch(
|
||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||
.select((s) => s.whenData((s) => s.playlists)),
|
||||
);
|
||||
final topPlaylists =
|
||||
ref.watch(historyTopPlaylistsProvider(HistoryDuration.allTime));
|
||||
|
||||
final playlistsData = playlists.asData?.value ?? [];
|
||||
final topPlaylistsNotifier = ref
|
||||
.watch(historyTopPlaylistsProvider(HistoryDuration.allTime).notifier);
|
||||
|
||||
final playlistsData = topPlaylists.asData?.value.items ?? [];
|
||||
|
||||
return Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
@ -25,17 +30,26 @@ class StatsPlaylistsPage extends HookConsumerWidget {
|
||||
centerTitle: false,
|
||||
title: Text("Playlists"),
|
||||
),
|
||||
body: ListView.builder(
|
||||
body: Skeletonizer(
|
||||
enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
|
||||
child: InfiniteList(
|
||||
onFetchData: () async {
|
||||
await topPlaylistsNotifier.fetchMore();
|
||||
},
|
||||
hasError: topPlaylists.hasError,
|
||||
isLoading: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
|
||||
hasReachedMax: topPlaylists.asData?.value.hasMore ?? true,
|
||||
itemCount: playlistsData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final playlist = playlistsData[index];
|
||||
return StatsPlaylistItem(
|
||||
playlist: playlist.playlist,
|
||||
info:
|
||||
Text("${compactNumberFormatter.format(playlist.count)} plays"),
|
||||
info: Text(
|
||||
"${compactNumberFormatter.format(playlist.count)} plays"),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/formatters.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/modules/stats/common/track_item.dart';
|
||||
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/history/top/tracks.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class StatsStreamsPage extends HookConsumerWidget {
|
||||
static const name = "stats_streams";
|
||||
@ -15,11 +19,12 @@ class StatsStreamsPage extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final topTracks = ref.watch(
|
||||
playbackHistoryTopProvider(HistoryDuration.allTime)
|
||||
.select((s) => s.whenData((s) => s.tracks)),
|
||||
historyTopTracksProvider(HistoryDuration.allTime),
|
||||
);
|
||||
final topTracksNotifier =
|
||||
ref.watch(historyTopTracksProvider(HistoryDuration.allTime).notifier);
|
||||
|
||||
final topTracksData = topTracks.asData?.value ?? [];
|
||||
final tracksData = topTracks.asData?.value.items ?? [];
|
||||
|
||||
return Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
@ -27,20 +32,28 @@ class StatsStreamsPage extends HookConsumerWidget {
|
||||
centerTitle: false,
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: ListView.separated(
|
||||
body: Skeletonizer(
|
||||
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
child: InfiniteList(
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
itemCount: topTracksData.length,
|
||||
onFetchData: () async {
|
||||
await topTracksNotifier.fetchMore();
|
||||
},
|
||||
hasError: topTracks.hasError,
|
||||
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
|
||||
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
|
||||
itemCount: tracksData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final (:track, :count) = topTracksData[index];
|
||||
|
||||
final track = tracksData[index];
|
||||
return StatsTrackItem(
|
||||
track: track,
|
||||
track: track.track,
|
||||
info: Text(
|
||||
"${compactNumberFormatter.format(count)} streams",
|
||||
"${compactNumberFormatter.format(track.count * track.track.duration!.inMinutes)} mins",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,4 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
|
||||
enum HistoryDuration {
|
||||
allTime(Duration(days: 365 * 2003)),
|
||||
@ -22,196 +15,3 @@ enum HistoryDuration {
|
||||
|
||||
final playbackHistoryTopDurationProvider =
|
||||
StateProvider((ref) => HistoryDuration.days30);
|
||||
|
||||
typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album});
|
||||
typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist});
|
||||
typedef PlaybackHistoryTrack = ({int count, Track track});
|
||||
typedef PlaybackHistoryArtist = ({int count, Artist artist});
|
||||
|
||||
class PlaybackHistoryTopState {
|
||||
final List<PlaybackHistoryTrack> tracks;
|
||||
final List<PlaybackHistoryAlbum> albums;
|
||||
final List<PlaybackHistoryPlaylist> playlists;
|
||||
final List<PlaybackHistoryArtist> artists;
|
||||
|
||||
const PlaybackHistoryTopState({
|
||||
required this.tracks,
|
||||
required this.albums,
|
||||
required this.playlists,
|
||||
required this.artists,
|
||||
});
|
||||
|
||||
PlaybackHistoryTopState copyWith({
|
||||
List<PlaybackHistoryTrack>? tracks,
|
||||
List<PlaybackHistoryAlbum>? albums,
|
||||
List<PlaybackHistoryPlaylist>? playlists,
|
||||
List<PlaybackHistoryArtist>? artists,
|
||||
}) {
|
||||
return PlaybackHistoryTopState(
|
||||
tracks: tracks ?? this.tracks,
|
||||
albums: albums ?? this.albums,
|
||||
playlists: playlists ?? this.playlists,
|
||||
artists: artists ?? this.artists,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaybackHistoryTopNotifier
|
||||
extends FamilyAsyncNotifier<PlaybackHistoryTopState, HistoryDuration> {
|
||||
@override
|
||||
build(arg) async {
|
||||
final database = ref.watch(databaseProvider);
|
||||
|
||||
final duration = arg.duration;
|
||||
|
||||
final tracksQuery = (database.select(database.historyTable)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.type.equalsValue(HistoryEntryType.track) &
|
||||
tbl.createdAt.isBiggerOrEqualValue(
|
||||
DateTime.now().subtract(duration),
|
||||
),
|
||||
));
|
||||
|
||||
final albumsQuery = database.select(database.historyTable)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.type.equalsValue(HistoryEntryType.album) &
|
||||
tbl.createdAt.isBiggerOrEqualValue(
|
||||
DateTime.now().subtract(duration),
|
||||
),
|
||||
);
|
||||
|
||||
final playlistsQuery = database.select(database.historyTable)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.type.equalsValue(HistoryEntryType.playlist) &
|
||||
tbl.createdAt.isBiggerOrEqualValue(
|
||||
DateTime.now().subtract(duration),
|
||||
),
|
||||
);
|
||||
|
||||
final subscriptions = <StreamSubscription>[
|
||||
tracksQuery.watch().listen((event) {
|
||||
if (state.asData == null) return;
|
||||
final artists = event
|
||||
.map((track) => track.track!.artists)
|
||||
.expand((e) => e ?? <Artist>[]);
|
||||
state = AsyncData(state.asData!.value.copyWith(
|
||||
tracks: getTracksWithCount(event),
|
||||
artists: getArtistsWithCount(artists),
|
||||
));
|
||||
}),
|
||||
albumsQuery.watch().listen((event) async {
|
||||
if (state.asData == null) return;
|
||||
final tracks = await tracksQuery.get();
|
||||
|
||||
final albumsWithTrackAlbums = [
|
||||
for (final historicAlbum in event) historicAlbum.album!,
|
||||
for (final track in tracks) track.track!.album!
|
||||
];
|
||||
|
||||
state = AsyncData(state.asData!.value.copyWith(
|
||||
albums: getAlbumsWithCount(albumsWithTrackAlbums),
|
||||
));
|
||||
}),
|
||||
playlistsQuery.watch().listen((event) {
|
||||
if (state.asData == null) return;
|
||||
state = AsyncData(state.asData!.value.copyWith(
|
||||
playlists: getPlaylistsWithCount(event),
|
||||
));
|
||||
}),
|
||||
];
|
||||
|
||||
ref.onDispose(() {
|
||||
for (final subscription in subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
return database.transaction(() async {
|
||||
final tracks = await tracksQuery.get();
|
||||
final albums = await albumsQuery.get();
|
||||
final playlists = await playlistsQuery.get();
|
||||
|
||||
final tracksWithCount = getTracksWithCount(tracks);
|
||||
|
||||
final albumsWithTrackAlbums = [
|
||||
for (final historicAlbum in albums) historicAlbum.album!,
|
||||
for (final track in tracks) track.track!.album!
|
||||
];
|
||||
|
||||
final albumsWithCount = getAlbumsWithCount(albumsWithTrackAlbums);
|
||||
|
||||
final artists = tracks
|
||||
.map((track) => track.track!.artists)
|
||||
.expand((e) => e ?? <Artist>[]);
|
||||
|
||||
final artistsWithCount = getArtistsWithCount(artists);
|
||||
|
||||
final playlistsWithCount = getPlaylistsWithCount(playlists);
|
||||
|
||||
return PlaybackHistoryTopState(
|
||||
tracks: tracksWithCount,
|
||||
albums: albumsWithCount,
|
||||
artists: artistsWithCount,
|
||||
playlists: playlistsWithCount,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
List<PlaybackHistoryTrack> getTracksWithCount(List<HistoryTableData> tracks) {
|
||||
return 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();
|
||||
}
|
||||
|
||||
List<PlaybackHistoryAlbum> getAlbumsWithCount(
|
||||
List<AlbumSimple> albumsWithTrackAlbums,
|
||||
) {
|
||||
return 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();
|
||||
}
|
||||
|
||||
List<PlaybackHistoryArtist> getArtistsWithCount(Iterable<Artist> artists) {
|
||||
return 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();
|
||||
}
|
||||
|
||||
List<PlaybackHistoryPlaylist> getPlaylistsWithCount(
|
||||
List<HistoryTableData> playlists,
|
||||
) {
|
||||
return groupBy(playlists, (playlist) => playlist.playlist!.id!)
|
||||
.entries
|
||||
.map((entry) {
|
||||
return (
|
||||
count: entry.value.length,
|
||||
playlist: entry.value.first.playlist!,
|
||||
);
|
||||
})
|
||||
.sorted((a, b) => b.count.compareTo(a.count))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
final playbackHistoryTopProvider = AsyncNotifierProviderFamily<
|
||||
PlaybackHistoryTopNotifier,
|
||||
PlaybackHistoryTopState,
|
||||
HistoryDuration>(PlaybackHistoryTopNotifier.new);
|
||||
|
135
lib/provider/history/top/albums.dart
Normal file
135
lib/provider/history/top/albums.dart
Normal file
@ -0,0 +1,135 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
|
||||
typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album});
|
||||
|
||||
class HistoryTopAlbumsState extends PaginatedState<PlaybackHistoryAlbum> {
|
||||
HistoryTopAlbumsState({
|
||||
required super.items,
|
||||
required super.offset,
|
||||
required super.limit,
|
||||
required super.hasMore,
|
||||
});
|
||||
|
||||
@override
|
||||
HistoryTopAlbumsState copyWith({
|
||||
List<PlaybackHistoryAlbum>? items,
|
||||
int? offset,
|
||||
int? limit,
|
||||
bool? hasMore,
|
||||
}) {
|
||||
return HistoryTopAlbumsState(
|
||||
items: items ?? this.items,
|
||||
offset: offset ?? this.offset,
|
||||
limit: limit ?? this.limit,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
PlaybackHistoryAlbum, HistoryTopAlbumsState, HistoryDuration> {
|
||||
HistoryTopAlbumsNotifier() : super();
|
||||
|
||||
Selectable<AlbumSimple> createAlbumsQuery({int? limit, int? offset}) {
|
||||
final database = ref.read(databaseProvider);
|
||||
|
||||
final duration = switch (arg) {
|
||||
HistoryDuration.allTime => '0',
|
||||
HistoryDuration.days7 => "strftime('%s', 'now', 'weekday 0', '-7 days')",
|
||||
HistoryDuration.days30 => "strftime('%s', 'now', 'start of month')",
|
||||
HistoryDuration.months6 =>
|
||||
"strftime('%s', 'start of month', '-5 months')",
|
||||
HistoryDuration.year => "strftime('%s', 'start of year')",
|
||||
HistoryDuration.years2 => "strftime('%s', 'start of year', '-1 year')",
|
||||
};
|
||||
|
||||
return database.customSelect(
|
||||
"""
|
||||
SELECT
|
||||
history_table.created_at,
|
||||
"""
|
||||
r"""
|
||||
json_extract(history_table.data, '$.album') as data,
|
||||
json_extract(history_table.data, '$.album.id') as item_id,
|
||||
json_extract(history_table.data, '$.album.type') as type
|
||||
"""
|
||||
"""
|
||||
FROM history_table
|
||||
WHERE type = 'track' AND
|
||||
created_at >= $duration
|
||||
UNION ALL
|
||||
SELECT
|
||||
history_table.created_at,
|
||||
history_table.data,
|
||||
history_table.item_id,
|
||||
history_table.type
|
||||
FROM history_table
|
||||
WHERE type = 'album' AND
|
||||
created_at >= $duration
|
||||
ORDER BY created_at desc
|
||||
${limit != null && offset != null ? 'LIMIT $limit OFFSET $offset' : ''}
|
||||
""",
|
||||
readsFrom: {database.historyTable},
|
||||
).map((row) {
|
||||
final data = row.read<String>('data');
|
||||
final album = AlbumSimple.fromJson(jsonDecode(data));
|
||||
return album;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
fetch(arg, offset, limit) async {
|
||||
final albumsQuery = createAlbumsQuery(limit: limit, offset: offset);
|
||||
|
||||
return getAlbumsWithCount(await albumsQuery.get());
|
||||
}
|
||||
|
||||
@override
|
||||
build(arg) async {
|
||||
final albums = await fetch(arg, 0, 20);
|
||||
|
||||
final subscription = createAlbumsQuery().watch().listen((event) {
|
||||
if (state.asData == null) return;
|
||||
state = AsyncData(state.asData!.value.copyWith(
|
||||
items: getAlbumsWithCount(event),
|
||||
hasMore: false,
|
||||
));
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription.cancel();
|
||||
});
|
||||
|
||||
return HistoryTopAlbumsState(
|
||||
items: albums,
|
||||
offset: albums.length,
|
||||
limit: 20,
|
||||
hasMore: true,
|
||||
);
|
||||
}
|
||||
|
||||
List<PlaybackHistoryAlbum> getAlbumsWithCount(
|
||||
List<AlbumSimple> albumsWithTrackAlbums,
|
||||
) {
|
||||
return 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 historyTopAlbumsProvider = AsyncNotifierProviderFamily<
|
||||
HistoryTopAlbumsNotifier, HistoryTopAlbumsState, HistoryDuration>(
|
||||
() => HistoryTopAlbumsNotifier(),
|
||||
);
|
104
lib/provider/history/top/playlists.dart
Normal file
104
lib/provider/history/top/playlists.dart
Normal file
@ -0,0 +1,104 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
|
||||
typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist});
|
||||
|
||||
class HistoryTopPlaylistsState extends PaginatedState<PlaybackHistoryPlaylist> {
|
||||
HistoryTopPlaylistsState({
|
||||
required super.items,
|
||||
required super.offset,
|
||||
required super.limit,
|
||||
required super.hasMore,
|
||||
});
|
||||
|
||||
@override
|
||||
HistoryTopPlaylistsState copyWith({
|
||||
List<PlaybackHistoryPlaylist>? items,
|
||||
int? offset,
|
||||
int? limit,
|
||||
bool? hasMore,
|
||||
}) {
|
||||
return HistoryTopPlaylistsState(
|
||||
items: items ?? this.items,
|
||||
offset: offset ?? this.offset,
|
||||
limit: limit ?? this.limit,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
PlaybackHistoryPlaylist, HistoryTopPlaylistsState, HistoryDuration> {
|
||||
HistoryTopPlaylistsNotifier() : super();
|
||||
|
||||
SimpleSelectStatement<$HistoryTableTable, HistoryTableData>
|
||||
createPlaylistsQuery() {
|
||||
final database = ref.read(databaseProvider);
|
||||
|
||||
return database.select(database.historyTable)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.type.equalsValue(HistoryEntryType.playlist) &
|
||||
tbl.createdAt.isBiggerOrEqualValue(
|
||||
DateTime.now().subtract(arg.duration),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
fetch(arg, offset, limit) async {
|
||||
final playlistsQuery = createPlaylistsQuery()..limit(limit, offset: offset);
|
||||
|
||||
return getPlaylistsWithCount(await playlistsQuery.get());
|
||||
}
|
||||
|
||||
@override
|
||||
build(arg) async {
|
||||
final playlists = await fetch(arg, 0, 20);
|
||||
|
||||
final subscription = createPlaylistsQuery().watch().listen((event) {
|
||||
if (state.asData == null) return;
|
||||
state = AsyncData(state.asData!.value.copyWith(
|
||||
items: getPlaylistsWithCount(event),
|
||||
hasMore: false,
|
||||
));
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription.cancel();
|
||||
});
|
||||
|
||||
return HistoryTopPlaylistsState(
|
||||
items: playlists,
|
||||
offset: playlists.length,
|
||||
limit: 20,
|
||||
hasMore: true,
|
||||
);
|
||||
}
|
||||
|
||||
List<PlaybackHistoryPlaylist> getPlaylistsWithCount(
|
||||
List<HistoryTableData> playlists,
|
||||
) {
|
||||
return groupBy(playlists, (playlist) => playlist.playlist!.id!)
|
||||
.entries
|
||||
.map((entry) {
|
||||
return (
|
||||
count: entry.value.length,
|
||||
playlist: entry.value.first.playlist!,
|
||||
);
|
||||
})
|
||||
.sorted((a, b) => b.count.compareTo(a.count))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
final historyTopPlaylistsProvider = AsyncNotifierProviderFamily<
|
||||
HistoryTopPlaylistsNotifier, HistoryTopPlaylistsState, HistoryDuration>(
|
||||
() => HistoryTopPlaylistsNotifier(),
|
||||
);
|
119
lib/provider/history/top/tracks.dart
Normal file
119
lib/provider/history/top/tracks.dart
Normal file
@ -0,0 +1,119 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/history/top.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
|
||||
typedef PlaybackHistoryTrack = ({int count, Track track});
|
||||
typedef PlaybackHistoryArtist = ({int count, Artist artist});
|
||||
|
||||
class HistoryTopTracksState extends PaginatedState<PlaybackHistoryTrack> {
|
||||
HistoryTopTracksState({
|
||||
required super.items,
|
||||
required super.offset,
|
||||
required super.limit,
|
||||
required super.hasMore,
|
||||
});
|
||||
|
||||
List<PlaybackHistoryArtist> get artists {
|
||||
return getArtistsWithCount(
|
||||
items.expand((e) => e.track.artists ?? <Artist>[]),
|
||||
);
|
||||
}
|
||||
|
||||
List<PlaybackHistoryArtist> getArtistsWithCount(Iterable<Artist> artists) {
|
||||
return 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();
|
||||
}
|
||||
|
||||
@override
|
||||
HistoryTopTracksState copyWith({
|
||||
List<PlaybackHistoryTrack>? items,
|
||||
int? offset,
|
||||
int? limit,
|
||||
bool? hasMore,
|
||||
}) {
|
||||
return HistoryTopTracksState(
|
||||
items: items ?? this.items,
|
||||
offset: offset ?? this.offset,
|
||||
limit: limit ?? this.limit,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
PlaybackHistoryTrack, HistoryTopTracksState, HistoryDuration> {
|
||||
HistoryTopTracksNotifier() : super();
|
||||
|
||||
SimpleSelectStatement<$HistoryTableTable, HistoryTableData>
|
||||
createTracksQuery() {
|
||||
final database = ref.read(databaseProvider);
|
||||
|
||||
return database.select(database.historyTable)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.type.equalsValue(HistoryEntryType.track) &
|
||||
tbl.createdAt.isBiggerOrEqualValue(
|
||||
DateTime.now().subtract(arg.duration),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
fetch(arg, offset, limit) async {
|
||||
final tracksQuery = createTracksQuery()..limit(limit, offset: offset);
|
||||
|
||||
return getTracksWithCount(await tracksQuery.get());
|
||||
}
|
||||
|
||||
@override
|
||||
build(arg) async {
|
||||
final tracks = await fetch(arg, 0, 20);
|
||||
|
||||
final subscription = createTracksQuery().watch().listen((event) {
|
||||
if (state.asData == null) return;
|
||||
state = AsyncData(state.asData!.value.copyWith(
|
||||
items: getTracksWithCount(event),
|
||||
hasMore: false,
|
||||
));
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription.cancel();
|
||||
});
|
||||
|
||||
return HistoryTopTracksState(
|
||||
items: tracks,
|
||||
offset: tracks.length,
|
||||
limit: 20,
|
||||
hasMore: true,
|
||||
);
|
||||
}
|
||||
|
||||
List<PlaybackHistoryTrack> getTracksWithCount(List<HistoryTableData> tracks) {
|
||||
return 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 historyTopTracksProvider = AsyncNotifierProviderFamily<
|
||||
HistoryTopTracksNotifier, HistoryTopTracksState, HistoryDuration>(
|
||||
() => HistoryTopTracksNotifier(),
|
||||
);
|
Loading…
Reference in New Issue
Block a user