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:shadcn_flutter/shadcn_flutter.dart';
class AnchorButton<T> extends HookWidget {
final String text;

View File

@ -148,7 +148,10 @@ class PlayerControls extends HookConsumerWidget {
),
),
child: IconButton(
icon: const Icon(SpotubeIcons.shuffle),
icon: Icon(
SpotubeIcons.shuffle,
color: shuffled ? theme.colorScheme.primary : null,
),
variance: shuffled
? ButtonVariance.secondary
: ButtonVariance.ghost,
@ -228,6 +231,9 @@ class PlayerControls extends HookConsumerWidget {
loopMode == PlaylistMode.single
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
color: loopMode != PlaylistMode.none
? theme.colorScheme.primary
: null,
),
variance: loopMode == PlaylistMode.single ||
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:url_launcher/url_launcher_string.dart';
import 'package:spotube/extensions/context.dart';
@ -19,7 +19,7 @@ class RootAppUpdateDialog extends StatelessWidget {
return AlertDialog(
title: Text(context.l10n.spotube_has_an_update),
actions: [
FilledButton(
Button.primary(
child: Text(context.l10n.download_now),
onPressed: () => launchUrlString(
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:spotube/components/ui/button_tile.dart';
import 'package:spotube/modules/album/album_card.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/artist_link.dart';
@ -14,8 +15,8 @@ class StatsAlbumItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
horizontalTitleGap: 8,
return ButtonTile(
style: ButtonVariance.ghost,
leading: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: UniversalImage(
@ -47,7 +48,7 @@ class StatsAlbumItem extends StatelessWidget {
],
),
trailing: info,
onTap: () {
onPressed: () {
ServiceUtils.pushNamed(
context,
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:spotube/components/image/universal_image.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/utils/service_utils.dart';
@ -16,18 +17,19 @@ class StatsArtistItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
return ButtonTile(
style: ButtonVariance.ghost,
title: Text(artist.name!),
horizontalTitleGap: 8,
leading: CircleAvatar(
backgroundImage: UniversalImage.imageProvider(
leading: Avatar(
initials: artist.name!.substring(0, 1),
provider: UniversalImage.imageProvider(
(artist.images).asUrlString(
placeholder: ImagePlaceholder.artist,
),
),
),
trailing: info,
onTap: () {
onPressed: () {
ServiceUtils.pushNamed(
context,
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:spotube/components/image/universal_image.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/extensions/string.dart';
import 'package:spotube/pages/playlist/playlist.dart';
@ -14,8 +15,8 @@ class StatsPlaylistItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
horizontalTitleGap: 8,
return ButtonTile(
style: ButtonVariance.ghost,
leading: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: UniversalImage(
@ -33,7 +34,7 @@ class StatsPlaylistItem extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
trailing: info,
onTap: () {
onPressed: () {
ServiceUtils.pushNamed(
context,
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:spotube/components/image/universal_image.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/pages/track/track.dart';
import 'package:spotube/utils/service_utils.dart';
@ -17,8 +18,8 @@ class StatsTrackItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
horizontalTitleGap: 8,
return ButtonTile(
style: ButtonVariance.ghost,
leading: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: UniversalImage(
@ -42,7 +43,7 @@ class StatsTrackItem extends StatelessWidget {
),
),
trailing: info,
onTap: () {
onPressed: () {
ServiceUtils.pushNamed(
context,
TrackPage.name,

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/artists.dart';
import 'package:spotube/modules/stats/top/tracks.dart';
@ -13,94 +15,90 @@ class StatsPageTopSection extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final tabController = useTabController(initialLength: 3);
final selectedIndex = useState(0);
final historyDuration = ref.watch(playbackHistoryTopDurationProvider);
final historyDurationNotifier =
ref.watch(playbackHistoryTopDurationProvider.notifier);
return SliverMainAxisGroup(
slivers: [
SliverAppBar(
floating: true,
flexibleSpace: TabBar(
controller: tabController,
tabs: [
Tab(
child: Padding(
padding: const EdgeInsets.all(5),
child: Text(context.l10n.top_tracks),
),
),
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,
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,
};
final dropdown = Select<HistoryDuration>(
popupConstraints: const BoxConstraints(maxWidth: 150),
popupWidthConstraint: PopoverConstraint.flexible,
padding: const EdgeInsets.all(4),
borderRadius: BorderRadius.circular(4),
underline: const SizedBox(),
value: historyDuration,
onChanged: (value) {
if (value == null) return;
historyDurationNotifier.update((_) => value);
},
icon: const Icon(Icons.arrow_drop_down),
items: [
DropdownMenuItem(
value: HistoryDuration.days7,
child: Text(context.l10n.this_week),
),
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(),
};
},
itemBuilder: (context, item) => Text(translations[item]!),
children: [
for (final item in HistoryDuration.values)
SelectItemButton(
value: item,
child: Text(translations[item]!),
),
],
);
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:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
@ -25,11 +25,13 @@ class StatsAlbumsPage extends HookConsumerWidget {
final albumsData = topAlbums.asData?.value.items ?? [];
return Scaffold(
appBar: TitleBar(
headers: [
TitleBar(
automaticallyImplyLeading: true,
title: Text(context.l10n.albums),
),
body: Skeletonizer(
)
],
child: Skeletonizer(
enabled: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
child: InfiniteList(
onFetchData: () async {

View File

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

View File

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

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:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
@ -28,11 +27,13 @@ class StatsMinutesPage extends HookConsumerWidget {
final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold(
appBar: TitleBar(
headers: [
TitleBar(
title: Text(context.l10n.minutes_listened),
automaticallyImplyLeading: true,
),
body: Skeletonizer(
)
],
child: Skeletonizer(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: InfiniteList(
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:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
@ -26,11 +26,13 @@ class StatsPlaylistsPage extends HookConsumerWidget {
final playlistsData = topPlaylists.asData?.value.items ?? [];
return Scaffold(
appBar: TitleBar(
headers: [
TitleBar(
automaticallyImplyLeading: true,
title: Text(context.l10n.playlists),
),
body: Skeletonizer(
)
],
child: Skeletonizer(
enabled: topPlaylists.isLoading && !topPlaylists.isLoadingNextPage,
child: InfiniteList(
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:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/modules/stats/summary/summary.dart';
import 'package:spotube/modules/stats/top/top.dart';
@ -16,8 +15,10 @@ class StatsPage extends HookConsumerWidget {
return SafeArea(
bottom: false,
child: Scaffold(
appBar: kIsMacOS || kIsMobile ? null : const TitleBar(),
body: CustomScrollView(
headers: [
if (kIsWindows || kIsLinux) const TitleBar(),
],
child: CustomScrollView(
slivers: [
if (kIsMacOS) const SliverGap(20),
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:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
@ -28,11 +27,13 @@ class StatsStreamsPage extends HookConsumerWidget {
final tracksData = topTracks.asData?.value.items ?? [];
return Scaffold(
appBar: TitleBar(
headers: [
TitleBar(
title: Text(context.l10n.streamed_songs),
automaticallyImplyLeading: true,
),
body: Skeletonizer(
)
],
child: Skeletonizer(
enabled: topTracks.isLoading && !topTracks.isLoadingNextPage,
child: InfiniteList(
separatorBuilder: (context, index) => const Gap(8),