feat: add individual minutes and streams page

This commit is contained in:
Kingkor Roy Tirtho 2024-05-09 23:44:43 +06:00
parent 4d5beb19fe
commit f0b6d660e2
11 changed files with 313 additions and 136 deletions

View File

@ -24,7 +24,9 @@ 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/minutes/minutes.dart';
import 'package:spotube/pages/stats/stats.dart';
import 'package:spotube/pages/stats/streams/streams.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
@ -230,6 +232,22 @@ final routerProvider = Provider((ref) {
pageBuilder: (context, state) => const SpotubePage(
child: StatsPage(),
),
routes: [
GoRoute(
path: "minutes",
name: StatsMinutesPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsMinutesPage(),
),
),
GoRoute(
path: "streams",
name: StatsStreamsPage.name,
pageBuilder: (context, state) => const SpotubePage(
child: StatsStreamsPage(),
),
)
],
)
],
),

View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:spotify/spotify.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/utils/service_utils.dart';
class StatsAlbumItem extends StatelessWidget {
final AlbumSimple album;
final Widget info;
const StatsAlbumItem({super.key, required this.album, required this.info});
@override
Widget build(BuildContext context) {
return ListTile(
horizontalTitleGap: 8,
leading: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: UniversalImage(
path: (album.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
width: 40,
height: 40,
),
),
title: Text(album.name!),
subtitle: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("${album.albumType?.formatted}"),
Flexible(
child: ArtistLink(
artists: album.artists!,
mainAxisAlignment: WrapAlignment.start,
),
),
],
),
trailing: info,
onTap: () {
ServiceUtils.pushNamed(
context,
AlbumPage.name,
pathParameters: {"id": album.id!},
extra: album,
);
},
);
}
}

View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:spotify/spotify.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/utils/service_utils.dart';
class StatsArtistItem extends StatelessWidget {
final Artist artist;
final Widget info;
const StatsArtistItem({
super.key,
required this.artist,
required this.info,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(artist.name!),
horizontalTitleGap: 8,
leading: CircleAvatar(
backgroundImage: UniversalImage.imageProvider(
(artist.images).asUrlString(
placeholder: ImagePlaceholder.artist,
),
),
),
trailing: info,
onTap: () {
ServiceUtils.pushNamed(
context,
ArtistPage.name,
pathParameters: {"id": artist.id!},
);
},
);
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:spotify/spotify.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/utils/service_utils.dart';
class StatsTrackItem extends StatelessWidget {
final Track track;
final Widget info;
const StatsTrackItem({
super.key,
required this.track,
required this.info,
});
@override
Widget build(BuildContext context) {
return ListTile(
horizontalTitleGap: 8,
leading: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: UniversalImage(
path: (track.album?.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
width: 40,
height: 40,
),
),
title: Text(track.name!),
subtitle: ArtistLink(
artists: track.artists!,
mainAxisAlignment: WrapAlignment.start,
),
trailing: info,
onTap: () {
ServiceUtils.pushNamed(
context,
TrackPage.name,
pathParameters: {
"id": track.id!,
},
);
},
);
}
}

View File

@ -3,7 +3,10 @@ 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/minutes/minutes.dart';
import 'package:spotube/pages/stats/streams/streams.dart';
import 'package:spotube/provider/history/summary.dart';
import 'package:spotube/utils/service_utils.dart';
class StatsPageSummarySection extends HookConsumerWidget {
const StatsPageSummarySection({super.key});
@ -34,12 +37,18 @@ class StatsPageSummarySection extends HookConsumerWidget {
unit: "minutes",
description: 'Listened to music',
color: Colors.purple,
onTap: () {
ServiceUtils.pushNamed(context, StatsMinutesPage.name);
},
),
SummaryCard(
title: summary.tracks.toDouble(),
unit: "songs",
description: 'Streamed overall',
color: Colors.lightBlue,
onTap: () {
ServiceUtils.pushNamed(context, StatsStreamsPage.name);
},
),
SummaryCard.unformatted(
title: usdFormatter.format(summary.fees.toDouble()),

View File

@ -7,6 +7,7 @@ class SummaryCard extends StatelessWidget {
final String title;
final String unit;
final String description;
final VoidCallback? onTap;
final MaterialColor color;
@ -16,6 +17,7 @@ class SummaryCard extends StatelessWidget {
required this.unit,
required this.description,
required this.color,
this.onTap,
}) : title = compactNumberFormatter.format(title);
const SummaryCard.unformatted({
@ -24,6 +26,7 @@ class SummaryCard extends StatelessWidget {
required this.unit,
required this.description,
required this.color,
this.onTap,
});
@override
@ -34,44 +37,48 @@ class SummaryCard extends StatelessWidget {
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: title,
style: textTheme.headlineLarge?.copyWith(
color: color.shade900,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
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: title,
style: textTheme.headlineLarge?.copyWith(
color: color.shade900,
),
),
),
TextSpan(
text: " $unit",
style: textTheme.titleMedium?.copyWith(
color: color.shade900,
TextSpan(
text: " $unit",
style: textTheme.titleMedium?.copyWith(
color: color.shade900,
),
),
),
],
],
),
maxLines: 1,
),
maxLines: 1,
),
const Gap(5),
AutoSizeText(
description,
maxLines: description.contains("\n")
? descriptionNewLines.length + 1
: 1,
minFontSize: 9,
style: textTheme.labelMedium!.copyWith(
color: color.shade900,
const Gap(5),
AutoSizeText(
description,
maxLines: description.contains("\n")
? descriptionNewLines.length + 1
: 1,
minFontSize: 9,
style: textTheme.labelMedium!.copyWith(
color: color.shade900,
),
),
),
],
],
),
),
),
);

