From c7817909ce0920fac6d70706bc596e6fa3039a09 Mon Sep 17 00:00:00 2001
From: Amin <23167933+aminsaedi@users.noreply.github.com>
Date: Sun, 29 Oct 2023 05:56:48 -0400
Subject: [PATCH 001/195] Updated README.md file (#847)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 71589794..d82af783 100644
--- a/README.md
+++ b/README.md
@@ -108,7 +108,7 @@ This handy table lists all methods you can use to install Spotube:
-
Then run: sudo apt install Spotube-linux-x86_64.deb
+ Then run: sudo apt install ./Spotube-linux-x86_64.deb
From ac0e2e74d803a902b0abef94f674f68ffcd81fd3 Mon Sep 17 00:00:00 2001
From: Kingkor Roy Tirtho
Date: Wed, 8 Nov 2023 11:43:17 +0600
Subject: [PATCH 002/195] refactor: extract settings section to separate files
---
lib/pages/home/genres.dart | 3 +-
lib/pages/settings/sections/about.dart | 84 +++
lib/pages/settings/sections/appearance.dart | 109 ++++
lib/pages/settings/sections/desktop.dart | 54 ++
lib/pages/settings/sections/developers.dart | 27 +
lib/pages/settings/sections/downloads.dart | 51 ++
.../settings/sections/language_region.dart | 74 +++
lib/pages/settings/sections/playback.dart | 218 +++++++
lib/pages/settings/settings.dart | 541 +-----------------
lib/utils/persisted_state_notifier.dart | 36 +-
pubspec.lock | 4 +-
11 files changed, 653 insertions(+), 548 deletions(-)
create mode 100644 lib/pages/settings/sections/about.dart
create mode 100644 lib/pages/settings/sections/appearance.dart
create mode 100644 lib/pages/settings/sections/desktop.dart
create mode 100644 lib/pages/settings/sections/developers.dart
create mode 100644 lib/pages/settings/sections/downloads.dart
create mode 100644 lib/pages/settings/sections/language_region.dart
create mode 100644 lib/pages/settings/sections/playback.dart
diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart
index b4e3c664..db1c58c5 100644
--- a/lib/pages/home/genres.dart
+++ b/lib/pages/home/genres.dart
@@ -74,7 +74,8 @@ class GenrePage extends HookConsumerWidget {
searchController: searchController,
searchFocus: searchFocus,
),
- if (!categoriesQuery.hasPageData)
+ if (!categoriesQuery.hasPageData &&
+ !categoriesQuery.isLoadingNextPage)
const ShimmerCategories()
else
Expanded(
diff --git a/lib/pages/settings/sections/about.dart b/lib/pages/settings/sections/about.dart
new file mode 100644
index 00000000..0340b27c
--- /dev/null
+++ b/lib/pages/settings/sections/about.dart
@@ -0,0 +1,84 @@
+import 'package:auto_size_text/auto_size_text.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotube/collections/env.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/components/settings/section_card_with_heading.dart';
+import 'package:spotube/components/shared/adaptive/adaptive_list_tile.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/provider/user_preferences_provider.dart';
+import 'package:url_launcher/url_launcher_string.dart';
+
+class SettingsAboutSection extends HookConsumerWidget {
+ const SettingsAboutSection({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final preferences = ref.watch(userPreferencesProvider);
+
+ return 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/about");
+ },
+ )
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/sections/appearance.dart b/lib/pages/settings/sections/appearance.dart
new file mode 100644
index 00000000..f4b097e8
--- /dev/null
+++ b/lib/pages/settings/sections/appearance.dart
@@ -0,0 +1,109 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+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_select_tile.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/provider/user_preferences_provider.dart';
+
+class SettingsAppearanceSection extends HookConsumerWidget {
+ const SettingsAppearanceSection({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final preferences = ref.watch(userPreferencesProvider);
+ final pickColorScheme = useCallback(() {
+ return () => showDialog(
+ context: context,
+ builder: (context) {
+ return const ColorSchemePickerDialog();
+ });
+ }, []);
+
+ return 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,
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/sections/desktop.dart b/lib/pages/settings/sections/desktop.dart
new file mode 100644
index 00000000..d12bcb41
--- /dev/null
+++ b/lib/pages/settings/sections/desktop.dart
@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/components/settings/section_card_with_heading.dart';
+import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/provider/user_preferences_provider.dart';
+
+class SettingsDesktopSection extends HookConsumerWidget {
+ const SettingsDesktopSection({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final preferences = ref.watch(userPreferencesProvider);
+
+ return 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,
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/sections/developers.dart b/lib/pages/settings/sections/developers.dart
new file mode 100644
index 00000000..4b5f58a6
--- /dev/null
+++ b/lib/pages/settings/sections/developers.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:go_router/go_router.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/components/settings/section_card_with_heading.dart';
+import 'package:spotube/extensions/context.dart';
+
+class SettingsDevelopersSection extends HookWidget {
+ const SettingsDevelopersSection({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return 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");
+ },
+ )
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/sections/downloads.dart b/lib/pages/settings/sections/downloads.dart
new file mode 100644
index 00000000..1f157037
--- /dev/null
+++ b/lib/pages/settings/sections/downloads.dart
@@ -0,0 +1,51 @@
+import 'package:file_picker/file_picker.dart';
+import 'package:file_selector/file_selector.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/collections/spotube_icons.dart';
+import 'package:spotube/components/settings/section_card_with_heading.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/provider/user_preferences_provider.dart';
+
+class SettingsDownloadsSection extends HookConsumerWidget {
+ const SettingsDownloadsSection({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final preferences = ref.watch(userPreferencesProvider);
+
+ final pickDownloadLocation = useCallback(() async {
+ if (DesktopTools.platform.isMobile) {
+ final dirStr = await FilePicker.platform.getDirectoryPath(
+ initialDirectory: preferences.downloadLocation,
+ );
+ if (dirStr == null) return;
+ preferences.setDownloadLocation(dirStr);
+ } else {
+ String? dirStr = await getDirectoryPath(
+ initialDirectory: preferences.downloadLocation,
+ );
+ if (dirStr == null) return;
+ preferences.setDownloadLocation(dirStr);
+ }
+ }, [preferences.downloadLocation]);
+
+ return 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,
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/sections/language_region.dart b/lib/pages/settings/sections/language_region.dart
new file mode 100644
index 00000000..64c56224
--- /dev/null
+++ b/lib/pages/settings/sections/language_region.dart
@@ -0,0 +1,74 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotify/spotify.dart';
+import 'package:spotube/collections/language_codes.dart';
+import 'package:spotube/collections/spotify_markets.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/components/settings/section_card_with_heading.dart';
+import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart';
+import 'package:spotube/extensions/constrains.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/l10n/l10n.dart';
+import 'package:spotube/provider/user_preferences_provider.dart';
+
+class SettingsLanguageRegionSection extends HookConsumerWidget {
+ const SettingsLanguageRegionSection({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final preferences = ref.watch(userPreferencesProvider);
+ final mediaQuery = MediaQuery.of(context);
+
+ return 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(),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart
new file mode 100644
index 00000000..cf7e33e9
--- /dev/null
+++ b/lib/pages/settings/sections/playback.dart
@@ -0,0 +1,218 @@
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:piped_client/piped_client.dart';
+import 'package:spotube/collections/spotube_icons.dart';
+import 'package:spotube/components/settings/section_card_with_heading.dart';
+import 'package:spotube/components/shared/adaptive/adaptive_select_tile.dart';
+import 'package:spotube/extensions/context.dart';
+import 'package:spotube/models/matched_track.dart';
+import 'package:spotube/provider/piped_instances_provider.dart';
+import 'package:spotube/provider/user_preferences_provider.dart';
+
+class SettingsPlaybackSection extends HookConsumerWidget {
+ const SettingsPlaybackSection({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final preferences = ref.watch(userPreferencesProvider);
+ final theme = Theme.of(context);
+
+ return 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),
+ 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);
+ },
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart
index 5632a89a..5b377a1f 100644
--- a/lib/pages/settings/settings.dart
+++ b/lib/pages/settings/settings.dart
@@ -1,34 +1,19 @@
-import 'package:auto_size_text/auto_size_text.dart';
-import 'package:collection/collection.dart';
-import 'package:file_picker/file_picker.dart';
-import 'package:file_selector/file_selector.dart';
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:go_router/go_router.dart';
-import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:piped_client/piped_client.dart';
-import 'package:spotify/spotify.dart';
-import 'package:spotube/collections/env.dart';
-import 'package:spotube/collections/language_codes.dart';
-import 'package:spotube/collections/spotube_icons.dart';
-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';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/l10n/l10n.dart';
-import 'package:spotube/models/matched_track.dart';
+import 'package:spotube/pages/settings/sections/about.dart';
import 'package:spotube/pages/settings/sections/accounts.dart';
+import 'package:spotube/pages/settings/sections/appearance.dart';
+import 'package:spotube/pages/settings/sections/desktop.dart';
+import 'package:spotube/pages/settings/sections/developers.dart';
+import 'package:spotube/pages/settings/sections/downloads.dart';
+import 'package:spotube/pages/settings/sections/language_region.dart';
+import 'package:spotube/pages/settings/sections/playback.dart';
import 'package:spotube/provider/user_preferences_provider.dart';
-import 'package:spotube/provider/piped_instances_provider.dart';
-import 'package:url_launcher/url_launcher_string.dart';
class SettingsPage extends HookConsumerWidget {
const SettingsPage({Key? key}) : super(key: key);
@@ -36,32 +21,6 @@ class SettingsPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider);
- final theme = Theme.of(context);
- final mediaQuery = MediaQuery.of(context);
-
- final pickColorScheme = useCallback(() {
- return () => showDialog(
- context: context,
- builder: (context) {
- return const ColorSchemePickerDialog();
- });
- }, []);
-
- final pickDownloadLocation = useCallback(() async {
- if (DesktopTools.platform.isMobile) {
- final dirStr = await FilePicker.platform.getDirectoryPath(
- initialDirectory: preferences.downloadLocation,
- );
- if (dirStr == null) return;
- preferences.setDownloadLocation(dirStr);
- } else {
- String? dirStr = await getDirectoryPath(
- initialDirectory: preferences.downloadLocation,
- );
- if (dirStr == null) return;
- preferences.setDownloadLocation(dirStr);
- }
- }, [preferences.downloadLocation]);
return SafeArea(
bottom: false,
@@ -80,486 +39,14 @@ class SettingsPage extends HookConsumerWidget {
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),
- 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,
- ),
- ],
- ),
+ const SettingsLanguageRegionSection(),
+ const SettingsAppearanceSection(),
+ const SettingsPlaybackSection(),
+ const SettingsDownloadsSection(),
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/about");
- },
- )
- ],
- ),
+ const SettingsDesktopSection(),
+ if (!kIsWeb) const SettingsDevelopersSection(),
+ const SettingsAboutSection(),
Center(
child: FilledButton(
onPressed: preferences.reset,
diff --git a/lib/utils/persisted_state_notifier.dart b/lib/utils/persisted_state_notifier.dart
index 2937bff9..218cd64a 100644
--- a/lib/utils/persisted_state_notifier.dart
+++ b/lib/utils/persisted_state_notifier.dart
@@ -59,32 +59,32 @@ abstract class PersistedStateNotifier extends StateNotifier {
static Future read(String key) async {
final localStorage = await SharedPreferences.getInstance();
- if (kIsMacOS || kIsIOS) {
+ if (kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak)) {
+ return localStorage.getString(key);
+ }
+
+ try {
+ await localStorage.setBool(kIsUsingEncryption, true);
+ return await secureStorage.read(key: key);
+ } catch (e) {
+ await localStorage.setBool(kIsUsingEncryption, false);
return localStorage.getString(key);
- } else {
- try {
- await localStorage.setBool(kIsUsingEncryption, true);
- return await secureStorage.read(key: key);
- } catch (e) {
- await localStorage.setBool(kIsUsingEncryption, false);
- return localStorage.getString(key);
- }
}
}
static Future write(String key, String value) async {
final localStorage = await SharedPreferences.getInstance();
- if (kIsMacOS || kIsIOS) {
+ if (kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak)) {
await localStorage.setString(key, value);
return;
- } else {
- try {
- await localStorage.setBool(kIsUsingEncryption, true);
- await secureStorage.write(key: key, value: value);
- } catch (e) {
- await localStorage.setBool(kIsUsingEncryption, false);
- await localStorage.setString(key, value);
- }
+ }
+
+ try {
+ await localStorage.setBool(kIsUsingEncryption, true);
+ await secureStorage.write(key: key, value: value);
+ } catch (e) {
+ await localStorage.setBool(kIsUsingEncryption, false);
+ await localStorage.setString(key, value);
}
}
diff --git a/pubspec.lock b/pubspec.lock
index 15a50f41..9c0161c6 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -517,10 +517,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
- sha256: "903dd4ba13eae7cef64acc480e91bf54c3ddd23b5b90b639c170f3911e489620"
+ sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6"
url: "https://pub.dev"
source: hosted
- version: "6.0.0"
+ version: "6.1.1"
file_selector:
dependency: "direct main"
description:
From 574406dd5fc410914b27e7fce374323696845012 Mon Sep 17 00:00:00 2001
From: Kingkor Roy Tirtho
Date: Wed, 8 Nov 2023 12:11:06 +0600
Subject: [PATCH 003/195] fix(playbutton_card): annoying animation
---
lib/components/shared/playbutton_card.dart | 245 +++++++++---------
.../proxy_playlist/next_fetcher_mixin.dart | 1 -
2 files changed, 122 insertions(+), 124 deletions(-)
diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart
index c9daa267..91c185c7 100644
--- a/lib/components/shared/playbutton_card.dart
+++ b/lib/components/shared/playbutton_card.dart
@@ -8,7 +8,6 @@ import 'package:spotube/components/shared/hover_builder.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/hooks/use_brightness_value.dart';
-import 'package:spotube/utils/platform.dart';
final htmlTagRegexp = RegExp(r"<[^>]*>", caseSensitive: true);
@@ -59,9 +58,9 @@ class PlaybuttonCard extends HookWidget {
);
final end = useBreakpointValue(
- xs: 15,
- sm: 15,
- others: 20,
+ xs: 10,
+ sm: 10,
+ others: 15,
);
final textsHeight = useState(
@@ -84,28 +83,29 @@ class PlaybuttonCard extends HookWidget {
return null;
}, [textsKey]);
- return Stack(
- children: [
- Container(
- constraints: BoxConstraints(maxWidth: size),
- margin: margin,
- child: Material(
- color: Color.lerp(
- theme.colorScheme.surfaceVariant,
- theme.colorScheme.surface,
- useBrightnessValue(.9, .7),
- ),
- borderRadius: radius,
- shadowColor: theme.colorScheme.background,
- elevation: 3,
- child: InkWell(
- mouseCursor: SystemMouseCursors.click,
- onTap: onTap,
- borderRadius: radius,
- splashFactory: theme.splashFactory,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
+ return Container(
+ constraints: BoxConstraints(maxWidth: size),
+ margin: margin,
+ child: Material(
+ color: Color.lerp(
+ theme.colorScheme.surfaceVariant,
+ theme.colorScheme.surface,
+ useBrightnessValue(.9, .7),
+ ),
+ borderRadius: radius,
+ shadowColor: theme.colorScheme.background,
+ elevation: 3,
+ child: InkWell(
+ mouseCursor: SystemMouseCursors.click,
+ onTap: onTap,
+ borderRadius: radius,
+ splashFactory: theme.splashFactory,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Stack(
+ clipBehavior: Clip.none,
children: [
Padding(
padding: const EdgeInsets.only(
@@ -121,115 +121,114 @@ class PlaybuttonCard extends HookWidget {
),
),
),
- Column(
- key: textsKey,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const SizedBox(height: 15),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12.0),
- child: AutoSizeText(
- title,
- maxLines: 1,
- minFontSize: theme.textTheme.bodyMedium!.fontSize!,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- if (cleanDescription != null)
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12.0),
- child: AutoSizeText(
- cleanDescription,
- maxLines: 2,
- style: theme.textTheme.bodySmall?.copyWith(
- color:
- theme.colorScheme.onSurface.withOpacity(.5),
+ if (isOwner)
+ Positioned(
+ top: 15,
+ left: 15,
+ child: AnimatedSize(
+ duration: const Duration(milliseconds: 150),
+ alignment: Alignment.centerLeft,
+ curve: Curves.easeInExpo,
+ child: HoverBuilder(builder: (context, isHovered) {
+ return Container(
+ padding: const EdgeInsets.all(4),
+ decoration: BoxDecoration(
+ color: Colors.blueAccent,
+ borderRadius: BorderRadius.circular(20),
),
- overflow: TextOverflow.ellipsis,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(
+ SpotubeIcons.user,
+ color: Colors.white,
+ size: 16,
+ ),
+ if (isHovered)
+ Text(
+ "Owned by you",
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: Colors.white,
+ ),
+ ),
+ ],
+ ),
+ );
+ }),
+ ),
+ ),
+ Positioned(
+ right: end,
+ bottom: -15,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (!isPlaying)
+ IconButton(
+ style: IconButton.styleFrom(
+ backgroundColor: theme.colorScheme.background,
+ foregroundColor: theme.colorScheme.primary,
+ minimumSize: const Size.square(10),
+ ),
+ icon: const Icon(SpotubeIcons.queueAdd),
+ onPressed: isLoading ? null : onAddToQueuePressed,
),
+ const SizedBox(height: 5),
+ IconButton(
+ style: IconButton.styleFrom(
+ backgroundColor: theme.colorScheme.primaryContainer,
+ foregroundColor: theme.colorScheme.primary,
+ minimumSize: const Size.square(10),
+ ),
+ icon: isLoading
+ ? SizedBox.fromSize(
+ size: const Size.square(15),
+ child: const CircularProgressIndicator(
+ strokeWidth: 2),
+ )
+ : isPlaying
+ ? const Icon(SpotubeIcons.pause)
+ : const Icon(SpotubeIcons.play),
+ onPressed: isLoading ? null : onPlaybuttonPressed,
),
- const SizedBox(height: 10),
- ],
+ ],
+ ),
),
],
),
- ),
- ),
- ),
- if (isOwner)
- Positioned(
- top: 15,
- left: 25,
- child: AnimatedSize(
- duration: const Duration(milliseconds: 150),
- alignment: Alignment.centerLeft,
- curve: Curves.easeInExpo,
- child: HoverBuilder(builder: (context, isHovered) {
- return Container(
- padding: const EdgeInsets.all(4),
- decoration: BoxDecoration(
- color: Colors.blueAccent,
- borderRadius: BorderRadius.circular(20),
+ Column(
+ key: textsKey,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 15),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12.0),
+ child: AutoSizeText(
+ title,
+ maxLines: 1,
+ minFontSize: theme.textTheme.bodyMedium!.fontSize!,
+ overflow: TextOverflow.ellipsis,
+ ),
),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- const Icon(
- SpotubeIcons.user,
- color: Colors.white,
- size: 16,
- ),
- if (isHovered)
- Text(
- "Owned by you",
- style: theme.textTheme.bodySmall?.copyWith(
- color: Colors.white,
- ),
+ if (cleanDescription != null)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12.0),
+ child: AutoSizeText(
+ cleanDescription,
+ maxLines: 2,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurface.withOpacity(.5),
),
- ],
- ),
- );
- }),
- ),
- ),
- AnimatedPositioned(
- duration: const Duration(milliseconds: 300),
- right: end,
- bottom: textsHeight.value - (kIsMobile ? 5 : 10),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (!isPlaying)
- IconButton(
- style: IconButton.styleFrom(
- backgroundColor: theme.colorScheme.background,
- foregroundColor: theme.colorScheme.primary,
- minimumSize: const Size.square(10),
- ),
- icon: const Icon(SpotubeIcons.queueAdd),
- onPressed: isLoading ? null : onAddToQueuePressed,
- ),
- const SizedBox(height: 5),
- IconButton(
- style: IconButton.styleFrom(
- backgroundColor: theme.colorScheme.primaryContainer,
- foregroundColor: theme.colorScheme.primary,
- minimumSize: const Size.square(10),
- ),
- icon: isLoading
- ? SizedBox.fromSize(
- size: const Size.square(15),
- child: const CircularProgressIndicator(strokeWidth: 2),
- )
- : isPlaying
- ? const Icon(SpotubeIcons.pause)
- : const Icon(SpotubeIcons.play),
- onPressed: isLoading ? null : onPlaybuttonPressed,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ const SizedBox(height: 10),
+ ],
),
],
),
),
- ],
+ ),
);
}
}
diff --git a/lib/provider/proxy_playlist/next_fetcher_mixin.dart b/lib/provider/proxy_playlist/next_fetcher_mixin.dart
index 61b86d8c..f6776234 100644
--- a/lib/provider/proxy_playlist/next_fetcher_mixin.dart
+++ b/lib/provider/proxy_playlist/next_fetcher_mixin.dart
@@ -1,5 +1,4 @@
import 'package:collection/collection.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/models/local_track.dart';
From 1d77556157d158600f29cf2ea5f26c567607dec7 Mon Sep 17 00:00:00 2001
From: Kingkor Roy Tirtho
Date: Wed, 8 Nov 2023 12:26:27 +0600
Subject: [PATCH 004/195] fix: check for unsynced lyrics and error handling for
timed lyrics query
---
lib/pages/lyrics/synced_lyrics.dart | 49 ++++++++++++++++++++++++++---
1 file changed, 45 insertions(+), 4 deletions(-)
diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart
index dab2103d..5f2afbc9 100644
--- a/lib/pages/lyrics/synced_lyrics.dart
+++ b/lib/pages/lyrics/synced_lyrics.dart
@@ -45,6 +45,11 @@ class SyncedLyrics extends HookConsumerWidget {
final lyricValue = timedLyricsQuery.data;
+ final isUnSyncLyric = useMemoized(
+ () => lyricValue?.lyrics.every((l) => l.time == Duration.zero),
+ [lyricValue],
+ );
+
final lyricsMap = useMemoized(
() =>
lyricValue?.lyrics
@@ -72,6 +77,9 @@ class SyncedLyrics extends HookConsumerWidget {
: textTheme.headlineMedium?.copyWith(fontSize: 25))
?.copyWith(color: palette.titleTextColor);
+ var bodyTextTheme = textTheme.bodyLarge?.copyWith(
+ color: palette.bodyTextColor,
+ );
return Stack(
children: [
Column(
@@ -93,7 +101,9 @@ class SyncedLyrics extends HookConsumerWidget {
: textTheme.titleLarge,
),
),
- if (lyricValue != null && lyricValue.lyrics.isNotEmpty)
+ if (lyricValue != null &&
+ lyricValue.lyrics.isNotEmpty &&
+ isUnSyncLyric == false)
Expanded(
child: ListView.builder(
controller: controller,
@@ -102,7 +112,7 @@ class SyncedLyrics extends HookConsumerWidget {
final lyricSlice = lyricValue.lyrics[index];
final isActive = lyricSlice.time.inSeconds == currentTime;
- if (isActive) {
+ if (isActive && isUnSyncLyric == true) {
controller.scrollToIndex(
index,
preferPosition: AutoScrollPosition.middle,
@@ -173,8 +183,39 @@ class SyncedLyrics extends HookConsumerWidget {
),
),
if (playlist.activeTrack != null &&
- (lyricValue == null || lyricValue.lyrics.isEmpty == true))
- const Expanded(child: ShimmerLyrics()),
+ (timedLyricsQuery.isLoading || timedLyricsQuery.isRefreshing))
+ const Expanded(child: ShimmerLyrics())
+ else if (playlist.activeTrack != null &&
+ (timedLyricsQuery.hasError))
+ Text(
+ "Sorry, no Lyrics were found for `${playlist.activeTrack?.name}` :'(\n${timedLyricsQuery.error.toString()}",
+ style: bodyTextTheme,
+ )
+ else if (isUnSyncLyric == true)
+ Expanded(
+ child: Center(
+ child: RichText(
+ textAlign: TextAlign.center,
+ text: TextSpan(
+ style: bodyTextTheme,
+ children: [
+ const TextSpan(
+ text:
+ "Synced lyrics is not available for this song. Please use the",
+ ),
+ TextSpan(
+ text: " Plain Lyrics ",
+ style: textTheme.bodyLarge?.copyWith(
+ color: palette.bodyTextColor,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const TextSpan(text: "tab instead."),
+ ],
+ ),
+ ),
+ ),
+ ),
],
),
Align(
From 5633367397812148f6d712d06e97a4f84033f968 Mon Sep 17 00:00:00 2001
From: Kingkor Roy Tirtho
Date: Wed, 8 Nov 2023 12:32:21 +0600
Subject: [PATCH 005/195] fix(album_card): show loading state during adding
track to queue/play
---
lib/components/album/album_card.dart | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart
index d8f8d85b..93b4cefc 100644
--- a/lib/components/album/album_card.dart
+++ b/lib/components/album/album_card.dart
@@ -51,7 +51,8 @@ class AlbumCard extends HookConsumerWidget {
),
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
isPlaying: isPlaylistPlaying,
- isLoading: isPlaylistPlaying && playlist.isFetching == true,
+ isLoading: (isPlaylistPlaying && playlist.isFetching == true) ||
+ updating.value,
title: album.name!,
description:
"${album.albumType?.formatted} • ${TypeConversionUtils.artists_X_String(album.artists ?? [])}",
@@ -92,7 +93,7 @@ class AlbumCard extends HookConsumerWidget {
"album-tracks/${album.id}",
() {
return spotify.albums
- .getTracks(album.id!)
+ .tracks(album.id!)
.all()
.then((value) => value.toList());
},
From 487c2ed6bdc4af33006ba52532eb4eaaa261dceb Mon Sep 17 00:00:00 2001
From: Kingkor Roy Tirtho
Date: Wed, 8 Nov 2023 14:41:15 +0600
Subject: [PATCH 006/195] fix: user_playlists layout, track tile index,
---
lib/components/genre/category_card.dart | 69 ++++++++------
lib/components/library/user_playlists.dart | 94 ++++++++++---------
.../shared/track_table/track_tile.dart | 2 +-
lib/pages/home/personalized.dart | 68 ++++++++------
4 files changed, 133 insertions(+), 100 deletions(-)
diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart
index 1aa33cd6..a8d67771 100644
--- a/lib/components/genre/category_card.dart
+++ b/lib/components/genre/category_card.dart
@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:flutter/material.dart' hide Page;
+import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -8,6 +9,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/waypoint.dart';
+import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/services/queries/queries.dart';
@@ -28,16 +30,23 @@ class CategoryCard extends HookConsumerWidget {
category.id!,
);
+ final playlists = useMemoized(
+ () => playlistQuery.pages.expand(
+ (page) {
+ return page.items?.where((i) => i != null) ?? const Iterable.empty();
+ },
+ ).toList(),
+ [playlistQuery.pages],
+ );
+
if (playlistQuery.hasErrors &&
!playlistQuery.hasPageData &&
!playlistQuery.isLoadingNextPage) {
return const SizedBox.shrink();
}
- final playlists = playlistQuery.pages.expand(
- (page) {
- return page.items?.where((i) => i != null) ?? const Iterable.empty();
- },
- ).toList();
+
+ final mediaQuery = MediaQuery.of(context);
+
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
@@ -48,29 +57,35 @@ class CategoryCard extends HookConsumerWidget {
category.name!,
style: Theme.of(context).textTheme.titleMedium,
),
- ScrollConfiguration(
- behavior: ScrollConfiguration.of(context).copyWith(
- dragDevices: {
- PointerDeviceKind.touch,
- PointerDeviceKind.mouse,
- },
- ),
- child: Waypoint(
- controller: scrollController,
- onTouchEdge: playlistQuery.fetchNext,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- controller: scrollController,
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- ...playlists.map((playlist) => PlaylistCard(playlist)),
- if (playlistQuery.hasNextPage)
- const ShimmerPlaybuttonCard(count: 1),
- ],
- ),
+ SizedBox(
+ height: mediaQuery.smAndDown ? 226 : 266,
+ child: ScrollConfiguration(
+ behavior: ScrollConfiguration.of(context).copyWith(
+ dragDevices: {
+ PointerDeviceKind.touch,
+ PointerDeviceKind.mouse,
+ },
),
+ child: ListView.builder(
+ controller: scrollController,
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ itemCount: playlists.length + 1,
+ itemBuilder: (context, index) {
+ if (index == playlists.length) {
+ if (!playlistQuery.hasNextPage) {
+ return const SizedBox.shrink();
+ }
+ return Waypoint(
+ controller: scrollController,
+ onTouchEdge: playlistQuery.fetchNext,
+ isGrid: true,
+ child: const ShimmerPlaybuttonCard(),
+ );
+ }
+ final playlist = playlists[index];
+ return PlaylistCard(playlist);
+ }),
),
),
],
diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart
index 8ed3e73d..ecf4fa12 100644
--- a/lib/components/library/user_playlists.dart
+++ b/lib/components/library/user_playlists.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart' hide Image;
+import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:collection/collection.dart';
@@ -8,7 +9,6 @@ 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';
@@ -80,20 +80,13 @@ class UserPlaylists extends HookConsumerWidget {
return RefreshIndicator(
onRefresh: playlistsQuery.refresh,
- child: InterScrollbar(
- controller: controller,
- child: SingleChildScrollView(
+ child: SafeArea(
+ child: CustomScrollView(
controller: controller,
- physics: const AlwaysScrollableScrollPhysics(),
- child: Waypoint(
- controller: controller,
- onTouchEdge: () {
- if (playlistsQuery.hasNextPage) {
- playlistsQuery.fetchNext();
- }
- },
- child: SafeArea(
+ slivers: [
+ SliverToBoxAdapter(
child: Column(
+ mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(10),
@@ -103,42 +96,53 @@ class UserPlaylists extends HookConsumerWidget {
leading: const Icon(SpotubeIcons.filter),
),
),
- AnimatedCrossFade(
- duration: const Duration(milliseconds: 300),
- crossFadeState: !playlistsQuery.hasPageData &&
- !playlistsQuery.hasPageError &&
- !playlistsQuery.isLoadingNextPage
- ? 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))
- ],
- ),
+ 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,
+ ),
+ 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 PlaylistCard(playlists[index]);
+ },
+ )
+ ],
),
),
);
diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart
index ff1b314b..4980f96b 100644
--- a/lib/components/shared/track_table/track_tile.dart
+++ b/lib/components/shared/track_table/track_tile.dart
@@ -113,7 +113,7 @@ class TrackTile extends HookConsumerWidget {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Text(
- '$index',
+ '${(index ?? 0) + 1}',
maxLines: 1,
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart
index f7e942be..4f0b655f 100644
--- a/lib/pages/home/personalized.dart
+++ b/lib/pages/home/personalized.dart
@@ -10,6 +10,7 @@ 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';
+import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/authentication_provider.dart';
@@ -38,6 +39,7 @@ class PersonalizedItemCard extends HookWidget {
@override
Widget build(BuildContext context) {
final scrollController = useScrollController();
+ final mediaQuery = MediaQuery.of(context);
return Padding(
padding: const EdgeInsets.all(8.0),
@@ -52,36 +54,48 @@ class PersonalizedItemCard extends HookWidget {
style: Theme.of(context).textTheme.titleLarge,
),
),
- ScrollConfiguration(
- behavior: ScrollConfiguration.of(context).copyWith(
- dragDevices: {
- PointerDeviceKind.touch,
- PointerDeviceKind.mouse,
- },
- ),
- child: Scrollbar(
- controller: scrollController,
- interactive: false,
- child: Waypoint(
+ SizedBox(
+ height: mediaQuery.smAndDown ? 226 : 266,
+ child: ScrollConfiguration(
+ behavior: ScrollConfiguration.of(context).copyWith(
+ dragDevices: {
+ PointerDeviceKind.touch,
+ PointerDeviceKind.mouse,
+ },
+ ),
+ child: Scrollbar(
controller: scrollController,
- onTouchEdge: hasNextPage ? onFetchMore : null,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- controller: scrollController,
- physics: const AlwaysScrollableScrollPhysics(),
+ interactive: false,
+ child: ListView.builder(
+ itemCount: (playlists?.length ?? albums?.length)! + 1,
padding: const EdgeInsets.symmetric(vertical: 8.0),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- ...?playlists?.map((playlist) => PlaylistCard(playlist)),
- ...?albums?.map(
- (album) => AlbumCard(
- TypeConversionUtils.simpleAlbum_X_Album(album),
+ scrollDirection: Axis.horizontal,
+ itemBuilder: (context, index) {
+ if (index == (playlists?.length ?? albums?.length)!) {
+ if (!hasNextPage) return const SizedBox.shrink();
+
+ return Waypoint(
+ controller: scrollController,
+ onTouchEdge: onFetchMore,
+ isGrid: true,
+ child: const ShimmerPlaybuttonCard(count: 1),
+ );
+ }
+
+ final item = playlists == null
+ ? albums!.elementAt(index)
+ : playlists!.elementAt(index);
+
+ if (playlists == null) {
+ return AlbumCard(
+ TypeConversionUtils.simpleAlbum_X_Album(
+ item as AlbumSimple,
),
- ),
- if (hasNextPage) const ShimmerPlaybuttonCard(count: 1),
- ],
- ),
+ );
+ }
+
+ return PlaylistCard(item as PlaylistSimple);
+ },
),
),
),
From 6b8ae88db4105039c6cbd40bc032a45febab7f63 Mon Sep 17 00:00:00 2001
From: Kingkor Roy Tirtho
Date: Wed, 8 Nov 2023 17:07:20 +0600
Subject: [PATCH 007/195] refactor: horizontal playbutton layout to use
ListView and breakdown search page into sections
---
lib/components/album/album_card.dart | 10 +-
lib/components/artist/artist_album_list.dart | 49 +--
lib/components/genre/category_card.dart | 63 +---
.../horizontal_playbutton_card_view.dart | 96 ++++++
lib/pages/artist/artist.dart | 7 -
lib/pages/home/genres.dart | 18 +-
lib/pages/home/personalized.dart | 125 +------
lib/pages/search/search.dart | 308 ++----------------
lib/pages/search/sections/albums.dart | 39 +++
lib/pages/search/sections/artists.dart | 37 +++
lib/pages/search/sections/playlists.dart | 35 ++
lib/pages/search/sections/tracks.dart | 98 ++++++
12 files changed, 374 insertions(+), 511 deletions(-)
create mode 100644 lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
create mode 100644 lib/pages/search/sections/albums.dart
create mode 100644 lib/pages/search/sections/artists.dart
create mode 100644 lib/pages/search/sections/playlists.dart
create mode 100644 lib/pages/search/sections/tracks.dart
diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart
index 93b4cefc..945f8ecf 100644
--- a/lib/components/album/album_card.dart
+++ b/lib/components/album/album_card.dart
@@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/playbutton_card.dart';
-import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
@@ -34,13 +33,6 @@ class AlbumCard extends HookConsumerWidget {
[playlist, album.id],
);
- final marginH = useBreakpointValue(
- xs: 10,
- sm: 10,
- md: 15,
- others: 20,
- );
-
final updating = useState(false);
final spotify = ref.watch(spotifyProvider);
@@ -49,7 +41,7 @@ class AlbumCard extends HookConsumerWidget {
album.images,
placeholder: ImagePlaceholder.collection,
),
- margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
+ margin: const EdgeInsets.symmetric(horizontal: 10),
isPlaying: isPlaylistPlaying,
isLoading: (isPlaylistPlaying && playlist.isFetching == true) ||
updating.value,
diff --git a/lib/components/artist/artist_album_list.dart b/lib/components/artist/artist_album_list.dart
index 8fa9be87..e075cd60 100644
--- a/lib/components/artist/artist_album_list.dart
+++ b/lib/components/artist/artist_album_list.dart
@@ -1,11 +1,9 @@
-import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
-import 'package:spotube/components/album/album_card.dart';
-import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
-import 'package:spotube/components/shared/waypoint.dart';
+import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
+import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/services/queries/queries.dart';
@@ -20,7 +18,6 @@ class ArtistAlbumList extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- final scrollController = useScrollController();
final albumsQuery = useQueries.artist.albumsOf(ref, artistId);
final albums = useMemoized(() {
@@ -29,40 +26,16 @@ class ArtistAlbumList extends HookConsumerWidget {
.toList();
}, [albumsQuery.pages]);
- final hasNextPage = albumsQuery.pages.isEmpty
- ? false
- : (albumsQuery.pages.last.items?.length ?? 0) == 5;
+ final theme = Theme.of(context);
- return Column(
- children: [
- ScrollConfiguration(
- behavior: ScrollConfiguration.of(context).copyWith(
- dragDevices: {
- PointerDeviceKind.touch,
- PointerDeviceKind.mouse,
- },
- ),
- child: Scrollbar(
- interactive: false,
- controller: scrollController,
- child: Waypoint(
- controller: scrollController,
- onTouchEdge: albumsQuery.fetchNext,
- child: SingleChildScrollView(
- controller: scrollController,
- scrollDirection: Axis.horizontal,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- ...albums.map((album) => AlbumCard(album)),
- if (hasNextPage) const ShimmerPlaybuttonCard(count: 1),
- ],
- ),
- ),
- ),
- ),
- ),
- ],
+ return HorizontalPlaybuttonCardView(
+ hasNextPage: albumsQuery.hasNextPage,
+ items: albums,
+ onFetchMore: albumsQuery.fetchNext,
+ title: Text(
+ context.l10n.albums,
+ style: theme.textTheme.headlineSmall,
+ ),
);
}
}
diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart
index a8d67771..d5809b5d 100644
--- a/lib/components/genre/category_card.dart
+++ b/lib/components/genre/category_card.dart
@@ -1,15 +1,10 @@
-import 'dart:ui';
-
+import 'package:collection/collection.dart';
import 'package:flutter/material.dart' hide Page;
-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:spotify/spotify.dart';
-import 'package:spotube/components/playlist/playlist_card.dart';
-import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
-import 'package:spotube/components/shared/waypoint.dart';
-import 'package:spotube/extensions/constrains.dart';
+import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/services/queries/queries.dart';
@@ -24,7 +19,6 @@ class CategoryCard extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- final scrollController = useScrollController();
final playlistQuery = useQueries.category.playlistsOf(
ref,
category.id!,
@@ -33,7 +27,8 @@ class CategoryCard extends HookConsumerWidget {
final playlists = useMemoized(
() => playlistQuery.pages.expand(
(page) {
- return page.items?.where((i) => i != null) ?? const Iterable.empty();
+ return page.items?.whereNotNull() ??
+ const Iterable.empty();
},
).toList(),
[playlistQuery.pages],
@@ -45,51 +40,11 @@ class CategoryCard extends HookConsumerWidget {
return const SizedBox.shrink();
}
- final mediaQuery = MediaQuery.of(context);
-
- return Padding(
- padding: const EdgeInsets.all(8.0),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- category.name!,
- style: Theme.of(context).textTheme.titleMedium,
- ),
- SizedBox(
- height: mediaQuery.smAndDown ? 226 : 266,
- child: ScrollConfiguration(
- behavior: ScrollConfiguration.of(context).copyWith(
- dragDevices: {
- PointerDeviceKind.touch,
- PointerDeviceKind.mouse,
- },
- ),
- child: ListView.builder(
- controller: scrollController,
- scrollDirection: Axis.horizontal,
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- itemCount: playlists.length + 1,
- itemBuilder: (context, index) {
- if (index == playlists.length) {
- if (!playlistQuery.hasNextPage) {
- return const SizedBox.shrink();
- }
- return Waypoint(
- controller: scrollController,
- onTouchEdge: playlistQuery.fetchNext,
- isGrid: true,
- child: const ShimmerPlaybuttonCard(),
- );
- }
- final playlist = playlists[index];
- return PlaylistCard(playlist);
- }),
- ),
- ),
- ],
- ),
+ return HorizontalPlaybuttonCardView(
+ title: Text(category.name!),
+ hasNextPage: playlistQuery.hasNextPage,
+ items: playlists,
+ onFetchMore: playlistQuery.fetchNext,
);
}
}
diff --git a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
new file mode 100644
index 00000000..a415d721
--- /dev/null
+++ b/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
@@ -0,0 +1,96 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:spotify/spotify.dart';
+import 'package:spotube/components/album/album_card.dart';
+import 'package:spotube/components/artist/artist_card.dart';
+import 'package:spotube/components/playlist/playlist_card.dart';
+import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
+import 'package:spotube/components/shared/waypoint.dart';
+import 'package:spotube/hooks/use_breakpoint_value.dart';
+
+class HorizontalPlaybuttonCardView extends HookWidget {
+ final Widget title;
+ final List items;
+ final VoidCallback onFetchMore;
+ final bool hasNextPage;
+ const HorizontalPlaybuttonCardView({
+ required this.title,
+ required this.items,
+ required this.hasNextPage,
+ required this.onFetchMore,
+ Key? key,
+ }) : assert(
+ items is List ||
+ items is List ||
+ items is List,
+ ),
+ super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData(:textTheme) = Theme.of(context);
+ final scrollController = useScrollController();
+ final height = useBreakpointValue(
+ xs: 226,
+ sm: 226,
+ md: 236,
+ others: 266,
+ );
+
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ DefaultTextStyle(
+ style: textTheme.titleMedium!,
+ child: title,
+ ),
+ SizedBox(
+ height: height,
+ child: ScrollConfiguration(
+ behavior: ScrollConfiguration.of(context).copyWith(
+ dragDevices: {
+ PointerDeviceKind.touch,
+ PointerDeviceKind.mouse,
+ },
+ ),
+ child: ListView.builder(
+ controller: scrollController,
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ itemCount: items.length + 1,
+ itemBuilder: (context, index) {
+ if (index == items.length) {
+ if (!hasNextPage) {
+ return const SizedBox.shrink();
+ }
+ return Waypoint(
+ controller: scrollController,
+ onTouchEdge: onFetchMore,
+ isGrid: true,
+ child: const ShimmerPlaybuttonCard(),
+ );
+ }
+ final item = items[index];
+
+ return switch (item.runtimeType) {
+ PlaylistSimple => PlaylistCard(item as PlaylistSimple),
+ Album => AlbumCard(item as Album),
+ Artist => Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12.0),
+ child: ArtistCard(item as Artist),
+ ),
+ _ => const SizedBox.shrink(),
+ };
+ }),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart
index 67a99d86..2f169583 100644
--- a/lib/pages/artist/artist.dart
+++ b/lib/pages/artist/artist.dart
@@ -410,13 +410,6 @@ class ArtistPage extends HookConsumerWidget {
},
),
const SizedBox(height: 50),
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(
- context.l10n.albums,
- style: theme.textTheme.headlineSmall,
- ),
- ),
ArtistAlbumList(artistId),
const SizedBox(height: 20),
Padding(
diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart
index db1c58c5..076305f2 100644
--- a/lib/pages/home/genres.dart
+++ b/lib/pages/home/genres.dart
@@ -85,15 +85,19 @@ class GenrePage extends HookConsumerWidget {
controller: scrollController,
itemCount: categories.length,
itemBuilder: (context, index) {
- return AnimatedCrossFade(
- crossFadeState: searchController.text.isEmpty &&
+ 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
- ? CrossFadeState.showFirst
- : CrossFadeState.showSecond,
- duration: const Duration(milliseconds: 300),
- firstChild: const ShimmerCategories(),
- secondChild: CategoryCard(categories[index]),
+ ? const ShimmerCategories()
+ : CategoryCard(categories[index]),
);
},
),
diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart
index 4f0b655f..bbffbc11 100644
--- a/lib/pages/home/personalized.dart
+++ b/lib/pages/home/personalized.dart
@@ -1,111 +1,16 @@
-import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
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/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/components/shared/shimmers/shimmer_playbutton_card.dart';
-import 'package:spotube/components/shared/waypoint.dart';
-import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
-import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
-class PersonalizedItemCard extends HookWidget {
- final Iterable? playlists;
- final Iterable? albums;
- final String title;
- final bool hasNextPage;
- final void Function() onFetchMore;
-
- PersonalizedItemCard({
- this.playlists,
- this.albums,
- required this.title,
- required this.hasNextPage,
- required this.onFetchMore,
- Key? key,
- }) : assert(playlists == null || albums == null),
- super(key: key);
-
- final logger = getLogger(PersonalizedItemCard);
-
- @override
- Widget build(BuildContext context) {
- final scrollController = useScrollController();
- final mediaQuery = MediaQuery.of(context);
-
- return Padding(
- padding: const EdgeInsets.all(8.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(
- title,
- style: Theme.of(context).textTheme.titleLarge,
- ),
- ),
- SizedBox(
- height: mediaQuery.smAndDown ? 226 : 266,
- child: ScrollConfiguration(
- behavior: ScrollConfiguration.of(context).copyWith(
- dragDevices: {
- PointerDeviceKind.touch,
- PointerDeviceKind.mouse,
- },
- ),
- child: Scrollbar(
- controller: scrollController,
- interactive: false,
- child: ListView.builder(
- itemCount: (playlists?.length ?? albums?.length)! + 1,
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- scrollDirection: Axis.horizontal,
- itemBuilder: (context, index) {
- if (index == (playlists?.length ?? albums?.length)!) {
- if (!hasNextPage) return const SizedBox.shrink();
-
- return Waypoint(
- controller: scrollController,
- onTouchEdge: onFetchMore,
- isGrid: true,
- child: const ShimmerPlaybuttonCard(count: 1),
- );
- }
-
- final item = playlists == null
- ? albums!.elementAt(index)
- : playlists!.elementAt(index);
-
- if (playlists == null) {
- return AlbumCard(
- TypeConversionUtils.simpleAlbum_X_Album(
- item as AlbumSimple,
- ),
- );
- }
-
- return PlaylistCard(item as PlaylistSimple);
- },
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }
-}
-
class PersonalizedPage extends HookConsumerWidget {
const PersonalizedPage({Key? key}) : super(key: key);
@@ -133,10 +38,12 @@ class PersonalizedPage extends HookConsumerWidget {
.whereType>()
.expand((page) => page.items ?? const [])
.where((album) {
- return album.artists
- ?.any((artist) => userArtists.contains(artist.id!)) ==
- true;
- }),
+ return album.artists
+ ?.any((artist) => userArtists.contains(artist.id!)) ==
+ true;
+ })
+ .map((album) => TypeConversionUtils.simpleAlbum_X_Album(album))
+ .toList(),
[newReleases.pages],
);
@@ -149,18 +56,18 @@ class PersonalizedPage extends HookConsumerWidget {
!featuredPlaylistsQuery.isLoadingNextPage)
const ShimmerCategories()
else
- PersonalizedItemCard(
- playlists: playlists,
- title: context.l10n.featured,
+ HorizontalPlaybuttonCardView(
+ items: playlists.toList(),
+ title: Text(context.l10n.featured),
hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNext,
),
if (auth != null &&
newReleases.hasPageData &&
userArtistsQuery.hasData)
- PersonalizedItemCard(
- albums: albums,
- title: context.l10n.new_releases,
+ HorizontalPlaybuttonCardView(
+ items: albums,
+ title: Text(context.l10n.new_releases),
hasNextPage: newReleases.hasNextPage,
onFetchMore: newReleases.fetchNext,
),
@@ -172,9 +79,9 @@ class PersonalizedPage extends HookConsumerWidget {
.cast() ??
[];
if (playlists.isEmpty) return const SizedBox.shrink();
- return PersonalizedItemCard(
- playlists: playlists,
- title: item["name"] ?? "",
+ 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 c192eb7b..d659e8e3 100644
--- a/lib/pages/search/search.dart
+++ b/lib/pages/search/search.dart
@@ -1,30 +1,24 @@
import 'dart:async';
-import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
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/dialogs/prompt_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/shared/page_window_title_bar.dart';
-import 'package:spotube/components/shared/track_table/track_tile.dart';
-import 'package:spotube/components/shared/waypoint.dart';
-import 'package:spotube/components/artist/artist_card.dart';
-import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
+import 'package:spotube/pages/search/sections/albums.dart';
+import 'package:spotube/pages/search/sections/artists.dart';
+import 'package:spotube/pages/search/sections/playlists.dart';
+import 'package:spotube/pages/search/sections/tracks.dart';
import 'package:spotube/provider/authentication_provider.dart';
-import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/platform.dart';
-import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:collection/collection.dart';
final searchTermStateProvider = StateProvider((ref) => "");
@@ -38,9 +32,6 @@ class SearchPage extends HookConsumerWidget {
ref.watch(AuthenticationNotifier.provider);
final authenticationNotifier =
ref.watch(AuthenticationNotifier.provider.notifier);
- final albumController = useScrollController();
- final playlistController = useScrollController();
- final artistController = useScrollController();
final mediaQuery = MediaQuery.of(context);
final searchTerm = ref.watch(searchTermStateProvider);
@@ -80,283 +71,26 @@ class SearchPage extends HookConsumerWidget {
searchTerm.isNotEmpty;
final resultWidget = HookBuilder(
- builder: (context) {
- final playlist = ref.watch(ProxyPlaylistNotifier.provider);
- final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
- List albums = [];
- List artists = [];
- List