refactor: migrate stats to shadcn

This commit is contained in:
Kingkor Roy Tirtho 2025-01-06 14:13:53 +06:00
parent e6408ccc0d
commit 6dd9b753b0
17 changed files with 206 additions and 194 deletions

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
class AnchorButton<T> extends HookWidget { class AnchorButton<T> extends HookWidget {
final String text; final String text;

View File

@ -148,7 +148,10 @@ class PlayerControls extends HookConsumerWidget {
), ),
), ),
child: IconButton( child: IconButton(
icon: const Icon(SpotubeIcons.shuffle), icon: Icon(
SpotubeIcons.shuffle,
color: shuffled ? theme.colorScheme.primary : null,
),
variance: shuffled variance: shuffled
? ButtonVariance.secondary ? ButtonVariance.secondary
: ButtonVariance.ghost, : ButtonVariance.ghost,
@ -228,6 +231,9 @@ class PlayerControls extends HookConsumerWidget {
loopMode == PlaylistMode.single loopMode == PlaylistMode.single
? SpotubeIcons.repeatOne ? SpotubeIcons.repeatOne
: SpotubeIcons.repeat, : SpotubeIcons.repeat,
color: loopMode != PlaylistMode.none
? theme.colorScheme.primary
: null,
), ),
variance: loopMode == PlaylistMode.single || variance: loopMode == PlaylistMode.single ||
loopMode == PlaylistMode.loop loopMode == PlaylistMode.loop

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/links/anchor_button.dart'; import 'package:spotube/components/links/anchor_button.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
@ -19,7 +19,7 @@ class RootAppUpdateDialog extends StatelessWidget {
return AlertDialog( return AlertDialog(
title: Text(context.l10n.spotube_has_an_update), title: Text(context.l10n.spotube_has_an_update),
actions: [ actions: [
FilledButton( Button.primary(
child: Text(context.l10n.download_now), child: Text(context.l10n.download_now),
onPressed: () => launchUrlString( onPressed: () => launchUrlString(
nightlyBuildNum != null ? nightlyUrl : url, nightlyBuildNum != null ? nightlyUrl : url,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/modules/album/album_card.dart'; import 'package:spotube/modules/album/album_card.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
@ -14,8 +15,8 @@ class StatsAlbumItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ButtonTile(
horizontalTitleGap: 8, style: ButtonVariance.ghost,
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: UniversalImage( child: UniversalImage(
@ -47,7 +48,7 @@ class StatsAlbumItem extends StatelessWidget {
], ],
), ),
trailing: info, trailing: info,
onTap: () { onPressed: () {
ServiceUtils.pushNamed( ServiceUtils.pushNamed(
context, context,
AlbumPage.name, AlbumPage.name,

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -16,18 +17,19 @@ class StatsArtistItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ButtonTile(
style: ButtonVariance.ghost,
title: Text(artist.name!), title: Text(artist.name!),
horizontalTitleGap: 8, leading: Avatar(
leading: CircleAvatar( initials: artist.name!.substring(0, 1),
backgroundImage: UniversalImage.imageProvider( provider: UniversalImage.imageProvider(
(artist.images).asUrlString( (artist.images).asUrlString(
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
), ),
), ),
), ),
trailing: info, trailing: info,
onTap: () { onPressed: () {
ServiceUtils.pushNamed( ServiceUtils.pushNamed(
context, context,
ArtistPage.name, ArtistPage.name,

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/string.dart'; import 'package:spotube/extensions/string.dart';
import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/pages/playlist/playlist.dart';
@ -14,8 +15,8 @@ class StatsPlaylistItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ButtonTile(
horizontalTitleGap: 8, style: ButtonVariance.ghost,
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: UniversalImage( child: UniversalImage(
@ -33,7 +34,7 @@ class StatsPlaylistItem extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
trailing: info, trailing: info,
onTap: () { onPressed: () {
ServiceUtils.pushNamed( ServiceUtils.pushNamed(
context, context,
PlaylistPage.name, PlaylistPage.name,

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/track/track.dart'; import 'package:spotube/pages/track/track.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -17,8 +18,8 @@ class StatsTrackItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ButtonTile(
horizontalTitleGap: 8, style: ButtonVariance.ghost,
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: UniversalImage( child: UniversalImage(
@ -42,7 +43,7 @@ class StatsTrackItem extends StatelessWidget {
), ),
), ),
trailing: info, trailing: info,
onTap: () { onPressed: () {
ServiceUtils.pushNamed( ServiceUtils.pushNamed(
context, context,
TrackPage.name, TrackPage.name,

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
@ -48,7 +48,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
title: summaryData.duration.inMinutes.toDouble(), title: summaryData.duration.inMinutes.toDouble(),
unit: context.l10n.summary_minutes, unit: context.l10n.summary_minutes,
description: context.l10n.summary_listened_to_music, description: context.l10n.summary_listened_to_music,
color: Colors.purple, color: Colors.indigo,
onTap: () { onTap: () {
ServiceUtils.pushNamed(context, StatsMinutesPage.name); ServiceUtils.pushNamed(context, StatsMinutesPage.name);
}, },
@ -57,7 +57,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
title: summaryData.tracks.toDouble(), title: summaryData.tracks.toDouble(),
unit: context.l10n.summary_songs, unit: context.l10n.summary_songs,
description: context.l10n.summary_streamed_overall, description: context.l10n.summary_streamed_overall,
color: Colors.lightBlue, color: Colors.blue,
onTap: () { onTap: () {
ServiceUtils.pushNamed(context, StatsStreamsPage.name); ServiceUtils.pushNamed(context, StatsStreamsPage.name);
}, },

View File

@ -1,6 +1,7 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart';
import 'package:gap/gap.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
class SummaryCard extends StatelessWidget { class SummaryCard extends StatelessWidget {
@ -9,7 +10,7 @@ class SummaryCard extends StatelessWidget {
final String description; final String description;
final VoidCallback? onTap; final VoidCallback? onTap;
final MaterialColor color; final ColorShades color;
SummaryCard({ SummaryCard({
super.key, super.key,
@ -31,15 +32,18 @@ class SummaryCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData(:textTheme, :brightness) = Theme.of(context); final ThemeData(:typography, :brightness) = Theme.of(context);
final descriptionNewLines = description.split("").where((s) => s == "\n"); final descriptionNewLines = description.split("").where((s) => s == "\n");
return Card( return Card(
color: brightness == Brightness.dark ? color.shade100 : color.shade50, fillColor: brightness == Brightness.dark ? color.shade100 : color.shade50,
child: InkWell( filled: true,
borderRadius: BorderRadius.circular(16), borderColor: color,
onTap: onTap, padding: EdgeInsets.zero,
borderRadius: context.theme.borderRadiusLg,
child: Button.ghost(
onPressed: onTap,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15), padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15),
child: Column( child: Column(
@ -52,13 +56,13 @@ class SummaryCard extends StatelessWidget {
children: [ children: [
TextSpan( TextSpan(
text: title, text: title,
style: textTheme.headlineLarge?.copyWith( style: typography.h2.copyWith(
color: color.shade900, color: color.shade900,
), ),
), ),
TextSpan( TextSpan(
text: " $unit", text: " $unit",
style: textTheme.titleMedium?.copyWith( style: typography.semiBold.copyWith(
color: color.shade900, color: color.shade900,
), ),
), ),
@ -73,7 +77,7 @@ class SummaryCard extends StatelessWidget {
? descriptionNewLines.length + 1 ? descriptionNewLines.length + 1
: 1, : 1,
minFontSize: 9, minFontSize: 9,
style: textTheme.labelMedium!.copyWith( style: typography.small.copyWith(
color: color.shade900, color: color.shade900,
), ),
), ),

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/modules/stats/top/albums.dart'; import 'package:spotube/modules/stats/top/albums.dart';
import 'package:spotube/modules/stats/top/artists.dart'; import 'package:spotube/modules/stats/top/artists.dart';
import 'package:spotube/modules/stats/top/tracks.dart'; import 'package:spotube/modules/stats/top/tracks.dart';
@ -13,94 +15,90 @@ class StatsPageTopSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final tabController = useTabController(initialLength: 3); final selectedIndex = useState(0);
final historyDuration = ref.watch(playbackHistoryTopDurationProvider); final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
final historyDurationNotifier = final historyDurationNotifier =
ref.watch(playbackHistoryTopDurationProvider.notifier); ref.watch(playbackHistoryTopDurationProvider.notifier);
return SliverMainAxisGroup( final translations = <HistoryDuration, String>{
slivers: [ HistoryDuration.days7: context.l10n.this_week,
SliverAppBar( HistoryDuration.days30: context.l10n.this_month,
floating: true, HistoryDuration.months6: context.l10n.last_6_months,
flexibleSpace: TabBar( HistoryDuration.year: context.l10n.this_year,
controller: tabController, HistoryDuration.years2: context.l10n.last_2_years,
tabs: [ HistoryDuration.allTime: context.l10n.all_time,
Tab( };
child: Padding(
padding: const EdgeInsets.all(5), final dropdown = Select<HistoryDuration>(
child: Text(context.l10n.top_tracks), popupConstraints: const BoxConstraints(maxWidth: 150),
), popupWidthConstraint: PopoverConstraint.flexible,
),
Tab(
child: Padding(
padding: const EdgeInsets.all(5),
child: Text(context.l10n.top_artists),
),
),
Tab(
child: Padding(
padding: const EdgeInsets.all(5),
child: Text(context.l10n.top_albums),
),
),
],
),
),
SliverToBoxAdapter(
child: Align(
alignment: Alignment.centerRight,
child: DropdownButton(
style: Theme.of(context).textTheme.bodySmall!,
isDense: true,
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
underline: const SizedBox(),
value: historyDuration, value: historyDuration,
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
historyDurationNotifier.update((_) => value); historyDurationNotifier.update((_) => value);
}, },
icon: const Icon(Icons.arrow_drop_down), itemBuilder: (context, item) => Text(translations[item]!),
items: [ children: [
DropdownMenuItem( for (final item in HistoryDuration.values)
value: HistoryDuration.days7, SelectItemButton(
child: Text(context.l10n.this_week), value: item,
), child: Text(translations[item]!),
DropdownMenuItem(
value: HistoryDuration.days30,
child: Text(context.l10n.this_month),
),
DropdownMenuItem(
value: HistoryDuration.months6,
child: Text(context.l10n.last_6_months),
),
DropdownMenuItem(
value: HistoryDuration.year,
child: Text(context.l10n.this_year),
),
DropdownMenuItem(
value: HistoryDuration.years2,
child: Text(context.l10n.last_2_years),
),
DropdownMenuItem(
value: HistoryDuration.allTime,
child: Text(context.l10n.all_time),
),
],
),
),
),
ListenableBuilder(
listenable: tabController,
builder: (context, _) {
return switch (tabController.index) {
1 => const TopArtists(),
2 => const TopAlbums(),
_ => const TopTracks(),
};
},
), ),
], ],
); );
return SliverLayoutBuilder(builder: (context, constraints) {
return SliverMainAxisGroup(
slivers: [
SliverAppBar(
floating: true,
elevation: 0,
backgroundColor: context.theme.colorScheme.background,
flexibleSpace: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
TabList(
index: selectedIndex.value,
children: [
TabButton(
child: Text(context.l10n.top_tracks),
onPressed: () => selectedIndex.value = 0,
),
TabButton(
child: Text(context.l10n.top_artists),
onPressed: () => selectedIndex.value = 1,
),
TabButton(
child: Text(context.l10n.top_albums),
onPressed: () => selectedIndex.value = 2,
),
],
),
if (constraints.mdAndUp) ...[
const Spacer(),
dropdown,
]
],
),
),
),
if (constraints.smAndDown)
SliverToBoxAdapter(
child: Align(
alignment: Alignment.centerRight,
child: dropdown,
),
),
switch (selectedIndex.value) {
1 => const TopArtists(),
2 => const TopAlbums(),
_ => const TopTracks(),
},
],
);
});
} }
} }

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
@ -25,11 +25,13 @@ class StatsAlbumsPage extends HookConsumerWidget {
final albumsData = topAlbums.asData?.value.items ?? []; final albumsData = topAlbums.asData?.value.items ?? [];
return Scaffold( return Scaffold(
appBar: TitleBar( headers: [
TitleBar(
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
title: Text(context.l10n.albums), title: Text(context.l10n.albums),
), )
body: Skeletonizer( ],
child: Skeletonizer(
enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage, enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
child: InfiniteList( child: InfiniteList(
onFetchData: () async { onFetchData: () async {

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
@ -28,11 +28,13 @@ class StatsArtistsPage extends HookConsumerWidget {
() => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]); () => topTracks.asData?.value.artists ?? [], [topTracks.asData?.value]);
return Scaffold( return Scaffold(
appBar: TitleBar( headers: [
TitleBar(
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
title: Text(context.l10n.artists), title: Text(context.l10n.artists),
), )
body: Skeletonizer( ],
child: Skeletonizer(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: InfiniteList( child: InfiniteList(
onFetchData: () async { onFetchData: () async {

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:sliver_tools/sliver_tools.dart'; import 'package:sliver_tools/sliver_tools.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
@ -20,7 +20,6 @@ class StatsStreamFeesPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :hintColor) = Theme.of(context);
final duration = useState<HistoryDuration>(HistoryDuration.days30); final duration = useState<HistoryDuration>(HistoryDuration.days30);
final topTracks = ref.watch( final topTracks = ref.watch(
@ -40,12 +39,23 @@ class StatsStreamFeesPage extends HookConsumerWidget {
[artistsData], [artistsData],
); );
final translations = <HistoryDuration, String>{
HistoryDuration.days7: context.l10n.this_week,
HistoryDuration.days30: context.l10n.this_month,
HistoryDuration.months6: context.l10n.last_6_months,
HistoryDuration.year: context.l10n.this_year,
HistoryDuration.years2: context.l10n.last_2_years,
HistoryDuration.allTime: context.l10n.all_time,
};
return Scaffold( return Scaffold(
appBar: TitleBar( headers: [
TitleBar(
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
title: Text(context.l10n.streaming_fees_hypothetical), title: Text(context.l10n.streaming_fees_hypothetical),
), )
body: CustomScrollView( ],
child: CustomScrollView(
slivers: [ slivers: [
SliverCrossAxisConstrained( SliverCrossAxisConstrained(
maxCrossAxisExtent: 600, maxCrossAxisExtent: 600,
@ -55,10 +65,7 @@ class StatsStreamFeesPage extends HookConsumerWidget {
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: Text( child: Text(
context.l10n.spotify_hipotetical_calculation, context.l10n.spotify_hipotetical_calculation,
style: textTheme.bodySmall?.copyWith( ).small().muted(),
color: hintColor,
),
),
), ),
), ),
), ),
@ -70,38 +77,21 @@ class StatsStreamFeesPage extends HookConsumerWidget {
children: [ children: [
Text( Text(
context.l10n.total_money(usdFormatter.format(total)), context.l10n.total_money(usdFormatter.format(total)),
style: textTheme.titleLarge, ).semiBold().large(),
), Select<HistoryDuration>(
DropdownButton<HistoryDuration>(
value: duration.value, value: duration.value,
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
duration.value = value; duration.value = value;
}, },
items: [ itemBuilder: (context, value) => Text(translations[value]!),
DropdownMenuItem( constraints: const BoxConstraints(maxWidth: 150),
value: HistoryDuration.days7, popupWidthConstraint: PopoverConstraint.anchorMaxSize,
child: Text(context.l10n.this_week), children: [
), for (final entry in translations.entries)
DropdownMenuItem( SelectItemButton(
value: HistoryDuration.days30, value: entry.key,
child: Text(context.l10n.this_month), child: Text(entry.value),
),
DropdownMenuItem(
value: HistoryDuration.months6,
child: Text(context.l10n.last_6_months),
),
DropdownMenuItem(
value: HistoryDuration.year,
child: Text(context.l10n.this_year),
),
DropdownMenuItem(
value: HistoryDuration.years2,
child: Text(context.l10n.last_2_years),
),
DropdownMenuItem(
value: HistoryDuration.allTime,
child: Text(context.l10n.all_time),
), ),
], ],
), ),

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
@ -28,11 +27,13 @@ class StatsMinutesPage extends HookConsumerWidget {
final tracksData = topTracks.asData?.value.items ?? []; final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold( return Scaffold(
appBar: TitleBar( headers: [
TitleBar(
title: Text(context.l10n.minutes_listened), title: Text(context.l10n.minutes_listened),
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
), )
body: Skeletonizer( ],
child: Skeletonizer(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: InfiniteList( child: InfiniteList(
separatorBuilder: (context, index) => const Gap(8), separatorBuilder: (context, index) => const Gap(8),

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
@ -26,11 +26,13 @@ class StatsPlaylistsPage extends HookConsumerWidget {
final playlistsData = topPlaylists.asData?.value.items ?? []; final playlistsData = topPlaylists.asData?.value.items ?? [];
return Scaffold( return Scaffold(
appBar: TitleBar( headers: [
TitleBar(
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
title: Text(context.l10n.playlists), title: Text(context.l10n.playlists),
), )
body: Skeletonizer( ],
child: Skeletonizer(
enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage, enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
child: InfiniteList( child: InfiniteList(
onFetchData: () async { onFetchData: () async {

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/modules/stats/summary/summary.dart'; import 'package:spotube/modules/stats/summary/summary.dart';
import 'package:spotube/modules/stats/top/top.dart'; import 'package:spotube/modules/stats/top/top.dart';
@ -16,8 +15,10 @@ class StatsPage extends HookConsumerWidget {
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
appBar: kIsMacOS || kIsMobile ? null : const TitleBar(), headers: [
body: CustomScrollView( if (kIsWindows || kIsLinux) const TitleBar(),
],
child: CustomScrollView(
slivers: [ slivers: [
if (kIsMacOS) const SliverGap(20), if (kIsMacOS) const SliverGap(20),
const StatsPageSummarySection(), const StatsPageSummarySection(),

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
@ -28,11 +27,13 @@ class StatsStreamsPage extends HookConsumerWidget {
final tracksData = topTracks.asData?.value.items ?? []; final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold( return Scaffold(
appBar: TitleBar( headers: [
TitleBar(
title: Text(context.l10n.streamed_songs), title: Text(context.l10n.streamed_songs),
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
), )
body: Skeletonizer( ],
child: Skeletonizer(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage, enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: InfiniteList( child: InfiniteList(
separatorBuilder: (context, index) => const Gap(8), separatorBuilder: (context, index) => const Gap(8),