View File

@ -1,13 +1,8 @@
import 'package:flutter/material.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/components/stats/common/album_item.dart';
import 'package:spotube/provider/history/top.dart';
import 'package:spotube/utils/service_utils.dart';
class TopAlbums extends HookConsumerWidget {
const TopAlbums({super.key});
@ -22,44 +17,11 @@ class TopAlbums extends HookConsumerWidget {
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(
return StatsAlbumItem(
album: album.album,
info: Text(
"${compactNumberFormatter.format(album.count)} plays",
),
onTap: () {
ServiceUtils.pushNamed(
context,
AlbumPage.name,
pathParameters: {
"id": album.album.id!,
},
extra: album.album,
);
},
);
},
);

View File

@ -1,11 +1,8 @@
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/components/stats/common/artist_item.dart';
import 'package:spotube/provider/history/top.dart';
import 'package:spotube/utils/service_utils.dart';
class TopArtists extends HookConsumerWidget {
const TopArtists({super.key});
@ -20,28 +17,9 @@ class TopArtists extends HookConsumerWidget {
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!,
},
);
},
return StatsArtistItem(
artist: artist.artist,
info: Text("${compactNumberFormatter.format(artist.count)} plays"),
);
},
);

View File

@ -1,12 +1,8 @@
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/components/stats/common/track_item.dart';
import 'package:spotube/provider/history/top.dart';
import 'package:spotube/utils/service_utils.dart';
class TopTracks extends HookConsumerWidget {
const TopTracks({super.key});
@ -14,42 +10,20 @@ class TopTracks extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
final tracks = ref.watch(playbackHistoryTopProvider(historyDuration)
.select((value) => value.tracks));
final tracks = ref.watch(
playbackHistoryTopProvider(historyDuration)
.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(
return StatsTrackItem(
track: track.track,
info: Text(
"${compactNumberFormatter.format(track.count)} plays",
),
onTap: () {
ServiceUtils.pushNamed(
context,
TrackPage.name,
pathParameters: {
"id": track.track.id!,
},
);
},
);
},
);

View File

@ -0,0 +1,44 @@
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/shared/page_window_title_bar.dart';
import 'package:spotube/components/stats/common/track_item.dart';
import 'package:spotube/provider/history/state.dart';
import 'package:spotube/provider/history/top.dart';
class StatsMinutesPage extends HookConsumerWidget {
static const name = "stats_minutes";
const StatsMinutesPage({super.key});
@override
Widget build(BuildContext context, ref) {
final topTracks = ref.watch(
playbackHistoryTopProvider(HistoryDuration.allTime)
.select((s) => s.tracks),
);
return Scaffold(
appBar: const PageWindowTitleBar(
title: Text("Minutes listened"),
centerTitle: false,
automaticallyImplyLeading: true,
),
body: ListView.separated(
separatorBuilder: (context, index) => const Gap(8),
itemCount: topTracks.length,
itemBuilder: (context, index) {
final (:track, :count) = topTracks[index];
return StatsTrackItem(
track: track,
info: Text(
"${compactNumberFormatter.format(count * track.duration!.inMinutes)} mins",
),
);
},
),
);
}
}

View File

@ -0,0 +1,44 @@
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/shared/page_window_title_bar.dart';
import 'package:spotube/components/stats/common/track_item.dart';
import 'package:spotube/provider/history/state.dart';
import 'package:spotube/provider/history/top.dart';
class StatsStreamsPage extends HookConsumerWidget {
static const name = "stats_streams";
const StatsStreamsPage({super.key});
@override
Widget build(BuildContext context, ref) {
final topTracks = ref.watch(
playbackHistoryTopProvider(HistoryDuration.allTime)
.select((s) => s.tracks),
);
return Scaffold(
appBar: const PageWindowTitleBar(
title: Text("Streamed songs"),
centerTitle: false,
automaticallyImplyLeading: true,
),
body: ListView.separated(
separatorBuilder: (context, index) => const Gap(8),
itemCount: topTracks.length,
itemBuilder: (context, index) {
final (:track, :count) = topTracks[index];
return StatsTrackItem(
track: track,
info: Text(
"${compactNumberFormatter.format(count)} streams",
),
);
},
),
);
}
}