mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: migrate stats to shadcn
This commit is contained in:
parent
e6408ccc0d
commit
6dd9b753b0
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
|
@ -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 {
|
||||||
|
@ -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(),
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user