chore: fix width of scrollbar & non-interactive scrollbar in android

This commit is contained in:
Kingkor Roy Tirtho 2023-10-01 13:19:34 +06:00
parent 5bb8231782
commit a3250882df
11 changed files with 900 additions and 794 deletions

View File

@ -7,6 +7,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/waypoint.dart';
@ -70,39 +71,42 @@ class UserAlbums extends HookConsumerWidget {
child: SearchBar(
onChanged: (value) => searchText.value = value,
leading: const Icon(SpotubeIcons.filter),
hintText: context.l10n.filter_artist,
hintText: context.l10n.filter_albums,
),
),
),
),
body: SizedBox.expand(
child: SingleChildScrollView(
padding: const EdgeInsets.all(8.0),
child: InterScrollbar(
controller: controller,
child: Wrap(
runSpacing: 20,
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (albums.isEmpty)
Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.all(16.0),
child: const ShimmerPlaybuttonCard(count: 4),
),
for (final album in albums)
AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(album),
),
if (albumsQuery.hasNextPage)
Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: albumsQuery.fetchNext,
child: const ShimmerPlaybuttonCard(count: 1),
)
],
child: SingleChildScrollView(
padding: const EdgeInsets.all(8.0),
controller: controller,
child: Wrap(
runSpacing: 20,
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (albums.isEmpty)
Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.all(16.0),
child: const ShimmerPlaybuttonCard(count: 4),
),
for (final album in albums)
AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(album),
),
if (albumsQuery.hasNextPage)
Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: albumsQuery.fetchNext,
child: const ShimmerPlaybuttonCard(count: 1),
)
],
),
),
),
),

