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/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.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/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
@ -70,39 +71,42 @@ class UserAlbums extends HookConsumerWidget {
child: SearchBar( child: SearchBar(
onChanged: (value) => searchText.value = value, onChanged: (value) => searchText.value = value,
leading: const Icon(SpotubeIcons.filter), leading: const Icon(SpotubeIcons.filter),
hintText: context.l10n.filter_artist, hintText: context.l10n.filter_albums,
), ),
), ),
), ),
), ),
body: SizedBox.expand( body: SizedBox.expand(
child: SingleChildScrollView( child: InterScrollbar(
padding: const EdgeInsets.all(8.0),
controller: controller, controller: controller,
child: Wrap( child: SingleChildScrollView(
runSpacing: 20, padding: const EdgeInsets.all(8.0),
alignment: WrapAlignment.center, controller: controller,
runAlignment: WrapAlignment.center, child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center, runSpacing: 20,
children: [ alignment: WrapAlignment.center,
if (albums.isEmpty) runAlignment: WrapAlignment.center,
Container( crossAxisAlignment: WrapCrossAlignment.center,
alignment: Alignment.topLeft, children: [
padding: const EdgeInsets.all(16.0), if (albums.isEmpty)
child: const ShimmerPlaybuttonCard(count: 4), Container(
), alignment: Alignment.topLeft,
for (final album in albums) padding: const EdgeInsets.all(16.0),
AlbumCard( child: const ShimmerPlaybuttonCard(count: 4),
TypeConversionUtils.simpleAlbum_X_Album(album), ),
), for (final album in albums)
if (albumsQuery.hasNextPage) AlbumCard(
Waypoint( TypeConversionUtils.simpleAlbum_X_Album(album),
controller: controller, ),
isGrid: true, if (albumsQuery.hasNextPage)
onTouchEdge: albumsQuery.fetchNext, Waypoint(
child: const ShimmerPlaybuttonCard(count: 1), 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/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/artist/artist_card.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/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -78,18 +79,21 @@ class UserArtists extends HookConsumerWidget {
onRefresh: () async { onRefresh: () async {
await artistQuery.refresh(); await artistQuery.refresh();
}, },
child: SingleChildScrollView( child: InterScrollbar(
controller: controller, controller: controller,
child: SizedBox( child: SingleChildScrollView(
width: double.infinity, controller: controller,
child: SafeArea( child: SizedBox(
child: Center( width: double.infinity,
child: Wrap( child: SafeArea(
spacing: 15, child: Center(
runSpacing: 5, child: Wrap(
children: filteredArtists spacing: 15,
.mapIndexed((index, artist) => ArtistCard(artist)) runSpacing: 5,
.toList(), 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:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.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/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart'; import 'package:spotube/components/shared/track_table/track_tile.dart';
@ -286,24 +287,26 @@ class UserLocalTracks extends HookConsumerWidget {
onRefresh: () async { onRefresh: () async {
ref.refresh(localTracksProvider); ref.refresh(localTracksProvider);
}, },
child: ListView.builder( child: InterScrollbar(
physics: const AlwaysScrollableScrollPhysics(), child: ListView.builder(
itemCount: filteredTracks.length, physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) { itemCount: filteredTracks.length,
final track = filteredTracks[index]; itemBuilder: (context, index) {
return TrackTile( final track = filteredTracks[index];
index: index, return TrackTile(
track: track, index: index,
userPlaylist: false, track: track,
onTap: () async { userPlaylist: false,
await playLocalTracks( onTap: () async {
ref, await playLocalTracks(
sortedTracks, ref,
currentTrack: track, sortedTracks,
); currentTrack: track,
}, );
); },
}, );
},
),
), ),
), ),
); );

View File

@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/playlist/playlist_create_dialog.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/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/playlist/playlist_card.dart';
@ -79,59 +80,62 @@ class UserPlaylists extends HookConsumerWidget {
return RefreshIndicator( return RefreshIndicator(
onRefresh: playlistsQuery.refresh, onRefresh: playlistsQuery.refresh,
child: SingleChildScrollView( child: InterScrollbar(
controller: controller, controller: controller,
physics: const AlwaysScrollableScrollPhysics(), child: SingleChildScrollView(
child: Waypoint(
controller: controller, controller: controller,
onTouchEdge: () { physics: const AlwaysScrollableScrollPhysics(),
if (playlistsQuery.hasNextPage) { child: Waypoint(
playlistsQuery.fetchNext(); controller: controller,
} onTouchEdge: () {
}, if (playlistsQuery.hasNextPage) {
child: SafeArea( playlistsQuery.fetchNext();
child: Column( }
children: [ },
Padding( child: SafeArea(
padding: const EdgeInsets.all(10), child: Column(
child: SearchBar( children: [
onChanged: (value) => searchText.value = value, Padding(
hintText: context.l10n.filter_playlists, padding: const EdgeInsets.all(10),
leading: const Icon(SpotubeIcons.filter), child: SearchBar(
onChanged: (value) => searchText.value = value,
hintText: context.l10n.filter_playlists,
leading: const Icon(SpotubeIcons.filter),
),
), ),
), AnimatedCrossFade(
AnimatedCrossFade( duration: const Duration(milliseconds: 300),
duration: const Duration(milliseconds: 300), crossFadeState: playlistsQuery.isLoadingPage ||
crossFadeState: playlistsQuery.isLoadingPage || !playlistsQuery.hasPageData
!playlistsQuery.hasPageData ? CrossFadeState.showFirst
? CrossFadeState.showFirst : CrossFadeState.showSecond,
: CrossFadeState.showSecond, firstChild:
firstChild: const Center(child: ShimmerPlaybuttonCard(count: 7)),
const Center(child: ShimmerPlaybuttonCard(count: 7)), secondChild: Wrap(
secondChild: Wrap( runSpacing: 10,
runSpacing: 10, alignment: WrapAlignment.center,
alignment: WrapAlignment.center, children: [
children: [ Row(
Row( children: [
children: [ const SizedBox(width: 10),
const SizedBox(width: 10), const PlaylistCreateDialogButton(),
const PlaylistCreateDialogButton(), const SizedBox(width: 10),
const SizedBox(width: 10), ElevatedButton.icon(
ElevatedButton.icon( icon: const Icon(SpotubeIcons.magic),
icon: const Icon(SpotubeIcons.magic), label: Text(context.l10n.generate_playlist),
label: Text(context.l10n.generate_playlist), onPressed: () {
onPressed: () { GoRouter.of(context).push("/library/generate");
GoRouter.of(context).push("/library/generate"); },
}, ),
), const SizedBox(width: 10),
const SizedBox(width: 10), ],
], ),
), ...playlists.map((playlist) => PlaylistCard(playlist))
...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:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/not_found.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/components/shared/track_table/track_tile.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
@ -196,21 +197,55 @@ class PlayerQueue extends HookConsumerWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
if (!isSearching.value && searchText.value.isEmpty) if (!isSearching.value && searchText.value.isEmpty)
Flexible( Flexible(
child: ReorderableListView.builder( child: InterScrollbar(
onReorder: (oldIndex, newIndex) { controller: controller,
playlistNotifier.moveTrack(oldIndex, newIndex); child: ReorderableListView.builder(
}, onReorder: (oldIndex, newIndex) {
scrollController: controller, playlistNotifier.moveTrack(oldIndex, newIndex);
itemCount: tracks.length, },
shrinkWrap: true, scrollController: controller,
buildDefaultDragHandles: false, itemCount: tracks.length,
itemBuilder: (context, i) { shrinkWrap: true,
final track = tracks.elementAt(i); buildDefaultDragHandles: false,
return AutoScrollTag( itemBuilder: (context, i) {
key: ValueKey(i), final track = tracks.elementAt(i);
controller: controller, return AutoScrollTag(
index: i, key: ValueKey(i),
child: Padding( 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: padding:
const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile( child: TrackTile(
@ -222,38 +257,10 @@ class PlayerQueue extends HookConsumerWidget {
} }
await playlistNotifier.jumpToTrack(track); 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/collections/spotube_icons.dart';
import 'package:spotube/components/playlist/playlist_create_dialog.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/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/page_window_title_bar.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'; 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 { onRefresh: () async {
await tracksSnapshot.refresh(); await tracksSnapshot.refresh();
}, },
child: CustomScrollView( child: InterScrollbar(
controller: controller, controller: controller,
physics: const AlwaysScrollableScrollPhysics(), child: CustomScrollView(
slivers: [ controller: controller,
SliverAppBar( physics: const AlwaysScrollableScrollPhysics(),
actions: [ slivers: [
AnimatedScale( SliverAppBar(
duration: const Duration(milliseconds: 200), actions: [
scale: collapsed.value ? 1 : 0, AnimatedScale(
child: Row( duration: const Duration(milliseconds: 200),
mainAxisSize: MainAxisSize.min, scale: collapsed.value ? 1 : 0,
children: buttons, 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,
), ),
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,
),
),
},
), ),
), AnimatedScale(
], duration: const Duration(milliseconds: 200),
floating: false, scale: collapsed.value ? 1 : 0,
pinned: true, child: IconButton(
expandedHeight: 400, tooltip: context.l10n.shuffle,
automaticallyImplyLeading: kIsMobile, icon: const Icon(SpotubeIcons.shuffle),
leading: onPressed: playingState == PlayButtonState.playing
kIsMobile ? const BackButton(color: Colors.white) : null, ? null
iconTheme: IconThemeData(color: color?.titleTextColor), : onShuffledPlay,
primary: true, ),
backgroundColor: color?.color.withOpacity(.8), ),
title: collapsed.value AnimatedScale(
? Text( duration: const Duration(milliseconds: 200),
title, scale: collapsed.value ? 1 : 0,
style: theme.textTheme.titleMedium!.copyWith( child: ElevatedButton(
color: color?.titleTextColor, style: ElevatedButton.styleFrom(
fontWeight: FontWeight.w600, shape: const CircleBorder(),
backgroundColor: theme.colorScheme.inversePrimary,
), ),
) onPressed: tracksSnapshot.data != null ? onPlay : null,
: null, child: switch (playingState) {
centerTitle: true, PlayButtonState.playing =>
flexibleSpace: FlexibleSpaceBar( const Icon(SpotubeIcons.pause),
background: TrackCollectionHeading<T>( PlayButtonState.notPlaying =>
color: color, const Icon(SpotubeIcons.play),
title: title, PlayButtonState.loading => const SizedBox(
description: description, height: 20,
titleImage: titleImage, width: 20,
playingState: playingState, child: CircularProgressIndicator(
onPlay: onPlay, strokeWidth: .7,
onShuffledPlay: onShuffledPlay, ),
tracksSnapshot: tracksSnapshot, ),
buttons: buttons, },
album: album, ),
),
],
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(
HookBuilder( builder: (context) {
builder: (context) { if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) {
if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) { return const ShimmerTrackTile();
return const ShimmerTrackTile(); } else if (tracksSnapshot.hasError) {
} else if (tracksSnapshot.hasError) { return SliverToBoxAdapter(
return SliverToBoxAdapter( child: Text(
child: Text( context.l10n.error(tracksSnapshot.error ?? ""),
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,
); );
}, }
);
}, 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/collections/spotube_icons.dart';
import 'package:spotube/components/genre/category_card.dart'; import 'package:spotube/components/genre/category_card.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.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/shimmers/shimmer_categories.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
@ -77,21 +78,24 @@ class GenrePage extends HookConsumerWidget {
const ShimmerCategories() const ShimmerCategories()
else else
Expanded( Expanded(
child: ListView.builder( child: InterScrollbar(
controller: scrollController, controller: scrollController,
itemCount: categories.length, child: ListView.builder(
itemBuilder: (context, index) { controller: scrollController,
return AnimatedCrossFade( itemCount: categories.length,
crossFadeState: searchController.text.isEmpty && itemBuilder: (context, index) {
index == categories.length - 1 && return AnimatedCrossFade(
categoriesQuery.hasNextPage crossFadeState: searchController.text.isEmpty &&
? CrossFadeState.showFirst index == categories.length - 1 &&
: CrossFadeState.showSecond, categoriesQuery.hasNextPage
duration: const Duration(milliseconds: 300), ? CrossFadeState.showFirst
firstChild: const ShimmerCategories(), : CrossFadeState.showSecond,
secondChild: CategoryCard(categories[index]), 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:spotify/spotify.dart';
import 'package:spotube/components/album/album_card.dart'; import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/playlist/playlist_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_categories.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
@ -96,6 +97,7 @@ class PersonalizedPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final controller = useScrollController();
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final featuredPlaylistsQuery = useQueries.playlist.featured(ref); final featuredPlaylistsQuery = useQueries.playlist.featured(ref);
final playlists = useMemoized( final playlists = useMemoized(
@ -124,40 +126,46 @@ class PersonalizedPage extends HookConsumerWidget {
[newReleases.pages], [newReleases.pages],
); );
return ListView( return InterScrollbar(
children: [ controller: controller,
if (!featuredPlaylistsQuery.hasPageData) child: ListView(
const ShimmerCategories() controller: controller,
else children: [
PersonalizedItemCard( if (!featuredPlaylistsQuery.hasPageData)
playlists: playlists, const ShimmerCategories()
title: context.l10n.featured, else
hasNextPage: featuredPlaylistsQuery.hasNextPage, PersonalizedItemCard(
onFetchMore: featuredPlaylistsQuery.fetchNext, playlists: playlists,
), title: context.l10n.featured,
if (auth != null && newReleases.hasPageData && userArtistsQuery.hasData) hasNextPage: featuredPlaylistsQuery.hasNextPage,
PersonalizedItemCard( onFetchMore: featuredPlaylistsQuery.fetchNext,
albums: albums, ),
title: context.l10n.new_releases, if (auth != null &&
hasNextPage: newReleases.hasNextPage, newReleases.hasPageData &&
onFetchMore: newReleases.fetchNext, userArtistsQuery.hasData)
), PersonalizedItemCard(
...?madeForUser.data?["content"]?["items"]?.map((item) { albums: albums,
final playlists = item["content"]?["items"] title: context.l10n.new_releases,
?.where((itemL2) => itemL2["type"] == "playlist") hasNextPage: newReleases.hasNextPage,
.map((itemL2) => PlaylistSimple.fromJson(itemL2)) onFetchMore: newReleases.fetchNext,
.toList() ),
.cast<PlaylistSimple>() ?? ...?madeForUser.data?["content"]?["items"]?.map((item) {
<PlaylistSimple>[]; final playlists = item["content"]?["items"]
if (playlists.isEmpty) return const SizedBox.shrink(); ?.where((itemL2) => itemL2["type"] == "playlist")
return PersonalizedItemCard( .map((itemL2) => PlaylistSimple.fromJson(itemL2))
playlists: playlists, .toList()
title: item["name"] ?? "", .cast<PlaylistSimple>() ??
hasNextPage: false, <PlaylistSimple>[];
onFetchMore: () {}, 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/settings/section_card_with_heading.dart';
import 'package:spotube/components/shared/adaptive/adaptive_list_tile.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/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/components/shared/page_window_title_bar.dart';
import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotify_markets.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
@ -70,504 +71,508 @@ class SettingsPage extends HookConsumerWidget {
Flexible( Flexible(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 1366), constraints: const BoxConstraints(maxWidth: 1366),
child: ListView( child: InterScrollbar(
children: [ child: ListView(
const SettingsAccountSection(), children: [
SectionCardWithHeading( const SettingsAccountSection(),
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)
SectionCardWithHeading( SectionCardWithHeading(
heading: context.l10n.desktop, heading: context.l10n.language_region,
children: [ children: [
AdaptiveSelectTile<CloseBehavior>( AdaptiveSelectTile<Locale>(
secondary: const Icon(SpotubeIcons.close), value: preferences.locale,
title: Text(context.l10n.close_behavior), onChanged: (locale) {
value: preferences.closeBehavior, if (locale == null) return;
preferences.setLocale(locale);
},
title: Text(context.l10n.language),
secondary: const Icon(SpotubeIcons.language),
options: [ options: [
DropdownMenuItem( DropdownMenuItem(
value: CloseBehavior.close, value: const Locale("system", "system"),
child: Text(context.l10n.close), 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( DropdownMenuItem(
value: CloseBehavior.minimizeToTray, value: LayoutMode.compact,
child: Text(context.l10n.minimize_to_tray), 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) { onChanged: (value) {
if (value != null) { if (value != null) {
preferences.setCloseBehavior(value); preferences.setThemeMode(value);
} }
}, },
), ),
SwitchListTile( SwitchListTile(
secondary: const Icon(SpotubeIcons.tray), secondary: const Icon(SpotubeIcons.amoled),
title: Text(context.l10n.show_tray_icon), title: Text(context.l10n.use_amoled_mode),
value: preferences.showSystemTrayIcon, subtitle: Text(context.l10n.pitch_dark_theme),
onChanged: preferences.setShowSystemTrayIcon, 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( SwitchListTile(
secondary: const Icon(SpotubeIcons.window), secondary: const Icon(SpotubeIcons.colorSync),
title: Text(context.l10n.use_system_title_bar), title: Text(context.l10n.sync_album_color),
value: preferences.systemTitleBar, subtitle:
onChanged: preferences.setSystemTitleBar, Text(context.l10n.sync_album_color_description),
value: preferences.albumColorSync,
onChanged: preferences.setAlbumColorSync,
), ),
], ],
), ),
if (!kIsWeb)
SectionCardWithHeading( 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: [ children: [
ListTile( ListTile(
leading: const Icon(SpotubeIcons.logs), leading: const Icon(SpotubeIcons.download),
title: Text(context.l10n.logs), 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), trailing: const Icon(SpotubeIcons.angleRight),
onTap: () { onTap: () {
GoRouter.of(context).push("/settings/logs"); GoRouter.of(context).push("/settings/about");
}, },
) )
], ],
), ),
SectionCardWithHeading( Center(
heading: context.l10n.about, child: FilledButton(
children: [ onPressed: preferences.reset,
AdaptiveListTile( child: Text(context.l10n.restore_defaults),
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/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 scrollbarTheme: const ScrollbarThemeData(
? const ScrollbarThemeData( thickness: MaterialStatePropertyAll(14),
interactive: true, ),
thickness: MaterialStatePropertyAll(18),
minThumbLength: 20,
)
: const ScrollbarThemeData(
thickness: MaterialStatePropertyAll(14),
),
); );
} }