From 5928185599f3739845391476c0ae47b9efa2cd36 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 12 Nov 2023 19:01:14 +0600 Subject: [PATCH 1/6] fix: new releases section flickering on scroll glitch --- lib/pages/home/personalized.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index bbffbc11..8a18fd0b 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -64,7 +64,8 @@ class PersonalizedPage extends HookConsumerWidget { ), if (auth != null && newReleases.hasPageData && - userArtistsQuery.hasData) + userArtistsQuery.hasData && + !newReleases.isLoadingNextPage) HorizontalPlaybuttonCardView( items: albums, title: Text(context.l10n.new_releases), From ee94b7cbb24e0f0bc22a6d49c830d4055aa02895 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 12 Nov 2023 19:03:27 +0600 Subject: [PATCH 2/6] fix: new releases section flickering on scroll glitch --- lib/pages/home/personalized.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index 8a18fd0b..30115889 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -71,7 +71,9 @@ class PersonalizedPage extends HookConsumerWidget { title: Text(context.l10n.new_releases), hasNextPage: newReleases.hasNextPage, onFetchMore: newReleases.fetchNext, - ), + ) + else + const ShimmerCategories(), ...?madeForUser.data?["content"]?["items"]?.map((item) { final playlists = item["content"]?["items"] ?.where((itemL2) => itemL2["type"] == "playlist") From 694ddf07a310ec3909cdb6a2617100054c3b3b9e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 12 Nov 2023 19:04:20 +0600 Subject: [PATCH 3/6] chore: revert --- lib/pages/home/personalized.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index 30115889..8a18fd0b 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -71,9 +71,7 @@ class PersonalizedPage extends HookConsumerWidget { title: Text(context.l10n.new_releases), hasNextPage: newReleases.hasNextPage, onFetchMore: newReleases.fetchNext, - ) - else - const ShimmerCategories(), + ), ...?madeForUser.data?["content"]?["items"]?.map((item) { final playlists = item["content"]?["items"] ?.where((itemL2) => itemL2["type"] == "playlist") From e29a38dfa43ddf7a38046d1d40424f01dbe62261 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 13 Nov 2023 20:59:54 +0600 Subject: [PATCH 4/6] fix: changed settings are not persisting after force stop #821 --- .../settings/color_scheme_picker_dialog.dart | 3 +- lib/pages/settings/sections/about.dart | 3 +- lib/pages/settings/sections/appearance.dart | 9 +- lib/pages/settings/sections/desktop.dart | 7 +- lib/pages/settings/sections/downloads.dart | 5 +- .../settings/sections/language_region.dart | 5 +- lib/pages/settings/sections/playback.dart | 17 +- lib/pages/settings/settings.dart | 4 +- lib/provider/user_preferences_provider.dart | 539 +++++++++--------- 9 files changed, 287 insertions(+), 305 deletions(-) diff --git a/lib/components/settings/color_scheme_picker_dialog.dart b/lib/components/settings/color_scheme_picker_dialog.dart index 170bae94..f06a9d84 100644 --- a/lib/components/settings/color_scheme_picker_dialog.dart +++ b/lib/components/settings/color_scheme_picker_dialog.dart @@ -49,6 +49,7 @@ class ColorSchemePickerDialog extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final preferences = ref.watch(userPreferencesProvider); + final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final scheme = preferences.accentColorScheme; final active = useState(colorsMap.firstWhere( (element) { @@ -57,7 +58,7 @@ class ColorSchemePickerDialog extends HookConsumerWidget { ).name); onOk() { - preferences.setAccentColorScheme( + preferencesNotifier.setAccentColorScheme( colorsMap.firstWhere( (element) { return element.name == active.value; diff --git a/lib/pages/settings/sections/about.dart b/lib/pages/settings/sections/about.dart index 0340b27c..85181355 100644 --- a/lib/pages/settings/sections/about.dart +++ b/lib/pages/settings/sections/about.dart @@ -16,6 +16,7 @@ class SettingsAboutSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final preferences = ref.watch(userPreferencesProvider); + final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); return SectionCardWithHeading( heading: context.l10n.about, @@ -68,7 +69,7 @@ class SettingsAboutSection extends HookConsumerWidget { secondary: const Icon(SpotubeIcons.update), title: Text(context.l10n.check_for_updates), value: preferences.checkUpdate, - onChanged: (checked) => preferences.setCheckUpdate(checked), + onChanged: (checked) => preferencesNotifier.setCheckUpdate(checked), ), ListTile( leading: const Icon(SpotubeIcons.info), diff --git a/lib/pages/settings/sections/appearance.dart b/lib/pages/settings/sections/appearance.dart index f4b097e8..5e1ffa50 100644 --- a/lib/pages/settings/sections/appearance.dart +++ b/lib/pages/settings/sections/appearance.dart @@ -15,6 +15,7 @@ class SettingsAppearanceSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final preferences = ref.watch(userPreferencesProvider); + final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final pickColorScheme = useCallback(() { return () => showDialog( context: context, @@ -33,7 +34,7 @@ class SettingsAppearanceSection extends HookConsumerWidget { value: preferences.layoutMode, onChanged: (value) { if (value != null) { - preferences.setLayoutMode(value); + preferencesNotifier.setLayoutMode(value); } }, options: [ @@ -71,7 +72,7 @@ class SettingsAppearanceSection extends HookConsumerWidget { ], onChanged: (value) { if (value != null) { - preferences.setThemeMode(value); + preferencesNotifier.setThemeMode(value); } }, ), @@ -80,7 +81,7 @@ class SettingsAppearanceSection extends HookConsumerWidget { title: Text(context.l10n.use_amoled_mode), subtitle: Text(context.l10n.pitch_dark_theme), value: preferences.amoledDarkTheme, - onChanged: preferences.setAmoledDarkTheme, + onChanged: preferencesNotifier.setAmoledDarkTheme, ), ListTile( leading: const Icon(SpotubeIcons.palette), @@ -101,7 +102,7 @@ class SettingsAppearanceSection extends HookConsumerWidget { title: Text(context.l10n.sync_album_color), subtitle: Text(context.l10n.sync_album_color_description), value: preferences.albumColorSync, - onChanged: preferences.setAlbumColorSync, + onChanged: preferencesNotifier.setAlbumColorSync, ), ], ); diff --git a/lib/pages/settings/sections/desktop.dart b/lib/pages/settings/sections/desktop.dart index d12bcb41..1cc2c5c8 100644 --- a/lib/pages/settings/sections/desktop.dart +++ b/lib/pages/settings/sections/desktop.dart @@ -12,6 +12,7 @@ class SettingsDesktopSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final preferences = ref.watch(userPreferencesProvider); + final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); return SectionCardWithHeading( heading: context.l10n.desktop, @@ -32,7 +33,7 @@ class SettingsDesktopSection extends HookConsumerWidget { ], onChanged: (value) { if (value != null) { - preferences.setCloseBehavior(value); + preferencesNotifier.setCloseBehavior(value); } }, ), @@ -40,13 +41,13 @@ class SettingsDesktopSection extends HookConsumerWidget { secondary: const Icon(SpotubeIcons.tray), title: Text(context.l10n.show_tray_icon), value: preferences.showSystemTrayIcon, - onChanged: preferences.setShowSystemTrayIcon, + onChanged: preferencesNotifier.setShowSystemTrayIcon, ), SwitchListTile( secondary: const Icon(SpotubeIcons.window), title: Text(context.l10n.use_system_title_bar), value: preferences.systemTitleBar, - onChanged: preferences.setSystemTitleBar, + onChanged: preferencesNotifier.setSystemTitleBar, ), ], ); diff --git a/lib/pages/settings/sections/downloads.dart b/lib/pages/settings/sections/downloads.dart index 1f157037..ff64cdea 100644 --- a/lib/pages/settings/sections/downloads.dart +++ b/lib/pages/settings/sections/downloads.dart @@ -14,6 +14,7 @@ class SettingsDownloadsSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final preferences = ref.watch(userPreferencesProvider); final pickDownloadLocation = useCallback(() async { @@ -22,13 +23,13 @@ class SettingsDownloadsSection extends HookConsumerWidget { initialDirectory: preferences.downloadLocation, ); if (dirStr == null) return; - preferences.setDownloadLocation(dirStr); + preferencesNotifier.setDownloadLocation(dirStr); } else { String? dirStr = await getDirectoryPath( initialDirectory: preferences.downloadLocation, ); if (dirStr == null) return; - preferences.setDownloadLocation(dirStr); + preferencesNotifier.setDownloadLocation(dirStr); } }, [preferences.downloadLocation]); diff --git a/lib/pages/settings/sections/language_region.dart b/lib/pages/settings/sections/language_region.dart index 64c56224..ece28455 100644 --- a/lib/pages/settings/sections/language_region.dart +++ b/lib/pages/settings/sections/language_region.dart @@ -17,6 +17,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final preferences = ref.watch(userPreferencesProvider); + final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final mediaQuery = MediaQuery.of(context); return SectionCardWithHeading( @@ -26,7 +27,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget { value: preferences.locale, onChanged: (locale) { if (locale == null) return; - preferences.setLocale(locale); + preferencesNotifier.setLocale(locale); }, title: Text(context.l10n.language), secondary: const Icon(SpotubeIcons.language), @@ -57,7 +58,7 @@ class SettingsLanguageRegionSection extends HookConsumerWidget { value: preferences.recommendationMarket, onChanged: (value) { if (value == null) return; - preferences.setRecommendationMarket(value); + preferencesNotifier.setRecommendationMarket(value); }, options: spotifyMarkets .map( diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index cf7e33e9..39d9b7c2 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -18,6 +18,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final preferences = ref.watch(userPreferencesProvider); + final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final theme = Theme.of(context); return SectionCardWithHeading( @@ -39,7 +40,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { ], onChanged: (value) { if (value != null) { - preferences.setAudioQuality(value); + preferencesNotifier.setAudioQuality(value); } }, ), @@ -55,7 +56,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { .toList(), onChanged: (value) { if (value == null) return; - preferences.setYoutubeApiType(value); + preferencesNotifier.setYoutubeApiType(value); }, ), AnimatedSwitcher( @@ -113,7 +114,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { .toList(), onChanged: (value) { if (value != null) { - preferences.setPipedInstance(value); + preferencesNotifier.setPipedInstance(value); } }, ); @@ -141,7 +142,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { .toList(), onChanged: (value) { if (value == null) return; - preferences.setSearchMode(value); + preferencesNotifier.setSearchMode(value); }, ), ), @@ -155,7 +156,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { title: Text(context.l10n.skip_non_music), value: preferences.skipNonMusic, onChanged: (state) { - preferences.setSkipNonMusic(state); + preferencesNotifier.setSkipNonMusic(state); }, ), ), @@ -172,7 +173,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { secondary: const Icon(SpotubeIcons.normalize), title: Text(context.l10n.normalize_audio), value: preferences.normalizeAudio, - onChanged: preferences.setNormalizeAudio, + onChanged: preferencesNotifier.setNormalizeAudio, ), AdaptiveSelectTile( secondary: const Icon(SpotubeIcons.stream), @@ -190,7 +191,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { .toList(), onChanged: (value) { if (value == null) return; - preferences.setStreamMusicCodec(value); + preferencesNotifier.setStreamMusicCodec(value); }, ), AdaptiveSelectTile( @@ -209,7 +210,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { .toList(), onChanged: (value) { if (value == null) return; - preferences.setDownloadMusicCodec(value); + preferencesNotifier.setDownloadMusicCodec(value); }, ), ], diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 5b377a1f..baf245b4 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -20,7 +20,7 @@ class SettingsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final preferences = ref.watch(userPreferencesProvider); + final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); return SafeArea( bottom: false, @@ -49,7 +49,7 @@ class SettingsPage extends HookConsumerWidget { const SettingsAboutSection(), Center( child: FilledButton( - onPressed: preferences.reset, + onPressed: preferencesNotifier.reset, child: Text(context.l10n.restore_defaults), ), ), diff --git a/lib/provider/user_preferences_provider.dart b/lib/provider/user_preferences_provider.dart index 3355adb0..80c71de9 100644 --- a/lib/provider/user_preferences_provider.dart +++ b/lib/provider/user_preferences_provider.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -13,7 +12,7 @@ import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/utils/persisted_change_notifier.dart'; +import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/platform.dart'; import 'package:path/path.dart' as path; @@ -48,220 +47,103 @@ enum MusicCodec { const MusicCodec._(this.label); } -class UserPreferences extends PersistedChangeNotifier { - AudioQuality audioQuality; - bool albumColorSync; - bool amoledDarkTheme; - bool checkUpdate; - bool normalizeAudio; - bool showSystemTrayIcon; - bool skipNonMusic; - bool systemTitleBar; - CloseBehavior closeBehavior; - late SpotubeColor accentColorScheme; - LayoutMode layoutMode; - Locale locale; - Market recommendationMarket; - SearchMode searchMode; +class UserPreferences { + final AudioQuality audioQuality; + final bool albumColorSync; + final bool amoledDarkTheme; + final bool checkUpdate; + final bool normalizeAudio; + final bool showSystemTrayIcon; + final bool skipNonMusic; + final bool systemTitleBar; + final CloseBehavior closeBehavior; + final SpotubeColor accentColorScheme; + final LayoutMode layoutMode; + final Locale locale; + final Market recommendationMarket; + final SearchMode searchMode; String downloadLocation; - String pipedInstance; - ThemeMode themeMode; - YoutubeApiType youtubeApiType; - MusicCodec streamMusicCodec; - MusicCodec downloadMusicCodec; + final String pipedInstance; + final ThemeMode themeMode; + final YoutubeApiType youtubeApiType; + final MusicCodec streamMusicCodec; + final MusicCodec downloadMusicCodec; - final Ref ref; - - UserPreferences( - this.ref, { - this.recommendationMarket = Market.US, - this.themeMode = ThemeMode.system, - this.layoutMode = LayoutMode.adaptive, - this.albumColorSync = true, - this.checkUpdate = true, - this.audioQuality = AudioQuality.high, - this.downloadLocation = "", - this.closeBehavior = CloseBehavior.close, - this.showSystemTrayIcon = true, - this.locale = const Locale("system", "system"), - this.pipedInstance = "https://pipedapi.kavin.rocks", - this.searchMode = SearchMode.youtube, - this.skipNonMusic = true, - this.youtubeApiType = YoutubeApiType.youtube, - this.systemTitleBar = false, - this.amoledDarkTheme = false, - this.normalizeAudio = true, - this.streamMusicCodec = MusicCodec.weba, - this.downloadMusicCodec = MusicCodec.m4a, - SpotubeColor? accentColorScheme, - }) : super() { - this.accentColorScheme = - accentColorScheme ?? SpotubeColor(Colors.blue.value, name: "Blue"); - if (downloadLocation.isEmpty && !kIsWeb) { + UserPreferences({ + required AudioQuality? audioQuality, + required bool? albumColorSync, + required bool? amoledDarkTheme, + required bool? checkUpdate, + required bool? normalizeAudio, + required bool? showSystemTrayIcon, + required bool? skipNonMusic, + required bool? systemTitleBar, + required CloseBehavior? closeBehavior, + required SpotubeColor? accentColorScheme, + required LayoutMode? layoutMode, + required Locale? locale, + required Market? recommendationMarket, + required SearchMode? searchMode, + required String? downloadLocation, + required String? pipedInstance, + required ThemeMode? themeMode, + required YoutubeApiType? youtubeApiType, + required MusicCodec? streamMusicCodec, + required MusicCodec? downloadMusicCodec, + }) : accentColorScheme = + accentColorScheme ?? const SpotubeColor(0xFF2196F3, name: "Blue"), + albumColorSync = albumColorSync ?? true, + amoledDarkTheme = amoledDarkTheme ?? false, + audioQuality = audioQuality ?? AudioQuality.high, + checkUpdate = checkUpdate ?? true, + closeBehavior = closeBehavior ?? CloseBehavior.close, + downloadLocation = downloadLocation ?? "", + downloadMusicCodec = downloadMusicCodec ?? MusicCodec.m4a, + layoutMode = layoutMode ?? LayoutMode.adaptive, + locale = locale ?? const Locale("system", "system"), + normalizeAudio = normalizeAudio ?? true, + pipedInstance = pipedInstance ?? "https://pipedapi.kavin.rocks", + recommendationMarket = recommendationMarket ?? Market.US, + searchMode = searchMode ?? SearchMode.youtube, + showSystemTrayIcon = showSystemTrayIcon ?? true, + skipNonMusic = skipNonMusic ?? true, + streamMusicCodec = streamMusicCodec ?? MusicCodec.weba, + systemTitleBar = systemTitleBar ?? false, + themeMode = themeMode ?? ThemeMode.system, + youtubeApiType = youtubeApiType ?? YoutubeApiType.youtube { + if (downloadLocation == null) { _getDefaultDownloadDirectory().then( - (value) { - downloadLocation = value; - }, + (value) => this.downloadLocation = value, ); } } - void reset() { - setRecommendationMarket(Market.US); - setThemeMode(ThemeMode.system); - setLayoutMode(LayoutMode.adaptive); - setAlbumColorSync(true); - setCheckUpdate(true); - setAudioQuality(AudioQuality.high); - setDownloadLocation(""); - setCloseBehavior(CloseBehavior.close); - setShowSystemTrayIcon(true); - setLocale(const Locale("system", "system")); - setPipedInstance("https://pipedapi.kavin.rocks"); - setSearchMode(SearchMode.youtube); - setSkipNonMusic(true); - setYoutubeApiType(YoutubeApiType.youtube); - setSystemTitleBar(false); - setAmoledDarkTheme(false); - setNormalizeAudio(true); - setAccentColorScheme(SpotubeColor(Colors.blue.value, name: "Blue")); - setStreamMusicCodec(MusicCodec.weba); - setDownloadMusicCodec(MusicCodec.m4a); + factory UserPreferences.withDefaults() { + return UserPreferences( + audioQuality: null, + albumColorSync: null, + amoledDarkTheme: null, + checkUpdate: null, + normalizeAudio: null, + showSystemTrayIcon: null, + skipNonMusic: null, + systemTitleBar: null, + closeBehavior: null, + accentColorScheme: null, + layoutMode: null, + locale: null, + recommendationMarket: null, + searchMode: null, + downloadLocation: null, + pipedInstance: null, + themeMode: null, + youtubeApiType: null, + streamMusicCodec: null, + downloadMusicCodec: null, + ); } - void setStreamMusicCodec(MusicCodec codec) { - streamMusicCodec = codec; - notifyListeners(); - updatePersistence(); - } - - void setDownloadMusicCodec(MusicCodec codec) { - downloadMusicCodec = codec; - notifyListeners(); - updatePersistence(); - } - - void setThemeMode(ThemeMode mode) { - themeMode = mode; - notifyListeners(); - updatePersistence(); - } - - void setRecommendationMarket(Market country) { - recommendationMarket = country; - notifyListeners(); - updatePersistence(); - } - - void setAccentColorScheme(SpotubeColor color) { - accentColorScheme = color; - notifyListeners(); - updatePersistence(); - } - - void setAlbumColorSync(bool sync) { - albumColorSync = sync; - if (!sync) { - ref.read(paletteProvider.notifier).state = null; - } else { - ref.read(ProxyPlaylistNotifier.notifier).updatePalette(); - } - notifyListeners(); - updatePersistence(); - } - - void setCheckUpdate(bool check) { - checkUpdate = check; - notifyListeners(); - updatePersistence(); - } - - void setAudioQuality(AudioQuality quality) { - audioQuality = quality; - notifyListeners(); - updatePersistence(); - } - - void setDownloadLocation(String downloadDir) { - if (downloadDir.isEmpty) return; - downloadLocation = downloadDir; - notifyListeners(); - updatePersistence(); - } - - void setLayoutMode(LayoutMode mode) { - layoutMode = mode; - notifyListeners(); - updatePersistence(); - } - - void setCloseBehavior(CloseBehavior behavior) { - closeBehavior = behavior; - notifyListeners(); - updatePersistence(); - } - - void setShowSystemTrayIcon(bool show) { - showSystemTrayIcon = show; - notifyListeners(); - updatePersistence(); - } - - void setLocale(Locale locale) { - this.locale = locale; - notifyListeners(); - updatePersistence(); - } - - void setPipedInstance(String instance) { - pipedInstance = instance; - notifyListeners(); - updatePersistence(); - } - - void setSearchMode(SearchMode mode) { - searchMode = mode; - notifyListeners(); - updatePersistence(); - } - - void setSkipNonMusic(bool skip) { - skipNonMusic = skip; - notifyListeners(); - updatePersistence(); - } - - void setYoutubeApiType(YoutubeApiType type) { - youtubeApiType = type; - notifyListeners(); - updatePersistence(); - } - - void setSystemTitleBar(bool isSystemTitleBar) { - systemTitleBar = isSystemTitleBar; - if (DesktopTools.platform.isDesktop) { - DesktopTools.window.setTitleBarStyle( - systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, - ); - } - notifyListeners(); - updatePersistence(); - } - - void setAmoledDarkTheme(bool isAmoled) { - amoledDarkTheme = isAmoled; - notifyListeners(); - updatePersistence(); - } - - void setNormalizeAudio(bool normalize) { - normalizeAudio = normalize; - audioPlayer.setAudioNormalization(normalize); - notifyListeners(); - updatePersistence(); - } - - Future _getDefaultDownloadDirectory() async { + static Future _getDefaultDownloadDirectory() async { if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; if (kIsMacOS) { @@ -273,102 +155,71 @@ class UserPreferences extends PersistedChangeNotifier { }); } - @override - FutureOr loadFromLocal(Map map) async { - recommendationMarket = Market.values.firstWhere( - (market) => - market.name == (map["recommendationMarket"] ?? recommendationMarket), - orElse: () => Market.US, - ); - checkUpdate = map["checkUpdate"] ?? checkUpdate; + static Future fromJson(Map json) async { + final localeMap = + json["locale"] != null ? jsonDecode(json["locale"]) : null; - themeMode = ThemeMode.values[map["themeMode"] ?? 0]; - accentColorScheme = map["accentColorScheme"] != null - ? SpotubeColor.fromString(map["accentColorScheme"]) - : accentColorScheme; - albumColorSync = map["albumColorSync"] ?? albumColorSync; - audioQuality = map["audioQuality"] != null - ? AudioQuality.values[map["audioQuality"]] - : audioQuality; - - if (!kIsWeb) { - downloadLocation = - map["downloadLocation"] ?? await _getDefaultDownloadDirectory(); + final systemTitleBar = json["systemTitleBar"] ?? false; + if (DesktopTools.platform.isDesktop) { + await DesktopTools.window.setTitleBarStyle( + systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, + ); } - layoutMode = LayoutMode.values.firstWhere( - (mode) => mode.name == map["layoutMode"], - orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact, - ); - - closeBehavior = map["closeBehavior"] != null - ? CloseBehavior.values[map["closeBehavior"]] - : closeBehavior; - - showSystemTrayIcon = map["showSystemTrayIcon"] ?? showSystemTrayIcon; - - final localeMap = map["locale"] != null ? jsonDecode(map["locale"]) : null; - locale = - localeMap != null ? Locale(localeMap?["lc"], localeMap?["cc"]) : locale; - - pipedInstance = map["pipedInstance"] ?? pipedInstance; - - searchMode = SearchMode.values.firstWhere( - (mode) => mode.name == map["searchMode"], - orElse: () => SearchMode.youtube, - ); - - skipNonMusic = map["skipNonMusic"] ?? skipNonMusic; - - youtubeApiType = YoutubeApiType.values.firstWhere( - (type) => type.name == map["youtubeApiType"], - orElse: () => YoutubeApiType.youtube, - ); - - systemTitleBar = map["systemTitleBar"] ?? systemTitleBar; - // updates the title bar - setSystemTitleBar(systemTitleBar); - - amoledDarkTheme = map["amoledDarkTheme"] ?? amoledDarkTheme; - - normalizeAudio = map["normalizeAudio"] ?? normalizeAudio; + final normalizeAudio = json["normalizeAudio"] ?? true; audioPlayer.setAudioNormalization(normalizeAudio); - streamMusicCodec = MusicCodec.values.firstWhere( - (codec) => codec.name == map["streamMusicCodec"], - orElse: () => MusicCodec.weba, - ); - - downloadMusicCodec = MusicCodec.values.firstWhere( - (codec) => codec.name == map["downloadMusicCodec"], - orElse: () => MusicCodec.m4a, + return UserPreferences( + accentColorScheme: json["accentColorScheme"] == null + ? null + : SpotubeColor.fromString(json["accentColorScheme"]), + albumColorSync: json["albumColorSync"], + amoledDarkTheme: json["amoledDarkTheme"], + audioQuality: AudioQuality.values[json["audioQuality"]], + checkUpdate: json["checkUpdate"], + closeBehavior: CloseBehavior.values[json["closeBehavior"]], + downloadLocation: + json["downloadLocation"] ?? await _getDefaultDownloadDirectory(), + downloadMusicCodec: MusicCodec.values[json["downloadMusicCodec"]], + layoutMode: LayoutMode.values[json["layoutMode"]], + locale: + localeMap == null ? null : Locale(localeMap?["lc"], localeMap?["cc"]), + normalizeAudio: json["normalizeAudio"], + pipedInstance: json["pipedInstance"], + recommendationMarket: Market.values[json["recommendationMarket"]], + searchMode: SearchMode.values[json["searchMode"]], + showSystemTrayIcon: json["showSystemTrayIcon"], + skipNonMusic: json["skipNonMusic"], + streamMusicCodec: MusicCodec.values[json["streamMusicCodec"]], + systemTitleBar: json["systemTitleBar"], + themeMode: ThemeMode.values[json["themeMode"]], + youtubeApiType: YoutubeApiType.values[json["youtubeApiType"]], ); } - @override - FutureOr> toMap() { + Map toJson() { return { - "recommendationMarket": recommendationMarket.name, + "recommendationMarket": recommendationMarket.index, "themeMode": themeMode.index, "accentColorScheme": accentColorScheme.toString(), "albumColorSync": albumColorSync, "checkUpdate": checkUpdate, "audioQuality": audioQuality.index, "downloadLocation": downloadLocation, - "layoutMode": layoutMode.name, + "layoutMode": layoutMode.index, "closeBehavior": closeBehavior.index, "showSystemTrayIcon": showSystemTrayIcon, "locale": jsonEncode({"lc": locale.languageCode, "cc": locale.countryCode}), "pipedInstance": pipedInstance, - "searchMode": searchMode.name, + "searchMode": searchMode.index, "skipNonMusic": skipNonMusic, - "youtubeApiType": youtubeApiType.name, + "youtubeApiType": youtubeApiType.index, 'systemTitleBar': systemTitleBar, "amoledDarkTheme": amoledDarkTheme, "normalizeAudio": normalizeAudio, - "streamMusicCodec": streamMusicCodec.name, - "downloadMusicCodec": downloadMusicCodec.name, + "streamMusicCodec": streamMusicCodec.index, + "downloadMusicCodec": downloadMusicCodec.index, }; } @@ -389,9 +240,13 @@ class UserPreferences extends PersistedChangeNotifier { YoutubeApiType? youtubeApiType, Market? recommendationMarket, bool? saveTrackLyrics, + bool? amoledDarkTheme, + bool? normalizeAudio, + MusicCodec? downloadMusicCodec, + MusicCodec? streamMusicCodec, + bool? systemTitleBar, }) { return UserPreferences( - ref, themeMode: themeMode ?? this.themeMode, accentColorScheme: accentColorScheme ?? this.accentColorScheme, albumColorSync: albumColorSync ?? this.albumColorSync, @@ -407,10 +262,130 @@ class UserPreferences extends PersistedChangeNotifier { skipNonMusic: skipNonMusic ?? this.skipNonMusic, youtubeApiType: youtubeApiType ?? this.youtubeApiType, recommendationMarket: recommendationMarket ?? this.recommendationMarket, + amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, + downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, + normalizeAudio: normalizeAudio ?? this.normalizeAudio, + streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, + systemTitleBar: systemTitleBar ?? this.systemTitleBar, ); } } -final userPreferencesProvider = ChangeNotifierProvider( - (ref) => UserPreferences(ref), +class UserPreferencesNotifier extends PersistedStateNotifier { + final Ref ref; + + UserPreferencesNotifier(this.ref) + : super(UserPreferences.withDefaults(), "preferences"); + + void reset() { + state = UserPreferences.withDefaults(); + } + + void setStreamMusicCodec(MusicCodec codec) { + state = state.copyWith(streamMusicCodec: codec); + } + + void setDownloadMusicCodec(MusicCodec codec) { + state = state.copyWith(downloadMusicCodec: codec); + } + + void setThemeMode(ThemeMode mode) { + state = state.copyWith(themeMode: mode); + } + + void setRecommendationMarket(Market country) { + state = state.copyWith(recommendationMarket: country); + } + + void setAccentColorScheme(SpotubeColor color) { + state = state.copyWith(accentColorScheme: color); + } + + void setAlbumColorSync(bool sync) { + state = state.copyWith(albumColorSync: sync); + + if (!sync) { + ref.read(paletteProvider.notifier).state = null; + } else { + ref.read(ProxyPlaylistNotifier.notifier).updatePalette(); + } + } + + void setCheckUpdate(bool check) { + state = state.copyWith(checkUpdate: check); + } + + void setAudioQuality(AudioQuality quality) { + state = state.copyWith(audioQuality: quality); + } + + void setDownloadLocation(String downloadDir) { + if (downloadDir.isEmpty) return; + state = state.copyWith(downloadLocation: downloadDir); + } + + void setLayoutMode(LayoutMode mode) { + state = state.copyWith(layoutMode: mode); + } + + void setCloseBehavior(CloseBehavior behavior) { + state = state.copyWith(closeBehavior: behavior); + } + + void setShowSystemTrayIcon(bool show) { + state = state.copyWith(showSystemTrayIcon: show); + } + + void setLocale(Locale locale) { + state = state.copyWith(locale: locale); + } + + void setPipedInstance(String instance) { + state = state.copyWith(pipedInstance: instance); + } + + void setSearchMode(SearchMode mode) { + state = state.copyWith(searchMode: mode); + } + + void setSkipNonMusic(bool skip) { + state = state.copyWith(skipNonMusic: skip); + } + + void setYoutubeApiType(YoutubeApiType type) { + state = state.copyWith(youtubeApiType: type); + } + + void setSystemTitleBar(bool isSystemTitleBar) { + state = state.copyWith(systemTitleBar: isSystemTitleBar); + if (DesktopTools.platform.isDesktop) { + DesktopTools.window.setTitleBarStyle( + isSystemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, + ); + } + } + + void setAmoledDarkTheme(bool isAmoled) { + state = state.copyWith(amoledDarkTheme: isAmoled); + } + + void setNormalizeAudio(bool normalize) { + state = state.copyWith(normalizeAudio: normalize); + audioPlayer.setAudioNormalization(normalize); + } + + @override + FutureOr fromJson(Map json) { + return UserPreferences.fromJson(json); + } + + @override + Map toJson() { + return state.toJson(); + } +} + +final userPreferencesProvider = + StateNotifierProvider( + (ref) => UserPreferencesNotifier(ref), ); From 0a6b54da367345b73fe6e954f1d9368d9f9ead71 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 13 Nov 2023 21:46:33 +0600 Subject: [PATCH 5/6] fix: scrobbling not working for first track or single track --- .../proxy_playlist_provider.dart | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 685a9942..bf7293ce 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -213,6 +213,26 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier Catcher2.reportCheckedError(e, stackTrace); } }); + + String? lastScrobbled; + audioPlayer.positionStream.listen((position) { + try { + final uid = state.activeTrack is LocalTrack + ? (state.activeTrack as LocalTrack).path + : state.activeTrack?.id; + + if (state.activeTrack == null || + lastScrobbled == uid || + position.inSeconds < 30) { + return; + } + + scrobbler.scrobble(state.activeTrack!); + lastScrobbled = uid; + } catch (e, stack) { + Catcher2.reportCheckedError(e, stack); + } + }); }(); } @@ -609,30 +629,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier @override set state(state) { - final hasActiveTrackChanged = super.state.activeTrack is SpotubeTrack - ? state.activeTrack?.id != super.state.activeTrack?.id - : super.state.activeTrack is LocalTrack && - state.activeTrack is LocalTrack - ? (super.state.activeTrack as LocalTrack).path != - (state.activeTrack as LocalTrack).path - : super.state.activeTrack?.id != state.activeTrack?.id; - - final oldTrack = super.state.activeTrack; - super.state = state; if (state.tracks.isEmpty && ref.read(paletteProvider) != null) { ref.read(paletteProvider.notifier).state = null; } else { updatePalette(); } - audioPlayer.position.then((position) { - final isMoreThan30secs = position != null && - (position == Duration.zero || position.inSeconds > 30); - - if (hasActiveTrackChanged && oldTrack != null && isMoreThan30secs) { - scrobbler.scrobble(oldTrack); - } - }); } @override From 2e2c44f0afef69bf9bc485db97d45127a0847c8e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 13 Nov 2023 23:17:16 +0600 Subject: [PATCH 6/6] feat(android): better quick scroll/drag to scroll implementation --- lib/components/library/user_local_tracks.dart | 4 + lib/components/library/user_playlists.dart | 116 +++---- lib/components/player/player_queue.dart | 308 +++++++++--------- .../player/sibling_tracks_sheet.dart | 6 + .../inter_scrollbar/inter_scrollbar.dart | 54 +-- lib/pages/home/genres.dart | 39 +-- lib/pages/home/personalized.dart | 80 +++-- lib/pages/search/search.dart | 40 ++- lib/pages/settings/blacklist.dart | 3 + lib/pages/settings/logs.dart | 3 + lib/pages/settings/settings.dart | 4 + pubspec.lock | 9 + pubspec.yaml | 4 + 13 files changed, 330 insertions(+), 340 deletions(-) diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index c7cd0682..0546c2a7 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -163,6 +163,8 @@ class UserLocalTracks extends HookConsumerWidget { final searchFocus = useFocusNode(); final isFiltering = useState(false); + final controller = useScrollController(); + return Column( children: [ Padding( @@ -256,7 +258,9 @@ class UserLocalTracks extends HookConsumerWidget { ref.refresh(localTracksProvider); }, child: InterScrollbar( + controller: controller, child: ListView.builder( + controller: controller, physics: const AlwaysScrollableScrollPhysics(), itemCount: filteredTracks.length, itemBuilder: (context, index) { diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index ecf4fa12..0102a3c7 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -9,6 +9,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'; @@ -81,68 +82,71 @@ class UserPlaylists extends HookConsumerWidget { return RefreshIndicator( onRefresh: playlistsQuery.refresh, child: SafeArea( - child: CustomScrollView( + child: InterScrollbar( controller: controller, - slivers: [ - SliverToBoxAdapter( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: SearchBar( - onChanged: (value) => searchText.value = value, - hintText: context.l10n.filter_playlists, - leading: const Icon(SpotubeIcons.filter), - ), - ), - 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"); - }, + child: CustomScrollView( + controller: controller, + slivers: [ + SliverToBoxAdapter( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: SearchBar( + onChanged: (value) => searchText.value = value, + hintText: context.l10n.filter_playlists, + leading: const Icon(SpotubeIcons.filter), ), - const SizedBox(width: 10), - ], - ), - ], + ), + 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), + ], + ), + ], + ), ), - ), - const SliverToBoxAdapter( - child: SizedBox(height: 10), - ), - SliverGrid.builder( - itemCount: playlists.length + 1, - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - mainAxisExtent: DesktopTools.platform.isMobile ? 225 : 250, - crossAxisSpacing: 8, - mainAxisSpacing: 8, + const SliverToBoxAdapter( + child: SizedBox(height: 10), ), - itemBuilder: (context, index) { - if (index == playlists.length) { - if (!playlistsQuery.hasNextPage) { - return const SizedBox.shrink(); + SliverGrid.builder( + itemCount: playlists.length + 1, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: DesktopTools.platform.isMobile ? 225 : 250, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemBuilder: (context, index) { + if (index == playlists.length) { + if (!playlistsQuery.hasNextPage) { + return const SizedBox.shrink(); + } + + return Waypoint( + controller: controller, + isGrid: true, + onTouchEdge: playlistsQuery.fetchNext, + child: const ShimmerPlaybuttonCard(count: 1), + ); } - return Waypoint( - controller: controller, - isGrid: true, - onTouchEdge: playlistsQuery.fetchNext, - child: const ShimmerPlaybuttonCard(count: 1), - ); - } - - return PlaylistCard(playlists[index]); - }, - ) - ], + return PlaylistCard(playlists[index]); + }, + ) + ], + ), ), ), ); diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index 2d8ba329..9e303cb8 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -44,6 +44,7 @@ class PlayerQueue extends HookConsumerWidget { topRight: Radius.circular(10), ); final theme = Theme.of(context); + final mediaQuery = MediaQuery.of(context); final headlineColor = theme.textTheme.headlineSmall?.color; final filteredTracks = useMemoized( @@ -108,171 +109,166 @@ class PlayerQueue extends HookConsumerWidget { searchText.value = ''; } }, - child: LayoutBuilder(builder: (context, constraints) { - return Column( - children: [ - if (!floating) - Container( - height: 5, - width: 100, - margin: const EdgeInsets.only(bottom: 5, top: 2), - decoration: BoxDecoration( - color: headlineColor, - borderRadius: BorderRadius.circular(20), - ), + child: Column( + children: [ + if (!floating) + Container( + height: 5, + width: 100, + margin: const EdgeInsets.only(bottom: 5, top: 2), + decoration: BoxDecoration( + color: headlineColor, + borderRadius: BorderRadius.circular(20), ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (constraints.mdAndUp || !isSearching.value) ...[ - const SizedBox(width: 10), - Text( - context.l10n.tracks_in_queue(tracks.length), - style: TextStyle( - color: headlineColor, - fontWeight: FontWeight.bold, - fontSize: 18, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (mediaQuery.mdAndUp || !isSearching.value) ...[ + const SizedBox(width: 10), + Text( + context.l10n.tracks_in_queue(tracks.length), + style: TextStyle( + color: headlineColor, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const Spacer(), + ], + if (mediaQuery.mdAndUp || isSearching.value) + TextField( + onChanged: (value) { + searchText.value = value; + }, + decoration: InputDecoration( + hintText: context.l10n.search, + isDense: true, + prefixIcon: mediaQuery.smAndDown + ? IconButton( + icon: const Icon( + Icons.arrow_back_ios_new_outlined, + ), + onPressed: () { + isSearching.value = false; + searchText.value = ''; + }, + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size.square(20), + ), + ) + : const Icon(SpotubeIcons.filter), + constraints: BoxConstraints( + maxHeight: 40, + maxWidth: mediaQuery.smAndDown + ? mediaQuery.size.width - 40 + : 300, ), ), - const Spacer(), - ], - if (constraints.mdAndUp || isSearching.value) - TextField( - onChanged: (value) { - searchText.value = value; - }, - decoration: InputDecoration( - hintText: context.l10n.search, - isDense: true, - prefixIcon: constraints.smAndDown - ? IconButton( - icon: const Icon( - Icons.arrow_back_ios_new_outlined, - ), - onPressed: () { - isSearching.value = false; - searchText.value = ''; - }, - style: IconButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: const Size.square(20), - ), - ) - : const Icon(SpotubeIcons.filter), - constraints: BoxConstraints( - maxHeight: 40, - maxWidth: constraints.smAndDown - ? constraints.maxWidth - 20 - : 300, + ) + else + IconButton.filledTonal( + icon: const Icon(SpotubeIcons.filter), + onPressed: () { + isSearching.value = !isSearching.value; + }, + ), + if (mediaQuery.mdAndUp || !isSearching.value) ...[ + const SizedBox(width: 10), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: + theme.scaffoldBackgroundColor.withOpacity(0.5), + foregroundColor: theme.textTheme.headlineSmall?.color, + ), + child: Row( + children: [ + const Icon(SpotubeIcons.playlistRemove), + const SizedBox(width: 5), + Text(context.l10n.clear_all), + ], + ), + onPressed: () { + playlistNotifier.stop(); + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 10), + ], + ], + ), + 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( + 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 - IconButton.filledTonal( - icon: const Icon(SpotubeIcons.filter), - onPressed: () { - isSearching.value = !isSearching.value; - }, - ), - if (constraints.mdAndUp || !isSearching.value) ...[ - const SizedBox(width: 10), - FilledButton( - style: FilledButton.styleFrom( - backgroundColor: - theme.scaffoldBackgroundColor.withOpacity(0.5), - foregroundColor: - theme.textTheme.headlineSmall?.color, - ), - child: Row( - children: [ - const Icon(SpotubeIcons.playlistRemove), - const SizedBox(width: 5), - Text(context.l10n.clear_all), - ], - ), - onPressed: () { - playlistNotifier.stop(); - Navigator.of(context).pop(); - }, - ), - const SizedBox(width: 10), - ], - ], - ), - const SizedBox(height: 10), - if (!isSearching.value && searchText.value.isEmpty) - Flexible( - child: InterScrollbar( + ); + }, + ), + ) + else + Flexible( + child: InterScrollbar( + controller: controller, + child: ListView.builder( 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, + itemCount: filteredTracks.length, + itemBuilder: (context, i) { + final track = filteredTracks.elementAt(i); + return Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: TrackTile( 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( - index: i, - track: track, - onTap: () async { - if (playlist.activeTrack?.id == track.id) { - return; - } - await playlistNotifier.jumpToTrack(track); - }, - ), - ); - }, - ), + track: track, + onTap: () async { + if (playlist.activeTrack?.id == track.id) { + return; + } + await playlistNotifier.jumpToTrack(track); + }, + ), + ); + }, ), ), - ], - ); - }), + ), + ], + ), ), ), ), diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index 14c042b8..8dc41026 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -56,6 +56,8 @@ class SiblingTracksSheet extends HookConsumerWidget { useValueListenable(searchController).text, ); + final controller = useScrollController(); + final searchRequest = useMemoized(() async { if (searchTerm.trim().isEmpty) { return []; @@ -204,8 +206,10 @@ class SiblingTracksSheet extends HookConsumerWidget { transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), child: InterScrollbar( + controller: controller, child: switch (isSearching.value) { false => ListView.builder( + controller: controller, itemCount: siblings.length, itemBuilder: (context, index) => itemBuilder(siblings[index]), @@ -223,7 +227,9 @@ class SiblingTracksSheet extends HookConsumerWidget { } return InterScrollbar( + controller: controller, child: ListView.builder( + controller: controller, itemCount: snapshot.data!.length, itemBuilder: (context, index) => itemBuilder(snapshot.data![index]), diff --git a/lib/components/shared/inter_scrollbar/inter_scrollbar.dart b/lib/components/shared/inter_scrollbar/inter_scrollbar.dart index 05eb174a..11f75829 100644 --- a/lib/components/shared/inter_scrollbar/inter_scrollbar.dart +++ b/lib/components/shared/inter_scrollbar/inter_scrollbar.dart @@ -1,29 +1,16 @@ +import 'package:draggable_scrollbar/draggable_scrollbar.dart'; 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; + final ScrollController controller; const InterScrollbar({ super.key, required this.child, - this.controller, - this.thumbVisibility, - this.trackVisibility, - this.thickness, - this.radius, - this.notificationPredicate, - this.interactive, - this.scrollbarOrientation, + required this.controller, }); @override @@ -32,38 +19,9 @@ class InterScrollbar extends HookWidget { 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, - ), + return DraggableScrollbar.semicircle( + controller: controller, + child: child, ); } } diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index 076305f2..6861853d 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -79,28 +79,25 @@ class GenrePage extends HookConsumerWidget { const ShimmerCategories() else Expanded( - child: InterScrollbar( + child: ListView.builder( controller: scrollController, - child: ListView.builder( - controller: scrollController, - itemCount: categories.length, - itemBuilder: (context, index) { - return AnimatedSwitcher( - transitionBuilder: (child, animation) { - return FadeTransition( - opacity: animation, - child: child, - ); - }, - duration: const Duration(milliseconds: 300), - child: searchController.text.isEmpty && - index == categories.length - 1 && - categoriesQuery.hasNextPage - ? const ShimmerCategories() - : CategoryCard(categories[index]), - ); - }, - ), + itemCount: categories.length, + itemBuilder: (context, index) { + return AnimatedSwitcher( + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + duration: const Duration(milliseconds: 300), + child: searchController.text.isEmpty && + index == categories.length - 1 && + categoriesQuery.hasNextPage + ? const ShimmerCategories() + : CategoryCard(categories[index]), + ); + }, ), ), ], diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index 8a18fd0b..b596a820 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; -import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; @@ -47,48 +46,45 @@ class PersonalizedPage extends HookConsumerWidget { [newReleases.pages], ); - return InterScrollbar( + return ListView( controller: controller, - child: ListView( - controller: controller, - children: [ - if (!featuredPlaylistsQuery.hasPageData && - !featuredPlaylistsQuery.isLoadingNextPage) - const ShimmerCategories() - else - HorizontalPlaybuttonCardView( - items: playlists.toList(), - title: Text(context.l10n.featured), - hasNextPage: featuredPlaylistsQuery.hasNextPage, - onFetchMore: featuredPlaylistsQuery.fetchNext, - ), - if (auth != null && - newReleases.hasPageData && - userArtistsQuery.hasData && - !newReleases.isLoadingNextPage) - HorizontalPlaybuttonCardView( - items: albums, - title: Text(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 HorizontalPlaybuttonCardView( - items: playlists, - title: Text(item["name"] ?? ""), - hasNextPage: false, - onFetchMore: () {}, - ); - }) - ], - ), + children: [ + if (!featuredPlaylistsQuery.hasPageData && + !featuredPlaylistsQuery.isLoadingNextPage) + const ShimmerCategories() + else + HorizontalPlaybuttonCardView( + items: playlists.toList(), + title: Text(context.l10n.featured), + hasNextPage: featuredPlaylistsQuery.hasNextPage, + onFetchMore: featuredPlaylistsQuery.fetchNext, + ), + if (auth != null && + newReleases.hasPageData && + userArtistsQuery.hasData && + !newReleases.isLoadingNextPage) + HorizontalPlaybuttonCardView( + items: albums, + title: Text(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 HorizontalPlaybuttonCardView( + items: playlists, + title: Text(item["name"] ?? ""), + hasNextPage: false, + onFetchMore: () {}, + ); + }) + ], ); } } diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index d659e8e3..b19162fa 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -71,26 +71,32 @@ class SearchPage extends HookConsumerWidget { searchTerm.isNotEmpty; final resultWidget = HookBuilder( - builder: (context) => InterScrollbar( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SearchTracksSection(query: searchTrack), - SearchPlaylistsSection(query: searchPlaylist), - const SizedBox(height: 20), - SearchArtistsSection(query: searchArtist), - const SizedBox(height: 20), - SearchAlbumsSection(query: searchAlbum), - ], + builder: (context) { + final controller = useScrollController(); + + return InterScrollbar( + controller: controller, + child: SingleChildScrollView( + controller: controller, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SearchTracksSection(query: searchTrack), + SearchPlaylistsSection(query: searchPlaylist), + const SizedBox(height: 20), + SearchArtistsSection(query: searchArtist), + const SizedBox(height: 20), + SearchAlbumsSection(query: searchAlbum), + ], + ), ), ), ), - ), - ), + ); + }, ); return SafeArea( diff --git a/lib/pages/settings/blacklist.dart b/lib/pages/settings/blacklist.dart index 69800633..b4ce5044 100644 --- a/lib/pages/settings/blacklist.dart +++ b/lib/pages/settings/blacklist.dart @@ -15,6 +15,7 @@ class BlackListPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final controller = useScrollController(); final blacklist = ref.watch(BlackListNotifier.provider); final searchText = useState(""); @@ -58,7 +59,9 @@ class BlackListPage extends HookConsumerWidget { ), ), InterScrollbar( + controller: controller, child: ListView.builder( + controller: controller, shrinkWrap: true, itemCount: filteredBlacklist.length, itemBuilder: (context, index) { diff --git a/lib/pages/settings/logs.dart b/lib/pages/settings/logs.dart index 91d87fbb..cfb28d18 100644 --- a/lib/pages/settings/logs.dart +++ b/lib/pages/settings/logs.dart @@ -52,6 +52,7 @@ class LogsPage extends HookWidget { @override Widget build(BuildContext context) { + final controller = useScrollController(); final logs = useState>([]); final rawLogs = useRef(""); final path = useRef(null); @@ -93,7 +94,9 @@ class LogsPage extends HookWidget { ), body: SafeArea( child: InterScrollbar( + controller: controller, child: ListView.builder( + controller: controller, itemCount: logs.value.length, itemBuilder: (context, index) { final log = logs.value[index]; diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index baf245b4..84b51d4d 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; @@ -20,6 +21,7 @@ class SettingsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final controller = useScrollController(); final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); return SafeArea( @@ -36,7 +38,9 @@ class SettingsPage extends HookConsumerWidget { child: Container( constraints: const BoxConstraints(maxWidth: 1366), child: InterScrollbar( + controller: controller, child: ListView( + controller: controller, children: [ const SettingsAccountSection(), const SettingsLanguageRegionSection(), diff --git a/pubspec.lock b/pubspec.lock index 9c0161c6..3d072e09 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -465,6 +465,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + draggable_scrollbar: + dependency: "direct main" + description: + path: "." + ref: cfd570035bf393de541d32e9b28808b5d7e602df + resolved-ref: cfd570035bf393de541d32e9b28808b5d7e602df + url: "https://github.com/thielepaul/flutter-draggable-scrollbar.git" + source: git + version: "0.1.0" duration: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 64b2b6a3..f9c1155f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -106,6 +106,10 @@ dependencies: simple_icons: ^7.10.0 audio_service_mpris: ^0.1.0 file_picker: ^6.0.0 + draggable_scrollbar: + git: + url: https://github.com/thielepaul/flutter-draggable-scrollbar.git + ref: cfd570035bf393de541d32e9b28808b5d7e602df dev_dependencies: build_runner: ^2.3.2