View File

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/artist/artist_card.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/queries/queries.dart';
@ -78,18 +79,21 @@ class UserArtists extends HookConsumerWidget {
onRefresh: () async {
await artistQuery.refresh();
},
child: SingleChildScrollView(
child: InterScrollbar(
controller: controller,
child: SizedBox(
width: double.infinity,
child: SafeArea(
child: Center(
child: Wrap(
spacing: 15,
runSpacing: 5,
children: filteredArtists
.mapIndexed((index, artist) => ArtistCard(artist))
.toList(),
child: SingleChildScrollView(
controller: controller,
child: SizedBox(
width: double.infinity,
child: SafeArea(
child: Center(
child: Wrap(
spacing: 15,
runSpacing: 5,
children: filteredArtists
.mapIndexed((index, artist) => ArtistCard(artist))
.toList(),
),
),
),
),

View File

@ -17,6 +17,7 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
@ -286,24 +287,26 @@ class UserLocalTracks extends HookConsumerWidget {
onRefresh: () async {
ref.refresh(localTracksProvider);
},
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: filteredTracks.length,
itemBuilder: (context, index) {
final track = filteredTracks[index];
return TrackTile(
index: index,
track: track,
userPlaylist: false,
onTap: () async {
await playLocalTracks(
ref,
sortedTracks,
currentTrack: track,
);
},
);
},
child: InterScrollbar(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: filteredTracks.length,
itemBuilder: (context, index) {
final track = filteredTracks[index];
return TrackTile(
index: index,
track: track,
userPlaylist: false,
onTap: () async {
await playLocalTracks(
ref,
sortedTracks,
currentTrack: track,
);
},
);
},
),
),
),
);

View File

@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
@ -79,59 +80,62 @@ class UserPlaylists extends HookConsumerWidget {
return RefreshIndicator(
onRefresh: playlistsQuery.refresh,
child: SingleChildScrollView(
child: InterScrollbar(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
child: Waypoint(
child: SingleChildScrollView(
controller: controller,
onTouchEdge: () {
if (playlistsQuery.hasNextPage) {
playlistsQuery.fetchNext();
}
},
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: SearchBar(
onChanged: (value) => searchText.value = value,
hintText: context.l10n.filter_playlists,
leading: const Icon(SpotubeIcons.filter),
physics: const AlwaysScrollableScrollPhysics(),
child: Waypoint(
controller: controller,
onTouchEdge: () {
if (playlistsQuery.hasNextPage) {
playlistsQuery.fetchNext();
}
},
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: SearchBar(
onChanged: (value) => searchText.value = value,
hintText: context.l10n.filter_playlists,
leading: const Icon(SpotubeIcons.filter),
),
),
),
AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: playlistsQuery.isLoadingPage ||
!playlistsQuery.hasPageData
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild:
const Center(child: ShimmerPlaybuttonCard(count: 7)),
secondChild: Wrap(
runSpacing: 10,
alignment: WrapAlignment.center,
children: [
Row(
children: [
const SizedBox(width: 10),
const PlaylistCreateDialogButton(),
const SizedBox(width: 10),
ElevatedButton.icon(
icon: const Icon(SpotubeIcons.magic),
label: Text(context.l10n.generate_playlist),
onPressed: () {
GoRouter.of(context).push("/library/generate");
},
),
const SizedBox(width: 10),
],
),
...playlists.map((playlist) => PlaylistCard(playlist))
],
AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: playlistsQuery.isLoadingPage ||
!playlistsQuery.hasPageData
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild:
const Center(child: ShimmerPlaybuttonCard(count: 7)),
secondChild: Wrap(
runSpacing: 10,
alignment: WrapAlignment.center,
children: [
Row(
children: [
const SizedBox(width: 10),
const PlaylistCreateDialogButton(),
const SizedBox(width: 10),
ElevatedButton.icon(
icon: const Icon(SpotubeIcons.magic),
label: Text(context.l10n.generate_playlist),
onPressed: () {
GoRouter.of(context).push("/library/generate");
},
),
const SizedBox(width: 10),
],
),
...playlists.map((playlist) => PlaylistCard(playlist))
],
),
),
),
],
],
),
),
),
),

View File

@ -10,6 +10,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/not_found.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
@ -196,21 +197,55 @@ class PlayerQueue extends HookConsumerWidget {
const SizedBox(height: 10),
if (!isSearching.value && searchText.value.isEmpty)
Flexible(
child: ReorderableListView.builder(
onReorder: (oldIndex, newIndex) {
playlistNotifier.moveTrack(oldIndex, newIndex);
},
scrollController: controller,
itemCount: tracks.length,
shrinkWrap: true,
buildDefaultDragHandles: false,
itemBuilder: (context, i) {
final track = tracks.elementAt(i);
return AutoScrollTag(
key: ValueKey(i),
controller: controller,
index: i,
child: Padding(
child: InterScrollbar(
controller: controller,
child: ReorderableListView.builder(
onReorder: (oldIndex, newIndex) {
playlistNotifier.moveTrack(oldIndex, newIndex);
},
scrollController: controller,
itemCount: tracks.length,
shrinkWrap: true,
buildDefaultDragHandles: false,
itemBuilder: (context, i) {
final track = tracks.elementAt(i);
return AutoScrollTag(
key: ValueKey(i),
controller: controller,
index: i,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile(
index: i,
track: track,
onTap: () async {
if (playlist.activeTrack?.id == track.id) {
return;
}
await playlistNotifier.jumpToTrack(track);
},
leadingActions: [
ReorderableDragStartListener(
index: i,
child: const Icon(SpotubeIcons.dragHandle),
),
],
),
),
);
},
),
),
)
else
Flexible(
child: InterScrollbar(
child: ListView.builder(
itemCount: filteredTracks.length,
itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i);
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile(
@ -222,38 +257,10 @@ class PlayerQueue extends HookConsumerWidget {
}
await playlistNotifier.jumpToTrack(track);
},
leadingActions: [
ReorderableDragStartListener(
index: i,
child: const Icon(SpotubeIcons.dragHandle),
),
],
),
),
);
},
),
)
else
Flexible(
child: ListView.builder(
itemCount: filteredTracks.length,
itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile(
index: i,
track: track,
onTap: () async {
if (playlist.activeTrack?.id == track.id) {
return;
}
await playlistNotifier.jumpToTrack(track);
},
),
);
},
);
},
),
),
),
],

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class InterScrollbar extends HookWidget {
final Widget child;
final ScrollController? controller;
final bool? thumbVisibility;
final bool? trackVisibility;
final double? thickness;
final Radius? radius;
final bool Function(ScrollNotification)? notificationPredicate;
final bool? interactive;
final ScrollbarOrientation? scrollbarOrientation;
const InterScrollbar({
super.key,
required this.child,
this.controller,
this.thumbVisibility,
this.trackVisibility,
this.thickness,
this.radius,
this.notificationPredicate,
this.interactive,
this.scrollbarOrientation,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (DesktopTools.platform.isDesktop) return child;
return ScrollbarTheme(
data: theme.scrollbarTheme.copyWith(
crossAxisMargin: 10,
minThumbLength: 80,
thickness: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered) ||
states.contains(MaterialState.dragged) ||
states.contains(MaterialState.pressed)) {
return 40;
}
return 20;
}),
radius: const Radius.circular(20),
thumbColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered) ||
states.contains(MaterialState.dragged)) {
return theme.colorScheme.onSurface.withOpacity(0.5);
}
return theme.colorScheme.onSurface.withOpacity(0.3);
}),
),
child: Scrollbar(
controller: controller,
thumbVisibility: thumbVisibility,
trackVisibility: trackVisibility,
thickness: thickness,
radius: radius,
notificationPredicate: notificationPredicate,
interactive: interactive ?? true,
scrollbarOrientation: scrollbarOrientation,
child: child,
),
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_heading.dart';
@ -139,131 +140,134 @@ class TrackCollectionView<T> extends HookConsumerWidget {
onRefresh: () async {
await tracksSnapshot.refresh();
},
child: CustomScrollView(
child: InterScrollbar(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverAppBar(
actions: [
AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: collapsed.value ? 1 : 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: buttons,
),
),
AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: collapsed.value ? 1 : 0,
child: IconButton(
tooltip: context.l10n.shuffle,
icon: const Icon(SpotubeIcons.shuffle),
onPressed: playingState == PlayButtonState.playing
? null
: onShuffledPlay,
),
),
AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: collapsed.value ? 1 : 0,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
backgroundColor: theme.colorScheme.inversePrimary,
child: CustomScrollView(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverAppBar(
actions: [
AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: collapsed.value ? 1 : 0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: buttons,
),
onPressed: tracksSnapshot.data != null ? onPlay : null,
child: switch (playingState) {
PlayButtonState.playing =>
const Icon(SpotubeIcons.pause),
PlayButtonState.notPlaying =>
const Icon(SpotubeIcons.play),
PlayButtonState.loading => const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: .7,
),
),
},
),
),
],
floating: false,
pinned: true,
expandedHeight: 400,
automaticallyImplyLeading: kIsMobile,
leading:
kIsMobile ? const BackButton(color: Colors.white) : null,
iconTheme: IconThemeData(color: color?.titleTextColor),
primary: true,
backgroundColor: color?.color.withOpacity(.8),
title: collapsed.value
? Text(
title,
style: theme.textTheme.titleMedium!.copyWith(
color: color?.titleTextColor,
fontWeight: FontWeight.w600,
AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: collapsed.value ? 1 : 0,
child: IconButton(
tooltip: context.l10n.shuffle,
icon: const Icon(SpotubeIcons.shuffle),
onPressed: playingState == PlayButtonState.playing
? null
: onShuffledPlay,
),
),
AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: collapsed.value ? 1 : 0,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
backgroundColor: theme.colorScheme.inversePrimary,
),
)
: null,
centerTitle: true,
flexibleSpace: FlexibleSpaceBar(
background: TrackCollectionHeading<T>(
color: color,
title: title,
description: description,
titleImage: titleImage,
playingState: playingState,
onPlay: onPlay,
onShuffledPlay: onShuffledPlay,
tracksSnapshot: tracksSnapshot,
buttons: buttons,
album: album,
onPressed: tracksSnapshot.data != null ? onPlay : null,
child: switch (playingState) {
PlayButtonState.playing =>
const Icon(SpotubeIcons.pause),
PlayButtonState.notPlaying =>
const Icon(SpotubeIcons.play),
PlayButtonState.loading => const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: .7,
),
),
},
),
),
],
floating: false,
pinned: true,
expandedHeight: 400,
automaticallyImplyLeading: kIsMobile,
leading:
kIsMobile ? const BackButton(color: Colors.white) : null,
iconTheme: IconThemeData(color: color?.titleTextColor),
primary: true,
backgroundColor: color?.color.withOpacity(.8),
title: collapsed.value
? Text(
title,
style: theme.textTheme.titleMedium!.copyWith(
color: color?.titleTextColor,
fontWeight: FontWeight.w600,
),
)
: null,
centerTitle: true,
flexibleSpace: FlexibleSpaceBar(
background: TrackCollectionHeading<T>(
color: color,
title: title,
description: description,
titleImage: titleImage,
playingState: playingState,
onPlay: onPlay,
onShuffledPlay: onShuffledPlay,
tracksSnapshot: tracksSnapshot,
buttons: buttons,
album: album,
),
),
),
),
HookBuilder(
builder: (context) {
if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) {
return const ShimmerTrackTile();
} else if (tracksSnapshot.hasError) {
return SliverToBoxAdapter(
child: Text(
context.l10n.error(tracksSnapshot.error ?? ""),
),
);
}
return TracksTableView(
(tracksSnapshot.data ?? []).map(
(track) {
if (track is Track) {
return track;
} else {
return TypeConversionUtils.simpleTrack_X_Track(
track,
album!,
);
}
},
).toList(),
onTrackPlayButtonPressed: onPlay,
playlistId: id,
userPlaylist: isOwned,
onFiltering: () {
// scroll the flexible space
// to allow more space for search results
controller.animateTo(
330,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
HookBuilder(
builder: (context) {
if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) {
return const ShimmerTrackTile();
} else if (tracksSnapshot.hasError) {
return SliverToBoxAdapter(
child: Text(
context.l10n.error(tracksSnapshot.error ?? ""),
),
);
},
);
},
)
],
}
return TracksTableView(
(tracksSnapshot.data ?? []).map(
(track) {
if (track is Track) {
return track;
} else {
return TypeConversionUtils.simpleTrack_X_Track(
track,
album!,
);
}
},
).toList(),
onTrackPlayButtonPressed: onPlay,
playlistId: id,
userPlaylist: isOwned,
onFiltering: () {
// scroll the flexible space
// to allow more space for search results
controller.animateTo(
330,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
);
},
);
},
)
],
),
),
));
}

