From a3250882dffacafa355f8e4b11f0b44f86650693 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 1 Oct 2023 13:19:34 +0600 Subject: [PATCH] chore: fix width of scrollbar & non-interactive scrollbar in android --- lib/components/library/user_albums.dart | 58 +- lib/components/library/user_artists.dart | 26 +- lib/components/library/user_local_tracks.dart | 39 +- lib/components/library/user_playlists.dart | 102 +- lib/components/player/player_queue.dart | 99 +- .../inter_scrollbar/inter_scrollbar.dart | 69 ++ .../track_collection_view.dart | 240 ++--- lib/pages/home/genres.dart | 32 +- lib/pages/home/personalized.dart | 76 +- lib/pages/settings/settings.dart | 941 +++++++++--------- lib/themes/theme.dart | 12 +- 11 files changed, 900 insertions(+), 794 deletions(-) create mode 100644 lib/components/shared/inter_scrollbar/inter_scrollbar.dart diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 8df34346..ccde43f9 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -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), + ) + ], + ), ), ), ), diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index c90d8010..881451b0 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -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(), + ), ), ), ), diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index 11c19c4b..0a6efadf 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -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, + ); + }, + ); + }, + ), ), ), ); diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 3f4029fe..a8b18212 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -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)) + ], + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index a5dee8c9..725af22b 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -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); - }, - ), - ); - }, + ); + }, + ), ), ), ], diff --git a/lib/components/shared/inter_scrollbar/inter_scrollbar.dart b/lib/components/shared/inter_scrollbar/inter_scrollbar.dart new file mode 100644 index 00000000..05eb174a --- /dev/null +++ b/lib/components/shared/inter_scrollbar/inter_scrollbar.dart @@ -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, + ), + ); + } +} diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart index 14d9598f..dcf01dd2 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart @@ -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 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( - 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( + 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, + ); + }, + ); + }, + ) + ], + ), ), )); } diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index 4680c901..b4e3c664 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -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]), + ); + }, + ), ), ), ], diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index b62170fb..d6192592 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -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() ?? - []; - 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() ?? + []; + if (playlists.isEmpty) return const SizedBox.shrink(); + return PersonalizedItemCard( + playlists: playlists, + title: item["name"] ?? "", + hasNextPage: false, + onFetchMore: () {}, + ); + }) + ], + ), ); } } diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 2cc0c271..878fdbb0 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - 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( - secondary: const Icon(SpotubeIcons.close), - title: Text(context.l10n.close_behavior), - value: preferences.closeBehavior, + AdaptiveSelectTile( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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), + ], + ), ), ), ), diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index a4077ba7..42420e8c 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -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), + ), ); }