View File

@ -7,6 +7,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/genre/category_card.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/components/shared/waypoint.dart';
@ -77,21 +78,24 @@ class GenrePage extends HookConsumerWidget {
const ShimmerCategories()
else
Expanded(
child: ListView.builder(
child: InterScrollbar(
controller: scrollController,
itemCount: categories.length,
itemBuilder: (context, index) {
return AnimatedCrossFade(
crossFadeState: searchController.text.isEmpty &&
index == categories.length - 1 &&
categoriesQuery.hasNextPage
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 300),
firstChild: const ShimmerCategories(),
secondChild: CategoryCard(categories[index]),
);
},
child: ListView.builder(
controller: scrollController,
itemCount: categories.length,
itemBuilder: (context, index) {
return AnimatedCrossFade(
crossFadeState: searchController.text.isEmpty &&
index == categories.length - 1 &&
categoriesQuery.hasNextPage
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 300),
firstChild: const ShimmerCategories(),
secondChild: CategoryCard(categories[index]),
);
},
),
),
),
],

View File

@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/waypoint.dart';
@ -96,6 +97,7 @@ class PersonalizedPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final controller = useScrollController();
final auth = ref.watch(AuthenticationNotifier.provider);
final featuredPlaylistsQuery = useQueries.playlist.featured(ref);
final playlists = useMemoized(
@ -124,40 +126,46 @@ class PersonalizedPage extends HookConsumerWidget {
[newReleases.pages],
);
return ListView(
children: [
if (!featuredPlaylistsQuery.hasPageData)
const ShimmerCategories()
else
PersonalizedItemCard(
playlists: playlists,
title: context.l10n.featured,
hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNext,
),
if (auth != null && newReleases.hasPageData && userArtistsQuery.hasData)
PersonalizedItemCard(
albums: albums,
title: context.l10n.new_releases,
hasNextPage: newReleases.hasNextPage,
onFetchMore: newReleases.fetchNext,
),
...?madeForUser.data?["content"]?["items"]?.map((item) {
final playlists = item["content"]?["items"]
?.where((itemL2) => itemL2["type"] == "playlist")
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
.toList()
.cast<PlaylistSimple>() ??
<PlaylistSimple>[];
if (playlists.isEmpty) return const SizedBox.shrink();
return PersonalizedItemCard(
playlists: playlists,
title: item["name"] ?? "",
hasNextPage: false,
onFetchMore: () {},
);
})
],
return InterScrollbar(
controller: controller,
child: ListView(
controller: controller,
children: [
if (!featuredPlaylistsQuery.hasPageData)
const ShimmerCategories()
else
PersonalizedItemCard(
playlists: playlists,
title: context.l10n.featured,
hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNext,
),
if (auth != null &&
newReleases.hasPageData &&
userArtistsQuery.hasData)
PersonalizedItemCard(
albums: albums,
title: context.l10n.new_releases,
hasNextPage: newReleases.hasNextPage,
onFetchMore: newReleases.fetchNext,
),
...?madeForUser.data?["content"]?["items"]?.map((item) {
final playlists = item["content"]?["items"]
?.where((itemL2) => itemL2["type"] == "playlist")
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
.toList()
.cast<PlaylistSimple>() ??
<PlaylistSimple>[];
if (playlists.isEmpty) return const SizedBox.shrink();
return PersonalizedItemCard(
playlists: playlists,
title: item["name"] ?? "",
hasNextPage: false,
onFetchMore: () {},
);
})
],
),
);
}
}

View File

@ -17,6 +17,7 @@ import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
import 'package:spotube/components/settings/section_card_with_heading.dart';
import 'package:spotube/components/shared/adaptive/adaptive_list_tile.dart';
import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/collections/spotify_markets.dart';
import 'package:spotube/extensions/constrains.dart';
@ -70,504 +71,508 @@ class SettingsPage extends HookConsumerWidget {
Flexible(
child: Container(
constraints: const BoxConstraints(maxWidth: 1366),
child: ListView(
children: [
const SettingsAccountSection(),
SectionCardWithHeading(
heading: context.l10n.language_region,
children: [
AdaptiveSelectTile<Locale>(
value: preferences.locale,
onChanged: (locale) {
if (locale == null) return;
preferences.setLocale(locale);
},
title: Text(context.l10n.language),
secondary: const Icon(SpotubeIcons.language),
options: [
DropdownMenuItem(
value: const Locale("system", "system"),
child: Text(context.l10n.system_default),
),
for (final locale in L10n.all)
DropdownMenuItem(
value: locale,
child: Builder(builder: (context) {
final isoCodeName =
LanguageLocals.getDisplayLanguage(
locale.languageCode,
);
return Text(
"${isoCodeName.name} (${isoCodeName.nativeName})",
);
}),
),
],
),
AdaptiveSelectTile<Market>(
breakLayout: mediaQuery.lgAndUp,
secondary: const Icon(SpotubeIcons.shoppingBag),
title: Text(context.l10n.market_place_region),
subtitle: Text(context.l10n.recommendation_country),
value: preferences.recommendationMarket,
onChanged: (value) {
if (value == null) return;
preferences.setRecommendationMarket(value);
},
options: spotifyMarkets
.map(
(country) => DropdownMenuItem(
value: country.$1,
child: Text(country.$2),
),
)
.toList(),
),
],
),
SectionCardWithHeading(
heading: context.l10n.appearance,
children: [
AdaptiveSelectTile<LayoutMode>(
secondary: const Icon(SpotubeIcons.dashboard),
title: Text(context.l10n.layout_mode),
subtitle: Text(context.l10n.override_layout_settings),
value: preferences.layoutMode,
onChanged: (value) {
if (value != null) {
preferences.setLayoutMode(value);
}
},
options: [
DropdownMenuItem(
value: LayoutMode.adaptive,
child: Text(context.l10n.adaptive),
),
DropdownMenuItem(
value: LayoutMode.compact,
child: Text(context.l10n.compact),
),
DropdownMenuItem(
value: LayoutMode.extended,
child: Text(context.l10n.extended),
),
],
),
AdaptiveSelectTile<ThemeMode>(
secondary: const Icon(SpotubeIcons.darkMode),
title: Text(context.l10n.theme),
value: preferences.themeMode,
options: [
DropdownMenuItem(
value: ThemeMode.dark,
child: Text(context.l10n.dark),
),
DropdownMenuItem(
value: ThemeMode.light,
child: Text(context.l10n.light),
),
DropdownMenuItem(
value: ThemeMode.system,
child: Text(context.l10n.system),
),
],
onChanged: (value) {
if (value != null) {
preferences.setThemeMode(value);
}
},
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.amoled),
title: Text(context.l10n.use_amoled_mode),
subtitle: Text(context.l10n.pitch_dark_theme),
value: preferences.amoledDarkTheme,
onChanged: preferences.setAmoledDarkTheme,
),
ListTile(
leading: const Icon(SpotubeIcons.palette),
title: Text(context.l10n.accent_color),
contentPadding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 5,
),
trailing: ColorTile.compact(
color: preferences.accentColorScheme,
onPressed: pickColorScheme(),
isActive: true,
),
onTap: pickColorScheme(),
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.colorSync),
title: Text(context.l10n.sync_album_color),
subtitle:
Text(context.l10n.sync_album_color_description),
value: preferences.albumColorSync,
onChanged: preferences.setAlbumColorSync,
),
],
),
SectionCardWithHeading(
heading: context.l10n.playback,
children: [
AdaptiveSelectTile<AudioQuality>(
secondary: const Icon(SpotubeIcons.audioQuality),
title: Text(context.l10n.audio_quality),
value: preferences.audioQuality,
options: [
DropdownMenuItem(
value: AudioQuality.high,
child: Text(context.l10n.high),
),
DropdownMenuItem(
value: AudioQuality.low,
child: Text(context.l10n.low),
),
],
onChanged: (value) {
if (value != null) {
preferences.setAudioQuality(value);
}
},
),
AdaptiveSelectTile<YoutubeApiType>(
secondary: const Icon(SpotubeIcons.api),
title: Text(context.l10n.youtube_api_type),
value: preferences.youtubeApiType,
options: YoutubeApiType.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.label),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferences.setYoutubeApiType(value);
},
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: preferences.youtubeApiType ==
YoutubeApiType.youtube
? const SizedBox.shrink()
: Consumer(builder: (context, ref, child) {
final instanceList =
ref.watch(pipedInstancesFutureProvider);
return instanceList.when(
data: (data) {
return AdaptiveSelectTile<String>(
secondary:
const Icon(SpotubeIcons.piped),
title:
Text(context.l10n.piped_instance),
subtitle: RichText(
text: TextSpan(
children: [
TextSpan(
text: context
.l10n.piped_description,
style:
theme.textTheme.bodyMedium,
),
const TextSpan(text: "\n"),
TextSpan(
text:
context.l10n.piped_warning,
style:
theme.textTheme.labelMedium,
)
],
),
),
value: preferences.pipedInstance,
showValueWhenUnfolded: false,
options: data
.sortedBy((e) => e.name)
.map(
(e) => DropdownMenuItem(
value: e.apiUrl,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text:
"${e.name.trim()}\n",
style: theme.textTheme
.labelLarge,
),
TextSpan(
text: e.locations
.map(
countryCodeToEmoji)
.join(""),
style: GoogleFonts
.notoColorEmoji(),
),
],
),
),
),
)
.toList(),
onChanged: (value) {
if (value != null) {
preferences.setPipedInstance(value);
}
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) =>
Text(error.toString()),
);
}),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: preferences.youtubeApiType ==
YoutubeApiType.youtube
? const SizedBox.shrink()
: AdaptiveSelectTile<SearchMode>(
secondary: const Icon(SpotubeIcons.search),
title: Text(context.l10n.search_mode),
value: preferences.searchMode,
options: SearchMode.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.label),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferences.setSearchMode(value);
},
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: preferences.searchMode ==
SearchMode.youtubeMusic &&
preferences.youtubeApiType ==
YoutubeApiType.piped
? const SizedBox.shrink()
: SwitchListTile(
secondary: const Icon(SpotubeIcons.skip),
title: Text(context.l10n.skip_non_music),
value: preferences.skipNonMusic,
onChanged: (state) {
preferences.setSkipNonMusic(state);
},
),
),
ListTile(
leading: const Icon(SpotubeIcons.playlistRemove),
title: Text(context.l10n.blacklist),
subtitle: Text(context.l10n.blacklist_description),
onTap: () {
GoRouter.of(context).push("/settings/blacklist");
},
trailing: const Icon(SpotubeIcons.angleRight),
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.normalize),
title: Text(context.l10n.normalize_audio),
subtitle: Text(context.l10n.blacklist_description),
value: preferences.normalizeAudio,
onChanged: preferences.setNormalizeAudio,
),
AdaptiveSelectTile<MusicCodec>(
secondary: const Icon(SpotubeIcons.stream),
title: Text(context.l10n.streaming_music_codec),
value: preferences.streamMusicCodec,
showValueWhenUnfolded: false,
options: MusicCodec.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(
e.label,
style: theme.textTheme.labelMedium,
),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferences.setStreamMusicCodec(value);
},
),
AdaptiveSelectTile<MusicCodec>(
secondary: const Icon(SpotubeIcons.file),
title: Text(context.l10n.download_music_codec),
value: preferences.downloadMusicCodec,
showValueWhenUnfolded: false,
options: MusicCodec.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(
e.label,
style: theme.textTheme.labelMedium,
),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferences.setDownloadMusicCodec(value);
},
),
],
),
SectionCardWithHeading(
heading: context.l10n.downloads,
children: [
ListTile(
leading: const Icon(SpotubeIcons.download),
title: Text(context.l10n.download_location),
subtitle: Text(preferences.downloadLocation),
trailing: FilledButton(
onPressed: pickDownloadLocation,
child: const Icon(SpotubeIcons.folder),
),
onTap: pickDownloadLocation,
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.lyrics),
title: Text(context.l10n.download_lyrics),
value: preferences.saveTrackLyrics,
onChanged: (state) {
preferences.setSaveTrackLyrics(state);
},
),
],
),
if (DesktopTools.platform.isDesktop)
child: InterScrollbar(
child: ListView(
children: [
const SettingsAccountSection(),
SectionCardWithHeading(
heading: context.l10n.desktop,
heading: context.l10n.language_region,
children: [
AdaptiveSelectTile<CloseBehavior>(
secondary: const Icon(SpotubeIcons.close),
title: Text(context.l10n.close_behavior),
value: preferences.closeBehavior,
AdaptiveSelectTile<Locale>(
value: preferences.locale,
onChanged: (locale) {
if (locale == null) return;
preferences.setLocale(locale);
},
title: Text(context.l10n.language),
secondary: const Icon(SpotubeIcons.language),
options: [
DropdownMenuItem(
value: CloseBehavior.close,
child: Text(context.l10n.close),
value: const Locale("system", "system"),
child: Text(context.l10n.system_default),
),
for (final locale in L10n.all)
DropdownMenuItem(
value: locale,
child: Builder(builder: (context) {
final isoCodeName =
LanguageLocals.getDisplayLanguage(
locale.languageCode,
);
return Text(
"${isoCodeName.name} (${isoCodeName.nativeName})",
);
}),
),
],
),
AdaptiveSelectTile<Market>(
breakLayout: mediaQuery.lgAndUp,
secondary: const Icon(SpotubeIcons.shoppingBag),
title: Text(context.l10n.market_place_region),
subtitle: Text(context.l10n.recommendation_country),
value: preferences.recommendationMarket,
onChanged: (value) {
if (value == null) return;
preferences.setRecommendationMarket(value);
},
options: spotifyMarkets
.map(
(country) => DropdownMenuItem(
value: country.$1,
child: Text(country.$2),
),
)
.toList(),
),
],
),
SectionCardWithHeading(
heading: context.l10n.appearance,
children: [
AdaptiveSelectTile<LayoutMode>(
secondary: const Icon(SpotubeIcons.dashboard),
title: Text(context.l10n.layout_mode),
subtitle:
Text(context.l10n.override_layout_settings),
value: preferences.layoutMode,
onChanged: (value) {
if (value != null) {
preferences.setLayoutMode(value);
}
},
options: [
DropdownMenuItem(
value: LayoutMode.adaptive,
child: Text(context.l10n.adaptive),
),
DropdownMenuItem(
value: CloseBehavior.minimizeToTray,
child: Text(context.l10n.minimize_to_tray),
value: LayoutMode.compact,
child: Text(context.l10n.compact),
),
DropdownMenuItem(
value: LayoutMode.extended,
child: Text(context.l10n.extended),
),
],
),
AdaptiveSelectTile<ThemeMode>(
secondary: const Icon(SpotubeIcons.darkMode),
title: Text(context.l10n.theme),
value: preferences.themeMode,
options: [
DropdownMenuItem(
value: ThemeMode.dark,
child: Text(context.l10n.dark),
),
DropdownMenuItem(
value: ThemeMode.light,
child: Text(context.l10n.light),
),
DropdownMenuItem(
value: ThemeMode.system,
child: Text(context.l10n.system),
),
],
onChanged: (value) {
if (value != null) {
preferences.setCloseBehavior(value);
preferences.setThemeMode(value);
}
},
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.tray),
title: Text(context.l10n.show_tray_icon),
value: preferences.showSystemTrayIcon,
onChanged: preferences.setShowSystemTrayIcon,
secondary: const Icon(SpotubeIcons.amoled),
title: Text(context.l10n.use_amoled_mode),
subtitle: Text(context.l10n.pitch_dark_theme),
value: preferences.amoledDarkTheme,
onChanged: preferences.setAmoledDarkTheme,
),
ListTile(
leading: const Icon(SpotubeIcons.palette),
title: Text(context.l10n.accent_color),
contentPadding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 5,
),
trailing: ColorTile.compact(
color: preferences.accentColorScheme,
onPressed: pickColorScheme(),
isActive: true,
),
onTap: pickColorScheme(),
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.window),
title: Text(context.l10n.use_system_title_bar),
value: preferences.systemTitleBar,
onChanged: preferences.setSystemTitleBar,
secondary: const Icon(SpotubeIcons.colorSync),
title: Text(context.l10n.sync_album_color),
subtitle:
Text(context.l10n.sync_album_color_description),
value: preferences.albumColorSync,
onChanged: preferences.setAlbumColorSync,
),
],
),
if (!kIsWeb)
SectionCardWithHeading(
heading: context.l10n.developers,
heading: context.l10n.playback,
children: [
AdaptiveSelectTile<AudioQuality>(
secondary: const Icon(SpotubeIcons.audioQuality),
title: Text(context.l10n.audio_quality),
value: preferences.audioQuality,
options: [
DropdownMenuItem(
value: AudioQuality.high,
child: Text(context.l10n.high),
),
DropdownMenuItem(
value: AudioQuality.low,
child: Text(context.l10n.low),
),
],
onChanged: (value) {
if (value != null) {
preferences.setAudioQuality(value);
}
},
),
AdaptiveSelectTile<YoutubeApiType>(
secondary: const Icon(SpotubeIcons.api),
title: Text(context.l10n.youtube_api_type),
value: preferences.youtubeApiType,
options: YoutubeApiType.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.label),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferences.setYoutubeApiType(value);
},
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: preferences.youtubeApiType ==
YoutubeApiType.youtube
? const SizedBox.shrink()
: Consumer(builder: (context, ref, child) {
final instanceList =
ref.watch(pipedInstancesFutureProvider);
return instanceList.when(
data: (data) {
return AdaptiveSelectTile<String>(
secondary:
const Icon(SpotubeIcons.piped),
title:
Text(context.l10n.piped_instance),
subtitle: RichText(
text: TextSpan(
children: [
TextSpan(
text: context
.l10n.piped_description,
style: theme
.textTheme.bodyMedium,
),
const TextSpan(text: "\n"),
TextSpan(
text: context
.l10n.piped_warning,
style: theme
.textTheme.labelMedium,
)
],
),
),
value: preferences.pipedInstance,
showValueWhenUnfolded: false,
options: data
.sortedBy((e) => e.name)
.map(
(e) => DropdownMenuItem(
value: e.apiUrl,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text:
"${e.name.trim()}\n",
style: theme.textTheme
.labelLarge,
),
TextSpan(
text: e.locations
.map(
countryCodeToEmoji)
.join(""),
style: GoogleFonts
.notoColorEmoji(),
),
],
),
),
),
)
.toList(),
onChanged: (value) {
if (value != null) {
preferences
.setPipedInstance(value);
}
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) =>
Text(error.toString()),
);
}),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: preferences.youtubeApiType ==
YoutubeApiType.youtube
? const SizedBox.shrink()
: AdaptiveSelectTile<SearchMode>(
secondary: const Icon(SpotubeIcons.search),
title: Text(context.l10n.search_mode),
value: preferences.searchMode,
options: SearchMode.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.label),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferences.setSearchMode(value);
},
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: preferences.searchMode ==
SearchMode.youtubeMusic &&
preferences.youtubeApiType ==
YoutubeApiType.piped
? const SizedBox.shrink()
: SwitchListTile(
secondary: const Icon(SpotubeIcons.skip),
title: Text(context.l10n.skip_non_music),
value: preferences.skipNonMusic,
onChanged: (state) {
preferences.setSkipNonMusic(state);
},
),
),
ListTile(
leading: const Icon(SpotubeIcons.playlistRemove),
title: Text(context.l10n.blacklist),
subtitle: Text(context.l10n.blacklist_description),
onTap: () {
GoRouter.of(context).push("/settings/blacklist");
},
trailing: const Icon(SpotubeIcons.angleRight),
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.normalize),
title: Text(context.l10n.normalize_audio),
subtitle: Text(context.l10n.blacklist_description),
value: preferences.normalizeAudio,
onChanged: preferences.setNormalizeAudio,
),
AdaptiveSelectTile<MusicCodec>(
secondary: const Icon(SpotubeIcons.stream),
title: Text(context.l10n.streaming_music_codec),
value: preferences.streamMusicCodec,
showValueWhenUnfolded: false,
options: MusicCodec.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(
e.label,
style: theme.textTheme.labelMedium,
),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferences.setStreamMusicCodec(value);
},
),
AdaptiveSelectTile<MusicCodec>(
secondary: const Icon(SpotubeIcons.file),
title: Text(context.l10n.download_music_codec),
value: preferences.downloadMusicCodec,
showValueWhenUnfolded: false,
options: MusicCodec.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(
e.label,
style: theme.textTheme.labelMedium,
),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferences.setDownloadMusicCodec(value);
},
),
],
),
SectionCardWithHeading(
heading: context.l10n.downloads,
children: [
ListTile(
leading: const Icon(SpotubeIcons.logs),
title: Text(context.l10n.logs),
leading: const Icon(SpotubeIcons.download),
title: Text(context.l10n.download_location),
subtitle: Text(preferences.downloadLocation),
trailing: FilledButton(
onPressed: pickDownloadLocation,
child: const Icon(SpotubeIcons.folder),
),
onTap: pickDownloadLocation,
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.lyrics),
title: Text(context.l10n.download_lyrics),
value: preferences.saveTrackLyrics,
onChanged: (state) {
preferences.setSaveTrackLyrics(state);
},
),
],
),
if (DesktopTools.platform.isDesktop)
SectionCardWithHeading(
heading: context.l10n.desktop,
children: [
AdaptiveSelectTile<CloseBehavior>(
secondary: const Icon(SpotubeIcons.close),
title: Text(context.l10n.close_behavior),
value: preferences.closeBehavior,
options: [
DropdownMenuItem(
value: CloseBehavior.close,
child: Text(context.l10n.close),
),
DropdownMenuItem(
value: CloseBehavior.minimizeToTray,
child: Text(context.l10n.minimize_to_tray),
),
],
onChanged: (value) {
if (value != null) {
preferences.setCloseBehavior(value);
}
},
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.tray),
title: Text(context.l10n.show_tray_icon),
value: preferences.showSystemTrayIcon,
onChanged: preferences.setShowSystemTrayIcon,
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.window),
title: Text(context.l10n.use_system_title_bar),
value: preferences.systemTitleBar,
onChanged: preferences.setSystemTitleBar,
),
],
),
if (!kIsWeb)
SectionCardWithHeading(
heading: context.l10n.developers,
children: [
ListTile(
leading: const Icon(SpotubeIcons.logs),
title: Text(context.l10n.logs),
trailing: const Icon(SpotubeIcons.angleRight),
onTap: () {
GoRouter.of(context).push("/settings/logs");
},
)
],
),
SectionCardWithHeading(
heading: context.l10n.about,
children: [
AdaptiveListTile(
leading: const Icon(
SpotubeIcons.heart,
color: Colors.pink,
),
title: SizedBox(
height: 50,
width: 200,
child: Align(
alignment: Alignment.centerLeft,
child: AutoSizeText(
context.l10n.u_love_spotube,
maxLines: 1,
style: const TextStyle(
color: Colors.pink,
fontWeight: FontWeight.bold,
),
),
),
),
trailing: (context, update) => FilledButton(
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.red[100]),
foregroundColor: const MaterialStatePropertyAll(
Colors.pinkAccent),
padding: const MaterialStatePropertyAll(
EdgeInsets.all(15)),
),
onPressed: () {
launchUrlString(
"https://opencollective.com/spotube",
mode: LaunchMode.externalApplication,
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(SpotubeIcons.heart),
const SizedBox(width: 5),
Text(context.l10n.please_sponsor),
],
),
),
),
if (Env.enableUpdateChecker)
SwitchListTile(
secondary: const Icon(SpotubeIcons.update),
title: Text(context.l10n.check_for_updates),
value: preferences.checkUpdate,
onChanged: (checked) =>
preferences.setCheckUpdate(checked),
),
ListTile(
leading: const Icon(SpotubeIcons.info),
title: Text(context.l10n.about_spotube),
trailing: const Icon(SpotubeIcons.angleRight),
onTap: () {
GoRouter.of(context).push("/settings/logs");
GoRouter.of(context).push("/settings/about");
},
)
],
),
SectionCardWithHeading(
heading: context.l10n.about,
children: [
AdaptiveListTile(
leading: const Icon(
SpotubeIcons.heart,
color: Colors.pink,
),
title: SizedBox(
height: 50,
width: 200,
child: Align(
alignment: Alignment.centerLeft,
child: AutoSizeText(
context.l10n.u_love_spotube,
maxLines: 1,
style: const TextStyle(
color: Colors.pink,
fontWeight: FontWeight.bold,
),
),
),
),
trailing: (context, update) => FilledButton(
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.red[100]),
foregroundColor: const MaterialStatePropertyAll(
Colors.pinkAccent),
padding: const MaterialStatePropertyAll(
EdgeInsets.all(15)),
),
onPressed: () {
launchUrlString(
"https://opencollective.com/spotube",
mode: LaunchMode.externalApplication,
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(SpotubeIcons.heart),
const SizedBox(width: 5),
Text(context.l10n.please_sponsor),
],
),
),
Center(
child: FilledButton(
onPressed: preferences.reset,
child: Text(context.l10n.restore_defaults),
),
if (Env.enableUpdateChecker)
SwitchListTile(
secondary: const Icon(SpotubeIcons.update),
title: Text(context.l10n.check_for_updates),
value: preferences.checkUpdate,
onChanged: (checked) =>
preferences.setCheckUpdate(checked),
),
ListTile(
leading: const Icon(SpotubeIcons.info),
title: Text(context.l10n.about_spotube),
trailing: const Icon(SpotubeIcons.angleRight),
onTap: () {
GoRouter.of(context).push("/settings/about");
},
)
],
),
Center(
child: FilledButton(
onPressed: preferences.reset,
child: Text(context.l10n.restore_defaults),
),
),
const SizedBox(height: 10),
],
const SizedBox(height: 10),
],
),
),
),
),

View File

@ -69,14 +69,8 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) {
),
),
),
scrollbarTheme: DesktopTools.platform.isMobile
? const ScrollbarThemeData(
interactive: true,
thickness: MaterialStatePropertyAll(18),
minThumbLength: 20,
)
: const ScrollbarThemeData(
thickness: MaterialStatePropertyAll(14),
),
scrollbarTheme: const ScrollbarThemeData(
thickness: MaterialStatePropertyAll(14),
),
);
}