From 854ab8910dffb2837c011d3439173a1f0ebe9c6c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 14 Oct 2023 12:31:10 +0600 Subject: [PATCH 001/131] feat: manual offline detection --- lib/pages/root/root_app.dart | 74 ++++++++++---------- lib/services/connectivity_adapter.dart | 97 ++++++++++++++++++++++++-- pubspec.lock | 8 --- pubspec.yaml | 1 - 4 files changed, 126 insertions(+), 54 deletions(-) diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 3a0bd643..b2bd4620 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -1,12 +1,11 @@ import 'dart:async'; -import 'package:collection/collection.dart'; +import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; @@ -52,44 +51,43 @@ class RootApp extends HookConsumerWidget { }); final subscription = - InternetConnectionChecker().onStatusChange.listen((status) { - switch (status) { - case InternetConnectionStatus.connected: - scaffoldMessenger.showSnackBar( - SnackBar( - content: Row( - children: [ - Icon( - SpotubeIcons.wifi, - color: theme.colorScheme.onPrimary, - ), - const SizedBox(width: 10), - Text(context.l10n.connection_restored), - ], - ), - backgroundColor: theme.colorScheme.primary, - showCloseIcon: true, - width: 350, + QueryClient.connectivity.onConnectivityChanged.listen((status) { + if (status) { + scaffoldMessenger.showSnackBar( + SnackBar( + content: Row( + children: [ + Icon( + SpotubeIcons.wifi, + color: theme.colorScheme.onPrimary, + ), + const SizedBox(width: 10), + Text(context.l10n.connection_restored), + ], ), - ); - case InternetConnectionStatus.disconnected: - scaffoldMessenger.showSnackBar( - SnackBar( - content: Row( - children: [ - Icon( - SpotubeIcons.noWifi, - color: theme.colorScheme.onError, - ), - const SizedBox(width: 10), - Text(context.l10n.you_are_offline), - ], - ), - backgroundColor: theme.colorScheme.error, - showCloseIcon: true, - width: 300, + backgroundColor: theme.colorScheme.primary, + showCloseIcon: true, + width: 350, + ), + ); + } else { + scaffoldMessenger.showSnackBar( + SnackBar( + content: Row( + children: [ + Icon( + SpotubeIcons.noWifi, + color: theme.colorScheme.onError, + ), + const SizedBox(width: 10), + Text(context.l10n.you_are_offline), + ], ), - ); + backgroundColor: theme.colorScheme.error, + showCloseIcon: true, + width: 300, + ), + ); } }); diff --git a/lib/services/connectivity_adapter.dart b/lib/services/connectivity_adapter.dart index 6a3a46ee..f5dc7737 100644 --- a/lib/services/connectivity_adapter.dart +++ b/lib/services/connectivity_adapter.dart @@ -1,12 +1,95 @@ +import 'dart:async'; +import 'dart:io'; + import 'package:fl_query/fl_query.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; +import 'package:flutter/widgets.dart'; -class FlQueryInternetConnectionCheckerAdapter extends ConnectivityAdapter { - @override - Future get isConnected => InternetConnectionChecker().hasConnection; +class FlQueryInternetConnectionCheckerAdapter extends ConnectivityAdapter + with WidgetsBindingObserver { + final _connectionStreamController = StreamController.broadcast(); + + FlQueryInternetConnectionCheckerAdapter() : super() { + Timer.periodic(const Duration(minutes: 3), (timer) async { + if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.paused) { + return; + } + await isConnected; + }); + } @override - Stream get onConnectivityChanged => InternetConnectionChecker() - .onStatusChange - .map((status) => status == InternetConnectionStatus.connected); + didChangeAppLifecycleState(AppLifecycleState state) async { + if (state == AppLifecycleState.resumed) { + await isConnected; + } + } + + final vpnNames = [ + 'tun', + 'tap', + 'ppp', + 'pptp', + 'l2tp', + 'ipsec', + 'vpn', + 'wireguard', + 'openvpn', + 'softether', + 'proton', + 'strongswan', + 'cisco', + 'forticlient', + 'fortinet', + 'hideme', + 'hidemy', + 'hideman', + 'hidester', + 'lightway', + ]; + + Future isVpnActive() async { + final interfaces = await NetworkInterface.list( + includeLoopback: false, + type: InternetAddressType.any, + ); + + if (interfaces.isEmpty) { + return false; + } + + return interfaces.any( + (interface) => + vpnNames.any((name) => interface.name.toLowerCase().contains(name)), + ); + } + + Future doesConnectTo(String address) async { + try { + final result = await InternetAddress.lookup(address); + if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { + return true; + } + return false; + } on SocketException catch (_) { + return false; + } + } + + Future _isConnected() async { + return await doesConnectTo('google.com') || + await doesConnectTo('www.baidu.com') || // for China + await isVpnActive(); // when VPN is active that means we are connected + } + + @override + Future get isConnected async { + final connected = await _isConnected(); + if (connected != isConnectedSync /*previous value*/) { + _connectionStreamController.add(connected); + } + return connected; + } + + @override + Stream get onConnectivityChanged => _connectionStreamController.stream; } diff --git a/pubspec.lock b/pubspec.lock index 3e61a09d..a3d2d059 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1125,14 +1125,6 @@ packages: description: flutter source: sdk version: "0.0.0" - internet_connection_checker: - dependency: "direct main" - description: - name: internet_connection_checker - sha256: "1c683e63e89c9ac66a40748b1b20889fd9804980da732bf2b58d6d5456c8e876" - url: "https://pub.dev" - source: hosted - version: "1.0.0+1" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index ab1dfe00..ae5b6a75 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,6 @@ dependencies: html: ^0.15.1 http: ^1.1.0 image_picker: ^1.0.4 - internet_connection_checker: ^1.0.0+1 intl: ^0.18.0 introduction_screen: ^3.0.2 json_annotation: ^4.8.1 From 0eb9ee8648bee43a8009e6752674b1be646c0916 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 14 Oct 2023 12:33:43 +0600 Subject: [PATCH 002/131] feat: ability to select/copy lyrics #802 --- lib/pages/lyrics/plain_lyrics.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index 1d1237e6..f6eaa5d5 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -104,7 +104,7 @@ class PlainLyrics extends HookConsumerWidget { ? 1.7 : 2, ), - child: Text( + child: SelectableText( lyrics == null && playlist.activeTrack == null ? "No Track being played currently" : lyrics ?? "", From d39667bfb9466aa79c2676fb58d80912c8ddbea3 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 14 Oct 2023 17:28:22 +0600 Subject: [PATCH 003/131] chore: use updated fl_query offline management --- lib/components/player/player.dart | 108 +++++++++++++------------ lib/services/connectivity_adapter.dart | 18 ++++- pubspec.lock | 27 ++++--- pubspec.yaml | 23 +++++- 4 files changed, 104 insertions(+), 72 deletions(-) diff --git a/lib/components/player/player.dart b/lib/components/player/player.dart index 42563d1a..811d24c5 100644 --- a/lib/components/player/player.dart +++ b/lib/components/player/player.dart @@ -95,62 +95,64 @@ class PlayerView extends HookConsumerWidget { }, child: IconTheme( data: theme.iconTheme.copyWith(color: bodyTextColor), - child: Scaffold( - key: scaffoldKey, - appBar: PreferredSize( - preferredSize: Size.fromHeight( - kToolbarHeight + topPadding, - ), - child: Padding( - padding: EdgeInsets.only(top: topPadding), - child: PageWindowTitleBar( - backgroundColor: Colors.transparent, - foregroundColor: titleTextColor, - toolbarOpacity: 1, - leading: IconButton( - icon: const Icon(SpotubeIcons.angleDown, size: 18), - onPressed: panelController.close, + child: AnimateGradient( + animateAlignments: true, + primaryBegin: Alignment.topLeft, + primaryEnd: Alignment.bottomLeft, + secondaryBegin: Alignment.bottomRight, + secondaryEnd: Alignment.topRight, + duration: const Duration(seconds: 15), + primaryColors: [ + palette.dominantColor?.color ?? theme.colorScheme.primary, + palette.mutedColor?.color ?? theme.colorScheme.secondary, + ], + secondaryColors: [ + (palette.darkVibrantColor ?? palette.lightVibrantColor)?.color ?? + theme.colorScheme.primaryContainer, + (palette.darkMutedColor ?? palette.lightMutedColor)?.color ?? + theme.colorScheme.secondaryContainer, + ], + child: Scaffold( + key: scaffoldKey, + backgroundColor: Colors.transparent, + appBar: PreferredSize( + preferredSize: Size.fromHeight( + kToolbarHeight + topPadding, + ), + child: Padding( + padding: EdgeInsets.only(top: topPadding), + child: PageWindowTitleBar( + backgroundColor: Colors.transparent, + foregroundColor: titleTextColor, + toolbarOpacity: 1, + leading: IconButton( + icon: const Icon(SpotubeIcons.angleDown, size: 18), + onPressed: panelController.close, + ), + actions: [ + IconButton( + icon: const Icon(SpotubeIcons.info, size: 18), + tooltip: context.l10n.details, + style: + IconButton.styleFrom(foregroundColor: bodyTextColor), + onPressed: currentTrack == null + ? null + : () { + showDialog( + context: context, + builder: (context) { + return TrackDetailsDialog( + track: currentTrack, + ); + }); + }, + ) + ], ), - actions: [ - IconButton( - icon: const Icon(SpotubeIcons.info, size: 18), - tooltip: context.l10n.details, - style: IconButton.styleFrom(foregroundColor: bodyTextColor), - onPressed: currentTrack == null - ? null - : () { - showDialog( - context: context, - builder: (context) { - return TrackDetailsDialog( - track: currentTrack, - ); - }); - }, - ) - ], ), ), - ), - extendBodyBehindAppBar: true, - body: AnimateGradient( - animateAlignments: true, - primaryBegin: Alignment.topLeft, - primaryEnd: Alignment.bottomLeft, - secondaryBegin: Alignment.bottomRight, - secondaryEnd: Alignment.topRight, - duration: const Duration(seconds: 15), - primaryColors: [ - palette.dominantColor?.color ?? theme.colorScheme.primary, - palette.mutedColor?.color ?? theme.colorScheme.secondary, - ], - secondaryColors: [ - (palette.darkVibrantColor ?? palette.lightVibrantColor)?.color ?? - theme.colorScheme.primaryContainer, - (palette.darkMutedColor ?? palette.lightMutedColor)?.color ?? - theme.colorScheme.secondaryContainer, - ], - child: SingleChildScrollView( + extendBodyBehindAppBar: true, + body: SingleChildScrollView( child: Container( alignment: Alignment.center, width: double.infinity, diff --git a/lib/services/connectivity_adapter.dart b/lib/services/connectivity_adapter.dart index f5dc7737..cc0847c7 100644 --- a/lib/services/connectivity_adapter.dart +++ b/lib/services/connectivity_adapter.dart @@ -9,11 +9,21 @@ class FlQueryInternetConnectionCheckerAdapter extends ConnectivityAdapter final _connectionStreamController = StreamController.broadcast(); FlQueryInternetConnectionCheckerAdapter() : super() { - Timer.periodic(const Duration(minutes: 3), (timer) async { - if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.paused) { - return; + Timer? timer; + + onConnectivityChanged.listen((connected) { + if (!connected && timer == null) { + timer = Timer.periodic(const Duration(seconds: 30), (timer) async { + if (WidgetsBinding.instance.lifecycleState == + AppLifecycleState.paused) { + return; + } + await isConnected; + }); + } else { + timer?.cancel(); + timer = null; } - await isConnected; }); } diff --git a/pubspec.lock b/pubspec.lock index a3d2d059..05059c0c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -580,26 +580,29 @@ packages: fl_query: dependency: "direct main" description: - name: fl_query - sha256: a97bd9234c3e8aefe43735d0ac6b7153154ea7aeeac123b0621afb0e4dca3291 - url: "https://pub.dev" - source: hosted + path: "packages/fl_query" + ref: HEAD + resolved-ref: a817713a0bb0c486e908e9ed74467c4f7f58bea7 + url: "https://github.com/KRTirtho/fl-query.git" + source: git version: "1.0.0-alpha.5" fl_query_devtools: dependency: "direct main" description: - name: fl_query_devtools - sha256: e827512a8601ba57272a9171581e789ffb68375a8a86c994750b45f8cdc0a993 - url: "https://pub.dev" - source: hosted + path: "packages/fl_query_devtools" + ref: HEAD + resolved-ref: a817713a0bb0c486e908e9ed74467c4f7f58bea7 + url: "https://github.com/KRTirtho/fl-query.git" + source: git version: "0.1.0-alpha.3" fl_query_hooks: dependency: "direct main" description: - name: fl_query_hooks - sha256: "97ad03d0d2d506353d8f3de62ac2aa0b465d85827d15641e3e4b76b16e0a5bbd" - url: "https://pub.dev" - source: hosted + path: "packages/fl_query_hooks" + ref: HEAD + resolved-ref: a817713a0bb0c486e908e9ed74467c4f7f58bea7 + url: "https://github.com/KRTirtho/fl-query.git" + source: git version: "1.0.0-alpha.5" fluentui_system_icons: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index ae5b6a75..1965f04e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,9 +32,18 @@ dependencies: duration: ^3.0.12 envied: ^0.3.0 file_selector: ^1.0.1 - fl_query: ^1.0.0-alpha.5 - fl_query_hooks: ^1.0.0-alpha.5 - fl_query_devtools: ^0.1.0-alpha.3 + fl_query: + git: + url: https://github.com/KRTirtho/fl-query.git + path: packages/fl_query + fl_query_hooks: + git: + url: https://github.com/KRTirtho/fl-query.git + path: packages/fl_query_hooks + fl_query_devtools: + git: + url: https://github.com/KRTirtho/fl-query.git + path: packages/fl_query_devtools fluentui_system_icons: ^1.1.189 flutter: sdk: flutter @@ -124,6 +133,14 @@ dev_dependencies: dependency_overrides: http: ^1.1.0 system_tray: 2.0.2 + fl_query: + git: + url: https://github.com/KRTirtho/fl-query.git + path: packages/fl_query + fl_query_hooks: + git: + url: https://github.com/KRTirtho/fl-query.git + path: packages/fl_query_hooks flutter: generate: true From 34b80a36b4623d9172c95cee771493571c487f63 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 15 Oct 2023 10:38:09 +0600 Subject: [PATCH 004/131] chore: fix normalize audio unneeded subtitle --- lib/pages/settings/settings.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 878fdbb0..4320580d 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -379,7 +379,6 @@ class SettingsPage extends HookConsumerWidget { SwitchListTile( secondary: const Icon(SpotubeIcons.normalize), title: Text(context.l10n.normalize_audio), - subtitle: Text(context.l10n.blacklist_description), value: preferences.normalizeAudio, onChanged: preferences.setNormalizeAudio, ), From 8de2196b61cd22981284aa6c22bd0af64f89742a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 15 Oct 2023 10:52:56 +0600 Subject: [PATCH 005/131] chore: add lastfm login field autofill support --- lib/pages/lastfm_login/lastfm_login.dart | 57 +++++++++++++++--------- lib/themes/theme.dart | 1 - 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart index bea43b55..f77d0abb 100644 --- a/lib/pages/lastfm_login/lastfm_login.dart +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -59,29 +59,42 @@ class LastFMLoginPage extends HookConsumerWidget { const SizedBox(height: 10), Text(context.l10n.login_with_your_lastfm), const SizedBox(height: 10), - TextFormField( - controller: username, - validator: ValidationBuilder().required().build(), - decoration: InputDecoration( - labelText: context.l10n.username, - ), - ), - const SizedBox(height: 10), - TextFormField( - controller: password, - validator: ValidationBuilder().required().build(), - obscureText: !passwordVisible.value, - decoration: InputDecoration( - labelText: context.l10n.password, - suffixIcon: IconButton( - icon: Icon( - passwordVisible.value - ? SpotubeIcons.eye - : SpotubeIcons.noEye, + AutofillGroup( + child: Column( + children: [ + TextFormField( + autofillHints: const [ + AutofillHints.username, + AutofillHints.email, + ], + controller: username, + validator: ValidationBuilder().required().build(), + decoration: InputDecoration( + labelText: context.l10n.username, + ), ), - onPressed: () => - passwordVisible.value = !passwordVisible.value, - ), + const SizedBox(height: 10), + TextFormField( + autofillHints: const [ + AutofillHints.password, + ], + controller: password, + validator: ValidationBuilder().required().build(), + obscureText: !passwordVisible.value, + decoration: InputDecoration( + labelText: context.l10n.password, + suffixIcon: IconButton( + icon: Icon( + passwordVisible.value + ? SpotubeIcons.eye + : SpotubeIcons.noEye, + ), + onPressed: () => passwordVisible.value = + !passwordVisible.value, + ), + ), + ), + ], ), ), const SizedBox(height: 10), diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index 42420e8c..8c968e1b 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { final scheme = ColorScheme.fromSeed( From edb8928d53f82ed65ca86ccd03b832a6eea02007 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 15 Oct 2023 10:56:09 +0600 Subject: [PATCH 006/131] chore: remove unused save lyrics toggle --- lib/l10n/app_ar.arb | 1 - lib/l10n/app_bn.arb | 1 - lib/l10n/app_ca.arb | 1 - lib/l10n/app_de.arb | 1 - lib/l10n/app_en.arb | 1 - lib/l10n/app_es.arb | 1 - lib/l10n/app_fa.arb | 3 +-- lib/l10n/app_fr.arb | 1 - lib/l10n/app_hi.arb | 1 - lib/l10n/app_ja.arb | 1 - lib/l10n/app_pl.arb | 1 - lib/l10n/app_pt.arb | 1 - lib/l10n/app_ru.arb | 1 - lib/l10n/app_uk.arb | 1 - lib/l10n/app_zh.arb | 1 - lib/pages/settings/settings.dart | 8 -------- lib/provider/user_preferences_provider.dart | 12 ------------ 17 files changed, 1 insertion(+), 36 deletions(-) diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 7a1ce1b3..37f11ba8 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -138,7 +138,6 @@ "skip_non_music": "تخطي المقاطع غير الموسيقية (SponsorBlock)", "blacklist_description": "المقطوعات والفنانون المدرجون في القائمة السوداء", "wait_for_download_to_finish": "يرجى الانتظار حتى انتهاء التنزيل الحالي", - "download_lyrics": "تحميل الكلمات مع المقطوعات", "desktop": "سطح المكتب", "close_behavior": "إغلاق التصرف", "close": "إغلاق", diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index df83f11f..bfae9d23 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -136,7 +136,6 @@ "skip_non_music": "গানের নন-মিউজিক সেগমেন্ট এড়িয়ে যান (SponsorBlock)", "blacklist_description": "কালো তালিকাভুক্ত গানের ট্র্যাক এবং শিল্পী", "wait_for_download_to_finish": "ডাউনলোড শেষ হওয়ার জন্য অপেক্ষা করুন", - "download_lyrics": "গানের সাথে লিরিক্স ডাউনলোড করুন", "desktop": "ডেস্কটপ", "close_behavior": "বন্ধ করার প্রক্রিয়া", "close": "বন্ধ করুন", diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index ab7d0817..a40ae8c4 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -136,7 +136,6 @@ "skip_non_music": "Ometre segments que no son música (SponsorBlock)", "blacklist_description": "Cançons i artistes de la llista negra", "wait_for_download_to_finish": "Si us plau, esperi que acabi la descàrrega actual", - "download_lyrics": "Descarregar lletres amb les cançons", "desktop": "Escriptori", "close_behavior": "Comportament al tancar", "close": "Tancar", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ef2e78a2..10c59ebc 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -136,7 +136,6 @@ "skip_non_music": "Überspringe Nicht-Musik-Segmente (SponsorBlock)", "blacklist_description": "Gesperrte Titel und Künstler", "wait_for_download_to_finish": "Bitte warten Sie, bis der aktuelle Download abgeschlossen ist", - "download_lyrics": "Songtexte zusammen mit den Tracks herunterladen", "desktop": "Desktop", "close_behavior": "Verhalten beim Schließen", "close": "Schließen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 719a98af..9d5be6bb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -138,7 +138,6 @@ "skip_non_music": "Skip non-music segments (SponsorBlock)", "blacklist_description": "Blacklisted tracks and artists", "wait_for_download_to_finish": "Please wait for the current download to finish", - "download_lyrics": "Download lyrics along with tracks", "desktop": "Desktop", "close_behavior": "Close Behavior", "close": "Close", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index f6918bc8..fec85abc 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -136,7 +136,6 @@ "skip_non_music": "Omitir segmentos que no son música (SponsorBlock)", "blacklist_description": "Canciones y artistas en la lista negra", "wait_for_download_to_finish": "Por favor, espera a que termine la descarga actual", - "download_lyrics": "Descargar letras junto con las canciones", "desktop": "Escritorio", "close_behavior": "Comportamiento al cerrar", "close": "Cerrar", diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 237787a1..d8156a5f 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -79,7 +79,7 @@ "track_will_play_next": "{track} پخش خواهد شد", "play_next": "پخش آهنگ بعدی", "removed_track_from_queue": "{track} از لیست پخش حذف شد", - "remove_from_queue":"از لیست پخش حذف شد", + "remove_from_queue": "از لیست پخش حذف شد", "remove_from_favorites": "از علاقمندی ها حدف شد", "save_as_favorite": "ذخیره به عنوان علاقمندی ها", "add_to_playlist": "به لیست پخش اضافه کردن", @@ -138,7 +138,6 @@ "skip_non_music": "رد شدن از پخش های غیر موسیقی (SponsorBlock)", "blacklist_description": "آهنگ ها و هنرمند های در لیست سیاه", "wait_for_download_to_finish": "لطفا صبر کنید تا دانلود آهنگ جاری تمام شود", - "download_lyrics": "دانلود متن آهنگ به همراه متن ", "desktop": "میز کار", "close_behavior": "رفتار نزدیک", "close": "بستن", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index d1a891ed..30bfa883 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -136,7 +136,6 @@ "skip_non_music": "Ignorer les segments non musicaux (SponsorBlock)", "blacklist_description": "Pistes et artistes en liste noire", "wait_for_download_to_finish": "Veuillez attendre la fin du téléchargement en cours", - "download_lyrics": "Télécharger les paroles avec les pistes", "desktop": "Bureau", "close_behavior": "Comportement de fermeture", "close": "Fermer", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 72d2c505..8beda1e9 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -136,7 +136,6 @@ "skip_non_music": "गाने के अलावा सेगमेंट्स को छोड़ें (स्पॉन्सरब्लॉक)", "blacklist_description": "ब्लैकलिस्ट में शामिल ट्रैक और कलाकार", "wait_for_download_to_finish": "वर्तमान डाउनलोड समाप्त होने तक कृपया प्रतीक्षा करें", - "download_lyrics": "गानों के साथ लिरिक्स डाउनलोड करें", "desktop": "डेस्कटॉप", "close_behavior": "बंद करने का व्यवहार", "close": "बंद करें", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 30a35ef1..c13b620a 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -136,7 +136,6 @@ "skip_non_music": "音楽でない部分をスキップ (SponsorBlock)", "blacklist_description": "曲とアーティストのブラックリスト", "wait_for_download_to_finish": "現在のダウンロードが完了するまでお待ちください", - "download_lyrics": "曲と共に歌詞もダウンロード", "desktop": "デスクトップ", "close_behavior": "閉じた時の動作", "close": "閉じる", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 7d6db657..0f384b2b 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -136,7 +136,6 @@ "skip_non_music": "Pomiń nie-muzyczne segmenty (SponsorBlock)", "blacklist_description": "Czarna lista utworów i artystów", "wait_for_download_to_finish": "Proszę poczekać na zakończenie obecnego pobierania.", - "download_lyrics": "Pobierz utwory razem z tekstem", "desktop": "Pulpit", "close_behavior": "Zamknij", "close": "Zamknij", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index a9cd3f32..654a8c59 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -136,7 +136,6 @@ "skip_non_music": "Pular segmentos não musicais (SponsorBlock)", "blacklist_description": "Faixas e artistas na lista negra", "wait_for_download_to_finish": "Aguarde o download atual ser concluído", - "download_lyrics": "Baixar letras junto com as faixas", "desktop": "Desktop", "close_behavior": "Comportamento de Fechamento", "close": "Fechar", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 4daf0e92..e6e00ee0 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -136,7 +136,6 @@ "skip_non_music": "Пропускать немузыкальные сегменты (SponsorBlock)", "blacklist_description": "Черный список треков и артистов", "wait_for_download_to_finish": "Пожалуйста, дождитесь завершения текущей загрузки", - "download_lyrics": "Скачивать тексты вместе с треками", "desktop": "Компьютер", "close_behavior": "Поведение при закрытии", "close": "Закрыть", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index fa452e69..1b926dea 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -138,7 +138,6 @@ "skip_non_music": "Пропустити не музичні сегменти", "blacklist_description": "Треки та виконавці в чорному списку", "wait_for_download_to_finish": "Зачекайте, поки завершиться поточна загрузка", - "download_lyrics": "Завантажувати тексти пісень разом з треками", "desktop": "Робочий стіл", "close_behavior": "Поведінка при закритті", "close": "Закрити", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index fe440ccf..92c414f0 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -136,7 +136,6 @@ "skip_non_music": "跳过非音乐片段(屏蔽赞助商)", "blacklist_description": "已屏蔽的歌曲与艺人", "wait_for_download_to_finish": "请等待当前下载任务完成", - "download_lyrics": "下载歌曲时同时下载歌词", "desktop": "桌面端设置", "close_behavior": "点击关闭按钮行为", "close": "关闭", diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 4320580d..e319997a 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -435,14 +435,6 @@ class SettingsPage extends HookConsumerWidget { ), onTap: pickDownloadLocation, ), - SwitchListTile( - secondary: const Icon(SpotubeIcons.lyrics), - title: Text(context.l10n.download_lyrics), - value: preferences.saveTrackLyrics, - onChanged: (state) { - preferences.setSaveTrackLyrics(state); - }, - ), ], ), if (DesktopTools.platform.isDesktop) diff --git a/lib/provider/user_preferences_provider.dart b/lib/provider/user_preferences_provider.dart index 086eb2b8..3355adb0 100644 --- a/lib/provider/user_preferences_provider.dart +++ b/lib/provider/user_preferences_provider.dart @@ -54,7 +54,6 @@ class UserPreferences extends PersistedChangeNotifier { bool amoledDarkTheme; bool checkUpdate; bool normalizeAudio; - bool saveTrackLyrics; bool showSystemTrayIcon; bool skipNonMusic; bool systemTitleBar; @@ -79,7 +78,6 @@ class UserPreferences extends PersistedChangeNotifier { this.themeMode = ThemeMode.system, this.layoutMode = LayoutMode.adaptive, this.albumColorSync = true, - this.saveTrackLyrics = false, this.checkUpdate = true, this.audioQuality = AudioQuality.high, this.downloadLocation = "", @@ -113,7 +111,6 @@ class UserPreferences extends PersistedChangeNotifier { setThemeMode(ThemeMode.system); setLayoutMode(LayoutMode.adaptive); setAlbumColorSync(true); - setSaveTrackLyrics(false); setCheckUpdate(true); setAudioQuality(AudioQuality.high); setDownloadLocation(""); @@ -150,12 +147,6 @@ class UserPreferences extends PersistedChangeNotifier { updatePersistence(); } - void setSaveTrackLyrics(bool shouldSave) { - saveTrackLyrics = shouldSave; - notifyListeners(); - updatePersistence(); - } - void setRecommendationMarket(Market country) { recommendationMarket = country; notifyListeners(); @@ -284,7 +275,6 @@ class UserPreferences extends PersistedChangeNotifier { @override FutureOr loadFromLocal(Map map) async { - saveTrackLyrics = map["saveTrackLyrics"] ?? false; recommendationMarket = Market.values.firstWhere( (market) => market.name == (map["recommendationMarket"] ?? recommendationMarket), @@ -358,7 +348,6 @@ class UserPreferences extends PersistedChangeNotifier { @override FutureOr> toMap() { return { - "saveTrackLyrics": saveTrackLyrics, "recommendationMarket": recommendationMarket.name, "themeMode": themeMode.index, "accentColorScheme": accentColorScheme.toString(), @@ -418,7 +407,6 @@ class UserPreferences extends PersistedChangeNotifier { skipNonMusic: skipNonMusic ?? this.skipNonMusic, youtubeApiType: youtubeApiType ?? this.youtubeApiType, recommendationMarket: recommendationMarket ?? this.recommendationMarket, - saveTrackLyrics: saveTrackLyrics ?? this.saveTrackLyrics, ); } } From 593bc2de90c0fa8432e81b0ac3f26efd43c72ec1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 15 Oct 2023 11:11:20 +0600 Subject: [PATCH 007/131] chore: connectivity adapter use http as well --- lib/components/library/user_local_tracks.dart | 13 +++++++------ lib/services/connectivity_adapter.dart | 13 +++++++++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index 0a6efadf..50ae64be 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -29,7 +29,8 @@ import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException; +import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' + show FfiException; const supportedAudioTypes = [ "audio/webm", @@ -77,14 +78,14 @@ final localTracksProvider = FutureProvider>((ref) async { final mimetype = lookupMimeType(file.path); return mimetype != null && supportedAudioTypes.contains(mimetype); }).map( - (f) async { + (file) async { try { - final metadata = await MetadataGod.readMetadata(file: f.path); + final metadata = await MetadataGod.readMetadata(file: file.path); final imageFile = File(join( (await getTemporaryDirectory()).path, "spotube", - basenameWithoutExtension(f.path) + + basenameWithoutExtension(file.path) + imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!, )); if (!await imageFile.exists() && metadata.picture != null) { @@ -95,10 +96,10 @@ final localTracksProvider = FutureProvider>((ref) async { ); } - return {"metadata": metadata, "file": f, "art": imageFile.path}; + return {"metadata": metadata, "file": file, "art": imageFile.path}; } catch (e, stack) { if (e is FfiException) { - return {"file": f}; + return {"file": file}; } Catcher2.reportCheckedError(e, stack); return {}; diff --git a/lib/services/connectivity_adapter.dart b/lib/services/connectivity_adapter.dart index cc0847c7..c628f2f7 100644 --- a/lib/services/connectivity_adapter.dart +++ b/lib/services/connectivity_adapter.dart @@ -1,14 +1,18 @@ import 'dart:async'; import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:fl_query/fl_query.dart'; import 'package:flutter/widgets.dart'; class FlQueryInternetConnectionCheckerAdapter extends ConnectivityAdapter with WidgetsBindingObserver { final _connectionStreamController = StreamController.broadcast(); + final Dio dio; - FlQueryInternetConnectionCheckerAdapter() : super() { + FlQueryInternetConnectionCheckerAdapter() + : dio = Dio(), + super() { Timer? timer; onConnectivityChanged.listen((connected) { @@ -81,7 +85,12 @@ class FlQueryInternetConnectionCheckerAdapter extends ConnectivityAdapter } return false; } on SocketException catch (_) { - return false; + try { + final response = await dio.head('https://$address'); + return (response.statusCode ?? 500) <= 400; + } on DioException catch (_) { + return false; + } } } From ed6ca006ce237ed8d509cde9ed47cd6ea3396b63 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 15 Oct 2023 12:09:14 +0600 Subject: [PATCH 008/131] fix: last track repeats --- .../audio_player/mk_state_player.dart | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/services/audio_player/mk_state_player.dart b/lib/services/audio_player/mk_state_player.dart index 91135756..386b0a0e 100644 --- a/lib/services/audio_player/mk_state_player.dart +++ b/lib/services/audio_player/mk_state_player.dart @@ -45,7 +45,6 @@ class MkPlayerWithState extends Player { if (!isCompleted) return; _playerStateStream.add(AudioPlaybackState.completed); - if (loopMode == PlaylistMode.single) { await super.open(_playlist!.medias[_playlist!.index], play: true); } else { @@ -166,10 +165,22 @@ class MkPlayerWithState extends Player { final isLast = _playlist!.index == _playlist!.medias.length - 1; - if (loopMode == PlaylistMode.loop && isLast) { - playlist = _playlist!.copyWith(index: 0); - return super.open(_playlist!.medias[_playlist!.index], play: true); - } else if (!isLast) { + if (isLast) { + switch (loopMode) { + case PlaylistMode.loop: + playlist = _playlist!.copyWith(index: 0); + super.open(_playlist!.medias[_playlist!.index], play: true); + break; + case PlaylistMode.none: + // Fixes auto-repeating the last track + await super.stop(); + await Future.delayed(const Duration(seconds: 2), () { + super.open(_playlist!.medias[_playlist!.index], play: false); + }); + break; + default: + } + } else { playlist = _playlist!.copyWith(index: _playlist!.index + 1); return super.open(_playlist!.medias[_playlist!.index], play: true); From e29cc2578cab36729e235b117c1b5489c3452902 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 16 Oct 2023 09:54:01 +0600 Subject: [PATCH 009/131] fix: use audio_service_mpris plugin --- .../audio_services/audio_services.dart | 53 ++++++------------- pubspec.lock | 8 +++ pubspec.yaml | 1 + 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index 6d6c9d43..645548fb 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -4,8 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/services/audio_player/audio_player.dart'; -import 'package:spotube/services/audio_services/linux_audio_service.dart'; import 'package:spotube/services/audio_services/mobile_audio_service.dart'; import 'package:spotube/services/audio_services/windows_audio_service.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -13,49 +11,33 @@ import 'package:spotube/utils/type_conversion_utils.dart'; class AudioServices { final MobileAudioService? mobile; final WindowsAudioService? smtc; - final LinuxAudioService? mpris; - AudioServices(this.mobile, this.smtc, this.mpris); + AudioServices(this.mobile, this.smtc); static Future create( Ref ref, ProxyPlaylistNotifier playback, ) async { - final mobile = - DesktopTools.platform.isMobile || DesktopTools.platform.isMacOS - ? await AudioService.init( - builder: () => MobileAudioService(playback), - config: const AudioServiceConfig( - androidNotificationChannelId: 'com.krtirtho.Spotube', - androidNotificationChannelName: 'Spotube', - androidNotificationOngoing: true, - ), - ) - : null; + final mobile = DesktopTools.platform.isMobile || + DesktopTools.platform.isMacOS || + DesktopTools.platform.isLinux + ? await AudioService.init( + builder: () => MobileAudioService(playback), + config: const AudioServiceConfig( + androidNotificationChannelId: 'com.krtirtho.Spotube', + androidNotificationChannelName: 'Spotube', + androidNotificationOngoing: true, + ), + ) + : null; final smtc = DesktopTools.platform.isWindows ? WindowsAudioService(ref, playback) : null; - final mpris = - DesktopTools.platform.isLinux ? LinuxAudioService(ref, playback) : null; - if (mpris != null) { - playback.addListener((state) { - mpris.player.updateProperties(); - }); - audioPlayer.playerStateStream.listen((state) { - mpris.player.updateProperties(); - }); - audioPlayer.positionStream.listen((state) async { - await mpris.player.emitPropertiesChanged( - "org.mpris.MediaPlayer2.Player", - changedProperties: { - "Position": (await mpris.player.getPosition()).returnValues.first, - }, - ); - }); - } - - return AudioServices(mobile, smtc, mpris); + return AudioServices( + mobile, + smtc, + ); } Future addTrack(Track track) async { @@ -86,6 +68,5 @@ class AudioServices { void dispose() { smtc?.dispose(); - mpris?.dispose(); } } diff --git a/pubspec.lock b/pubspec.lock index 05059c0c..3dbc3cbf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.12" + audio_service_mpris: + dependency: "direct main" + description: + name: audio_service_mpris + sha256: "31be5de2db0c71b217157afce1974ac6d0ad329bd91deb1f19ad094d29340d8e" + url: "https://pub.dev" + source: hosted + version: "0.1.0" audio_service_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1965f04e..8e45a92a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -113,6 +113,7 @@ dependencies: path: plugins/window_size youtube_explode_dart: ^2.0.1 simple_icons: ^7.10.0 + audio_service_mpris: ^0.1.0 dev_dependencies: build_runner: ^2.3.2 From f3e331ecf733995da24c9b907efc5ed4bd02ffdd Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 16 Oct 2023 10:07:40 +0600 Subject: [PATCH 010/131] fix: add xdg-user-dirs as deps --- aur-struct/.SRCINFO | 1 + aur-struct/PKGBUILD | 2 +- linux/packaging/deb/make_config.yaml | 1 + linux/packaging/rpm/make_config.yaml | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/aur-struct/.SRCINFO b/aur-struct/.SRCINFO index 4f50a951..ae0b6d10 100644 --- a/aur-struct/.SRCINFO +++ b/aur-struct/.SRCINFO @@ -10,6 +10,7 @@ pkgbase = spotube-bin depends = libsecret depends = jsoncpp depends = libnotify + depends = xdg-user-dirs source = https://github.com/KRTirtho/spotube/releases/download/v2.3.0/Spotube-linux-x86_64.tar.xz md5sums = 8cd6a7385c5c75d203dccd762f1d63ec diff --git a/aur-struct/PKGBUILD b/aur-struct/PKGBUILD index 313cd308..4663c3ab 100644 --- a/aur-struct/PKGBUILD +++ b/aur-struct/PKGBUILD @@ -8,7 +8,7 @@ arch=(x86_64) url="https://github.com/KRTirtho/spotube/" license=('BSD-4-Clause') groups=() -depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify') +depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs') makedepends=() checkdepends=() optdepends=() diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml index 2cffd859..46493122 100644 --- a/linux/packaging/deb/make_config.yaml +++ b/linux/packaging/deb/make_config.yaml @@ -17,6 +17,7 @@ dependencies: - libnotify-bin - libjsoncpp25 - libmpv1 | libmpv2 + - xdg-user-dirs essential: false icon: assets/spotube-logo.png diff --git a/linux/packaging/rpm/make_config.yaml b/linux/packaging/rpm/make_config.yaml index 0e3e1624..00f4c20e 100644 --- a/linux/packaging/rpm/make_config.yaml +++ b/linux/packaging/rpm/make_config.yaml @@ -12,6 +12,7 @@ requires: - jsoncpp - libsecret - libnotify + - xdg-user-dirs display_name: Spotube From aef81a6d83d9bec0d5ab5ee4e2d7e6bdacb7de09 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 16 Oct 2023 10:42:44 +0600 Subject: [PATCH 011/131] cd: windows build silently failing --- .github/workflows/spotube-release-binary.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index ca10dc92..9475b489 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -78,6 +78,7 @@ jobs: make innoinstall flutter_distributor package --platform=windows --targets=exe --skip-clean mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe + if (!(Test-Path dist\Spotube-windows-x86_64-setup.exe)) { exit 1 } - name: Create Chocolatey Package and set hash if: ${{ inputs.channel == 'stable' }} From 082cf5d81fe7ac0e6bd80a85395ebb47e051cae1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 16 Oct 2023 11:25:34 +0600 Subject: [PATCH 012/131] cd: use windows 2019 --- .github/workflows/spotube-release-binary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 9475b489..70d94ef3 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -30,7 +30,7 @@ env: jobs: windows: - runs-on: windows-latest + runs-on: windows-2019 steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2.10.0 From 49419350bb664d7e8947a20bf521b80370cba971 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 16 Oct 2023 11:59:57 +0600 Subject: [PATCH 013/131] cd: use verbose --- .github/workflows/spotube-release-binary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 70d94ef3..6beedff0 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -76,7 +76,7 @@ jobs: run: | dart pub global activate flutter_distributor make innoinstall - flutter_distributor package --platform=windows --targets=exe --skip-clean + flutter_distributor package --platform=windows --targets=exe --skip-clean --flutter-build-args=verbose mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe if (!(Test-Path dist\Spotube-windows-x86_64-setup.exe)) { exit 1 } From 2f77a61b185774ea48aacd59c9e39f26e04c4416 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 16 Oct 2023 12:31:11 +0600 Subject: [PATCH 014/131] cd: individually specify files to upload to avoid silent errors --- .github/workflows/spotube-release-binary.yml | 33 ++++++-------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 6beedff0..eb75b03f 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -30,7 +30,7 @@ env: jobs: windows: - runs-on: windows-2019 + runs-on: windows-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2.10.0 @@ -76,9 +76,8 @@ jobs: run: | dart pub global activate flutter_distributor make innoinstall - flutter_distributor package --platform=windows --targets=exe --skip-clean --flutter-build-args=verbose + flutter_distributor package --platform=windows --targets=exe --skip-clean mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe - if (!(Test-Path dist\Spotube-windows-x86_64-setup.exe)) { exit 1 } - name: Create Chocolatey Package and set hash if: ${{ inputs.channel == 'stable' }} @@ -98,7 +97,9 @@ jobs: uses: actions/upload-artifact@v3 with: name: Spotube-Release-Binaries - path: dist/ + path: | + dist/Spotube-windows-x86_64.nupkg + dist/Spotube-windows-x86_64-setup.exe linux: runs-on: ubuntu-latest @@ -187,7 +188,11 @@ jobs: - uses: actions/upload-artifact@v3 with: name: Spotube-Release-Binaries - path: dist/ + path: | + dist/Spotube-linux-x86_64.AppImage + dist/Spotube-linux-x86_64.deb + dist/Spotube-linux-x86_64.rpm + dist/spotube-linux-${{ env.BUILD_VERSION }}-x86_64.tar.xz android: runs-on: ubuntu-latest @@ -321,24 +326,6 @@ jobs: name: Spotube-Release-Binaries path: | build/Spotube-macos-universal.dmg - - # linux_arm: - # runs-on: ubuntu-latest - # steps: - # - run: | - # sudo apt-get update -y - # sudo apt-get install -y curl - - # - name: Extract branch name - # shell: bash - # run: echo "BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV - - # - name: Trigger CircleCI Pipeline - # run: | - # curl -X POST https://circleci.com/api/v2/project/cci-f9azl/spotube/pipeline \ - # --header "Circle-Token: ${{secrets.CCI_TOKEN}}" \ - # --header "content-type: application/json" \ - # --data '{"branch": "${{env.BRANCH}}", "parameters":{"GHA_Action":"true","version":"${{inputs.version}}","channel":"${{inputs.channel}}","dry_run":${{inputs.dry_run}}}}' upload: runs-on: ubuntu-latest From bebc543e3cf4253d83f795aab0939f693b1e8e58 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 16 Oct 2023 12:56:51 +0600 Subject: [PATCH 015/131] cd: throw error if upload files not found --- .github/workflows/spotube-release-binary.yml | 66 ++++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index eb75b03f..cf029582 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -87,20 +87,22 @@ jobs: make choco mv dist/spotube.*.nupkg dist/Spotube-windows-x86_64.nupkg + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + if-no-files-found: error + name: Spotube-Release-Binaries + path: | + dist/Spotube-windows-x86_64.nupkg + dist/Spotube-windows-x86_64-setup.exe + - name: Debug With SSH When fails if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} uses: mxschmitt/action-tmate@v3 with: limit-access-to-actor: true - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - name: Spotube-Release-Binaries - path: | - dist/Spotube-windows-x86_64.nupkg - dist/Spotube-windows-x86_64-setup.exe - linux: runs-on: ubuntu-latest steps: @@ -179,14 +181,10 @@ jobs: mv dist/**/spotube-*-linux.rpm dist/Spotube-linux-x86_64.rpm mv dist/**/spotube-*-linux.AppImage dist/Spotube-linux-x86_64.AppImage - - name: Debug With SSH When fails - if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} - uses: mxschmitt/action-tmate@v3 - with: - limit-access-to-actor: true - uses: actions/upload-artifact@v3 with: + if-no-files-found: error name: Spotube-Release-Binaries path: | dist/Spotube-linux-x86_64.AppImage @@ -194,6 +192,13 @@ jobs: dist/Spotube-linux-x86_64.rpm dist/spotube-linux-${{ env.BUILD_VERSION }}-x86_64.tar.xz + - name: Debug With SSH When fails + if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} + uses: mxschmitt/action-tmate@v3 + with: + limit-access-to-actor: true + + android: runs-on: ubuntu-latest steps: @@ -254,20 +259,23 @@ jobs: flutter build appbundle --flavor ${{ inputs.channel }} mv build/app/outputs/bundle/${{ inputs.channel }}Release/app-${{ inputs.channel }}-release.aab build/Spotube-playstore-all-arch.aab + + - uses: actions/upload-artifact@v3 + with: + if-no-files-found: error + name: Spotube-Release-Binaries + path: | + build/Spotube-android-all-arch.apk + build/Spotube-playstore-all-arch.aab + - name: Debug With SSH When fails if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} uses: mxschmitt/action-tmate@v3 with: limit-access-to-actor: true - - uses: actions/upload-artifact@v3 - with: - name: Spotube-Release-Binaries - path: | - build/Spotube-android-all-arch.apk - build/Spotube-playstore-all-arch.aab - macos: + runs-on: macos-12 steps: - uses: actions/checkout@v4 @@ -315,20 +323,23 @@ jobs: mkdir -p build/${{ env.BUILD_VERSION }} appdmg appdmg.json build/Spotube-macos-universal.dmg + + - uses: actions/upload-artifact@v3 + with: + if-no-files-found: error + name: Spotube-Release-Binaries + path: | + build/Spotube-macos-universal.dmg + - name: Debug With SSH When fails if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} uses: mxschmitt/action-tmate@v3 with: limit-access-to-actor: true - - - uses: actions/upload-artifact@v3 - with: - name: Spotube-Release-Binaries - path: | - build/Spotube-macos-universal.dmg - + upload: runs-on: ubuntu-latest + needs: - windows - linux @@ -352,6 +363,7 @@ jobs: - uses: actions/upload-artifact@v3 with: + if-no-files-found: error name: Spotube-Release-Binaries path: | RELEASE.md5sum From 65e9dba82f60c529fdac2ed750b256513c81e74d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 16 Oct 2023 13:26:36 +0600 Subject: [PATCH 016/131] chore: bump version and generate CHANGELOG --- .github/workflows/spotube-release-binary.yml | 2 +- CHANGELOG.md | 38 ++++++++++++++++++++ pubspec.yaml | 2 +- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index cf029582..d461e296 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -4,7 +4,7 @@ on: inputs: version: description: Version to release (x.x.x) - default: 3.1.2 + default: 3.2.0 required: true channel: type: choice diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f3710f..3710d812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [3.2.0](https://github.com/KRTirtho/spotube/compare/v3.1.2...v3.2.0) (2023-10-16) + + +### Features + +* ability to select/copy lyrics [#802](https://github.com/KRTirtho/spotube/issues/802) ([0eb9ee8](https://github.com/KRTirtho/spotube/commit/0eb9ee8648bee43a8009e6752674b1be646c0916)) +* add Amoled theme [#724](https://github.com/KRTirtho/spotube/issues/724) ([5c5dbf6](https://github.com/KRTirtho/spotube/commit/5c5dbf69ecea95c92d3c3900ad690a500d75b4e2)) +* add audio normalization [#164](https://github.com/KRTirtho/spotube/issues/164) ([da10ab2](https://github.com/KRTirtho/spotube/commit/da10ab2e291d4ba4d3082b9a6ae535639fb8f1b7)) +* add restore default settings button ([94c3866](https://github.com/KRTirtho/spotube/commit/94c386638f2e5a42d21c8f157835443333ee6d5c)) +* configurable audio normalization switch ([c325911](https://github.com/KRTirtho/spotube/commit/c325911c0d87758a203a52df02179c1513bad3fd)) +* customizable stream/download file formats ([#757](https://github.com/KRTirtho/spotube/issues/757)) ([e54762b](https://github.com/KRTirtho/spotube/commit/e54762be6add6524ab614d103fc3557a101c75f4)) +* improve and unify the logging framework ([#738](https://github.com/KRTirtho/spotube/issues/738)) ([c7432bb](https://github.com/KRTirtho/spotube/commit/c7432bbd986d576a93957f0a22bdbca5c1e87f20)) +* LastFM scrobbling support ([#761](https://github.com/KRTirtho/spotube/issues/761)) ([f5bd907](https://github.com/KRTirtho/spotube/commit/f5bd90731d9abc19d684c8bcb231eff399e73023)) +* loading indicator for genre and personalized pages ([ffe8d9c](https://github.com/KRTirtho/spotube/commit/ffe8d9ca6da25cb3e6fd2c781d5ed3a7b919510e)) +* manual offline detection ([854ab89](https://github.com/KRTirtho/spotube/commit/854ab8910dffb2837c011d3439173a1f0ebe9c6c)) +* show error dialog on failed to login ([101c325](https://github.com/KRTirtho/spotube/commit/101c32523d3be8c05527261f6f63f939d388ad79)) +* sliding up player support ([083319f](https://github.com/KRTirtho/spotube/commit/083319fd2445ab179e3dcda0a6aeaca6f13dda29)) +* swipe to open player view ([#765](https://github.com/KRTirtho/spotube/issues/765)) ([9aee056](https://github.com/KRTirtho/spotube/commit/9aee0568bf42eed9fea8d517e960a010abf0ebf2)) +* thicken the scrollbars & make 'em interactive for mobile ([#764](https://github.com/KRTirtho/spotube/issues/764)) ([84a4bcd](https://github.com/KRTirtho/spotube/commit/84a4bcd948ab459489aaf6f39d6954776c3401d7)) +* **translations:** add Arabic Translations ([#740](https://github.com/KRTirtho/spotube/issues/740)) ([38493f9](https://github.com/KRTirtho/spotube/commit/38493f9dd75303890857a626c0b276ee1ab75bb2)) +* **translations:** add Farsi Translations ([#760](https://github.com/KRTirtho/spotube/issues/760)) ([fe42cfe](https://github.com/KRTirtho/spotube/commit/fe42cfe8430035d9b67dd158fb7b835ee4071497)) + + +### Bug Fixes + +* add libmpv1 for ubuntu-based systems ([#739](https://github.com/KRTirtho/spotube/issues/739)) ([5115e04](https://github.com/KRTirtho/spotube/commit/5115e041e78c20fce798a80f1d844bfc60746958)) +* add xdg-user-dirs as deps ([f3e331e](https://github.com/KRTirtho/spotube/commit/f3e331ecf733995da24c9b907efc5ed4bd02ffdd)) +* **android :** file_selector getDirectoryPath returns unusable content urls [#720](https://github.com/KRTirtho/spotube/issues/720) ([b3cf639](https://github.com/KRTirtho/spotube/commit/b3cf639ee2f970f4df9b394b260c3ad8a5732a9c)) +* **android:** audio doesn't resume on interruption end ([15d466a](https://github.com/KRTirtho/spotube/commit/15d466a04538ec70c3a0c132f2baaaf8690f8d4e)) +* **android:** system navigator back doesn't close player ([20d7092](https://github.com/KRTirtho/spotube/commit/20d70927c909347e84ffa8e456f8fab88d49d179)) +* get rid of overflow errors & status bar dark color ([5bb8231](https://github.com/KRTirtho/spotube/commit/5bb8231782287faf75c778fadb3a03ac774d14f0)) +* keyboard shortcuts changing route but not update sidebar ([2d93441](https://github.com/KRTirtho/spotube/commit/2d934411887bd104d8265236df5bf595c5ad2278)) +* last track repeats ([ed6ca00](https://github.com/KRTirtho/spotube/commit/ed6ca006ce237ed8d509cde9ed47cd6ea3396b63)) +* minor glitches ([e5d0aaf](https://github.com/KRTirtho/spotube/commit/e5d0aaf80d22b2291b6f7e7c5e18dd99ae1a7a82)) +* not fetching all followed artists ([#759](https://github.com/KRTirtho/spotube/issues/759)) ([c09a572](https://github.com/KRTirtho/spotube/commit/c09a5729251d8df820442d55477455f78c19c52e)) +* use audio_service_mpris plugin ([e29cc25](https://github.com/KRTirtho/spotube/commit/e29cc2578cab36729e235b117c1b5489c3452902)) +* valid non-ASCII characters get removed from downloaded file name [#745](https://github.com/KRTirtho/spotube/issues/745) ([a7e102f](https://github.com/KRTirtho/spotube/commit/a7e102ffc726d00df369560ec9a7f742f9d387bb)) + ## [3.1.2](https://github.com/KRTirtho/spotube/compare/v3.1.1...v3.1.2) (2023-09-15) diff --git a/pubspec.yaml b/pubspec.yaml index 8e45a92a..04f2d8b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El publish_to: "none" -version: 3.1.2+24 +version: 3.2.0+25 homepage: https://spotube.krtirtho.dev repository: https://github.com/KRTirtho/spotube From 433ae3d0c7d590b7580da3fe033224f4aa32ba9f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 16 Oct 2023 13:39:47 +0600 Subject: [PATCH 017/131] chore: generate library credits and finish all translations --- README.md | 14 +- lib/l10n/app_ar.arb | 19 ++- lib/l10n/app_bn.arb | 19 ++- lib/l10n/app_ca.arb | 19 ++- lib/l10n/app_de.arb | 19 ++- lib/l10n/app_es.arb | 19 ++- lib/l10n/app_fa.arb | 19 ++- lib/l10n/app_fr.arb | 19 ++- lib/l10n/app_hi.arb | 19 ++- lib/l10n/app_ja.arb | 19 ++- lib/l10n/app_pl.arb | 19 ++- lib/l10n/app_pt.arb | 19 ++- lib/l10n/app_ru.arb | 19 ++- lib/l10n/app_uk.arb | 19 ++- lib/l10n/app_zh.arb | 19 ++- untranslated_messages.json | 282 +------------------------------------ 16 files changed, 261 insertions(+), 301 deletions(-) diff --git a/README.md b/README.md index f1982bb6..71589794 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds. 1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button. 1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets. +1. [catcher_2](https://github.com/ThexXTURBOXx/catcher_2) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer. 1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections. 1. [cupertino_icons](https://pub.dev/packages/cupertino_icons) - Default icons asset for Cupertino widgets based on Apple styled icons 1. [curved_navigation_bar](https://github.com/rafalbednarczuk/curved_navigation_bar) - Stunning Animating Curved Shape Navigation Bar. Adjustable color, background color, animation curve, animation duration. @@ -215,9 +216,6 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration. 1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times. 1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI. -1. [fl_query](https://fl-query.vercel.app) - Asynchronous data caching, refetching & invalidation library for Flutter -1. [fl_query_hooks](https://fl-query.vercel.app) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter -1. [fl_query_devtools](https://fl-query.vercel.app) - Devtools support for Fl-Query 1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft. 1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite. 1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices. @@ -238,7 +236,6 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser. 1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests. 1. [image_picker](https://pub.dev/packages/image_picker) - Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. -1. [internet_connection_checker](https://github.com/RounakTadvi/internet_connection_checker/tree/main) - A pure Dart library that checks for internet by opening a socket to a list of specified addresses, each with individual port and timeout. Defaults are provided for convenience. 1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues. 1. [introduction_screen](https://github.com/pyozer/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities 1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package. @@ -265,11 +262,13 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS 1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos. 1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. -1. [uuid](https://github.com/Daegalus/dart-uuid) - RFC4122 (v1, v4, v5, v6, v7, v8) UUID Generator and Parser for Dart +1. [uuid](https://pub.dev/packages/uuid) - RFC4122 (v1, v4, v5, v6, v7, v8) UUID Generator and Parser for Dart 1. [version](https://github.com/dartninja/version) - Provides a simple class for parsing and comparing semantic versions as defined by http://semver.org/ 1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback. 1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window. 1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key. +1. [simple_icons](https://jlnrrg.github.io/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands. +1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification. 1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation. 1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied. 1. [flutter_distributor](https://distributor.leanflutter.org) - A complete tool for packaging and publishing your Flutter apps. @@ -280,8 +279,11 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes. 1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information. 1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting. -1. [catcher](https://github.com/jhomlala/catcher) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer. +1. [fl_query](https://fl-query.vercel.app) - Asynchronous data caching, refetching & invalidation library for Flutter +1. [fl_query_hooks](https://fl-query.vercel.app) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter +1. [fl_query_devtools](https://fl-query.vercel.app) - Devtools support for Fl-Query 1. [flutter_desktop_tools](https://github.com/KRTirtho/flutter_desktop_tools) - Essential collection of tools for flutter desktop app development +1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark. 1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter. diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 37f11ba8..f587710c 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -262,5 +262,22 @@ "connection_restored": "تمت استعادة اتصالك بالإنترنت", "use_system_title_bar": "استخدم شريط عنوان النظام", "crunching_results": "تدمير النتائج", - "search_to_get_results": "إبحث للحصول على النتائج" + "search_to_get_results": "إبحث للحصول على النتائج", + "use_amoled_mode": "استخدم وضع AMOLED", + "pitch_dark_theme": "موضوع دارت الأسود الفحمي", + "normalize_audio": "تطبيع الصوت", + "change_cover": "تغيير الغلاف", + "add_cover": "إضافة غلاف", + "restore_defaults": "استعادة الإعدادات الافتراضية", + "download_music_codec": "تنزيل ترميز الموسيقى", + "streaming_music_codec": "ترميز الموسيقى بالتدفق", + "login_with_lastfm": "تسجيل الدخول باستخدام Last.fm", + "connect": "اتصال", + "disconnect_lastfm": "قطع الاتصال بـ Last.fm", + "disconnect": "قطع الاتصال", + "username": "اسم المستخدم", + "password": "كلمة المرور", + "login": "تسجيل الدخول", + "login_with_your_lastfm": "تسجيل الدخول باستخدام حساب Last.fm الخاص بك", + "scrobble_to_lastfm": "تسجيل الاستماع على Last.fm" } \ No newline at end of file diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index bfae9d23..02402179 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -262,5 +262,22 @@ "update_playlist": "প্লেলিস্ট আপডেট করুন", "update": "আপডেট", "crunching_results": "ফলাফল বিশ্লেষণ করা হচ্ছে...", - "search_to_get_results": "ফলাফল পেতে খোঁজ করুন" + "search_to_get_results": "ফলাফল পেতে খোঁজ করুন", + "use_amoled_mode": "AMOLED মোড ব্যবহার করুন", + "pitch_dark_theme": "পিচ ব্ল্যাক ডার্ট থিম", + "normalize_audio": "অডিও স্তরমান করুন", + "change_cover": "কভার পরিবর্তন করুন", + "add_cover": "কভার যোগ করুন", + "restore_defaults": "ডিফল্ট সেটিংস পুনরুদ্ধার করুন", + "download_music_codec": "সঙ্গীত কোডেক ডাউনলোড করুন", + "streaming_music_codec": "স্ট্রিমিং সঙ্গীত কোডেক", + "login_with_lastfm": "Last.fm দিয়ে লগইন করুন", + "connect": "সংযোগ করুন", + "disconnect_lastfm": "Last.fm সংযোগ বিচ্ছিন্ন করুন", + "disconnect": "সংযোগ বিচ্ছিন্ন করুন", + "username": "ব্যবহারকারীর নাম", + "password": "পাসওয়ার্ড", + "login": "লগইন", + "login_with_your_lastfm": "আপনার Last.fm অ্যাকাউন্ট দিয়ে লগইন করুন", + "scrobble_to_lastfm": "Last.fm এ স্ক্রবল করুন" } \ No newline at end of file diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index a40ae8c4..81a11082 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -262,5 +262,22 @@ "update_playlist": "Actualitzar la llista de reproducció", "update": "Actualitzar", "crunching_results": "Processant resultats...", - "search_to_get_results": "Cerca per obtenir resultats" + "search_to_get_results": "Cerca per obtenir resultats", + "use_amoled_mode": "Utilitza el mode AMOLED", + "pitch_dark_theme": "Tema de dart negre intens", + "normalize_audio": "Normalitza l'àudio", + "change_cover": "Canvia la coberta", + "add_cover": "Afegeix una coberta", + "restore_defaults": "Restaura els valors per defecte", + "download_music_codec": "Descarrega el codec de música", + "streaming_music_codec": "Codec de música en streaming", + "login_with_lastfm": "Inicia la sessió amb Last.fm", + "connect": "Connecta", + "disconnect_lastfm": "Desconnecta de Last.fm", + "disconnect": "Desconnecta", + "username": "Nom d'usuari", + "password": "Contrasenya", + "login": "Inicia la sessió", + "login_with_your_lastfm": "Inicia la sessió amb el teu compte de Last.fm", + "scrobble_to_lastfm": "Scrobble a Last.fm" } \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 10c59ebc..339a8d65 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -262,5 +262,22 @@ "update_playlist": "Wiedergabeliste aktualisieren", "update": "Aktualisieren", "crunching_results": "Ergebnisse werden verarbeitet...", - "search_to_get_results": "Suche, um Ergebnisse zu erhalten" + "search_to_get_results": "Suche, um Ergebnisse zu erhalten", + "use_amoled_mode": "AMOLED-Modus verwenden", + "pitch_dark_theme": "Pitch Black Dart Theme", + "normalize_audio": "Audio normalisieren", + "change_cover": "Cover ändern", + "add_cover": "Cover hinzufügen", + "restore_defaults": "Standardeinstellungen wiederherstellen", + "download_music_codec": "Musik-Codec herunterladen", + "streaming_music_codec": "Streaming-Musik-Codec", + "login_with_lastfm": "Mit Last.fm anmelden", + "connect": "Verbinden", + "disconnect_lastfm": "Last.fm trennen", + "disconnect": "Trennen", + "username": "Benutzername", + "password": "Passwort", + "login": "Anmelden", + "login_with_your_lastfm": "Mit Ihrem Last.fm-Konto anmelden", + "scrobble_to_lastfm": "Auf Last.fm scrobbeln" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index fec85abc..f617705e 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -262,5 +262,22 @@ "update_playlist": "Actualizar lista de reproducción", "update": "Actualizar", "crunching_results": "Procesando resultados...", - "search_to_get_results": "Buscar para obtener resultados" + "search_to_get_results": "Buscar para obtener resultados", + "use_amoled_mode": "Usar modo AMOLED", + "pitch_dark_theme": "Tema oscuro de dart", + "normalize_audio": "Normalizar audio", + "change_cover": "Cambiar portada", + "add_cover": "Agregar portada", + "restore_defaults": "Restaurar valores predeterminados", + "download_music_codec": "Descargar códec de música", + "streaming_music_codec": "Códec de música en streaming", + "login_with_lastfm": "Iniciar sesión con Last.fm", + "connect": "Conectar", + "disconnect_lastfm": "Desconectar de Last.fm", + "disconnect": "Desconectar", + "username": "Nombre de usuario", + "password": "Contraseña", + "login": "Iniciar sesión", + "login_with_your_lastfm": "Iniciar sesión con tu cuenta de Last.fm", + "scrobble_to_lastfm": "Scrobble a Last.fm" } \ No newline at end of file diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index d8156a5f..5454b13b 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -262,5 +262,22 @@ "connection_restored": "اتصال به اینترنت شما بازیابی شد ", "use_system_title_bar": "از نوار عنوان سیستم استفاده کنید ", "crunching_results": "نتایج خرد کردن...", - "search_to_get_results": "جستجو کنید تا به نتیجه برسید" + "search_to_get_results": "جستجو کنید تا به نتیجه برسید", + "use_amoled_mode": "استفاده از حالت AMOLED", + "pitch_dark_theme": "تم تیره دارت", + "normalize_audio": "نرمال کردن صدا", + "change_cover": "تغییر جلد", + "add_cover": "افزودن جلد", + "restore_defaults": "بازیابی پیش فرض ها", + "download_music_codec": "دانلود کدک موسیقی", + "streaming_music_codec": "کدک موسیقی استریمینگ", + "login_with_lastfm": "ورود با Last.fm", + "connect": "اتصال", + "disconnect_lastfm": "قطع ارتباط با Last.fm", + "disconnect": "قطع ارتباط", + "username": "نام کاربری", + "password": "رمز عبور", + "login": "ورود", + "login_with_your_lastfm": "ورود با حساب کاربری Last.fm خود", + "scrobble_to_lastfm": "Scrobble به Last.fm" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 30bfa883..fbe5c335 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -262,5 +262,22 @@ "update_playlist": "Mettre à jour la playlist", "update": "Mettre à jour", "crunching_results": "Traitement des résultats...", - "search_to_get_results": "Recherche pour obtenir des résultats" + "search_to_get_results": "Recherche pour obtenir des résultats", + "use_amoled_mode": "Utiliser le mode AMOLED", + "pitch_dark_theme": "Thème Dart noir intense", + "normalize_audio": "Normaliser l'audio", + "change_cover": "Changer de couverture", + "add_cover": "Ajouter une couverture", + "restore_defaults": "Restaurer les valeurs par défaut", + "download_music_codec": "Télécharger le codec musical", + "streaming_music_codec": "Codec de musique en streaming", + "login_with_lastfm": "Se connecter avec Last.fm", + "connect": "Connecter", + "disconnect_lastfm": "Déconnecter de Last.fm", + "disconnect": "Déconnecter", + "username": "Nom d'utilisateur", + "password": "Mot de passe", + "login": "Se connecter", + "login_with_your_lastfm": "Se connecter avec votre compte Last.fm", + "scrobble_to_lastfm": "Scrobble à Last.fm" } \ No newline at end of file diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 8beda1e9..d33f41dc 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -262,5 +262,22 @@ "update_playlist": "प्लेलिस्ट अपडेट करें", "update": "अपडेट करें", "crunching_results": "परिणाम को प्रसंस्कृत किया जा रहा है...", - "search_to_get_results": "परिणाम प्राप्त करने के लिए खोजें" + "search_to_get_results": "परिणाम प्राप्त करने के लिए खोजें", + "use_amoled_mode": "AMOLED मोड का उपयोग करें", + "pitch_dark_theme": "पिच ब्लैक डार्ट थीम", + "normalize_audio": "ऑडियो को सामान्य करें", + "change_cover": "कवर बदलें", + "add_cover": "कवर जोड़ें", + "restore_defaults": "डिफ़ॉल्ट सेटिंग्स को बहाल करें", + "download_music_codec": "संगीत कोडेक डाउनलोड करें", + "streaming_music_codec": "स्ट्रीमिंग संगीत कोडेक", + "login_with_lastfm": "Last.fm से लॉगिन करें", + "connect": "कनेक्ट करें", + "disconnect_lastfm": "Last.fm से डिस्कनेक्ट करें", + "disconnect": "डिस्कनेक्ट करें", + "username": "उपयोगकर्ता नाम", + "password": "पासवर्ड", + "login": "लॉग इन करें", + "login_with_your_lastfm": "अपने Last.fm अकाउंट से लॉगिन करें", + "scrobble_to_lastfm": "Last.fm पर स्क्रॉबल करें" } \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index c13b620a..50c9369f 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -262,5 +262,22 @@ "update_playlist": "プレイリストを更新", "update": "更新", "crunching_results": "結果を処理中...", - "search_to_get_results": "結果を取得するために検索" + "search_to_get_results": "結果を取得するために検索", + "use_amoled_mode": "AMOLEDモードを使用する", + "pitch_dark_theme": "ピッチブラックダートテーマ", + "normalize_audio": "オーディオを正規化する", + "change_cover": "カバーを変更する", + "add_cover": "カバーを追加する", + "restore_defaults": "デフォルト値に戻す", + "download_music_codec": "音楽コーデックをダウンロードする", + "streaming_music_codec": "ストリーミング音楽コーデック", + "login_with_lastfm": "Last.fmでログインする", + "connect": "接続する", + "disconnect_lastfm": "Last.fmから切断する", + "disconnect": "切断する", + "username": "ユーザー名", + "password": "パスワード", + "login": "ログインする", + "login_with_your_lastfm": "あなたのLast.fmアカウントでログインする", + "scrobble_to_lastfm": "Last.fmにスクロブルする" } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 0f384b2b..1a946615 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -262,5 +262,22 @@ "update_playlist": "Zaktualizuj playlistę", "update": "Aktualizuj", "crunching_results": "Przetwarzanie wyników...", - "search_to_get_results": "Szukaj, aby uzyskać wyniki" + "search_to_get_results": "Szukaj, aby uzyskać wyniki", + "use_amoled_mode": "Tryb AMOLED", + "pitch_dark_theme": "Ciemny motyw", + "normalize_audio": "Normalizuj dźwięk", + "change_cover": "Zmień okładkę", + "add_cover": "Dodaj okładkę", + "restore_defaults": "Przywróć domyślne", + "download_music_codec": "Pobierz kodek muzyczny", + "streaming_music_codec": "Kodek strumieniowy muzyki", + "login_with_lastfm": "Zaloguj się z Last.fm", + "connect": "Połącz", + "disconnect_lastfm": "Rozłącz z Last.fm", + "disconnect": "Rozłącz", + "username": "Nazwa użytkownika", + "password": "Hasło", + "login": "Zaloguj", + "login_with_your_lastfm": "Zaloguj się na swoje konto Last.fm", + "scrobble_to_lastfm": "Scrobbluj do Last.fm" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 654a8c59..97df3db3 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -262,5 +262,22 @@ "update_playlist": "Atualizar lista de reprodução", "update": "Atualizar", "crunching_results": "Processando resultados...", - "search_to_get_results": "Pesquisar para obter resultados" + "search_to_get_results": "Pesquisar para obter resultados", + "use_amoled_mode": "Modo AMOLED", + "pitch_dark_theme": "Tema escuro", + "normalize_audio": "Normalizar áudio", + "change_cover": "Alterar capa", + "add_cover": "Adicionar capa", + "restore_defaults": "Restaurar padrões", + "download_music_codec": "Descarregar codec de música", + "streaming_music_codec": "Codec de streaming de música", + "login_with_lastfm": "Iniciar sessão com o Last.fm", + "connect": "Ligar", + "disconnect_lastfm": "Desligar do Last.fm", + "disconnect": "Desligar", + "username": "Nome de utilizador", + "password": "Palavra-passe", + "login": "Iniciar sessão", + "login_with_your_lastfm": "Inicie sessão na sua conta Last.fm", + "scrobble_to_lastfm": "Scrobble para o Last.fm" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index e6e00ee0..098e73c7 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -262,5 +262,22 @@ "update_playlist": "Обновить плейлист", "update": "Обновить", "crunching_results": "Обработка результатов...", - "search_to_get_results": "Поиск для получения результатов" + "search_to_get_results": "Поиск для получения результатов", + "use_amoled_mode": "Режим AMOLED", + "pitch_dark_theme": "Темная тема", + "normalize_audio": "Нормализовать звук", + "change_cover": "Изменить обложку", + "add_cover": "Добавить обложку", + "restore_defaults": "Восстановить настройки по умолчанию", + "download_music_codec": "Загрузить кодек для музыки", + "streaming_music_codec": "Кодек потоковой передачи музыки", + "login_with_lastfm": "Войти с помощью Last.fm", + "connect": "Подключить", + "disconnect_lastfm": "Отключиться от Last.fm", + "disconnect": "Отключить", + "username": "Имя пользователя", + "password": "Пароль", + "login": "Войти", + "login_with_your_lastfm": "Войти в свою учетную запись Last.fm", + "scrobble_to_lastfm": "Скробблинг на Last.fm" } \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 1b926dea..fa0877d1 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -262,5 +262,22 @@ "connection_restored": "Ваше інтернет-з'єднання відновлено", "use_system_title_bar": "Використовувати системний заголовок", "crunching_results": "Опрацювання результатів...", - "search_to_get_results": "Почніть пошук, щоб отримати результати" + "search_to_get_results": "Почніть пошук, щоб отримати результати", + "use_amoled_mode": "Режим AMOLED", + "pitch_dark_theme": "Темна тема", + "normalize_audio": "Нормалізувати звук", + "change_cover": "Змінити обкладинку", + "add_cover": "Додати обкладинку", + "restore_defaults": "Відновити налаштування за замовчуванням", + "download_music_codec": "Завантажити кодек для музики", + "streaming_music_codec": "Кодек потокової передачі музики", + "login_with_lastfm": "Увійти з Last.fm", + "connect": "Підключити", + "disconnect_lastfm": "Відключитися від Last.fm", + "disconnect": "Відключити", + "username": "Ім'я користувача", + "password": "Пароль", + "login": "Увійти", + "login_with_your_lastfm": "Увійти в свій обліковий запис Last.fm", + "scrobble_to_lastfm": "Скробблінг на Last.fm" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 92c414f0..9936c812 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -262,5 +262,22 @@ "update_playlist": "更新播放列表", "update": "更新", "crunching_results": "处理结果中...", - "search_to_get_results": "搜索以获取结果" + "search_to_get_results": "搜索以获取结果", + "use_amoled_mode": "使用 AMOLED 模式", + "pitch_dark_theme": "深色主题", + "normalize_audio": "标准化音频", + "change_cover": "更改封面", + "add_cover": "添加封面", + "restore_defaults": "恢复默认值", + "download_music_codec": "下载音乐编解码器", + "streaming_music_codec": "流媒体音乐编解码器", + "login_with_lastfm": "使用 Last.fm 登录", + "connect": "连接", + "disconnect_lastfm": "断开 Last.fm 连接", + "disconnect": "断开连接", + "username": "用户名", + "password": "密码", + "login": "登录", + "login_with_your_lastfm": "使用您的 Last.fm 帐户登录", + "scrobble_to_lastfm": "在 Last.fm 上记录播放" } \ No newline at end of file diff --git a/untranslated_messages.json b/untranslated_messages.json index 0921e8db..9e26dfee 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,281 +1 @@ -{ - "ar": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "bn": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "ca": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "de": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "es": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "fa": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "fr": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "hi": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "ja": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "pl": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "pt": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "ru": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "uk": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ], - - "zh": [ - "use_amoled_mode", - "pitch_dark_theme", - "normalize_audio", - "change_cover", - "add_cover", - "restore_defaults", - "download_music_codec", - "streaming_music_codec", - "login_with_lastfm", - "connect", - "disconnect_lastfm", - "disconnect", - "username", - "password", - "login", - "login_with_your_lastfm", - "scrobble_to_lastfm" - ] -} +{} \ No newline at end of file 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 018/131] 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: Debian/Ubuntu Download -

Then run: sudo apt install Spotube-linux-x86_64.deb

+

Then run: sudo apt install ./Spotube-linux-x86_64.deb

From d056dbf9eeef7033dbc012d0c05800063e820042 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 29 Oct 2023 19:02:39 +0600 Subject: [PATCH 019/131] fix: android invalid download location Download not starting or not explaining error #720 --- lib/pages/settings/settings.dart | 21 +++++++++++++-------- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index e319997a..5632a89a 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -1,5 +1,6 @@ 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'; @@ -47,15 +48,19 @@ class SettingsPage extends HookConsumerWidget { }, []); final pickDownloadLocation = useCallback(() async { - String? dirStr = await getDirectoryPath( - initialDirectory: preferences.downloadLocation, - ); - if (dirStr == null) return; - if (DesktopTools.platform.isAndroid && dirStr.startsWith("content://")) { - dirStr = - "/storage/emulated/0/${Uri.decodeFull(dirStr).split("primary:").last}"; + 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.setDownloadLocation(dirStr); }, [preferences.downloadLocation]); return SafeArea( diff --git a/pubspec.lock b/pubspec.lock index 3dbc3cbf..bd50225a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -513,6 +513,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "903dd4ba13eae7cef64acc480e91bf54c3ddd23b5b90b639c170f3911e489620" + url: "https://pub.dev" + source: hosted + version: "6.0.0" file_selector: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 04f2d8b8..75b14bc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -114,6 +114,7 @@ dependencies: youtube_explode_dart: ^2.0.1 simple_icons: ^7.10.0 audio_service_mpris: ^0.1.0 + file_picker: ^6.0.0 dev_dependencies: build_runner: ^2.3.2 From 286ef83e8ec516db70019398d9e3e724437a4172 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 29 Oct 2023 19:16:46 +0600 Subject: [PATCH 020/131] fix: trim login field padding --- lib/components/desktop_login/login_form.dart | 2 +- lib/pages/lastfm_login/lastfm_login.dart | 2 +- lib/provider/authentication_provider.dart | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/components/desktop_login/login_form.dart b/lib/components/desktop_login/login_form.dart index b9783f87..f2b183f4 100644 --- a/lib/components/desktop_login/login_form.dart +++ b/lib/components/desktop_login/login_form.dart @@ -63,7 +63,7 @@ class TokenLoginForm extends HookConsumerWidget { return; } final cookieHeader = - "sp_dc=${directCodeController.text}; sp_key=${keyCodeController.text}"; + "sp_dc=${directCodeController.text.trim()}; sp_key=${keyCodeController.text.trim()}"; authenticationNotifier.setCredentials( await AuthenticationCredentials.fromCookie( diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart index f77d0abb..4280328f 100644 --- a/lib/pages/lastfm_login/lastfm_login.dart +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -108,7 +108,7 @@ class LastFMLoginPage extends HookConsumerWidget { return; } await scrobblerNotifier.login( - username.text, + username.text.trim(), password.text, ); router.pop(); diff --git a/lib/provider/authentication_provider.dart b/lib/provider/authentication_provider.dart index f1cf58ec..cd77e7bb 100644 --- a/lib/provider/authentication_provider.dart +++ b/lib/provider/authentication_provider.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:fl_query/fl_query.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; @@ -51,7 +52,8 @@ class AuthenticationCredentials { ), ); } catch (e) { - if (rootNavigatorKey?.currentContext != null) { + if (rootNavigatorKey?.currentContext != null && + await QueryClient.connectivity.isConnected) { showPromptDialog( context: rootNavigatorKey!.currentContext!, title: rootNavigatorKey!.currentContext!.l10n From 58e569864dddd74c3064624998dfc184046e97eb Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 29 Oct 2023 19:51:53 +0600 Subject: [PATCH 021/131] fix: last track of queue keeps repeating #718 --- lib/components/library/user_local_tracks.dart | 35 +---------------- .../shared/track_table/track_tile.dart | 4 +- lib/hooks/use_get_storage_perms.dart | 38 +++++++++++++++++++ lib/main.dart | 2 + .../audio_player/mk_state_player.dart | 3 -- 5 files changed, 44 insertions(+), 38 deletions(-) create mode 100644 lib/hooks/use_get_storage_perms.dart diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index 50ae64be..c7cd0682 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:catcher_2/catcher_2.dart'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -12,7 +11,6 @@ import 'package:metadata_god/metadata_god.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -22,15 +20,12 @@ import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; import 'package:spotube/components/shared/track_table/track_tile.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_async_effect.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; -import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' - show FfiException; +import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException; const supportedAudioTypes = [ "audio/webm", @@ -162,40 +157,12 @@ class UserLocalTracks extends HookConsumerWidget { final trackSnapshot = ref.watch(localTracksProvider); final isPlaylistPlaying = playlist.containsTracks(trackSnapshot.value ?? []); - final isMounted = useIsMounted(); final searchController = useTextEditingController(); useValueListenable(searchController); final searchFocus = useFocusNode(); final isFiltering = useState(false); - useAsyncEffect( - () async { - if (!kIsMobile) return; - - final androidInfo = await DeviceInfoPlugin().androidInfo; - - final hasNoStoragePerm = androidInfo.version.sdkInt < 33 && - !await Permission.storage.isGranted && - !await Permission.storage.isLimited; - - final hasNoAudioPerm = androidInfo.version.sdkInt >= 33 && - !await Permission.audio.isGranted && - !await Permission.audio.isLimited; - - if (hasNoStoragePerm) { - await Permission.storage.request(); - if (isMounted()) ref.refresh(localTracksProvider); - } - if (hasNoAudioPerm) { - await Permission.audio.request(); - if (isMounted()) ref.refresh(localTracksProvider); - } - }, - null, - [], - ); - return Column( children: [ Padding( diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index 0666b7f9..ff1b314b 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -91,7 +91,9 @@ class TrackTile extends HookConsumerWidget { isLoading.value = true; await onTap?.call(); } finally { - isLoading.value = false; + if (context.mounted) { + isLoading.value = false; + } } }, onLongPress: onLongPress, diff --git a/lib/hooks/use_get_storage_perms.dart b/lib/hooks/use_get_storage_perms.dart new file mode 100644 index 00000000..d83c60f6 --- /dev/null +++ b/lib/hooks/use_get_storage_perms.dart @@ -0,0 +1,38 @@ +import 'package:device_info_plus/device_info_plus.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:permission_handler/permission_handler.dart'; +import 'package:spotube/components/library/user_local_tracks.dart'; +import 'package:spotube/hooks/use_async_effect.dart'; + +void useGetStoragePermissions(WidgetRef ref) { + final isMounted = useIsMounted(); + + useAsyncEffect( + () async { + if (!DesktopTools.platform.isMobile) return; + + final androidInfo = await DeviceInfoPlugin().androidInfo; + + final hasNoStoragePerm = androidInfo.version.sdkInt < 33 && + !await Permission.storage.isGranted && + !await Permission.storage.isLimited; + + final hasNoAudioPerm = androidInfo.version.sdkInt >= 33 && + !await Permission.audio.isGranted && + !await Permission.audio.isLimited; + + if (hasNoStoragePerm) { + await Permission.storage.request(); + if (isMounted()) ref.refresh(localTracksProvider); + } + if (hasNoAudioPerm) { + await Permission.audio.request(); + if (isMounted()) ref.refresh(localTracksProvider); + } + }, + null, + [], + ); +} diff --git a/lib/main.dart b/lib/main.dart index f8c3aa8c..b92dfaf1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/hooks/use_disable_battery_optimizations.dart'; +import 'package:spotube/hooks/use_get_storage_perms.dart'; import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/models/matched_track.dart'; @@ -181,6 +182,7 @@ class SpotubeState extends ConsumerState { }, []); useDisableBatteryOptimizations(); + useGetStoragePermissions(ref); final lightTheme = useMemoized( () => theme(paletteColor ?? accentMaterialColor, Brightness.light, false), diff --git a/lib/services/audio_player/mk_state_player.dart b/lib/services/audio_player/mk_state_player.dart index 386b0a0e..af94a0e8 100644 --- a/lib/services/audio_player/mk_state_player.dart +++ b/lib/services/audio_player/mk_state_player.dart @@ -174,9 +174,6 @@ class MkPlayerWithState extends Player { case PlaylistMode.none: // Fixes auto-repeating the last track await super.stop(); - await Future.delayed(const Duration(seconds: 2), () { - super.open(_playlist!.medias[_playlist!.index], play: false); - }); break; default: } From 4956bf367baae39c88b5de7c6c136513a14f8ad2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 29 Oct 2023 19:54:58 +0600 Subject: [PATCH 022/131] fix: shuffle doesn't move active track to top --- lib/services/audio_player/mk_state_player.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/services/audio_player/mk_state_player.dart b/lib/services/audio_player/mk_state_player.dart index af94a0e8..a556afec 100644 --- a/lib/services/audio_player/mk_state_player.dart +++ b/lib/services/audio_player/mk_state_player.dart @@ -96,7 +96,10 @@ class MkPlayerWithState extends Player { if (shuffle) { _tempMedias = _playlist!.medias; final active = _playlist!.medias[_playlist!.index]; - final newMedias = _playlist!.medias.toList()..shuffle(); + final newMedias = _playlist!.medias.toList() + ..shuffle() + ..remove(active) + ..insert(0, active); playlist = _playlist!.copyWith( medias: newMedias, index: newMedias.indexOf(active), From 83c0b49da962d9f3d40de9525f90f0b320e8f7b8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 29 Oct 2023 20:19:03 +0600 Subject: [PATCH 023/131] fix: 0:00 media duration in queue after application restart #782 --- lib/extensions/track.dart | 41 ++++++++++--------- lib/models/local_track.dart | 20 +-------- lib/models/spotube_track.dart | 20 +-------- .../proxy_playlist/proxy_playlist.dart | 12 +++--- 4 files changed, 33 insertions(+), 60 deletions(-) diff --git a/lib/extensions/track.dart b/lib/extensions/track.dart index e17e851e..51498b33 100644 --- a/lib/extensions/track.dart +++ b/lib/extensions/track.dart @@ -4,26 +4,29 @@ import 'package:spotube/extensions/artist_simple.dart'; extension TrackJson on Track { Map toJson() { + return TrackJson.trackToJson(this); + } + + static Map trackToJson(Track track) { return { - "album": album?.toJson(), - "artists": artists?.map((artist) => artist.toJson()).toList(), - "availableMarkets": availableMarkets?.map((e) => e.name).toList(), - "discNumber": discNumber, - "duration": duration.toString(), - "durationMs": durationMs, - "explicit": explicit, - // "externalIds": externalIds, - // "externalUrls": externalUrls, - "href": href, - "id": id, - "isPlayable": isPlayable, - // "linkedFrom": linkedFrom, - "name": name, - "popularity": popularity, - "previewUrl": previewUrl, - "trackNumber": trackNumber, - "type": type, - "uri": uri, + "album": track.album?.toJson(), + "artists": track.artists?.map((artist) => artist.toJson()).toList(), + "available_markets": track.availableMarkets?.map((e) => e.name).toList(), + "disc_number": track.discNumber, + "duration_ms": track.durationMs, + "explicit": track.explicit, + // "external_ids"track.: externalIds, + // "external_urls"track.: externalUrls, + "href": track.href, + "id": track.id, + "is_playable": track.isPlayable, + // "linked_from"track.: linkedFrom, + "name": track.name, + "popularity": track.popularity, + "preview_rrl": track.previewUrl, + "track_number": track.trackNumber, + "type": track.type, + "uri": track.uri, }; } } diff --git a/lib/models/local_track.dart b/lib/models/local_track.dart index e297e974..134cd327 100644 --- a/lib/models/local_track.dart +++ b/lib/models/local_track.dart @@ -1,6 +1,5 @@ import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/album_simple.dart'; -import 'package:spotube/extensions/artist_simple.dart'; +import 'package:spotube/extensions/track.dart'; class LocalTrack extends Track { final String path; @@ -38,22 +37,7 @@ class LocalTrack extends Track { Map toJson() { return { - "album": album?.toJson(), - "artists": artists?.map((artist) => artist.toJson()).toList(), - "availableMarkets": availableMarkets?.map((m) => m.name), - "discNumber": discNumber, - "duration": duration.toString(), - "durationMs": durationMs, - "explicit": explicit, - "href": href, - "id": id, - "isPlayable": isPlayable, - "name": name, - "popularity": popularity, - "previewUrl": previewUrl, - "trackNumber": trackNumber, - "type": type, - "uri": uri, + ...TrackJson.trackToJson(this), 'path': path, }; } diff --git a/lib/models/spotube_track.dart b/lib/models/spotube_track.dart index a8b94ef5..68641010 100644 --- a/lib/models/spotube_track.dart +++ b/lib/models/spotube_track.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/album_simple.dart'; -import 'package:spotube/extensions/artist_simple.dart'; +import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/matched_track.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/youtube/youtube.dart'; @@ -264,22 +263,7 @@ class SpotubeTrack extends Track { Map toJson() { return { // super values - "album": album?.toJson(), - "artists": artists?.map((artist) => artist.toJson()).toList(), - "availableMarkets": availableMarkets?.map((m) => m.name), - "discNumber": discNumber, - "duration": duration.toString(), - "durationMs": durationMs, - "explicit": explicit, - "href": href, - "id": id, - "isPlayable": isPlayable, - "name": name, - "popularity": popularity, - "previewUrl": previewUrl, - "trackNumber": trackNumber, - "type": type, - "uri": uri, + ...TrackJson.trackToJson(this), // this values "ytTrack": ytTrack.toJson(), "ytUri": ytUri, diff --git a/lib/provider/proxy_playlist/proxy_playlist.dart b/lib/provider/proxy_playlist/proxy_playlist.dart index c0563f21..e5dfa7e8 100644 --- a/lib/provider/proxy_playlist/proxy_playlist.dart +++ b/lib/provider/proxy_playlist/proxy_playlist.dart @@ -54,12 +54,14 @@ class ProxyPlaylist { } } + /// To make sure proper instance method is used for JSON serialization + /// Otherwise default super.toJson() is used static Map _makeAppropriateTrackJson(Track track) { - if (track is LocalTrack) { - return track.toJson(); - } else { - return track.toJson(); - } + return switch (track.runtimeType) { + LocalTrack => track.toJson(), + SpotubeTrack => track.toJson(), + _ => track.toJson(), + }; } Map toJson() { From 353ca79be334077c3ac27b4f64e8b4b15eca7175 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 29 Oct 2023 20:59:56 +0600 Subject: [PATCH 024/131] fix: spotube doesn't exit properly, hangs in infinite loop #768 --- lib/collections/intents.dart | 5 +++-- lib/components/shared/page_window_title_bar.dart | 6 +++--- lib/hooks/use_init_sys_tray.dart | 4 +++- lib/l10n/app_en.arb | 4 ++-- lib/services/audio_services/linux_audio_service.dart | 3 +-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart index 8c7ea73b..abccb3ad 100644 --- a/lib/collections/intents.dart +++ b/lib/collections/intents.dart @@ -1,6 +1,7 @@ +import 'dart:io'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:spotube/components/player/player_controls.dart'; @@ -115,7 +116,7 @@ class CloseAppAction extends Action { @override invoke(intent) { if (kIsDesktop) { - DesktopTools.window.close(); + exit(0); } else { SystemNavigator.pop(); } diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart index b1086eed..50d468aa 100644 --- a/lib/components/shared/page_window_title_bar.dart +++ b/lib/components/shared/page_window_title_bar.dart @@ -6,7 +6,7 @@ import 'package:spotube/utils/platform.dart'; import 'package:titlebar_buttons/titlebar_buttons.dart'; import 'dart:math'; import 'package:flutter/foundation.dart' show kIsWeb; -import 'dart:io' show Platform; +import 'dart:io' show Platform, exit; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:local_notifier/local_notifier.dart'; @@ -17,7 +17,7 @@ final closeNotification = DesktopTools.createNotification( LocalNotificationAction(text: 'Close The App'), ], )?..onClickAction = (value) { - DesktopTools.window.close(); + exit(0); }; class PageWindowTitleBar extends StatefulHookConsumerWidget @@ -113,7 +113,7 @@ class WindowTitleBarButtons extends HookConsumerWidget { Future onClose() async { if (preferences.closeBehavior == CloseBehavior.close) { - await DesktopTools.window.close(); + exit(0); } else { await DesktopTools.window.hide(); await closeNotification?.show(); diff --git a/lib/hooks/use_init_sys_tray.dart b/lib/hooks/use_init_sys_tray.dart index e9aa05b6..f342c24a 100644 --- a/lib/hooks/use_init_sys_tray.dart +++ b/lib/hooks/use_init_sys_tray.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -70,7 +72,7 @@ void useInitSysTray(WidgetRef ref) { label: "Quit", name: "quit", onClicked: (item) async { - await DesktopTools.window.close(); + exit(0); }, ), ], diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9d5be6bb..730f51ea 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -263,8 +263,8 @@ "use_system_title_bar": "Use system title bar", "crunching_results": "Crunching results...", "search_to_get_results": "Search to get results", - "use_amoled_mode": "Use AMOLED mode", - "pitch_dark_theme": "Pitch black dart theme", + "use_amoled_mode": "Pitch black dark theme", + "pitch_dark_theme": "AMOLED Mode", "normalize_audio": "Normalize audio", "change_cover": "Change cover", "add_cover": "Add cover", diff --git a/lib/services/audio_services/linux_audio_service.dart b/lib/services/audio_services/linux_audio_service.dart index 28370c86..bfe022d6 100644 --- a/lib/services/audio_services/linux_audio_service.dart +++ b/lib/services/audio_services/linux_audio_service.dart @@ -86,8 +86,7 @@ class _MprisMediaPlayer2 extends DBusObject { /// Implementation of org.mpris.MediaPlayer2.Quit() Future doQuit() async { - await windowManager.close(); - return DBusMethodSuccessResponse(); + exit(0); } @override From 1334a62aaea31f97031b3ebf455e94c583f37314 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 29 Oct 2023 21:38:48 +0600 Subject: [PATCH 025/131] fix: infinite list disappearing for a moment everytime new page is fetched --- lib/components/genre/category_card.dart | 4 ++- lib/pages/home/personalized.dart | 3 ++- lib/pages/search/search.dart | 11 ++++++--- pubspec.lock | 33 +++++++++++-------------- pubspec.yaml | 23 +++-------------- 5 files changed, 31 insertions(+), 43 deletions(-) diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart index 42654ed9..1aa33cd6 100644 --- a/lib/components/genre/category_card.dart +++ b/lib/components/genre/category_card.dart @@ -28,7 +28,9 @@ class CategoryCard extends HookConsumerWidget { category.id!, ); - if (playlistQuery.hasErrors && !playlistQuery.hasPageData) { + if (playlistQuery.hasErrors && + !playlistQuery.hasPageData && + !playlistQuery.isLoadingNextPage) { return const SizedBox.shrink(); } final playlists = playlistQuery.pages.expand( diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index d6192592..f7e942be 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -131,7 +131,8 @@ class PersonalizedPage extends HookConsumerWidget { child: ListView( controller: controller, children: [ - if (!featuredPlaylistsQuery.hasPageData) + if (!featuredPlaylistsQuery.hasPageData && + !featuredPlaylistsQuery.isLoadingNextPage) const ShimmerCategories() else PersonalizedItemCard( diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 19a9aafa..c192eb7b 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -222,7 +222,8 @@ class SearchPage extends HookConsumerWidget { ), ), if (!searchPlaylist.hasPageData && - !searchPlaylist.hasPageError) + !searchPlaylist.hasPageError && + !searchPlaylist.isLoadingNextPage) const CircularProgressIndicator(), if (searchPlaylist.hasPageError) Padding( @@ -280,7 +281,9 @@ class SearchPage extends HookConsumerWidget { ), ), ), - if (!searchArtist.hasPageData && !searchArtist.hasPageError) + if (!searchArtist.hasPageData && + !searchArtist.hasPageError && + !searchArtist.isLoadingNextPage) const CircularProgressIndicator(), if (searchArtist.hasPageError) Padding( @@ -336,7 +339,9 @@ class SearchPage extends HookConsumerWidget { ), ), ), - if (!searchAlbum.hasPageData && !searchAlbum.hasPageError) + if (!searchAlbum.hasPageData && + !searchAlbum.hasPageError && + !searchAlbum.isLoadingNextPage) const CircularProgressIndicator(), if (searchAlbum.hasPageError) Padding( diff --git a/pubspec.lock b/pubspec.lock index bd50225a..15a50f41 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -596,30 +596,27 @@ packages: fl_query: dependency: "direct main" description: - path: "packages/fl_query" - ref: HEAD - resolved-ref: a817713a0bb0c486e908e9ed74467c4f7f58bea7 - url: "https://github.com/KRTirtho/fl-query.git" - source: git - version: "1.0.0-alpha.5" + name: fl_query + sha256: daee5ab0ed8899baa201b89b5813107df5258144a9e2bcf192dbcf922c57d985 + url: "https://pub.dev" + source: hosted + version: "1.0.0" fl_query_devtools: dependency: "direct main" description: - path: "packages/fl_query_devtools" - ref: HEAD - resolved-ref: a817713a0bb0c486e908e9ed74467c4f7f58bea7 - url: "https://github.com/KRTirtho/fl-query.git" - source: git - version: "0.1.0-alpha.3" + name: fl_query_devtools + sha256: "2ae8905fd4a95f1d245a1b54057c31c8d27fc961223bcb7ce13088bcf6595059" + url: "https://pub.dev" + source: hosted + version: "0.1.0" fl_query_hooks: dependency: "direct main" description: - path: "packages/fl_query_hooks" - ref: HEAD - resolved-ref: a817713a0bb0c486e908e9ed74467c4f7f58bea7 - url: "https://github.com/KRTirtho/fl-query.git" - source: git - version: "1.0.0-alpha.5" + name: fl_query_hooks + sha256: "6c88b3bfbdc3e1330931b927903929d7351f86fc63266ac93b3acb9f133a09a9" + url: "https://pub.dev" + source: hosted + version: "1.0.0" fluentui_system_icons: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 75b14bc1..64b2b6a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,18 +32,9 @@ dependencies: duration: ^3.0.12 envied: ^0.3.0 file_selector: ^1.0.1 - fl_query: - git: - url: https://github.com/KRTirtho/fl-query.git - path: packages/fl_query - fl_query_hooks: - git: - url: https://github.com/KRTirtho/fl-query.git - path: packages/fl_query_hooks - fl_query_devtools: - git: - url: https://github.com/KRTirtho/fl-query.git - path: packages/fl_query_devtools + fl_query: ^1.0.0 + fl_query_hooks: ^1.0.0 + fl_query_devtools: ^0.1.0 fluentui_system_icons: ^1.1.189 flutter: sdk: flutter @@ -135,14 +126,6 @@ dev_dependencies: dependency_overrides: http: ^1.1.0 system_tray: 2.0.2 - fl_query: - git: - url: https://github.com/KRTirtho/fl-query.git - path: packages/fl_query - fl_query_hooks: - git: - url: https://github.com/KRTirtho/fl-query.git - path: packages/fl_query_hooks flutter: generate: true From ac0e2e74d803a902b0abef94f674f68ffcd81fd3 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 8 Nov 2023 11:43:17 +0600 Subject: [PATCH 026/131] 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 027/131] 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 028/131] 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 029/131] 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 030/131] 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 031/131] 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 tracks = []; - List playlists = []; - final pages = [ - ...searchTrack.pages, - ...searchAlbum.pages, - ...searchPlaylist.pages, - ...searchArtist.pages, - ].expand((page) => page).toList(); - for (MapEntry page in pages.asMap().entries) { - for (var item in page.value.items ?? []) { - if (item is AlbumSimple) { - albums.add(item); - } else if (item is PlaylistSimple) { - playlists.add(item); - } else if (item is Artist) { - artists.add(item); - } else if (item is Track) { - tracks.add(item); - } - } - } - - return InterScrollbar( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (tracks.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - context.l10n.songs, - style: theme.textTheme.titleLarge!, - ), - ), - if (!searchTrack.hasPageData && - !searchTrack.hasPageError && - !searchTrack.isLoadingNextPage) - const CircularProgressIndicator() - else if (searchTrack.hasPageError) - Text( - searchTrack.errors.lastOrNull?.toString() ?? "", - ) - else - ...tracks.mapIndexed((i, track) { - return TrackTile( - index: i, - track: track, - onTap: () async { - final isTrackPlaying = - playlist.activeTrack?.id == track.id; - if (!isTrackPlaying && context.mounted) { - final shouldPlay = (playlist.tracks.length) > 20 - ? await showPromptDialog( - context: context, - title: context.l10n.playing_track( - track.name!, - ), - message: context.l10n.queue_clear_alert( - playlist.tracks.length, - ), - ) - : true; - - if (shouldPlay) { - await playlistNotifier.load( - [track], - autoPlay: true, - ); - } - } - }, - ); - }), - if (searchTrack.hasNextPage && tracks.isNotEmpty) - Center( - child: TextButton( - onPressed: searchTrack.isLoadingNextPage - ? null - : () => searchTrack.fetchNext(), - child: searchTrack.isLoadingNextPage - ? const CircularProgressIndicator() - : Text(context.l10n.load_more), - ), - ), - if (playlists.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - context.l10n.playlists, - style: theme.textTheme.titleLarge!, - ), - ), - const SizedBox(height: 10), - ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - scrollbarOrientation: mediaQuery.lgAndUp - ? ScrollbarOrientation.bottom - : ScrollbarOrientation.top, - controller: playlistController, - child: Waypoint( - onTouchEdge: () { - searchPlaylist.fetchNext(); - }, - controller: playlistController, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: playlistController, - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - children: [ - ...playlists.mapIndexed( - (i, playlist) { - if (i == playlists.length - 1 && - searchPlaylist.hasNextPage) { - return const ShimmerPlaybuttonCard( - count: 1); - } - return PlaylistCard(playlist); - }, - ), - ], - ), - ), - ), - ), - ), - if (!searchPlaylist.hasPageData && - !searchPlaylist.hasPageError && - !searchPlaylist.isLoadingNextPage) - const CircularProgressIndicator(), - if (searchPlaylist.hasPageError) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - searchPlaylist.errors.lastOrNull?.toString() ?? "", - ), - ), - const SizedBox(height: 20), - if (artists.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - context.l10n.artists, - style: theme.textTheme.titleLarge!, - ), - ), - const SizedBox(height: 10), - ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - controller: artistController, - child: Waypoint( - controller: artistController, - onTouchEdge: () { - searchArtist.fetchNext(); - }, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: artistController, - child: Row( - children: [ - ...artists.mapIndexed( - (i, artist) { - if (i == artists.length - 1 && - searchArtist.hasNextPage) { - return const ShimmerPlaybuttonCard( - count: 1); - } - return Container( - margin: const EdgeInsets.symmetric( - horizontal: 15), - child: ArtistCard(artist), - ); - }, - ), - ], - ), - ), - ), - ), - ), - if (!searchArtist.hasPageData && - !searchArtist.hasPageError && - !searchArtist.isLoadingNextPage) - const CircularProgressIndicator(), - if (searchArtist.hasPageError) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - searchArtist.errors.lastOrNull?.toString() ?? "", - ), - ), - const SizedBox(height: 20), - if (albums.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - context.l10n.albums, - style: theme.textTheme.titleLarge!, - ), - ), - const SizedBox(height: 10), - ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - controller: albumController, - child: Waypoint( - controller: albumController, - onTouchEdge: () { - searchAlbum.fetchNext(); - }, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: albumController, - child: Row( - children: [ - ...albums.mapIndexed((i, album) { - if (i == albums.length - 1 && - searchAlbum.hasNextPage) { - return const ShimmerPlaybuttonCard( - count: 1); - } - return AlbumCard( - TypeConversionUtils.simpleAlbum_X_Album( - album, - ), - ); - }), - ], - ), - ), - ), - ), - ), - if (!searchAlbum.hasPageData && - !searchAlbum.hasPageError && - !searchAlbum.isLoadingNextPage) - const CircularProgressIndicator(), - if (searchAlbum.hasPageError) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - searchAlbum.errors.lastOrNull?.toString() ?? "", - ), - ), - ], - ), + 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), + ], ), ), ), - ); - }, + ), + ), ); return SafeArea( diff --git a/lib/pages/search/sections/albums.dart b/lib/pages/search/sections/albums.dart new file mode 100644 index 00000000..787a1924 --- /dev/null +++ b/lib/pages/search/sections/albums.dart @@ -0,0 +1,39 @@ +import 'package:fl_query/fl_query.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/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +class SearchAlbumsSection extends HookConsumerWidget { + final InfiniteQuery>, dynamic, int> query; + const SearchAlbumsSection({ + required this.query, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final albums = useMemoized( + () => query.pages + .expand( + (page) => page.map((p) => p.items!).expand((element) => element), + ) + .whereType() + .map((e) => TypeConversionUtils.simpleAlbum_X_Album(e)) + .toList(), + [query.pages], + ); + + return HorizontalPlaybuttonCardView( + hasNextPage: query.hasNextPage, + items: albums, + onFetchMore: query.fetchNext, + title: Text(context.l10n.albums), + ); + } +} diff --git a/lib/pages/search/sections/artists.dart b/lib/pages/search/sections/artists.dart new file mode 100644 index 00000000..7abd5250 --- /dev/null +++ b/lib/pages/search/sections/artists.dart @@ -0,0 +1,37 @@ +import 'package:fl_query/fl_query.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/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/extensions/context.dart'; + +class SearchArtistsSection extends HookConsumerWidget { + final InfiniteQuery>, dynamic, int> query; + + const SearchArtistsSection({ + Key? key, + required this.query, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final artists = useMemoized( + () => query.pages + .expand( + (page) => page.map((p) => p.items!).expand((element) => element), + ) + .whereType() + .toList(), + [query.pages], + ); + + return HorizontalPlaybuttonCardView( + hasNextPage: query.hasNextPage, + items: artists, + onFetchMore: query.fetchNext, + title: Text(context.l10n.albums), + ); + } +} diff --git a/lib/pages/search/sections/playlists.dart b/lib/pages/search/sections/playlists.dart new file mode 100644 index 00000000..620e914b --- /dev/null +++ b/lib/pages/search/sections/playlists.dart @@ -0,0 +1,35 @@ +import 'package:fl_query/fl_query.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/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/extensions/context.dart'; + +class SearchPlaylistsSection extends HookConsumerWidget { + final InfiniteQuery>, dynamic, int> query; + const SearchPlaylistsSection({ + required this.query, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final playlists = useMemoized( + () => query.pages + .expand( + (page) => page.map((p) => p.items!).expand((element) => element), + ) + .whereType() + .toList(), + [query.pages], + ); + + return HorizontalPlaybuttonCardView( + hasNextPage: query.hasNextPage, + items: playlists, + onFetchMore: query.fetchNext, + title: Text(context.l10n.playlists), + ); + } +} diff --git a/lib/pages/search/sections/tracks.dart b/lib/pages/search/sections/tracks.dart new file mode 100644 index 00000000..59c6a4e1 --- /dev/null +++ b/lib/pages/search/sections/tracks.dart @@ -0,0 +1,98 @@ +import 'package:collection/collection.dart'; +import 'package:fl_query/fl_query.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/shared/dialogs/prompt_dialog.dart'; +import 'package:spotube/components/shared/track_table/track_tile.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; + +class SearchTracksSection extends HookConsumerWidget { + final InfiniteQuery>, dynamic, int> query; + const SearchTracksSection({ + Key? key, + required this.query, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final searchTrack = query; + final tracks = useMemoized( + () => searchTrack.pages + .expand( + (page) => page.map((p) => p.items!).expand((element) => element), + ) + .whereType(), + [searchTrack.pages], + ); + final playlistNotifier = ref.watch(ProxyPlaylistNotifier.provider.notifier); + final playlist = ref.watch(ProxyPlaylistNotifier.provider); + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (tracks.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + context.l10n.songs, + style: theme.textTheme.titleLarge!, + ), + ), + if (!searchTrack.hasPageData && + !searchTrack.hasPageError && + !searchTrack.isLoadingNextPage) + const CircularProgressIndicator() + else if (searchTrack.hasPageError) + Text( + searchTrack.errors.lastOrNull?.toString() ?? "", + ) + else + ...tracks.mapIndexed((i, track) { + return TrackTile( + index: i, + track: track, + onTap: () async { + final isTrackPlaying = playlist.activeTrack?.id == track.id; + if (!isTrackPlaying && context.mounted) { + final shouldPlay = (playlist.tracks.length) > 20 + ? await showPromptDialog( + context: context, + title: context.l10n.playing_track( + track.name!, + ), + message: context.l10n.queue_clear_alert( + playlist.tracks.length, + ), + ) + : true; + + if (shouldPlay) { + await playlistNotifier.load( + [track], + autoPlay: true, + ); + } + } + }, + ); + }), + if (searchTrack.hasNextPage && tracks.isNotEmpty) + Center( + child: TextButton( + onPressed: searchTrack.isLoadingNextPage + ? null + : () => searchTrack.fetchNext(), + child: searchTrack.isLoadingNextPage + ? const CircularProgressIndicator() + : Text(context.l10n.load_more), + ), + ) + ], + ); + } +} From da04f068f9b7effff8d50cb5714d93ea80c22b7f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 8 Nov 2023 17:48:59 +0600 Subject: [PATCH 032/131] fix: Navigating to settings, redirects to home page #812 --- lib/components/root/sidebar.dart | 18 +++++++++++------- .../root/spotube_navigation_bar.dart | 8 +++++--- lib/pages/root/root_app.dart | 4 ++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 0dc8b5b4..7fb1b95f 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -22,7 +22,7 @@ import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class Sidebar extends HookConsumerWidget { - final int selectedIndex; + final int? selectedIndex; final void Function(int) onSelectedIndexChanged; final Widget child; @@ -57,7 +57,7 @@ class Sidebar extends HookConsumerWidget { ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); final controller = useSidebarXController( - selectedIndex: selectedIndex, + selectedIndex: selectedIndex ?? 0, extended: mediaQuery.lgAndUp, ); @@ -75,17 +75,21 @@ class Sidebar extends HookConsumerWidget { ); useEffect(() { - if (controller.selectedIndex != selectedIndex) { - controller.selectIndex(selectedIndex); + if (controller.selectedIndex != selectedIndex && selectedIndex != null) { + controller.selectIndex(selectedIndex!); } return null; }, [selectedIndex]); useEffect(() { - controller.addListener(() { + void listener() { onSelectedIndexChanged(controller.selectedIndex); - }); - return null; + } + + controller.addListener(listener); + return () { + controller.removeListener(listener); + }; }, [controller]); useEffect(() { diff --git a/lib/components/root/spotube_navigation_bar.dart b/lib/components/root/spotube_navigation_bar.dart index 9cea5603..b62d19d1 100644 --- a/lib/components/root/spotube_navigation_bar.dart +++ b/lib/components/root/spotube_navigation_bar.dart @@ -16,7 +16,7 @@ import 'package:spotube/provider/user_preferences_provider.dart'; final navigationPanelHeight = StateProvider((ref) => 50); class SpotubeNavigationBar extends HookConsumerWidget { - final int selectedIndex; + final int? selectedIndex; final void Function(int) onSelectedIndexChanged; const SpotubeNavigationBar({ @@ -33,7 +33,7 @@ class SpotubeNavigationBar extends HookConsumerWidget { final layoutMode = ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); - final insideSelectedIndex = useState(selectedIndex); + final insideSelectedIndex = useState(selectedIndex ?? 0); final buttonColor = useBrightnessValue( theme.colorScheme.inversePrimary, @@ -46,7 +46,9 @@ class SpotubeNavigationBar extends HookConsumerWidget { final panelHeight = ref.watch(navigationPanelHeight); useEffect(() { - insideSelectedIndex.value = selectedIndex; + if (selectedIndex != null) { + insideSelectedIndex.value = selectedIndex!; + } return null; }, [selectedIndex]); diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index b2bd4620..bdbc1c75 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -159,7 +159,7 @@ class RootApp extends HookConsumerWidget { return Scaffold( body: Sidebar( - selectedIndex: rootPaths[location] ?? 0, + selectedIndex: rootPaths[location], onSelectedIndexChanged: onSelectIndexChanged, child: child, ), @@ -169,7 +169,7 @@ class RootApp extends HookConsumerWidget { children: [ BottomPlayer(), SpotubeNavigationBar( - selectedIndex: rootPaths[location] ?? 0, + selectedIndex: rootPaths[location], onSelectedIndexChanged: onSelectIndexChanged, ), ], From a1cc44759b63e731a7f73e24fd9ff29636e9bb77 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 8 Nov 2023 18:51:19 +0600 Subject: [PATCH 033/131] refactor: show queue from side in desktop --- lib/components/player/player_actions.dart | 22 +- lib/components/player/player_queue.dart | 338 +++++++++--------- .../player/sibling_tracks_sheet.dart | 274 +++++++------- lib/pages/root/root_app.dart | 18 + 4 files changed, 333 insertions(+), 319 deletions(-) diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index b3a1e340..7a248aa5 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -5,10 +5,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart' hide Offset; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/player/player_queue.dart'; import 'package:spotube/components/player/sibling_tracks_sheet.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/heart_button.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/models/local_track.dart'; @@ -35,6 +35,7 @@ class PlayerActions extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final mediaQuery = MediaQuery.of(context); final playlist = ref.watch(ProxyPlaylistNotifier.provider); final isLocalTrack = playlist.activeTrack is LocalTrack; ref.watch(downloadManagerProvider); @@ -86,23 +87,7 @@ class PlayerActions extends HookConsumerWidget { tooltip: context.l10n.queue, onPressed: playlist.activeTrack != null ? () { - showModalBottomSheet( - context: context, - isDismissible: true, - enableDrag: true, - isScrollControlled: true, - backgroundColor: Colors.black12, - barrierColor: Colors.black12, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * .7, - ), - builder: (context) { - return PlayerQueue(floating: floatingQueue); - }, - ); + Scaffold.of(context).openEndDrawer(); } : null, ), @@ -119,6 +104,7 @@ class PlayerActions extends HookConsumerWidget { isScrollControlled: true, backgroundColor: Colors.black12, barrierColor: Colors.black12, + elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index 725af22b..2d8ba329 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -36,7 +36,9 @@ class PlayerQueue extends HookConsumerWidget { final tracks = playlist.tracks; final borderRadius = floating - ? BorderRadius.circular(10) + ? const BorderRadius.only( + topLeft: Radius.circular(10), + ) : const BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), @@ -80,140 +82,177 @@ class PlayerQueue extends HookConsumerWidget { return const NotFound(vertical: true); } - return BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 12.0, - sigmaY: 12.0, - ), - child: Container( - margin: EdgeInsets.all(floating ? 8.0 : 0), - padding: const EdgeInsets.only( - top: 5.0, + return ClipRRect( + borderRadius: borderRadius, + clipBehavior: Clip.hardEdge, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 15, + sigmaY: 15, ), - decoration: BoxDecoration( - color: theme.scaffoldBackgroundColor.withOpacity(0.5), - borderRadius: borderRadius, - ), - child: CallbackShortcuts( - bindings: { - LogicalKeySet(LogicalKeyboardKey.escape): () { - if (!isSearching.value) { - Navigator.of(context).pop(); + child: Container( + padding: const EdgeInsets.only( + top: 5.0, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant.withOpacity(0.5), + borderRadius: borderRadius, + ), + child: CallbackShortcuts( + bindings: { + LogicalKeySet(LogicalKeyboardKey.escape): () { + if (!isSearching.value) { + Navigator.of(context).pop(); + } + isSearching.value = false; + searchText.value = ''; } - isSearching.value = false; - searchText.value = ''; - } - }, - child: LayoutBuilder(builder: (context, constraints) { - return Column( - children: [ - 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, - ), + }, + 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), ), - 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, + ), + 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, ), ), - ) - 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, + 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; + }, ), - child: Row( - children: [ - const Icon(SpotubeIcons.playlistRemove), - const SizedBox(width: 5), - Text(context.l10n.clear_all), - ], + 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(); + }, ), - onPressed: () { - playlistNotifier.stop(); - Navigator.of(context).pop(); - }, - ), - const SizedBox(width: 10), + const SizedBox(width: 10), + ], ], - ], - ), - const SizedBox(height: 10), - if (!isSearching.value && searchText.value.isEmpty) - Flexible( - child: InterScrollbar( - controller: controller, - child: ReorderableListView.builder( - onReorder: (oldIndex, newIndex) { - playlistNotifier.moveTrack(oldIndex, newIndex); - }, - scrollController: controller, - itemCount: tracks.length, - shrinkWrap: true, - buildDefaultDragHandles: false, - itemBuilder: (context, i) { - final track = tracks.elementAt(i); - return AutoScrollTag( - key: ValueKey(i), - controller: controller, - index: i, - child: Padding( + ), + const SizedBox(height: 10), + if (!isSearching.value && searchText.value.isEmpty) + Flexible( + child: InterScrollbar( + controller: controller, + child: ReorderableListView.builder( + onReorder: (oldIndex, newIndex) { + playlistNotifier.moveTrack(oldIndex, newIndex); + }, + scrollController: controller, + itemCount: tracks.length, + shrinkWrap: true, + buildDefaultDragHandles: false, + itemBuilder: (context, i) { + final track = tracks.elementAt(i); + return AutoScrollTag( + key: ValueKey(i), + controller: controller, + index: i, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: TrackTile( + index: i, + track: track, + onTap: () async { + if (playlist.activeTrack?.id == track.id) { + return; + } + await playlistNotifier.jumpToTrack(track); + }, + leadingActions: [ + ReorderableDragStartListener( + index: i, + child: + const Icon(SpotubeIcons.dragHandle), + ), + ], + ), + ), + ); + }, + ), + ), + ) + else + Flexible( + child: InterScrollbar( + child: ListView.builder( + itemCount: filteredTracks.length, + itemBuilder: (context, i) { + final track = filteredTracks.elementAt(i); + return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: TrackTile( @@ -225,47 +264,16 @@ class PlayerQueue extends HookConsumerWidget { } 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); - }, - ), - ); - }, - ), - ), - ), - ], - ); - }), + ], + ); + }), + ), ), ), ); diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index 6587b8b3..14c042b8 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -86,151 +86,153 @@ class SiblingTracksSheet extends HookConsumerWidget { return null; }, [playlist.activeTrack]); - final itemBuilder = useCallback((YoutubeVideoInfo video) { - return ListTile( - title: Text(video.title), - leading: Padding( - padding: const EdgeInsets.all(8.0), - child: UniversalImage( - path: video.thumbnailUrl, - height: 60, - width: 60, + final itemBuilder = useCallback( + (YoutubeVideoInfo video) { + return ListTile( + title: Text(video.title), + leading: Padding( + padding: const EdgeInsets.all(8.0), + child: UniversalImage( + path: video.thumbnailUrl, + height: 60, + width: 60, + ), ), - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - trailing: Text(video.duration.toHumanReadableString()), - subtitle: Text(video.channelName), - enabled: playlist.isFetching != true, - selected: playlist.isFetching != true && - video.id == (playlist.activeTrack as SpotubeTrack).ytTrack.id, - selectedTileColor: theme.popupMenuTheme.color, - onTap: () { - if (playlist.isFetching == false && - video.id != (playlist.activeTrack as SpotubeTrack).ytTrack.id) { - playlistNotifier.swapSibling(video); - Navigator.of(context).pop(); - } - }, - ); - }, [ - playlist.isFetching, - playlist.activeTrack, - siblings, - ]); + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + trailing: Text(video.duration.toHumanReadableString()), + subtitle: Text(video.channelName), + enabled: playlist.isFetching != true, + selected: playlist.isFetching != true && + video.id == (playlist.activeTrack as SpotubeTrack).ytTrack.id, + selectedTileColor: theme.popupMenuTheme.color, + onTap: () { + if (playlist.isFetching == false && + video.id != (playlist.activeTrack as SpotubeTrack).ytTrack.id) { + playlistNotifier.swapSibling(video); + Navigator.of(context).pop(); + } + }, + ); + }, + [playlist.isFetching, playlist.activeTrack, siblings], + ); var mediaQuery = MediaQuery.of(context); return SafeArea( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 12.0, - sigmaY: 12.0, - ), - child: AnimatedSize( - duration: const Duration(milliseconds: 300), - child: Container( - height: isSearching.value && mediaQuery.smAndDown - ? mediaQuery.size.height - : mediaQuery.size.height * .6, - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - borderRadius: borderRadius, - color: theme.scaffoldBackgroundColor.withOpacity(.3), - ), - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - centerTitle: true, - title: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: !isSearching.value - ? Text( - context.l10n.alternative_track_sources, - style: theme.textTheme.headlineSmall, - ) - : TextField( - autofocus: true, - controller: searchController, - decoration: InputDecoration( - hintText: context.l10n.search, - hintStyle: theme.textTheme.headlineSmall, - border: InputBorder.none, - ), - style: theme.textTheme.headlineSmall, - ), - ), - automaticallyImplyLeading: false, + child: ClipRRect( + borderRadius: borderRadius, + clipBehavior: Clip.hardEdge, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 12.0, + sigmaY: 12.0, + ), + child: AnimatedSize( + duration: const Duration(milliseconds: 300), + child: Container( + height: isSearching.value && mediaQuery.smAndDown + ? mediaQuery.size.height - mediaQuery.padding.top + : mediaQuery.size.height * .6, + decoration: BoxDecoration( + borderRadius: borderRadius, + color: theme.colorScheme.surfaceVariant.withOpacity(.5), + ), + child: Scaffold( backgroundColor: Colors.transparent, - actions: [ - if (!isSearching.value) - IconButton( - icon: const Icon(SpotubeIcons.search, size: 18), - onPressed: () { - isSearching.value = true; - }, - ) - else ...[ - if (preferences.youtubeApiType == YoutubeApiType.piped) - PopupMenuButton( - icon: const Icon(SpotubeIcons.filter, size: 18), - onSelected: (SearchMode mode) { - searchMode.value = mode; + appBar: AppBar( + centerTitle: true, + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: !isSearching.value + ? Text( + context.l10n.alternative_track_sources, + style: theme.textTheme.headlineSmall, + ) + : TextField( + autofocus: true, + controller: searchController, + decoration: InputDecoration( + hintText: context.l10n.search, + hintStyle: theme.textTheme.headlineSmall, + border: InputBorder.none, + ), + style: theme.textTheme.headlineSmall, + ), + ), + automaticallyImplyLeading: false, + backgroundColor: Colors.transparent, + actions: [ + if (!isSearching.value) + IconButton( + icon: const Icon(SpotubeIcons.search, size: 18), + onPressed: () { + isSearching.value = true; + }, + ) + else ...[ + if (preferences.youtubeApiType == YoutubeApiType.piped) + PopupMenuButton( + icon: const Icon(SpotubeIcons.filter, size: 18), + onSelected: (SearchMode mode) { + searchMode.value = mode; + }, + initialValue: searchMode.value, + itemBuilder: (context) => SearchMode.values + .map( + (e) => PopupMenuItem( + value: e, + child: Text(e.label), + ), + ) + .toList(), + ), + IconButton( + icon: const Icon(SpotubeIcons.close, size: 18), + onPressed: () { + isSearching.value = false; }, - initialValue: searchMode.value, - itemBuilder: (context) => SearchMode.values - .map( - (e) => PopupMenuItem( - value: e, - child: Text(e.label), - ), - ) - .toList(), ), - IconButton( - icon: const Icon(SpotubeIcons.close, size: 18), - onPressed: () { - isSearching.value = false; + ] + ], + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: InterScrollbar( + child: switch (isSearching.value) { + false => ListView.builder( + itemCount: siblings.length, + itemBuilder: (context, index) => + itemBuilder(siblings[index]), + ), + true => FutureBuilder( + future: searchRequest, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Text(snapshot.error.toString()), + ); + } else if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator()); + } + + return InterScrollbar( + child: ListView.builder( + itemCount: snapshot.data!.length, + itemBuilder: (context, index) => + itemBuilder(snapshot.data![index]), + ), + ); + }, + ), }, ), - ] - ], - ), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) => - FadeTransition(opacity: animation, child: child), - child: InterScrollbar( - child: switch (isSearching.value) { - false => ListView.builder( - itemCount: siblings.length, - itemBuilder: (context, index) => - itemBuilder(siblings[index]), - ), - true => FutureBuilder( - future: searchRequest, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Center( - child: Text(snapshot.error.toString()), - ); - } else if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator()); - } - - return InterScrollbar( - child: ListView.builder( - itemCount: snapshot.data!.length, - itemBuilder: (context, index) => - itemBuilder(snapshot.data![index]), - ), - ); - }, - ), - }, ), ), ), diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index bdbc1c75..5797b63f 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -3,11 +3,13 @@ import 'dart:async'; import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/player/player_queue.dart'; import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/components/root/bottom_player.dart'; import 'package:spotube/components/root/sidebar.dart'; @@ -164,6 +166,22 @@ class RootApp extends HookConsumerWidget { child: child, ), extendBody: true, + drawerScrimColor: Colors.transparent, + endDrawer: DesktopTools.platform.isDesktop + ? Container( + constraints: const BoxConstraints(maxWidth: 800), + decoration: BoxDecoration( + boxShadow: theme.brightness == Brightness.light + ? null + : kElevationToShadow[8], + ), + margin: const EdgeInsets.only( + top: 40, + bottom: 100, + ), + child: const PlayerQueue(floating: true), + ) + : null, bottomNavigationBar: Column( mainAxisSize: MainAxisSize.min, children: [ From 0c22469503f32dbbf1a5d31419c1b76c699fa966 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 12 Nov 2023 09:22:26 +0600 Subject: [PATCH 034/131] feat(translations): add Turkish translations --- lib/collections/language_codes.dart | 8 +- lib/l10n/app_tr.arb | 283 ++++++++++++++++++++++++++++ lib/l10n/l10n.dart | 10 +- 3 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 lib/l10n/app_tr.arb diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index 58328b75..0518363e 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -660,10 +660,10 @@ abstract class LanguageLocals { // name: "Tonga (Tonga Islands)", // nativeName: "faka Tonga", // ), - // "tr": const ISOLanguageName( - // name: "Turkish", - // nativeName: "Türkçe", - // ), + "tr": const ISOLanguageName( + name: "Turkish", + nativeName: "Türkçe", + ), // "ts": const ISOLanguageName( // name: "Tsonga", // nativeName: "Xitsonga", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb new file mode 100644 index 00000000..1d72ec85 --- /dev/null +++ b/lib/l10n/app_tr.arb @@ -0,0 +1,283 @@ +{ + "guest": "Misafir", + "browse": "Gözat", + "search": "Ara", + "library": "Kütüphane", + "lyrics": "Sözler", + "settings": "Ayarlar", + "genre_categories_filter": "Kategorileri veya türleri filtreleyin...", + "genre": "Tür", + "personalized": "Kişiselleştirilmiş", + "featured": "Öne Çıkanlar", + "new_releases": "Yeni Çıkanlar", + "songs": "Şarkılar", + "playing_track": "Oynatılıyor {track}", + "queue_clear_alert": "Bu, mevcut kuyruğu temizleyecektir. {track_length} parçaları kaldırılacaktır\nDevam etmek istiyor musunuz?", + "load_more": "Daha fazlasını yükle", + "playlists": "Çalma Listeleri", + "artists": "Sanatçılar", + "albums": "Albümler", + "tracks": "Parçalar", + "downloads": "İndirmeler", + "filter_playlists": "Çalma listelerinizi filtreleyin...", + "liked_tracks": "Beğenilen Parçalar", + "liked_tracks_description": "Beğendiğiniz tüm parçalar", + "create_playlist": "Çalma Listesi Oluştur", + "create_a_playlist": "Bir çalma listesi oluştur", + "update_playlist": "Çalma listesini güncelle", + "create": "Oluştur", + "cancel": "İptal", + "update": "Güncelle", + "playlist_name": "Çalma Listesi Adı", + "name_of_playlist": "Çalma listesi adı", + "description": "Açıklama", + "public": "Halka açık", + "collaborative": "İşbirliği", + "search_local_tracks": "Yerel parçaları arayın...", + "play": "Oynat", + "delete": "Sil", + "none": "Hiçbiri", + "sort_a_z": "A'dan Z'ye sırala", + "sort_z_a": "Z'dan A'ye sırala", + "sort_artist": "Sanatçıya Göre Sırala", + "sort_album": "Albüme Göre Sırala", + "sort_tracks": "Parçaları Sırala", + "currently_downloading": "Şu Anda İndiriliyor ({tracks_length})", + "cancel_all": "Tümünü İptal Et", + "filter_artist": "Sanatçıları filtrele...", + "followers": "{followers} Takipçiler", + "add_artist_to_blacklist": "Sanatçıyı kara listeye ekle", + "top_tracks": "En İyi Parçalar", + "fans_also_like": "Hayranlar ayrıca şunları beğendi", + "loading": "Yükleniyor...", + "artist": "Sanatçı", + "blacklisted": "Kara Listede", + "following": "Takip Ediliyor", + "follow": "Takip Et", + "artist_url_copied": "Sanatçı bağlantısı panoya kopyalandı", + "added_to_queue": "Kuyruğa {tracks} parçaları eklendi", + "filter_albums": "Albümleri filtrele...", + "synced": "Eşitlendi", + "plain": "Sade", + "shuffle": "Karıştır", + "search_tracks": "Parça ara...", + "released": "Yayınlandı", + "error": "Hata {error}", + "title": "Başlık", + "time": "Zaman", + "more_actions": "Daha fazla işlem", + "download_count": "İndir ({count})", + "add_count_to_playlist": "Çalma Listesine ({count}) Ekle", + "add_count_to_queue": "Sıraya ({count}) ekle", + "play_count_next": "Oynat ({count}) sonraki", + "album": "Albüm", + "copied_to_clipboard": "Panoya {data} kopyalandı", + "add_to_following_playlists": "Aşağıdaki Çalma Listelerine {track} ekle", + "add": "Ekle", + "added_track_to_queue": "Sıraya {track} eklendi", + "add_to_queue": "Kuyruğa ekle", + "track_will_play_next": "{track} sonraki çalacak", + "play_next": "Sıradaki", + "removed_track_from_queue": "Sıradan {track} kaldırıldı", + "remove_from_queue": "Kuyruktan çıkar", + "remove_from_favorites": "Favorilerden kaldır", + "save_as_favorite": "Favori olarak kaydet", + "add_to_playlist": "Çalma listesine ekle", + "remove_from_playlist": "Çalma listesinden kaldır", + "add_to_blacklist": "Kara listeye ekle", + "remove_from_blacklist": "Kara listeden çıkar", + "share": "Paylaş", + "mini_player": "Mini Oynatıcı", + "slide_to_seek": "İleri veya geri arama yapmak için kaydırın", + "shuffle_playlist": "Çalma listesini karıştır", + "unshuffle_playlist": "Karışık çalma listesi", + "previous_track": "Önceki parça", + "next_track": "Sonraki parça", + "pause_playback": "Çalmayı Duraklat", + "resume_playback": "Çalmaya Devam Et", + "loop_track": "Döngü parçası", + "repeat_playlist": "Çalma listesini tekrarla", + "queue": "Sıra", + "alternative_track_sources": "Alternatif parça kaynakları", + "download_track": "Parçayı indir", + "tracks_in_queue": "{tracks} sıradaki parçalar", + "clear_all": "Tümünü temizle", + "show_hide_ui_on_hover": "Üzerine gelindiğinde kullanıcı arayüzünü göster/gizle", + "always_on_top": "Her zaman en üstte", + "exit_mini_player": "Mini oynatıcıdan çık", + "download_location": "İndirme konumu", + "account": "Hesap", + "login_with_spotify": "Spotify hesabınız ile giriş yapın", + "connect_with_spotify": "Spotify ile bağlantı kurun", + "logout": "Çıkış Yap", + "logout_of_this_account": "Bu hesaptan çıkış yap", + "language_region": "Dil & Bölge", + "language": "Dil", + "system_default": "Sistem Varsayılanı", + "market_place_region": "Mevcut Bölge", + "recommendation_country": "Tavsiye Edilen Ülke", + "appearance": "Görünüm", + "layout_mode": "Düzen Modu", + "override_layout_settings": "Duyarlı düzen modu ayarlarını geçersiz kıl", + "adaptive": "Uyarlanabilir", + "compact": "Sıkıştırılmış", + "extended": "Genişletilmiş", + "theme": "Tema", + "dark": "Karanlık", + "light": "Aydınlık", + "system": "Sistem", + "accent_color": "Vurgu Rengi", + "sync_album_color": "Albüm rengini eşitle", + "sync_album_color_description": "Albüm resminin baskın rengini vurgu rengi olarak kullanır", + "playback": "Çalma", + "audio_quality": "Ses Kalitesi", + "high": "Yüksek", + "low": "Düşük", + "pre_download_play": "Önceden indir ve oynat", + "pre_download_play_description": "Ses akışı yerine, baytları indirin ve oynatın (Daha yüksek bant genişliği kullanıcıları için önerilir)", + "skip_non_music": "Müzik olmayan bölümleri atla (SponsorBlock)", + "blacklist_description": "Kara listeye alınan parçalar ve sanatçılar", + "wait_for_download_to_finish": "Lütfen mevcut indirme işleminin bitmesini bekleyin", + "desktop": "Masaüstü", + "close_behavior": "Yakın Davranış", + "close": "Kapat", + "minimize_to_tray": "Tepsiye küçült", + "show_tray_icon": "Sistem tepsisi simgesini göster", + "about": "Hakkında", + "u_love_spotube": "Spotube'u sevdiğinizi biliyoruz", + "check_for_updates": "Güncellemeleri kontrol et", + "about_spotube": "Spotube Hakkında", + "blacklist": "Kara Liste", + "please_sponsor": "Lütfen Sponsor Olun/Bağış Yapın", + "spotube_description": "Spotube, hafif, platformlar arası, herkesin kullanabileceği ücretsiz bir Spotify istemcisidir.", + "version": "Sürüm", + "build_number": "Derleme Numarası", + "founder": "Kurucu", + "repository": "Depo", + "bug_issues": "Hata + Sorunlar", + "made_with": "❤️ ile Bangladesh🇧🇩 adresinde yapılmıştır.", + "kingkor_roy_tirtho": "Kingkor Roy Tirtho", + "copyright": "© 2021-{current_year} Kingkor Roy Tirtho", + "license": "Lisans", + "add_spotify_credentials": "Başlamak için spotify bilgilerinizi ekleyin", + "credentials_will_not_be_shared_disclaimer": "Endişelenmeyin, bilgileriniz toplanmayacak veya kimseyle paylaşılmayacak", + "know_how_to_login": "Nasıl yapılacağını bilmiyor musunuz?", + "follow_step_by_step_guide": "Adım Adım kılavuzu takip edin", + "spotify_cookie": "Spotify {name} Çerez", + "cookie_name_cookie": "{name} Çerez", + "fill_in_all_fields": "Lütfen tüm alanları doldurun", + "submit": "Gönder", + "exit": "Çık", + "previous": "Önceki", + "next": "Sonraki", + "done": "Bitti", + "step_1": "1. Adım", + "first_go_to": "İlk önce şu adrese gidin", + "login_if_not_logged_in": "ve oturum açmadıysanız Giriş Yapın/Kaydolun", + "step_2": "2. Adım", + "step_2_steps": "1. Giriş yaptıktan sonra, Tarayıcı devtools.\n2'yi açmak için F12'ye basın veya Fare Sağ Tıklaması > İncele'ye basın. Ardından \"Uygulama\" Sekmesine (Chrome, Edge, Brave vb.) veya \"Depolama\" Sekmesine (Firefox, Palemoon vb.) gidin\n3. \"Çerezler\" bölümüne ve ardından \"https://accounts.spotify.com\" alt bölümüne gidin", + "step_3": "3. Adım", + "step_3_steps": "\"sp_dc\" ve \"sp_key\" (veya sp_gaid) Çerezlerinin değerlerini kopyalayın", + "success_emoji": "Başarılı🥳", + "success_message": "Şimdi Spotify hesabınızla başarılı bir şekilde oturum açtınız. İyi iş, dostum!", + "step_4": "4. Adım", + "step_4_steps": "Kopyalanan \"sp_dc\" ve \"sp_key\" (veya sp_gaid) değerlerini ilgili alanlara yapıştırın", + "something_went_wrong": "Bir şeyler ters gitti", + "piped_instance": "Piped Sunucu Örneği", + "piped_description": "Parça eşleştirme için kullanılacak Piped sunucu örneği", + "piped_warning": "Bazıları iyi çalışmayabilir. Bu yüzden riski size ait olmak üzere kullanın", + "generate_playlist": "Çalma Listesi Oluştur", + "track_exists": "Track {track} zaten mevcut", + "replace_downloaded_tracks": "İndirilen tüm parçaları değiştir", + "skip_download_tracks": "İndirilen tüm parçaları indirmeyi atla", + "do_you_want_to_replace": "Mevcut parçayı değiştirmek mi istiyorsunuz?", + "replace": "Değiştir", + "skip": "Atla", + "select_up_to_count_type": "En fazla {count} {type} seçin", + "select_genres": "Tür Seç", + "add_genres": "Tür Ekle", + "country": "Ülke", + "number_of_tracks_generate": "Oluşturulacak parça sayısı", + "acousticness": "Akustiklik", + "danceability": "Dansedilebilirlik", + "energy": "Enerji", + "instrumentalness": "Enstrümansallık", + "liveness": "Canlılık", + "loudness": "Yükseklik", + "speechiness": "Konuşkanlık", + "valence": "Değerlilik", + "popularity": "Popülerlik", + "key": "Anahtar", + "duration": "Süre (sn)", + "tempo": "Tempo (BPM)", + "mode": "Mod", + "time_signature": "Zaman İmzası", + "short": "Kısa", + "medium": "Orta", + "long": "Uzun", + "min": "Min", + "max": "Maks", + "target": "Hedef", + "moderate": "Orta", + "deselect_all": "Tüm Seçimleri Kaldır", + "select_all": "Tümünü Seç", + "are_you_sure": "Emin misiniz?", + "generating_playlist": "Özel çalma listenizi oluşturun...", + "selected_count_tracks": "Seçilen {count} parçalar", + "download_warning": "Tüm Parçaları toplu olarak indirirseniz, açıkça Müzik korsanlığı yapmış ve yaratıcı Müzik toplumuna zarar vermiş olursunuz. Umarım bunun farkındasınızdır. Her zaman, Sanatçıların sıkı çalışmalarına saygı duymayı ve desteklemeyi deneyin", + "download_ip_ban_warning": "Bu arada, normalden fazla indirme isteği nedeniyle IP adresiniz YouTube'da engellenebilir. IP engeli, o IP cihazından en az 2-3 ay boyunca YouTube'u (giriş yapmış olsanız bile) kullanamayacağınız anlamına gelir. Ve Spotube böyle bir durumda herhangi bir sorumluluk kabul etmez", + "by_clicking_accept_terms": "'Kabul et' seçeneğine tıklayarak aşağıdaki şartları kabul etmiş olursunuz:", + "download_agreement_1": "Müzik korsanlığı yaptığımı biliyorum. Ben malım.", + "download_agreement_2": "Sanatçıları elimden geldiğince destekleyeceğim ve bunu sadece sanatlarını satın alacak param olmadığı için yapıyorum", + "download_agreement_3": "IP adresimin YouTube'da engellenebileceğinin tamamen farkındayım ve mevcut eylemimin neden olduğu herhangi bir kazadan Spotube'u veya sahiplerini/dağıtıcılarını sorumlu tutmuyorum", + "decline": "Reddet", + "accept": "Kabul et", + "details": "Detaylar", + "youtube": "YouTube", + "channel": "Kanal", + "likes": "Beğeniler", + "dislikes": "Beğenmemeler", + "views": "İzlenmeler", + "streamUrl": "Yayın Bağlantısı", + "stop": "Dur", + "sort_newest": "En yeni eklenene göre sırala", + "sort_oldest": "En eski eklenene göre sırala", + "sleep_timer": "Uyku Zamanlayıcısı", + "mins": "{minutes} Dakikalar", + "hours": "{hours} Saat", + "hour": "{hours} Saatler", + "custom_hours": "Özel Saatler", + "logs": "Günlükler", + "developers": "Geliştiriciler", + "not_logged_in": "Giriş yapmadınız", + "search_mode": "Arama Modu", + "youtube_api_type": "API Türü", + "ok": "Tamam", + "failed_to_encrypt": "Şifreleme başarısız oldu", + "encryption_failed_warning": "Spotube, verilerinizi güvenli bir şekilde depolamak için şifreleme kullanır. Ancak bunu başaramadı. Bu nedenle güvensiz bir depolamaya geri dönecektir. Linux kullanıyorsanız, lütfen gnome-keyring, kde-wallet, keepassxc vb. gibi bir güvenlik hizmetinizin kurulu olduğundan emin olun.", + "querying_info": "Bilgi sorgulanıyor...", + "piped_api_down": "Piped API kapalı", + "piped_down_error_instructions": "Piped örneği {pipedInstance} şu anda kapalı\n\nYa örneği değiştirin ya da 'API türünü' resmi YouTube API'si olarak değiştirin\n\nDeğişiklikten sonra uygulamayı yeniden başlattığınızdan emin olun", + "you_are_offline": "Şu anda çevrimdışısınız", + "connection_restored": "İnternet bağlantınız yeniden kuruldu", + "use_system_title_bar": "Sistem başlık çubuğunu kullan", + "crunching_results": "Sonuçlar kırılıyor...", + "search_to_get_results": "Sonuç almak için arama yap", + "use_amoled_mode": "AMOLED modunu kullan", + "pitch_dark_theme": "Zifiri siyah dart teması", + "normalize_audio": "Sesi normalleştir", + "change_cover": "Kapağı değiştir", + "add_cover": "Kapak ekle", + "restore_defaults": "Varsayılanları geri yükle", + "download_music_codec": "Müzik codec bileşenini indirin", + "streaming_music_codec": "Müzik akışı codec bileşeni", + "login_with_lastfm": "Last.fm ile giriş yap", + "connect": "Bağlan", + "disconnect_lastfm": "Last.fm bağlantısını kes", + "disconnect": "Bağlantıyı Kes", + "username": "Kullanıcı Adı", + "password": "Şifre", + "login": "Giriş Yap", + "login_with_your_lastfm": "Last.fm hesabınız ile giriş yapın", + "scrobble_to_lastfm": "Last.fm için Scrobble" +} \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 13cf0e24..61a6d097 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -6,24 +6,26 @@ /// iceyear@github => Simplified Chinese /// TexturedPolak@github => Polish /// yuri-val@github => Ukrainian +/// mdksec@github => Turkish import 'package:flutter/material.dart'; class L10n { static final all = [ const Locale('en'), + const Locale('ar', 'SA'), const Locale('bn', 'BD'), - const Locale('de', 'GE'), const Locale('ca', 'AD'), + const Locale('de', 'GE'), const Locale('es', 'ES'), const Locale("fa", "IR"), const Locale('fr', 'FR'), const Locale('hi', 'IN'), const Locale('ja', 'JP'), - const Locale('zh', 'CN'), const Locale('pl', 'PL'), - const Locale('ru', 'RU'), const Locale('pt', 'PT'), + const Locale('ru', 'RU'), const Locale('uk', 'UA'), - const Locale('ar', 'SA'), + const Locale('tr', 'TR'), + const Locale('zh', 'CN'), ]; } From 5928185599f3739845391476c0ae47b9efa2cd36 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 12 Nov 2023 19:01:14 +0600 Subject: [PATCH 035/131] 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 036/131] 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 037/131] 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 038/131] 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 039/131] 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 040/131] 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 From 7b72a90bc65b541cbe2e24ef2234524b522ad71d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 14 Nov 2023 19:04:59 +0600 Subject: [PATCH 041/131] fix: alternative track source safearea overflow #876 --- lib/components/player/sibling_tracks_sheet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index 8dc41026..c4100b9a 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -135,7 +135,7 @@ class SiblingTracksSheet extends HookConsumerWidget { duration: const Duration(milliseconds: 300), child: Container( height: isSearching.value && mediaQuery.smAndDown - ? mediaQuery.size.height - mediaQuery.padding.top + ? mediaQuery.size.height - 50 : mediaQuery.size.height * .6, decoration: BoxDecoration( borderRadius: borderRadius, From fed36ecdd81e8a0f8358693eff0a6233dea32e5d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 14 Nov 2023 19:36:07 +0600 Subject: [PATCH 042/131] fix: Add to Playlist Dialog memory leak #817 --- .../dialogs/playlist_add_track_dialog.dart | 36 +++++++------------ lib/services/queries/playlist.dart | 23 ++++++++++++ 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/shared/dialogs/playlist_add_track_dialog.dart index 29f64268..aadcd9d6 100644 --- a/lib/components/shared/dialogs/playlist_add_track_dialog.dart +++ b/lib/components/shared/dialogs/playlist_add_track_dialog.dart @@ -1,4 +1,3 @@ -import 'package:async/async.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -21,32 +20,21 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final spotify = ref.watch(spotifyProvider); - final userPlaylists = useQueries.playlist.ofMine(ref); - - useEffect(() { - final op = CancelableOperation.fromFuture( - () async { - while (userPlaylists.hasNextPage) { - await userPlaylists.fetchNext(); - } - }(), - ); - - return () { - op.cancel(); - }; - }, [userPlaylists.hasNextPage]); + final userPlaylists = useQueries.playlist.ofMineAll(ref); final me = useQueries.user.me(ref); final filteredPlaylists = useMemoized( - () => userPlaylists.pages - .expand((page) => page.items?.toList() ?? []) - .where( - (playlist) => - playlist.owner?.id != null && playlist.owner!.id == me.data?.id, - ), - [userPlaylists.pages, me.data?.id], + () => + userPlaylists.data + ?.where( + (playlist) => + playlist.owner?.id != null && + playlist.owner!.id == me.data?.id, + ) + .toList() ?? + [], + [userPlaylists.data, me.data?.id], ); final playlistsCheck = useState({}); @@ -93,7 +81,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { content: SizedBox( height: 300, width: 300, - child: userPlaylists.hasNextPage + child: userPlaylists.isLoading ? const Center(child: CircularProgressIndicator()) : ListView.builder( shrinkWrap: true, diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index ac8dc73f..c0434a6e 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -143,6 +143,29 @@ class PlaylistQueries { ); } + Query, dynamic> ofMineAll(WidgetRef ref) { + return useSpotifyQuery, dynamic>( + "current-user-all-playlists", + (spotify) async { + var page = await spotify.playlists.me.getPage(50); + final playlists = []; + + if (page.isLast == true) { + return page.items?.toList() ?? []; + } + + playlists.addAll(page.items ?? []); + while (!page.isLast) { + page = await spotify.playlists.me.getPage(50, page.nextOffset); + playlists.addAll(page.items ?? []); + } + + return playlists; + }, + ref: ref, + ); + } + Future> likedTracks( SpotifyApi spotify, WidgetRef ref, From 0e075067168f817ca2de0e25cd47d81fa86184c8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 14 Nov 2023 19:46:54 +0600 Subject: [PATCH 043/131] refactor: organize hooks --- lib/collections/env.dart | 4 +++- lib/components/artist/artist_card.dart | 4 ++-- .../lyrics}/use_synced_lyrics.dart | 0 lib/components/player/player.dart | 4 ++-- lib/components/player/player_controls.dart | 2 +- lib/components/player/player_overlay.dart | 2 +- lib/components/player/player_queue.dart | 2 +- .../player/sibling_tracks_sheet.dart | 2 +- .../player}/use_progress.dart | 0 lib/components/root/bottom_player.dart | 2 +- lib/components/root/sidebar.dart | 4 ++-- .../root/spotube_navigation_bar.dart | 2 +- .../horizontal_playbutton_card_view.dart | 2 +- lib/components/shared/playbutton_card.dart | 4 ++-- .../shimmers/shimmer_artist_profile.dart | 2 +- .../shared/shimmers/shimmer_categories.dart | 2 +- .../shimmers/shimmer_playbutton_card.dart | 2 +- .../shared/themed_button_tab_bar.dart | 4 ++-- .../track_collection_view.dart | 6 +++--- .../use_disable_battery_optimizations.dart | 2 +- .../use_get_storage_perms.dart | 2 +- .../{ => configurators}/use_init_sys_tray.dart | 0 .../use_update_checker.dart | 2 +- .../use_auto_scroll_controller.dart | 0 .../{ => controllers}/use_package_info.dart | 0 .../use_sidebarx_controller.dart | 0 .../use_spotify_infinite_query.dart | 0 .../{ => spotify}/use_spotify_mutation.dart | 0 lib/hooks/{ => spotify}/use_spotify_query.dart | 0 lib/hooks/use_is_current_route.dart | 18 ------------------ lib/hooks/use_shared_preferences.dart | 9 --------- lib/hooks/{ => utils}/use_async_effect.dart | 0 .../{ => utils}/use_breakpoint_value.dart | 0 .../{ => utils}/use_brightness_value.dart | 0 .../use_custom_status_bar_color.dart | 0 lib/hooks/{ => utils}/use_debounce.dart | 0 lib/hooks/{ => utils}/use_force_update.dart | 0 lib/hooks/{ => utils}/use_palette_color.dart | 0 lib/main.dart | 6 +++--- lib/pages/artist/artist.dart | 2 +- lib/pages/lyrics/lyrics.dart | 4 ++-- lib/pages/lyrics/mini_lyrics.dart | 2 +- lib/pages/lyrics/synced_lyrics.dart | 4 ++-- lib/pages/root/root_app.dart | 2 +- lib/pages/settings/about.dart | 2 +- lib/services/mutations/album.dart | 2 +- lib/services/mutations/playlist.dart | 2 +- lib/services/mutations/track.dart | 2 +- lib/services/queries/album.dart | 4 ++-- lib/services/queries/artist.dart | 4 ++-- lib/services/queries/category.dart | 2 +- lib/services/queries/lyrics.dart | 2 +- lib/services/queries/playlist.dart | 4 ++-- lib/services/queries/search.dart | 2 +- lib/services/queries/user.dart | 2 +- 55 files changed, 53 insertions(+), 78 deletions(-) rename lib/{hooks => components/lyrics}/use_synced_lyrics.dart (100%) rename lib/{hooks => components/player}/use_progress.dart (100%) rename lib/hooks/{ => configurators}/use_disable_battery_optimizations.dart (96%) rename lib/hooks/{ => configurators}/use_get_storage_perms.dart (95%) rename lib/hooks/{ => configurators}/use_init_sys_tray.dart (100%) rename lib/hooks/{ => configurators}/use_update_checker.dart (98%) rename lib/hooks/{ => controllers}/use_auto_scroll_controller.dart (100%) rename lib/hooks/{ => controllers}/use_package_info.dart (100%) rename lib/hooks/{ => controllers}/use_sidebarx_controller.dart (100%) rename lib/hooks/{ => spotify}/use_spotify_infinite_query.dart (100%) rename lib/hooks/{ => spotify}/use_spotify_mutation.dart (100%) rename lib/hooks/{ => spotify}/use_spotify_query.dart (100%) delete mode 100644 lib/hooks/use_is_current_route.dart delete mode 100644 lib/hooks/use_shared_preferences.dart rename lib/hooks/{ => utils}/use_async_effect.dart (100%) rename lib/hooks/{ => utils}/use_breakpoint_value.dart (100%) rename lib/hooks/{ => utils}/use_brightness_value.dart (100%) rename lib/hooks/{ => utils}/use_custom_status_bar_color.dart (100%) rename lib/hooks/{ => utils}/use_debounce.dart (100%) rename lib/hooks/{ => utils}/use_force_update.dart (100%) rename lib/hooks/{ => utils}/use_palette_color.dart (100%) diff --git a/lib/collections/env.dart b/lib/collections/env.dart index 1b9de3de..8086ada7 100644 --- a/lib/collections/env.dart +++ b/lib/collections/env.dart @@ -1,4 +1,5 @@ import 'package:envied/envied.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; part 'env.g.dart'; @@ -30,5 +31,6 @@ abstract class Env { @EnviedField(varName: 'ENABLE_UPDATE_CHECK', defaultValue: "1") static final String _enableUpdateChecker = _Env._enableUpdateChecker; - static bool get enableUpdateChecker => _enableUpdateChecker == "1"; + static bool get enableUpdateChecker => + DesktopTools.platform.isFlatpak || _enableUpdateChecker == "1"; } diff --git a/lib/components/artist/artist_card.dart b/lib/components/artist/artist_card.dart index 993e9f6a..434b90ad 100644 --- a/lib/components/artist/artist_card.dart +++ b/lib/components/artist/artist_card.dart @@ -5,8 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_breakpoint_value.dart'; -import 'package:spotube/hooks/use_brightness_value.dart'; +import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; +import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; diff --git a/lib/hooks/use_synced_lyrics.dart b/lib/components/lyrics/use_synced_lyrics.dart similarity index 100% rename from lib/hooks/use_synced_lyrics.dart rename to lib/components/lyrics/use_synced_lyrics.dart diff --git a/lib/components/player/player.dart b/lib/components/player/player.dart index 811d24c5..889b7c5c 100644 --- a/lib/components/player/player.dart +++ b/lib/components/player/player.dart @@ -18,8 +18,8 @@ import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/panels/sliding_up_panel.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_custom_status_bar_color.dart'; -import 'package:spotube/hooks/use_palette_color.dart'; +import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; +import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/provider/authentication_provider.dart'; diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart index 07a6b7ba..1000af18 100644 --- a/lib/components/player/player_controls.dart +++ b/lib/components/player/player_controls.dart @@ -8,7 +8,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; -import 'package:spotube/hooks/use_progress.dart'; +import 'package:spotube/components/player/use_progress.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart index 354d1a36..4869a0fa 100644 --- a/lib/components/player/player_overlay.dart +++ b/lib/components/player/player_overlay.dart @@ -9,7 +9,7 @@ import 'package:spotube/components/root/spotube_navigation_bar.dart'; import 'package:spotube/components/shared/panels/sliding_up_panel.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/intents.dart'; -import 'package:spotube/hooks/use_progress.dart'; +import 'package:spotube/components/player/use_progress.dart'; import 'package:spotube/components/player/player.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index 9e303cb8..a6f69925 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -14,7 +14,7 @@ import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/track_table/track_tile.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_auto_scroll_controller.dart'; +import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index c4100b9a..f6702e0c 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -11,7 +11,7 @@ import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; -import 'package:spotube/hooks/use_debounce.dart'; +import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/models/matched_track.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; diff --git a/lib/hooks/use_progress.dart b/lib/components/player/use_progress.dart similarity index 100% rename from lib/hooks/use_progress.dart rename to lib/components/player/use_progress.dart diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 6d2e8319..4bc08fc0 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -14,7 +14,7 @@ import 'package:spotube/components/player/player_controls.dart'; import 'package:spotube/components/player/volume_slider.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_brightness_value.dart'; +import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotube/provider/authentication_provider.dart'; diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 7fb1b95f..f7cdcac3 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -11,8 +11,8 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_brightness_value.dart'; -import 'package:spotube/hooks/use_sidebarx_controller.dart'; +import 'package:spotube/hooks/utils/use_brightness_value.dart'; +import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication_provider.dart'; diff --git a/lib/components/root/spotube_navigation_bar.dart b/lib/components/root/spotube_navigation_bar.dart index b62d19d1..d602f390 100644 --- a/lib/components/root/spotube_navigation_bar.dart +++ b/lib/components/root/spotube_navigation_bar.dart @@ -9,7 +9,7 @@ import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_brightness_value.dart'; +import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; 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 index a415d721..f17a9714 100644 --- 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 @@ -8,7 +8,7 @@ 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'; +import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; class HorizontalPlaybuttonCardView extends HookWidget { final Widget title; diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index 91c185c7..4fef72c0 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -6,8 +6,8 @@ import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; 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/hooks/utils/use_breakpoint_value.dart'; +import 'package:spotube/hooks/utils/use_brightness_value.dart'; final htmlTagRegexp = RegExp(r"<[^>]*>", caseSensitive: true); diff --git a/lib/components/shared/shimmers/shimmer_artist_profile.dart b/lib/components/shared/shimmers/shimmer_artist_profile.dart index 940c4e81..d0b0288f 100644 --- a/lib/components/shared/shimmers/shimmer_artist_profile.dart +++ b/lib/components/shared/shimmers/shimmer_artist_profile.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:skeleton_text/skeleton_text.dart'; import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; import 'package:spotube/extensions/theme.dart'; -import 'package:spotube/hooks/use_breakpoint_value.dart'; +import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; class ShimmerArtistProfile extends HookWidget { const ShimmerArtistProfile({Key? key}) : super(key: key); diff --git a/lib/components/shared/shimmers/shimmer_categories.dart b/lib/components/shared/shimmers/shimmer_categories.dart index e9f442d4..9bc773da 100644 --- a/lib/components/shared/shimmers/shimmer_categories.dart +++ b/lib/components/shared/shimmers/shimmer_categories.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/extensions/theme.dart'; -import 'package:spotube/hooks/use_breakpoint_value.dart'; +import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; class ShimmerCategories extends HookWidget { const ShimmerCategories({Key? key}) : super(key: key); diff --git a/lib/components/shared/shimmers/shimmer_playbutton_card.dart b/lib/components/shared/shimmers/shimmer_playbutton_card.dart index 82da5bd9..2259c9b0 100644 --- a/lib/components/shared/shimmers/shimmer_playbutton_card.dart +++ b/lib/components/shared/shimmers/shimmer_playbutton_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:spotube/hooks/use_breakpoint_value.dart'; +import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; class ShimmerPlaybuttonCardPainter extends CustomPainter { final Color background; diff --git a/lib/components/shared/themed_button_tab_bar.dart b/lib/components/shared/themed_button_tab_bar.dart index d38c3a19..079a4e8a 100644 --- a/lib/components/shared/themed_button_tab_bar.dart +++ b/lib/components/shared/themed_button_tab_bar.dart @@ -1,8 +1,8 @@ import 'package:buttons_tabbar/buttons_tabbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:spotube/hooks/use_breakpoint_value.dart'; -import 'package:spotube/hooks/use_brightness_value.dart'; +import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; +import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/utils/platform.dart'; class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget { diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart index dcf01dd2..f211a521 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart @@ -13,8 +13,8 @@ import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_heading.dart'; import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_custom_status_bar_color.dart'; -import 'package:spotube/hooks/use_palette_color.dart'; +import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; +import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; @@ -237,7 +237,7 @@ class TrackCollectionView extends HookConsumerWidget { ), ); } - + return TracksTableView( (tracksSnapshot.data ?? []).map( (track) { diff --git a/lib/hooks/use_disable_battery_optimizations.dart b/lib/hooks/configurators/use_disable_battery_optimizations.dart similarity index 96% rename from lib/hooks/use_disable_battery_optimizations.dart rename to lib/hooks/configurators/use_disable_battery_optimizations.dart index 267655b6..c1155d19 100644 --- a/lib/hooks/use_disable_battery_optimizations.dart +++ b/lib/hooks/configurators/use_disable_battery_optimizations.dart @@ -1,7 +1,7 @@ import 'package:disable_battery_optimization/disable_battery_optimization.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:spotube/hooks/use_async_effect.dart'; +import 'package:spotube/hooks/utils/use_async_effect.dart'; bool _asked = false; void useDisableBatteryOptimizations() { diff --git a/lib/hooks/use_get_storage_perms.dart b/lib/hooks/configurators/use_get_storage_perms.dart similarity index 95% rename from lib/hooks/use_get_storage_perms.dart rename to lib/hooks/configurators/use_get_storage_perms.dart index d83c60f6..3fcb369b 100644 --- a/lib/hooks/use_get_storage_perms.dart +++ b/lib/hooks/configurators/use_get_storage_perms.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:spotube/components/library/user_local_tracks.dart'; -import 'package:spotube/hooks/use_async_effect.dart'; +import 'package:spotube/hooks/utils/use_async_effect.dart'; void useGetStoragePermissions(WidgetRef ref) { final isMounted = useIsMounted(); diff --git a/lib/hooks/use_init_sys_tray.dart b/lib/hooks/configurators/use_init_sys_tray.dart similarity index 100% rename from lib/hooks/use_init_sys_tray.dart rename to lib/hooks/configurators/use_init_sys_tray.dart diff --git a/lib/hooks/use_update_checker.dart b/lib/hooks/configurators/use_update_checker.dart similarity index 98% rename from lib/hooks/use_update_checker.dart rename to lib/hooks/configurators/use_update_checker.dart index 33df5397..515d6701 100644 --- a/lib/hooks/use_update_checker.dart +++ b/lib/hooks/configurators/use_update_checker.dart @@ -8,7 +8,7 @@ import 'package:http/http.dart' as http; import 'package:spotube/collections/env.dart'; import 'package:spotube/components/shared/links/anchor_button.dart'; -import 'package:spotube/hooks/use_package_info.dart'; +import 'package:spotube/hooks/controllers/use_package_info.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:version/version.dart'; diff --git a/lib/hooks/use_auto_scroll_controller.dart b/lib/hooks/controllers/use_auto_scroll_controller.dart similarity index 100% rename from lib/hooks/use_auto_scroll_controller.dart rename to lib/hooks/controllers/use_auto_scroll_controller.dart diff --git a/lib/hooks/use_package_info.dart b/lib/hooks/controllers/use_package_info.dart similarity index 100% rename from lib/hooks/use_package_info.dart rename to lib/hooks/controllers/use_package_info.dart diff --git a/lib/hooks/use_sidebarx_controller.dart b/lib/hooks/controllers/use_sidebarx_controller.dart similarity index 100% rename from lib/hooks/use_sidebarx_controller.dart rename to lib/hooks/controllers/use_sidebarx_controller.dart diff --git a/lib/hooks/use_spotify_infinite_query.dart b/lib/hooks/spotify/use_spotify_infinite_query.dart similarity index 100% rename from lib/hooks/use_spotify_infinite_query.dart rename to lib/hooks/spotify/use_spotify_infinite_query.dart diff --git a/lib/hooks/use_spotify_mutation.dart b/lib/hooks/spotify/use_spotify_mutation.dart similarity index 100% rename from lib/hooks/use_spotify_mutation.dart rename to lib/hooks/spotify/use_spotify_mutation.dart diff --git a/lib/hooks/use_spotify_query.dart b/lib/hooks/spotify/use_spotify_query.dart similarity index 100% rename from lib/hooks/use_spotify_query.dart rename to lib/hooks/spotify/use_spotify_query.dart diff --git a/lib/hooks/use_is_current_route.dart b/lib/hooks/use_is_current_route.dart deleted file mode 100644 index b7b6490a..00000000 --- a/lib/hooks/use_is_current_route.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:go_router/go_router.dart'; - -bool? useIsCurrentRoute([String matcher = "/"]) { - final isCurrentRoute = useState(null); - final context = useContext(); - useEffect(() { - WidgetsBinding.instance.addPostFrameCallback((timer) { - final isCurrent = GoRouterState.of(context).matchedLocation == matcher; - if (isCurrent != isCurrentRoute.value) { - isCurrentRoute.value = isCurrent; - } - }); - return null; - }); - return isCurrentRoute.value; -} diff --git a/lib/hooks/use_shared_preferences.dart b/lib/hooks/use_shared_preferences.dart deleted file mode 100644 index 922beaa6..00000000 --- a/lib/hooks/use_shared_preferences.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -SharedPreferences? useSharedPreferences() { - final future = useMemoized(SharedPreferences.getInstance); - final snapshot = useFuture(future, initialData: null); - - return snapshot.data; -} diff --git a/lib/hooks/use_async_effect.dart b/lib/hooks/utils/use_async_effect.dart similarity index 100% rename from lib/hooks/use_async_effect.dart rename to lib/hooks/utils/use_async_effect.dart diff --git a/lib/hooks/use_breakpoint_value.dart b/lib/hooks/utils/use_breakpoint_value.dart similarity index 100% rename from lib/hooks/use_breakpoint_value.dart rename to lib/hooks/utils/use_breakpoint_value.dart diff --git a/lib/hooks/use_brightness_value.dart b/lib/hooks/utils/use_brightness_value.dart similarity index 100% rename from lib/hooks/use_brightness_value.dart rename to lib/hooks/utils/use_brightness_value.dart diff --git a/lib/hooks/use_custom_status_bar_color.dart b/lib/hooks/utils/use_custom_status_bar_color.dart similarity index 100% rename from lib/hooks/use_custom_status_bar_color.dart rename to lib/hooks/utils/use_custom_status_bar_color.dart diff --git a/lib/hooks/use_debounce.dart b/lib/hooks/utils/use_debounce.dart similarity index 100% rename from lib/hooks/use_debounce.dart rename to lib/hooks/utils/use_debounce.dart diff --git a/lib/hooks/use_force_update.dart b/lib/hooks/utils/use_force_update.dart similarity index 100% rename from lib/hooks/use_force_update.dart rename to lib/hooks/utils/use_force_update.dart diff --git a/lib/hooks/use_palette_color.dart b/lib/hooks/utils/use_palette_color.dart similarity index 100% rename from lib/hooks/use_palette_color.dart rename to lib/hooks/utils/use_palette_color.dart diff --git a/lib/main.dart b/lib/main.dart index b92dfaf1..e1f0bd53 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,8 +14,8 @@ import 'package:metadata_god/metadata_god.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/intents.dart'; -import 'package:spotube/hooks/use_disable_battery_optimizations.dart'; -import 'package:spotube/hooks/use_get_storage_perms.dart'; +import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart'; +import 'package:spotube/hooks/configurators/use_get_storage_perms.dart'; import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/models/matched_track.dart'; @@ -29,7 +29,7 @@ import 'package:spotube/themes/theme.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:system_theme/system_theme.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:spotube/hooks/use_init_sys_tray.dart'; +import 'package:spotube/hooks/configurators/use_init_sys_tray.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 2f169583..299bf9f5 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -16,7 +16,7 @@ import 'package:spotube/components/artist/artist_album_list.dart'; import 'package:spotube/components/artist/artist_card.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_breakpoint_value.dart'; +import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index c97649d7..ac4b61e7 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -11,8 +11,8 @@ import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/themed_button_tab_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_custom_status_bar_color.dart'; -import 'package:spotube/hooks/use_palette_color.dart'; +import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; +import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/provider/authentication_provider.dart'; diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index ad3a13ef..be32dbc9 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -11,7 +11,7 @@ import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_force_update.dart'; +import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; import 'package:spotube/provider/authentication_provider.dart'; diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 5f2afbc9..8147915f 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -7,8 +7,8 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/lyrics/zoom_controls.dart'; import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart'; import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/hooks/use_auto_scroll_controller.dart'; -import 'package:spotube/hooks/use_synced_lyrics.dart'; +import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; +import 'package:spotube/components/lyrics/use_synced_lyrics.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 5797b63f..87be587c 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -15,7 +15,7 @@ import 'package:spotube/components/root/bottom_player.dart'; import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/root/spotube_navigation_bar.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_update_checker.dart'; +import 'package:spotube/hooks/configurators/use_update_checker.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index 97a0aae9..00263680 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -5,7 +5,7 @@ import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/links/hyper_link.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_package_info.dart'; +import 'package:spotube/hooks/controllers/use_package_info.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/services/mutations/album.dart b/lib/services/mutations/album.dart index dfc72fcc..144b6a8f 100644 --- a/lib/services/mutations/album.dart +++ b/lib/services/mutations/album.dart @@ -1,6 +1,6 @@ import 'package:fl_query/fl_query.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/hooks/use_spotify_mutation.dart'; +import 'package:spotube/hooks/spotify/use_spotify_mutation.dart'; class AlbumMutations { const AlbumMutations(); diff --git a/lib/services/mutations/playlist.dart b/lib/services/mutations/playlist.dart index 176b5cd8..a88e8512 100644 --- a/lib/services/mutations/playlist.dart +++ b/lib/services/mutations/playlist.dart @@ -2,7 +2,7 @@ import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/hooks/use_spotify_mutation.dart'; +import 'package:spotube/hooks/spotify/use_spotify_mutation.dart'; import 'package:spotube/services/queries/queries.dart'; typedef PlaylistCRUDVariables = ({ diff --git a/lib/services/mutations/track.dart b/lib/services/mutations/track.dart index 2245c497..f8208b5e 100644 --- a/lib/services/mutations/track.dart +++ b/lib/services/mutations/track.dart @@ -1,6 +1,6 @@ import 'package:fl_query/fl_query.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/hooks/use_spotify_mutation.dart'; +import 'package:spotube/hooks/spotify/use_spotify_mutation.dart'; class TrackMutations { const TrackMutations(); diff --git a/lib/services/queries/album.dart b/lib/services/queries/album.dart index 53fcaf86..2e2e8f14 100644 --- a/lib/services/queries/album.dart +++ b/lib/services/queries/album.dart @@ -2,8 +2,8 @@ import 'package:catcher_2/catcher_2.dart'; import 'package:fl_query/fl_query.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/hooks/use_spotify_infinite_query.dart'; -import 'package:spotube/hooks/use_spotify_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_query.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; class AlbumQueries { diff --git a/lib/services/queries/artist.dart b/lib/services/queries/artist.dart index 6dad2718..7501d619 100644 --- a/lib/services/queries/artist.dart +++ b/lib/services/queries/artist.dart @@ -1,8 +1,8 @@ import 'package:fl_query/fl_query.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/hooks/use_spotify_infinite_query.dart'; -import 'package:spotube/hooks/use_spotify_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_query.dart'; class ArtistQueries { const ArtistQueries(); diff --git a/lib/services/queries/category.dart b/lib/services/queries/category.dart index 33668d82..dbdd2a11 100644 --- a/lib/services/queries/category.dart +++ b/lib/services/queries/category.dart @@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_spotify_infinite_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; diff --git a/lib/services/queries/lyrics.dart b/lib/services/queries/lyrics.dart index 989a2e97..b51016b4 100644 --- a/lib/services/queries/lyrics.dart +++ b/lib/services/queries/lyrics.dart @@ -6,7 +6,7 @@ import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/map.dart'; -import 'package:spotube/hooks/use_spotify_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_query.dart'; import 'package:spotube/models/lyrics.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/utils/service_utils.dart'; diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index c0434a6e..c4532aa8 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -7,8 +7,8 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/components/library/playlist_generate/recommendation_attribute_dials.dart'; import 'package:spotube/extensions/map.dart'; import 'package:spotube/extensions/track.dart'; -import 'package:spotube/hooks/use_spotify_infinite_query.dart'; -import 'package:spotube/hooks/use_spotify_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_query.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; diff --git a/lib/services/queries/search.dart b/lib/services/queries/search.dart index eaf9c1b7..f11f4399 100644 --- a/lib/services/queries/search.dart +++ b/lib/services/queries/search.dart @@ -1,7 +1,7 @@ import 'package:fl_query/fl_query.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/hooks/use_spotify_infinite_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; class SearchQueries { const SearchQueries(); diff --git a/lib/services/queries/user.dart b/lib/services/queries/user.dart index 89792592..40799c1e 100644 --- a/lib/services/queries/user.dart +++ b/lib/services/queries/user.dart @@ -2,7 +2,7 @@ import 'package:fl_query/fl_query.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/hooks/use_spotify_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_query.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; From dc980b024edad3132e72cbb2f0087297a4b76469 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 14 Nov 2023 20:58:16 +0600 Subject: [PATCH 044/131] fix(genres): lag while scrolling --- lib/components/artist/artist_album_list.dart | 1 + lib/components/genre/category_card.dart | 1 + .../horizontal_playbutton_card_view.dart | 28 ++++++------- lib/pages/home/genres.dart | 25 ++++-------- lib/pages/home/personalized.dart | 3 ++ lib/pages/search/sections/albums.dart | 1 + lib/pages/search/sections/artists.dart | 1 + lib/pages/search/sections/playlists.dart | 1 + lib/pages/settings/settings.dart | 40 +++++++++---------- pubspec.lock | 8 ++++ pubspec.yaml | 1 + 11 files changed, 56 insertions(+), 54 deletions(-) diff --git a/lib/components/artist/artist_album_list.dart b/lib/components/artist/artist_album_list.dart index e075cd60..5114170c 100644 --- a/lib/components/artist/artist_album_list.dart +++ b/lib/components/artist/artist_album_list.dart @@ -29,6 +29,7 @@ class ArtistAlbumList extends HookConsumerWidget { final theme = Theme.of(context); return HorizontalPlaybuttonCardView( + isLoadingNextPage: albumsQuery.isLoadingNextPage, hasNextPage: albumsQuery.hasNextPage, items: albums, onFetchMore: albumsQuery.fetchNext, diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart index d5809b5d..7f580157 100644 --- a/lib/components/genre/category_card.dart +++ b/lib/components/genre/category_card.dart @@ -42,6 +42,7 @@ class CategoryCard extends HookConsumerWidget { return HorizontalPlaybuttonCardView( title: Text(category.name!), + isLoadingNextPage: playlistQuery.isLoadingNextPage, 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 index f17a9714..dca77233 100644 --- 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 @@ -7,19 +7,22 @@ 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/utils/use_breakpoint_value.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class HorizontalPlaybuttonCardView extends HookWidget { final Widget title; final List items; final VoidCallback onFetchMore; + final bool isLoadingNextPage; final bool hasNextPage; + const HorizontalPlaybuttonCardView({ required this.title, required this.items, required this.hasNextPage, required this.onFetchMore, + required this.isLoadingNextPage, Key? key, }) : assert( items is List || @@ -58,23 +61,18 @@ class HorizontalPlaybuttonCardView extends HookWidget { PointerDeviceKind.mouse, }, ), - child: ListView.builder( - controller: scrollController, + child: InfiniteList( + scrollController: scrollController, scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(vertical: 8.0), - itemCount: items.length + 1, + itemCount: items.length, + onFetchData: onFetchMore, + loadingBuilder: (context) => const ShimmerPlaybuttonCard(), + emptyBuilder: (context) => + const ShimmerPlaybuttonCard(count: 5), + isLoading: isLoadingNextPage, + hasReachedMax: !hasNextPage, 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) { diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index 6861853d..84082811 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -7,12 +7,12 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/genre/category_card.dart'; import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; -import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/queries/queries.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; class GenrePage extends HookConsumerWidget { const GenrePage({Key? key}) : super(key: key); @@ -79,24 +79,15 @@ class GenrePage extends HookConsumerWidget { const ShimmerCategories() else Expanded( - child: ListView.builder( - controller: scrollController, + child: InfiniteList( + scrollController: scrollController, itemCount: categories.length, + onFetchData: categoriesQuery.fetchNext, + isLoading: categoriesQuery.isLoadingNextPage, + hasReachedMax: !categoriesQuery.hasNextPage, + loadingBuilder: (context) => const ShimmerCategories(), 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]), - ); + return CategoryCard(categories[index]); }, ), ), diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index b596a820..16cfc3a8 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -56,6 +56,7 @@ class PersonalizedPage extends HookConsumerWidget { HorizontalPlaybuttonCardView( items: playlists.toList(), title: Text(context.l10n.featured), + isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage, hasNextPage: featuredPlaylistsQuery.hasNextPage, onFetchMore: featuredPlaylistsQuery.fetchNext, ), @@ -66,6 +67,7 @@ class PersonalizedPage extends HookConsumerWidget { HorizontalPlaybuttonCardView( items: albums, title: Text(context.l10n.new_releases), + isLoadingNextPage: newReleases.isLoadingNextPage, hasNextPage: newReleases.hasNextPage, onFetchMore: newReleases.fetchNext, ), @@ -81,6 +83,7 @@ class PersonalizedPage extends HookConsumerWidget { items: playlists, title: Text(item["name"] ?? ""), hasNextPage: false, + isLoadingNextPage: false, onFetchMore: () {}, ); }) diff --git a/lib/pages/search/sections/albums.dart b/lib/pages/search/sections/albums.dart index 787a1924..8aa33feb 100644 --- a/lib/pages/search/sections/albums.dart +++ b/lib/pages/search/sections/albums.dart @@ -30,6 +30,7 @@ class SearchAlbumsSection extends HookConsumerWidget { ); return HorizontalPlaybuttonCardView( + isLoadingNextPage: query.isLoadingNextPage, hasNextPage: query.hasNextPage, items: albums, onFetchMore: query.fetchNext, diff --git a/lib/pages/search/sections/artists.dart b/lib/pages/search/sections/artists.dart index 7abd5250..b736bf13 100644 --- a/lib/pages/search/sections/artists.dart +++ b/lib/pages/search/sections/artists.dart @@ -28,6 +28,7 @@ class SearchArtistsSection extends HookConsumerWidget { ); return HorizontalPlaybuttonCardView( + isLoadingNextPage: query.isLoadingNextPage, hasNextPage: query.hasNextPage, items: artists, onFetchMore: query.fetchNext, diff --git a/lib/pages/search/sections/playlists.dart b/lib/pages/search/sections/playlists.dart index 620e914b..47614a70 100644 --- a/lib/pages/search/sections/playlists.dart +++ b/lib/pages/search/sections/playlists.dart @@ -26,6 +26,7 @@ class SearchPlaylistsSection extends HookConsumerWidget { ); return HorizontalPlaybuttonCardView( + isLoadingNextPage: query.isLoadingNextPage, hasNextPage: query.hasNextPage, items: playlists, onFetchMore: query.fetchNext, diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 84b51d4d..f14fb453 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -3,7 +3,6 @@ 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'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/settings/sections/about.dart'; @@ -37,29 +36,26 @@ class SettingsPage extends HookConsumerWidget { Flexible( child: Container( constraints: const BoxConstraints(maxWidth: 1366), - child: InterScrollbar( + child: ListView( controller: controller, - child: ListView( - controller: controller, - children: [ - const SettingsAccountSection(), - const SettingsLanguageRegionSection(), - const SettingsAppearanceSection(), - const SettingsPlaybackSection(), - const SettingsDownloadsSection(), - if (DesktopTools.platform.isDesktop) - const SettingsDesktopSection(), - if (!kIsWeb) const SettingsDevelopersSection(), - const SettingsAboutSection(), - Center( - child: FilledButton( - onPressed: preferencesNotifier.reset, - child: Text(context.l10n.restore_defaults), - ), + children: [ + const SettingsAccountSection(), + const SettingsLanguageRegionSection(), + const SettingsAppearanceSection(), + const SettingsPlaybackSection(), + const SettingsDownloadsSection(), + if (DesktopTools.platform.isDesktop) + const SettingsDesktopSection(), + if (!kIsWeb) const SettingsDevelopersSection(), + const SettingsAboutSection(), + Center( + child: FilledButton( + onPressed: preferencesNotifier.reset, + child: Text(context.l10n.restore_defaults), ), - const SizedBox(height: 10), - ], - ), + ), + const SizedBox(height: 10), + ], ), ), ), diff --git a/pubspec.lock b/pubspec.lock index 3d072e09..39e92028 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2180,6 +2180,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + very_good_infinite_list: + dependency: "direct main" + description: + name: very_good_infinite_list + sha256: "6f5ad429edbce6084e1c600e56b26b1de8c6b138e8e8fc2de41b686166029aa5" + url: "https://pub.dev" + source: hosted + version: "0.7.1" visibility_detector: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f9c1155f..590aaae4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -110,6 +110,7 @@ dependencies: git: url: https://github.com/thielepaul/flutter-draggable-scrollbar.git ref: cfd570035bf393de541d32e9b28808b5d7e602df + very_good_infinite_list: ^0.7.1 dev_dependencies: build_runner: ^2.3.2 From 57c03ad045e9b9a4ceb13ac8b061d79396990949 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 14 Nov 2023 22:48:02 +0600 Subject: [PATCH 045/131] refactor: use json serializer for preferences --- lib/components/library/user_local_tracks.dart | 2 +- .../player/sibling_tracks_sheet.dart | 3 +- lib/components/root/bottom_player.dart | 3 +- lib/components/root/sidebar.dart | 3 +- .../root/spotube_navigation_bar.dart | 3 +- .../settings/color_scheme_picker_dialog.dart | 2 +- .../shared/dialogs/piped_down_dialog.dart | 2 +- .../shared/page_window_title_bar.dart | 3 +- .../shared/track_table/tracks_table_view.dart | 3 +- .../configurators/use_init_sys_tray.dart | 2 +- .../configurators/use_update_checker.dart | 2 +- lib/main.dart | 2 +- lib/models/spotube_track.dart | 2 +- lib/pages/home/genres.dart | 2 +- .../playlist_generate/playlist_generate.dart | 2 +- lib/pages/settings/sections/about.dart | 2 +- lib/pages/settings/sections/appearance.dart | 4 +- lib/pages/settings/sections/desktop.dart | 3 +- lib/pages/settings/sections/downloads.dart | 2 +- .../settings/sections/language_region.dart | 2 +- lib/pages/settings/sections/playback.dart | 3 +- lib/pages/settings/settings.dart | 2 +- lib/provider/download_manager_provider.dart | 3 +- .../proxy_playlist/next_fetcher_mixin.dart | 2 +- .../proxy_playlist_provider.dart | 3 +- .../user_preferences_provider.dart | 165 ++++++++ .../user_preferences_state.dart | 273 ++++++++++++ .../user_preferences_state.g.dart | 381 +++++++++++++++++ lib/provider/user_preferences_provider.dart | 391 ------------------ lib/provider/youtube_provider.dart | 2 +- lib/services/queries/album.dart | 2 +- lib/services/queries/category.dart | 2 +- lib/services/queries/playlist.dart | 2 +- lib/services/queries/views.dart | 2 +- lib/services/youtube/youtube.dart | 2 +- 35 files changed, 861 insertions(+), 423 deletions(-) create mode 100644 lib/provider/user_preferences/user_preferences_provider.dart create mode 100644 lib/provider/user_preferences/user_preferences_state.dart create mode 100644 lib/provider/user_preferences/user_preferences_state.g.dart delete mode 100644 lib/provider/user_preferences_provider.dart diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index 0546c2a7..354d9fe6 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -22,7 +22,7 @@ import 'package:spotube/components/shared/track_table/track_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException; diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index f6702e0c..ee8d9719 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -15,7 +15,8 @@ import 'package:spotube/hooks/utils/use_debounce.dart'; import 'package:spotube/models/matched_track.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/services/youtube/youtube.dart'; import 'package:spotube/utils/service_utils.dart'; diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 4bc08fc0..617e760b 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -19,7 +19,8 @@ import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index f7cdcac3..ac5233ed 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -16,7 +16,8 @@ import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; diff --git a/lib/components/root/spotube_navigation_bar.dart b/lib/components/root/spotube_navigation_bar.dart index d602f390..0853c60c 100644 --- a/lib/components/root/spotube_navigation_bar.dart +++ b/lib/components/root/spotube_navigation_bar.dart @@ -11,7 +11,8 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/provider/download_manager_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; final navigationPanelHeight = StateProvider((ref) => 50); diff --git a/lib/components/settings/color_scheme_picker_dialog.dart b/lib/components/settings/color_scheme_picker_dialog.dart index f06a9d84..e0c3d618 100644 --- a/lib/components/settings/color_scheme_picker_dialog.dart +++ b/lib/components/settings/color_scheme_picker_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:system_theme/system_theme.dart'; class SpotubeColor extends Color { diff --git a/lib/components/shared/dialogs/piped_down_dialog.dart b/lib/components/shared/dialogs/piped_down_dialog.dart index 03362ed4..6220adeb 100644 --- a/lib/components/shared/dialogs/piped_down_dialog.dart +++ b/lib/components/shared/dialogs/piped_down_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; class PipedDownDialog extends HookConsumerWidget { const PipedDownDialog({Key? key}) : super(key: key); diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart index 50d468aa..43435f7d 100644 --- a/lib/components/shared/page_window_title_bar.dart +++ b/lib/components/shared/page_window_title_bar.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/utils/platform.dart'; import 'package:titlebar_buttons/titlebar_buttons.dart'; import 'dart:math'; diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index d03e92d7..14a4f1a9 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -22,7 +22,8 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/utils/service_utils.dart'; final trackCollectionSortState = diff --git a/lib/hooks/configurators/use_init_sys_tray.dart b/lib/hooks/configurators/use_init_sys_tray.dart index f342c24a..db4964ce 100644 --- a/lib/hooks/configurators/use_init_sys_tray.dart +++ b/lib/hooks/configurators/use_init_sys_tray.dart @@ -7,7 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; void useInitSysTray(WidgetRef ref) { final context = useContext(); diff --git a/lib/hooks/configurators/use_update_checker.dart b/lib/hooks/configurators/use_update_checker.dart index 515d6701..1a6a5be5 100644 --- a/lib/hooks/configurators/use_update_checker.dart +++ b/lib/hooks/configurators/use_update_checker.dart @@ -9,7 +9,7 @@ import 'package:spotube/collections/env.dart'; import 'package:spotube/components/shared/links/anchor_button.dart'; import 'package:spotube/hooks/controllers/use_package_info.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:version/version.dart'; diff --git a/lib/main.dart b/lib/main.dart index e1f0bd53..f46f02c1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,7 +21,7 @@ import 'package:spotube/models/logger.dart'; import 'package:spotube/models/matched_track.dart'; import 'package:spotube/models/skip_segment.dart'; import 'package:spotube/provider/palette_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/cli/cli.dart'; import 'package:spotube/services/connectivity_adapter.dart'; diff --git a/lib/models/spotube_track.dart b/lib/models/spotube_track.dart index 68641010..67b09ad8 100644 --- a/lib/models/spotube_track.dart +++ b/lib/models/spotube_track.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/matched_track.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/youtube/youtube.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:collection/collection.dart'; diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index 84082811..b3904e2e 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -10,7 +10,7 @@ import 'package:spotube/components/shared/expandable_search/expandable_search.da import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; import 'package:spotube/components/shared/waypoint.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index 33e244b0..4b8dddaf 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -17,7 +17,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart'; import 'package:spotube/provider/spotify_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; diff --git a/lib/pages/settings/sections/about.dart b/lib/pages/settings/sections/about.dart index 85181355..9fe59662 100644 --- a/lib/pages/settings/sections/about.dart +++ b/lib/pages/settings/sections/about.dart @@ -7,7 +7,7 @@ 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:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SettingsAboutSection extends HookConsumerWidget { diff --git a/lib/pages/settings/sections/appearance.dart b/lib/pages/settings/sections/appearance.dart index 5e1ffa50..5de36c63 100644 --- a/lib/pages/settings/sections/appearance.dart +++ b/lib/pages/settings/sections/appearance.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -7,7 +6,8 @@ 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'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; class SettingsAppearanceSection extends HookConsumerWidget { const SettingsAppearanceSection({Key? key}) : super(key: key); diff --git a/lib/pages/settings/sections/desktop.dart b/lib/pages/settings/sections/desktop.dart index 1cc2c5c8..41d6d61e 100644 --- a/lib/pages/settings/sections/desktop.dart +++ b/lib/pages/settings/sections/desktop.dart @@ -4,7 +4,8 @@ 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'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; class SettingsDesktopSection extends HookConsumerWidget { const SettingsDesktopSection({Key? key}) : super(key: key); diff --git a/lib/pages/settings/sections/downloads.dart b/lib/pages/settings/sections/downloads.dart index ff64cdea..12026909 100644 --- a/lib/pages/settings/sections/downloads.dart +++ b/lib/pages/settings/sections/downloads.dart @@ -7,7 +7,7 @@ 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'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; class SettingsDownloadsSection extends HookConsumerWidget { const SettingsDownloadsSection({Key? key}) : super(key: key); diff --git a/lib/pages/settings/sections/language_region.dart b/lib/pages/settings/sections/language_region.dart index ece28455..9465feb3 100644 --- a/lib/pages/settings/sections/language_region.dart +++ b/lib/pages/settings/sections/language_region.dart @@ -9,7 +9,7 @@ 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'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; class SettingsLanguageRegionSection extends HookConsumerWidget { const SettingsLanguageRegionSection({Key? key}) : super(key: key); diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 39d9b7c2..5e000231 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -10,7 +10,8 @@ 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'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; class SettingsPlaybackSection extends HookConsumerWidget { const SettingsPlaybackSection({Key? key}) : super(key: key); diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index f14fb453..842d5240 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -13,7 +13,7 @@ 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/user_preferences/user_preferences_provider.dart'; class SettingsPage extends HookConsumerWidget { const SettingsPage({Key? key}) : super(key: key); diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index 46c7ee7e..889641f4 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -10,7 +10,8 @@ import 'package:metadata_god/metadata_god.dart'; import 'package:path/path.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/spotube_track.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/services/download_manager/download_manager.dart'; import 'package:spotube/services/youtube/youtube.dart'; diff --git a/lib/provider/proxy_playlist/next_fetcher_mixin.dart b/lib/provider/proxy_playlist/next_fetcher_mixin.dart index f6776234..b447f1ef 100644 --- a/lib/provider/proxy_playlist/next_fetcher_mixin.dart +++ b/lib/provider/proxy_playlist/next_fetcher_mixin.dart @@ -6,7 +6,7 @@ import 'package:spotube/models/logger.dart'; import 'package:spotube/models/matched_track.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/supabase.dart'; import 'package:spotube/services/youtube/youtube.dart'; diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index bf7293ce..50024661 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -20,7 +20,8 @@ import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/next_fetcher_mixin.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/scrobbler_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/audio_services.dart'; diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart new file mode 100644 index 00000000..db4b73dc --- /dev/null +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -0,0 +1,165 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/models/matched_track.dart'; +import 'package:spotube/provider/palette_provider.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; + +import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:path/path.dart' as path; + +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); + } + + Future _getDefaultDownloadDirectory() async { + if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; + + if (kIsMacOS) { + return path.join((await getLibraryDirectory()).path, "Caches"); + } + + return getDownloadsDirectory().then((dir) { + return path.join(dir!.path, "Spotube"); + }); + } + + @override + FutureOr onInit() async { + if (state.downloadLocation.isEmpty) { + state = state.copyWith( + downloadLocation: await _getDefaultDownloadDirectory(), + ); + } + + if (DesktopTools.platform.isDesktop) { + await DesktopTools.window.setTitleBarStyle( + state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, + ); + } + + await audioPlayer.setAudioNormalization(state.normalizeAudio); + } + + @override + FutureOr fromJson(Map json) { + return UserPreferences.fromJson(json); + } + + @override + Map toJson() { + return state.toJson(); + } +} + +final userPreferencesProvider = + StateNotifierProvider( + (ref) => UserPreferencesNotifier(ref), +); diff --git a/lib/provider/user_preferences/user_preferences_state.dart b/lib/provider/user_preferences/user_preferences_state.dart new file mode 100644 index 00000000..ff98fa8e --- /dev/null +++ b/lib/provider/user_preferences/user_preferences_state.dart @@ -0,0 +1,273 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; +import 'package:spotube/models/matched_track.dart'; + +part 'user_preferences_state.g.dart'; + +@JsonEnum() +enum LayoutMode { + compact, + extended, + adaptive, +} + +@JsonEnum() +enum AudioQuality { + high, + low, +} + +@JsonEnum() +enum CloseBehavior { + minimizeToTray, + close, +} + +@JsonEnum() +enum YoutubeApiType { + youtube, + piped; + + String get label => name[0].toUpperCase() + name.substring(1); +} + +@JsonEnum() +enum MusicCodec { + m4a._("M4a (Best for downloaded music)"), + weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"); + + final String label; + const MusicCodec._(this.label); +} + +@JsonSerializable() +final class UserPreferences { + @JsonKey( + defaultValue: AudioQuality.high, + unknownEnumValue: AudioQuality.high, + ) + final AudioQuality audioQuality; + + @JsonKey(defaultValue: true) + final bool albumColorSync; + + @JsonKey(defaultValue: false) + final bool amoledDarkTheme; + + @JsonKey(defaultValue: true) + final bool checkUpdate; + + @JsonKey(defaultValue: false) + final bool normalizeAudio; + + @JsonKey(defaultValue: true) + final bool showSystemTrayIcon; + + @JsonKey(defaultValue: true) + final bool skipNonMusic; + + @JsonKey(defaultValue: false) + final bool systemTitleBar; + + @JsonKey( + defaultValue: CloseBehavior.minimizeToTray, + unknownEnumValue: CloseBehavior.minimizeToTray, + ) + final CloseBehavior closeBehavior; + + static SpotubeColor _accentColorSchemeFromJson(Map json) { + return SpotubeColor.fromString(json["color"]); + } + + static Map? _accentColorSchemeReadValue( + Map json, String key) { + if (json[key] is String) { + return {"color": json[key]}; + } + + return json[key] as Map?; + } + + static Map _accentColorSchemeToJson(SpotubeColor color) { + return {"color": color.toString()}; + } + + static SpotubeColor _defaultAccentColorScheme() => + const SpotubeColor(0xFF2196F3, name: "Blue"); + + @JsonKey( + defaultValue: UserPreferences._defaultAccentColorScheme, + fromJson: UserPreferences._accentColorSchemeFromJson, + toJson: UserPreferences._accentColorSchemeToJson, + readValue: UserPreferences._accentColorSchemeReadValue, + ) + final SpotubeColor accentColorScheme; + + @JsonKey( + defaultValue: LayoutMode.adaptive, + unknownEnumValue: LayoutMode.adaptive, + ) + final LayoutMode layoutMode; + + static Locale _localeFromJson(Map json) { + return Locale(json["languageCode"], json["countryCode"]); + } + + static Map _localeToJson(Locale locale) { + return { + "languageCode": locale.languageCode, + "countryCode": locale.countryCode, + }; + } + + static Map? _localeReadValue( + Map json, String key) { + if (json[key] is String) { + final map = jsonDecode(json[key]); + return { + "languageCode": map["lc"], + "countryCode": map["cc"], + }; + } + + return json[key] as Map?; + } + + static Locale _defaultLocaleValue() => const Locale("system", "system"); + + @JsonKey( + defaultValue: UserPreferences._defaultLocaleValue, + toJson: UserPreferences._localeToJson, + fromJson: UserPreferences._localeFromJson, + readValue: UserPreferences._localeReadValue, + ) + final Locale locale; + + @JsonKey( + defaultValue: Market.US, + unknownEnumValue: Market.US, + ) + final Market recommendationMarket; + + @JsonKey( + defaultValue: SearchMode.youtube, + unknownEnumValue: SearchMode.youtube, + ) + final SearchMode searchMode; + + @JsonKey(defaultValue: "") + final String downloadLocation; + + @JsonKey(defaultValue: "https://pipedapi.kavin.rocks") + final String pipedInstance; + + @JsonKey( + defaultValue: ThemeMode.system, + unknownEnumValue: ThemeMode.system, + ) + final ThemeMode themeMode; + + @JsonKey( + defaultValue: YoutubeApiType.youtube, + unknownEnumValue: YoutubeApiType.youtube, + ) + final YoutubeApiType youtubeApiType; + + @JsonKey( + defaultValue: MusicCodec.weba, + unknownEnumValue: MusicCodec.weba, + ) + final MusicCodec streamMusicCodec; + + @JsonKey( + defaultValue: MusicCodec.m4a, + unknownEnumValue: MusicCodec.m4a, + ) + final MusicCodec downloadMusicCodec; + + UserPreferences({ + required this.audioQuality, + required this.albumColorSync, + required this.amoledDarkTheme, + required this.checkUpdate, + required this.normalizeAudio, + required this.showSystemTrayIcon, + required this.skipNonMusic, + required this.systemTitleBar, + required this.closeBehavior, + required this.accentColorScheme, + required this.layoutMode, + required this.locale, + required this.recommendationMarket, + required this.searchMode, + required this.downloadLocation, + required this.pipedInstance, + required this.themeMode, + required this.youtubeApiType, + required this.streamMusicCodec, + required this.downloadMusicCodec, + }); + + factory UserPreferences.withDefaults() { + return UserPreferences.fromJson({}); + } + + factory UserPreferences.fromJson(Map json) { + return _$UserPreferencesFromJson(json); + } + + Map toJson() { + return _$UserPreferencesToJson(this); + } + + UserPreferences copyWith({ + ThemeMode? themeMode, + SpotubeColor? accentColorScheme, + bool? albumColorSync, + bool? checkUpdate, + AudioQuality? audioQuality, + String? downloadLocation, + LayoutMode? layoutMode, + CloseBehavior? closeBehavior, + bool? showSystemTrayIcon, + Locale? locale, + String? pipedInstance, + SearchMode? searchMode, + bool? skipNonMusic, + YoutubeApiType? youtubeApiType, + Market? recommendationMarket, + bool? saveTrackLyrics, + bool? amoledDarkTheme, + bool? normalizeAudio, + MusicCodec? downloadMusicCodec, + MusicCodec? streamMusicCodec, + bool? systemTitleBar, + }) { + return UserPreferences( + themeMode: themeMode ?? this.themeMode, + accentColorScheme: accentColorScheme ?? this.accentColorScheme, + albumColorSync: albumColorSync ?? this.albumColorSync, + checkUpdate: checkUpdate ?? this.checkUpdate, + audioQuality: audioQuality ?? this.audioQuality, + downloadLocation: downloadLocation ?? this.downloadLocation, + layoutMode: layoutMode ?? this.layoutMode, + closeBehavior: closeBehavior ?? this.closeBehavior, + showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, + locale: locale ?? this.locale, + pipedInstance: pipedInstance ?? this.pipedInstance, + searchMode: searchMode ?? this.searchMode, + 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, + ); + } +} diff --git a/lib/provider/user_preferences/user_preferences_state.g.dart b/lib/provider/user_preferences/user_preferences_state.g.dart new file mode 100644 index 00000000..9e3eeee9 --- /dev/null +++ b/lib/provider/user_preferences/user_preferences_state.g.dart @@ -0,0 +1,381 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_preferences_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserPreferences _$UserPreferencesFromJson(Map json) => + UserPreferences( + audioQuality: $enumDecodeNullable( + _$AudioQualityEnumMap, json['audioQuality'], + unknownValue: AudioQuality.high) ?? + AudioQuality.high, + albumColorSync: json['albumColorSync'] as bool? ?? true, + amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false, + checkUpdate: json['checkUpdate'] as bool? ?? true, + normalizeAudio: json['normalizeAudio'] as bool? ?? false, + showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? true, + skipNonMusic: json['skipNonMusic'] as bool? ?? true, + systemTitleBar: json['systemTitleBar'] as bool? ?? false, + closeBehavior: $enumDecodeNullable( + _$CloseBehaviorEnumMap, json['closeBehavior'], + unknownValue: CloseBehavior.minimizeToTray) ?? + CloseBehavior.minimizeToTray, + accentColorScheme: UserPreferences._accentColorSchemeReadValue( + json, 'accentColorScheme') == + null + ? UserPreferences._defaultAccentColorScheme() + : UserPreferences._accentColorSchemeFromJson( + UserPreferences._accentColorSchemeReadValue( + json, 'accentColorScheme') as Map), + layoutMode: $enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode'], + unknownValue: LayoutMode.adaptive) ?? + LayoutMode.adaptive, + locale: UserPreferences._localeReadValue(json, 'locale') == null + ? UserPreferences._defaultLocaleValue() + : UserPreferences._localeFromJson( + UserPreferences._localeReadValue(json, 'locale') + as Map), + recommendationMarket: $enumDecodeNullable( + _$MarketEnumMap, json['recommendationMarket'], + unknownValue: Market.US) ?? + Market.US, + searchMode: $enumDecodeNullable(_$SearchModeEnumMap, json['searchMode'], + unknownValue: SearchMode.youtube) ?? + SearchMode.youtube, + downloadLocation: json['downloadLocation'] as String? ?? '', + pipedInstance: + json['pipedInstance'] as String? ?? 'https://pipedapi.kavin.rocks', + themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode'], + unknownValue: ThemeMode.system) ?? + ThemeMode.system, + youtubeApiType: $enumDecodeNullable( + _$YoutubeApiTypeEnumMap, json['youtubeApiType'], + unknownValue: YoutubeApiType.youtube) ?? + YoutubeApiType.youtube, + streamMusicCodec: $enumDecodeNullable( + _$MusicCodecEnumMap, json['streamMusicCodec'], + unknownValue: MusicCodec.weba) ?? + MusicCodec.weba, + downloadMusicCodec: $enumDecodeNullable( + _$MusicCodecEnumMap, json['downloadMusicCodec'], + unknownValue: MusicCodec.m4a) ?? + MusicCodec.m4a, + ); + +Map _$UserPreferencesToJson(UserPreferences instance) => + { + 'audioQuality': _$AudioQualityEnumMap[instance.audioQuality]!, + 'albumColorSync': instance.albumColorSync, + 'amoledDarkTheme': instance.amoledDarkTheme, + 'checkUpdate': instance.checkUpdate, + 'normalizeAudio': instance.normalizeAudio, + 'showSystemTrayIcon': instance.showSystemTrayIcon, + 'skipNonMusic': instance.skipNonMusic, + 'systemTitleBar': instance.systemTitleBar, + 'closeBehavior': _$CloseBehaviorEnumMap[instance.closeBehavior]!, + 'accentColorScheme': + UserPreferences._accentColorSchemeToJson(instance.accentColorScheme), + 'layoutMode': _$LayoutModeEnumMap[instance.layoutMode]!, + 'locale': UserPreferences._localeToJson(instance.locale), + 'recommendationMarket': _$MarketEnumMap[instance.recommendationMarket]!, + 'searchMode': _$SearchModeEnumMap[instance.searchMode]!, + 'downloadLocation': instance.downloadLocation, + 'pipedInstance': instance.pipedInstance, + 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, + 'youtubeApiType': _$YoutubeApiTypeEnumMap[instance.youtubeApiType]!, + 'streamMusicCodec': _$MusicCodecEnumMap[instance.streamMusicCodec]!, + 'downloadMusicCodec': _$MusicCodecEnumMap[instance.downloadMusicCodec]!, + }; + +const _$AudioQualityEnumMap = { + AudioQuality.high: 'high', + AudioQuality.low: 'low', +}; + +const _$CloseBehaviorEnumMap = { + CloseBehavior.minimizeToTray: 'minimizeToTray', + CloseBehavior.close: 'close', +}; + +const _$LayoutModeEnumMap = { + LayoutMode.compact: 'compact', + LayoutMode.extended: 'extended', + LayoutMode.adaptive: 'adaptive', +}; + +const _$MarketEnumMap = { + Market.AD: 'AD', + Market.AE: 'AE', + Market.AF: 'AF', + Market.AG: 'AG', + Market.AI: 'AI', + Market.AL: 'AL', + Market.AM: 'AM', + Market.AO: 'AO', + Market.AQ: 'AQ', + Market.AR: 'AR', + Market.AS: 'AS', + Market.AT: 'AT', + Market.AU: 'AU', + Market.AW: 'AW', + Market.AX: 'AX', + Market.AZ: 'AZ', + Market.BA: 'BA', + Market.BB: 'BB', + Market.BD: 'BD', + Market.BE: 'BE', + Market.BF: 'BF', + Market.BG: 'BG', + Market.BH: 'BH', + Market.BI: 'BI', + Market.BJ: 'BJ', + Market.BL: 'BL', + Market.BM: 'BM', + Market.BN: 'BN', + Market.BO: 'BO', + Market.BQ: 'BQ', + Market.BR: 'BR', + Market.BS: 'BS', + Market.BT: 'BT', + Market.BV: 'BV', + Market.BW: 'BW', + Market.BY: 'BY', + Market.BZ: 'BZ', + Market.CA: 'CA', + Market.CC: 'CC', + Market.CD: 'CD', + Market.CF: 'CF', + Market.CG: 'CG', + Market.CH: 'CH', + Market.CI: 'CI', + Market.CK: 'CK', + Market.CL: 'CL', + Market.CM: 'CM', + Market.CN: 'CN', + Market.CO: 'CO', + Market.CR: 'CR', + Market.CU: 'CU', + Market.CV: 'CV', + Market.CW: 'CW', + Market.CX: 'CX', + Market.CY: 'CY', + Market.CZ: 'CZ', + Market.DE: 'DE', + Market.DJ: 'DJ', + Market.DK: 'DK', + Market.DM: 'DM', + Market.DO: 'DO', + Market.DZ: 'DZ', + Market.EC: 'EC', + Market.EE: 'EE', + Market.EG: 'EG', + Market.EH: 'EH', + Market.ER: 'ER', + Market.ES: 'ES', + Market.ET: 'ET', + Market.FI: 'FI', + Market.FJ: 'FJ', + Market.FK: 'FK', + Market.FM: 'FM', + Market.FO: 'FO', + Market.FR: 'FR', + Market.GA: 'GA', + Market.GB: 'GB', + Market.GD: 'GD', + Market.GE: 'GE', + Market.GF: 'GF', + Market.GG: 'GG', + Market.GH: 'GH', + Market.GI: 'GI', + Market.GL: 'GL', + Market.GM: 'GM', + Market.GN: 'GN', + Market.GP: 'GP', + Market.GQ: 'GQ', + Market.GR: 'GR', + Market.GS: 'GS', + Market.GT: 'GT', + Market.GU: 'GU', + Market.GW: 'GW', + Market.GY: 'GY', + Market.HK: 'HK', + Market.HM: 'HM', + Market.HN: 'HN', + Market.HR: 'HR', + Market.HT: 'HT', + Market.HU: 'HU', + Market.ID: 'ID', + Market.IE: 'IE', + Market.IL: 'IL', + Market.IM: 'IM', + Market.IN: 'IN', + Market.IO: 'IO', + Market.IQ: 'IQ', + Market.IR: 'IR', + Market.IS: 'IS', + Market.IT: 'IT', + Market.JE: 'JE', + Market.JM: 'JM', + Market.JO: 'JO', + Market.JP: 'JP', + Market.KE: 'KE', + Market.KG: 'KG', + Market.KH: 'KH', + Market.KI: 'KI', + Market.KM: 'KM', + Market.KN: 'KN', + Market.KP: 'KP', + Market.KR: 'KR', + Market.KW: 'KW', + Market.KY: 'KY', + Market.KZ: 'KZ', + Market.LA: 'LA', + Market.LB: 'LB', + Market.LC: 'LC', + Market.LI: 'LI', + Market.LK: 'LK', + Market.LR: 'LR', + Market.LS: 'LS', + Market.LT: 'LT', + Market.LU: 'LU', + Market.LV: 'LV', + Market.LY: 'LY', + Market.MA: 'MA', + Market.MC: 'MC', + Market.MD: 'MD', + Market.ME: 'ME', + Market.MF: 'MF', + Market.MG: 'MG', + Market.MH: 'MH', + Market.MK: 'MK', + Market.ML: 'ML', + Market.MM: 'MM', + Market.MN: 'MN', + Market.MO: 'MO', + Market.MP: 'MP', + Market.MQ: 'MQ', + Market.MR: 'MR', + Market.MS: 'MS', + Market.MT: 'MT', + Market.MU: 'MU', + Market.MV: 'MV', + Market.MW: 'MW', + Market.MX: 'MX', + Market.MY: 'MY', + Market.MZ: 'MZ', + Market.NA: 'NA', + Market.NC: 'NC', + Market.NE: 'NE', + Market.NF: 'NF', + Market.NG: 'NG', + Market.NI: 'NI', + Market.NL: 'NL', + Market.NO: 'NO', + Market.NP: 'NP', + Market.NR: 'NR', + Market.NU: 'NU', + Market.NZ: 'NZ', + Market.OM: 'OM', + Market.PA: 'PA', + Market.PE: 'PE', + Market.PF: 'PF', + Market.PG: 'PG', + Market.PH: 'PH', + Market.PK: 'PK', + Market.PL: 'PL', + Market.PM: 'PM', + Market.PN: 'PN', + Market.PR: 'PR', + Market.PS: 'PS', + Market.PT: 'PT', + Market.PW: 'PW', + Market.PY: 'PY', + Market.QA: 'QA', + Market.RE: 'RE', + Market.RO: 'RO', + Market.RS: 'RS', + Market.RU: 'RU', + Market.RW: 'RW', + Market.SA: 'SA', + Market.SB: 'SB', + Market.SC: 'SC', + Market.SD: 'SD', + Market.SE: 'SE', + Market.SG: 'SG', + Market.SH: 'SH', + Market.SI: 'SI', + Market.SJ: 'SJ', + Market.SK: 'SK', + Market.SL: 'SL', + Market.SM: 'SM', + Market.SN: 'SN', + Market.SO: 'SO', + Market.SR: 'SR', + Market.SS: 'SS', + Market.ST: 'ST', + Market.SV: 'SV', + Market.SX: 'SX', + Market.SY: 'SY', + Market.SZ: 'SZ', + Market.TC: 'TC', + Market.TD: 'TD', + Market.TF: 'TF', + Market.TG: 'TG', + Market.TH: 'TH', + Market.TJ: 'TJ', + Market.TK: 'TK', + Market.TL: 'TL', + Market.TM: 'TM', + Market.TN: 'TN', + Market.TO: 'TO', + Market.TR: 'TR', + Market.TT: 'TT', + Market.TV: 'TV', + Market.TW: 'TW', + Market.TZ: 'TZ', + Market.UA: 'UA', + Market.UG: 'UG', + Market.UM: 'UM', + Market.US: 'US', + Market.UY: 'UY', + Market.UZ: 'UZ', + Market.VA: 'VA', + Market.VC: 'VC', + Market.VE: 'VE', + Market.VG: 'VG', + Market.VI: 'VI', + Market.VN: 'VN', + Market.VU: 'VU', + Market.WF: 'WF', + Market.WS: 'WS', + Market.XK: 'XK', + Market.YE: 'YE', + Market.YT: 'YT', + Market.ZA: 'ZA', + Market.ZM: 'ZM', + Market.ZW: 'ZW', +}; + +const _$SearchModeEnumMap = { + SearchMode.youtube: 'youtube', + SearchMode.youtubeMusic: 'youtubeMusic', +}; + +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; + +const _$YoutubeApiTypeEnumMap = { + YoutubeApiType.youtube: 'youtube', + YoutubeApiType.piped: 'piped', +}; + +const _$MusicCodecEnumMap = { + MusicCodec.m4a: 'm4a', + MusicCodec.weba: 'weba', +}; diff --git a/lib/provider/user_preferences_provider.dart b/lib/provider/user_preferences_provider.dart deleted file mode 100644 index 80c71de9..00000000 --- a/lib/provider/user_preferences_provider.dart +++ /dev/null @@ -1,391 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; -import 'package:spotube/models/matched_track.dart'; -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_state_notifier.dart'; -import 'package:spotube/utils/platform.dart'; -import 'package:path/path.dart' as path; - -enum LayoutMode { - compact, - extended, - adaptive, -} - -enum AudioQuality { - high, - low, -} - -enum CloseBehavior { - minimizeToTray, - close, -} - -enum YoutubeApiType { - youtube, - piped; - - String get label => name[0].toUpperCase() + name.substring(1); -} - -enum MusicCodec { - m4a._("M4a (Best for downloaded music)"), - weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"); - - final String label; - const MusicCodec._(this.label); -} - -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; - final String pipedInstance; - final ThemeMode themeMode; - final YoutubeApiType youtubeApiType; - final MusicCodec streamMusicCodec; - final MusicCodec downloadMusicCodec; - - 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) => this.downloadLocation = value, - ); - } - } - - 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, - ); - } - - static Future _getDefaultDownloadDirectory() async { - if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; - - if (kIsMacOS) { - return path.join((await getLibraryDirectory()).path, "Caches"); - } - - return getDownloadsDirectory().then((dir) { - return path.join(dir!.path, "Spotube"); - }); - } - - static Future fromJson(Map json) async { - final localeMap = - json["locale"] != null ? jsonDecode(json["locale"]) : null; - - final systemTitleBar = json["systemTitleBar"] ?? false; - if (DesktopTools.platform.isDesktop) { - await DesktopTools.window.setTitleBarStyle( - systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, - ); - } - - final normalizeAudio = json["normalizeAudio"] ?? true; - audioPlayer.setAudioNormalization(normalizeAudio); - - 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"]], - ); - } - - Map toJson() { - return { - "recommendationMarket": recommendationMarket.index, - "themeMode": themeMode.index, - "accentColorScheme": accentColorScheme.toString(), - "albumColorSync": albumColorSync, - "checkUpdate": checkUpdate, - "audioQuality": audioQuality.index, - "downloadLocation": downloadLocation, - "layoutMode": layoutMode.index, - "closeBehavior": closeBehavior.index, - "showSystemTrayIcon": showSystemTrayIcon, - "locale": - jsonEncode({"lc": locale.languageCode, "cc": locale.countryCode}), - "pipedInstance": pipedInstance, - "searchMode": searchMode.index, - "skipNonMusic": skipNonMusic, - "youtubeApiType": youtubeApiType.index, - 'systemTitleBar': systemTitleBar, - "amoledDarkTheme": amoledDarkTheme, - "normalizeAudio": normalizeAudio, - "streamMusicCodec": streamMusicCodec.index, - "downloadMusicCodec": downloadMusicCodec.index, - }; - } - - UserPreferences copyWith({ - ThemeMode? themeMode, - SpotubeColor? accentColorScheme, - bool? albumColorSync, - bool? checkUpdate, - AudioQuality? audioQuality, - String? downloadLocation, - LayoutMode? layoutMode, - CloseBehavior? closeBehavior, - bool? showSystemTrayIcon, - Locale? locale, - String? pipedInstance, - SearchMode? searchMode, - bool? skipNonMusic, - YoutubeApiType? youtubeApiType, - Market? recommendationMarket, - bool? saveTrackLyrics, - bool? amoledDarkTheme, - bool? normalizeAudio, - MusicCodec? downloadMusicCodec, - MusicCodec? streamMusicCodec, - bool? systemTitleBar, - }) { - return UserPreferences( - themeMode: themeMode ?? this.themeMode, - accentColorScheme: accentColorScheme ?? this.accentColorScheme, - albumColorSync: albumColorSync ?? this.albumColorSync, - checkUpdate: checkUpdate ?? this.checkUpdate, - audioQuality: audioQuality ?? this.audioQuality, - downloadLocation: downloadLocation ?? this.downloadLocation, - layoutMode: layoutMode ?? this.layoutMode, - closeBehavior: closeBehavior ?? this.closeBehavior, - showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon, - locale: locale ?? this.locale, - pipedInstance: pipedInstance ?? this.pipedInstance, - searchMode: searchMode ?? this.searchMode, - 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, - ); - } -} - -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), -); diff --git a/lib/provider/youtube_provider.dart b/lib/provider/youtube_provider.dart index 0e7b7d0e..33e0496f 100644 --- a/lib/provider/youtube_provider.dart +++ b/lib/provider/youtube_provider.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/youtube/youtube.dart'; final youtubeProvider = Provider((ref) { diff --git a/lib/services/queries/album.dart b/lib/services/queries/album.dart index 2e2e8f14..546b3d15 100644 --- a/lib/services/queries/album.dart +++ b/lib/services/queries/album.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; import 'package:spotube/hooks/spotify/use_spotify_query.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; class AlbumQueries { const AlbumQueries(); diff --git a/lib/services/queries/category.dart b/lib/services/queries/category.dart index dbdd2a11..960b5702 100644 --- a/lib/services/queries/category.dart +++ b/lib/services/queries/category.dart @@ -6,7 +6,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; class CategoryQueries { const CategoryQueries(); diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index c4532aa8..2c6c38be 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -11,7 +11,7 @@ import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; import 'package:spotube/hooks/spotify/use_spotify_query.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; typedef RecommendationParameters = ({ RecommendationAttribute acousticness, diff --git a/lib/services/queries/views.dart b/lib/services/queries/views.dart index b56f07d9..4864ffe1 100644 --- a/lib/services/queries/views.dart +++ b/lib/services/queries/views.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; class ViewsQueries { const ViewsQueries(); diff --git a/lib/services/youtube/youtube.dart b/lib/services/youtube/youtube.dart index c8c277e3..2b52864b 100644 --- a/lib/services/youtube/youtube.dart +++ b/lib/services/youtube/youtube.dart @@ -4,7 +4,7 @@ import 'package:piped_client/piped_client.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/components/shared/dialogs/piped_down_dialog.dart'; import 'package:spotube/models/matched_track.dart'; -import 'package:spotube/provider/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; From 14069cd4fe08597c8d9aa0810270fb4c386c1d55 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 15 Nov 2023 18:34:46 +0600 Subject: [PATCH 046/131] feat: Add JioSaavn as audio source (#881) * feat: implement new SourcedTrack for youtube and piped * refactor: replace old spotube track with sourced track * feat: add jiosaavn as audio source * fix: download not working other than jiosaavn * Merge branch 'dev' into feat-jiosaavn --- .../library/user_downloads/download_item.dart | 36 +-- .../player/sibling_tracks_sheet.dart | 57 ++-- .../shared/dialogs/track_details_dialog.dart | 31 +- .../shared/track_table/track_options.dart | 2 +- .../shared/track_table/tracks_table_view.dart | 4 +- lib/main.dart | 14 +- lib/models/current_playlist.dart | 9 +- lib/models/matched_track.dart | 69 ----- lib/models/matched_track.g.dart | 86 ------ lib/models/source_match.dart | 54 ++++ lib/models/source_match.g.dart | 119 ++++++++ lib/models/spotube_track.dart | 274 ------------------ lib/pages/album/album.dart | 4 +- lib/pages/playlist/playlist.dart | 4 +- lib/pages/settings/sections/playback.dart | 113 ++++---- lib/provider/download_manager_provider.dart | 73 ++--- lib/provider/piped_instances_provider.dart | 7 +- .../proxy_playlist/next_fetcher_mixin.dart | 53 +--- .../proxy_playlist/proxy_playlist.dart | 18 +- .../proxy_playlist_provider.dart | 91 +++--- .../user_preferences_provider.dart | 12 +- .../user_preferences_state.dart | 63 ++-- .../user_preferences_state.g.dart | 54 ++-- lib/provider/youtube_provider.dart | 8 - lib/services/audio_player/audio_player.dart | 2 +- .../audio_player/audio_player_impl.dart | 8 +- .../audio_services/audio_services.dart | 6 +- .../audio_services/linux_audio_service.dart | 7 +- lib/services/queries/lyrics.dart | 4 +- lib/services/sourced_track/enums.dart | 18 ++ lib/services/sourced_track/exceptions.dart | 7 + .../sourced_track/models/source_info.dart | 33 +++ .../sourced_track/models/source_info.g.dart | 30 ++ .../sourced_track/models/source_map.dart | 58 ++++ .../sourced_track/models/source_map.g.dart | 35 +++ .../sourced_track/models/video_info.dart | 114 ++++++++ lib/services/sourced_track/sourced_track.dart | 171 +++++++++++ .../sourced_track/sources/jiosaavn.dart | 159 ++++++++++ lib/services/sourced_track/sources/piped.dart | 257 ++++++++++++++++ .../sourced_track/sources/youtube.dart | 256 ++++++++++++++++ lib/services/supabase.dart | 6 +- lib/services/youtube/youtube.dart | 248 ---------------- lib/utils/service_utils.dart | 6 +- pubspec.lock | 17 ++ pubspec.yaml | 3 + 45 files changed, 1691 insertions(+), 1009 deletions(-) delete mode 100644 lib/models/matched_track.dart delete mode 100644 lib/models/matched_track.g.dart create mode 100644 lib/models/source_match.dart create mode 100644 lib/models/source_match.g.dart delete mode 100644 lib/models/spotube_track.dart delete mode 100644 lib/provider/youtube_provider.dart create mode 100644 lib/services/sourced_track/enums.dart create mode 100644 lib/services/sourced_track/exceptions.dart create mode 100644 lib/services/sourced_track/models/source_info.dart create mode 100644 lib/services/sourced_track/models/source_info.g.dart create mode 100644 lib/services/sourced_track/models/source_map.dart create mode 100644 lib/services/sourced_track/models/source_map.g.dart create mode 100644 lib/services/sourced_track/models/video_info.dart create mode 100644 lib/services/sourced_track/sourced_track.dart create mode 100644 lib/services/sourced_track/sources/jiosaavn.dart create mode 100644 lib/services/sourced_track/sources/piped.dart create mode 100644 lib/services/sourced_track/sources/youtube.dart delete mode 100644 lib/services/youtube/youtube.dart diff --git a/lib/components/library/user_downloads/download_item.dart b/lib/components/library/user_downloads/download_item.dart index ae8a2513..10dec410 100644 --- a/lib/components/library/user_downloads/download_item.dart +++ b/lib/components/library/user_downloads/download_item.dart @@ -5,9 +5,9 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/services/download_manager/download_status.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class DownloadItem extends HookConsumerWidget { @@ -24,25 +24,25 @@ class DownloadItem extends HookConsumerWidget { final taskStatus = useState(null); useEffect(() { - if (track is! SpotubeTrack) return null; - final notifier = downloadManager.getStatusNotifier(track as SpotubeTrack); + if (track is! SourcedTrack) return null; + final notifier = downloadManager.getStatusNotifier(track as SourcedTrack); taskStatus.value = notifier?.value; - listener() { + + void listener() { taskStatus.value = notifier?.value; } - downloadManager - .getStatusNotifier(track as SpotubeTrack) - ?.addListener(listener); + notifier?.addListener(listener); return () { - downloadManager - .getStatusNotifier(track as SpotubeTrack) - ?.removeListener(listener); + notifier?.removeListener(listener); }; }, [track]); + final isQueryingSourceInfo = + taskStatus.value == null || track is! SourcedTrack; + return ListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 5), @@ -63,7 +63,7 @@ class DownloadItem extends HookConsumerWidget { track.artists ?? [], mainAxisAlignment: WrapAlignment.start, ), - trailing: taskStatus.value == null || track is! SpotubeTrack + trailing: isQueryingSourceInfo ? Text( context.l10n.querying_info, style: Theme.of(context).textTheme.labelMedium, @@ -72,7 +72,7 @@ class DownloadItem extends HookConsumerWidget { DownloadStatus.downloading => HookBuilder(builder: (context) { final taskProgress = useListenable(useMemoized( () => downloadManager - .getProgressNotifier(track as SpotubeTrack), + .getProgressNotifier(track as SourcedTrack), [track], )); return SizedBox( @@ -86,13 +86,13 @@ class DownloadItem extends HookConsumerWidget { IconButton( icon: const Icon(SpotubeIcons.pause), onPressed: () { - downloadManager.pause(track as SpotubeTrack); + downloadManager.pause(track as SourcedTrack); }), const SizedBox(width: 10), IconButton( icon: const Icon(SpotubeIcons.close), onPressed: () { - downloadManager.cancel(track as SpotubeTrack); + downloadManager.cancel(track as SourcedTrack); }), ], ), @@ -104,13 +104,13 @@ class DownloadItem extends HookConsumerWidget { IconButton( icon: const Icon(SpotubeIcons.play), onPressed: () { - downloadManager.resume(track as SpotubeTrack); + downloadManager.resume(track as SourcedTrack); }), const SizedBox(width: 10), IconButton( icon: const Icon(SpotubeIcons.close), onPressed: () { - downloadManager.cancel(track as SpotubeTrack); + downloadManager.cancel(track as SourcedTrack); }) ], ), @@ -126,7 +126,7 @@ class DownloadItem extends HookConsumerWidget { IconButton( icon: const Icon(SpotubeIcons.refresh), onPressed: () { - downloadManager.retry(track as SpotubeTrack); + downloadManager.retry(track as SourcedTrack); }, ), ], @@ -137,7 +137,7 @@ class DownloadItem extends HookConsumerWidget { DownloadStatus.queued => IconButton( icon: const Icon(SpotubeIcons.close), onPressed: () { - downloadManager.removeFromQueue(track as SpotubeTrack); + downloadManager.removeFromQueue(track as SourcedTrack); }), }, ); diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index ee8d9719..cf1429b9 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -12,13 +13,13 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/hooks/utils/use_debounce.dart'; -import 'package:spotube/models/matched_track.dart'; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; -import 'package:spotube/provider/youtube_provider.dart'; -import 'package:spotube/services/youtube/youtube.dart'; +import 'package:spotube/services/sourced_track/models/source_info.dart'; +import 'package:spotube/services/sourced_track/models/video_info.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -35,7 +36,6 @@ class SiblingTracksSheet extends HookConsumerWidget { final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final preferences = ref.watch(userPreferencesProvider); - final youtube = ref.watch(youtubeProvider); final isSearching = useState(false); final searchMode = useState(preferences.searchMode); @@ -61,18 +61,31 @@ class SiblingTracksSheet extends HookConsumerWidget { final searchRequest = useMemoized(() async { if (searchTerm.trim().isEmpty) { - return []; + return []; } - return youtube.search(searchTerm.trim()); + final results = await youtubeClient.search.search(searchTerm.trim()); + + return await Future.wait( + results.map(YoutubeVideoInfo.fromVideo).mapIndexed((i, video) async { + final siblingType = await YoutubeSourcedTrack.toSiblingType(i, video); + return siblingType.info; + }), + ); }, [ searchTerm, searchMode.value, ]); - final siblings = playlist.isFetching == false - ? (playlist.activeTrack as SpotubeTrack).siblings - : []; + final siblings = useMemoized( + () => playlist.isFetching == false + ? [ + (playlist.activeTrack as SourcedTrack).sourceInfo, + ...(playlist.activeTrack as SourcedTrack).siblings, + ] + : [], + [playlist.isFetching, playlist.activeTrack], + ); final borderRadius = floating ? BorderRadius.circular(10) @@ -82,21 +95,21 @@ class SiblingTracksSheet extends HookConsumerWidget { ); useEffect(() { - if (playlist.activeTrack is SpotubeTrack && - (playlist.activeTrack as SpotubeTrack).siblings.isEmpty) { + if (playlist.activeTrack is SourcedTrack && + (playlist.activeTrack as SourcedTrack).siblings.isEmpty) { playlistNotifier.populateSibling(); } return null; }, [playlist.activeTrack]); final itemBuilder = useCallback( - (YoutubeVideoInfo video) { + (SourceInfo sourceInfo) { return ListTile( - title: Text(video.title), + title: Text(sourceInfo.title), leading: Padding( padding: const EdgeInsets.all(8.0), child: UniversalImage( - path: video.thumbnailUrl, + path: sourceInfo.thumbnail, height: 60, width: 60, ), @@ -104,16 +117,18 @@ class SiblingTracksSheet extends HookConsumerWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), - trailing: Text(video.duration.toHumanReadableString()), - subtitle: Text(video.channelName), + trailing: Text(sourceInfo.duration.toHumanReadableString()), + subtitle: Text(sourceInfo.artist), enabled: playlist.isFetching != true, selected: playlist.isFetching != true && - video.id == (playlist.activeTrack as SpotubeTrack).ytTrack.id, + sourceInfo.id == + (playlist.activeTrack as SourcedTrack).sourceInfo.id, selectedTileColor: theme.popupMenuTheme.color, onTap: () { if (playlist.isFetching == false && - video.id != (playlist.activeTrack as SpotubeTrack).ytTrack.id) { - playlistNotifier.swapSibling(video); + sourceInfo.id != + (playlist.activeTrack as SourcedTrack).sourceInfo.id) { + playlistNotifier.swapSibling(sourceInfo); Navigator.of(context).pop(); } }, @@ -175,7 +190,7 @@ class SiblingTracksSheet extends HookConsumerWidget { }, ) else ...[ - if (preferences.youtubeApiType == YoutubeApiType.piped) + if (preferences.audioSource == AudioSource.piped) PopupMenuButton( icon: const Icon(SpotubeIcons.filter, size: 18), onSelected: (SearchMode mode) { diff --git a/lib/components/shared/dialogs/track_details_dialog.dart b/lib/components/shared/dialogs/track_details_dialog.dart index 9e29c32d..8634776f 100644 --- a/lib/components/shared/dialogs/track_details_dialog.dart +++ b/lib/components/shared/dialogs/track_details_dialog.dart @@ -6,8 +6,7 @@ import 'package:spotube/components/shared/links/hyper_link.dart'; import 'package:spotube/components/shared/links/link_text.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/models/spotube_track.dart'; -import 'package:spotube/utils/primitive_utils.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/extensions/duration.dart'; @@ -37,8 +36,8 @@ class TrackDetailsDialog extends HookWidget { overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.blue), ), - context.l10n.duration: (track is SpotubeTrack - ? (track as SpotubeTrack).ytTrack.duration + context.l10n.duration: (track is SourcedTrack + ? (track as SourcedTrack).sourceInfo.duration : track.duration!) .toHumanReadableString(), if (track.album!.releaseDate != null) @@ -46,33 +45,27 @@ class TrackDetailsDialog extends HookWidget { context.l10n.popularity: track.popularity?.toString() ?? "0", }; - final ytTrack = - track is SpotubeTrack ? (track as SpotubeTrack).ytTrack : null; + final sourceInfo = + track is SourcedTrack ? (track as SourcedTrack).sourceInfo : null; - final ytTracksDetailsMap = ytTrack == null + final ytTracksDetailsMap = sourceInfo == null ? {} : { context.l10n.youtube: Hyperlink( - "https://piped.video/watch?v=${ytTrack.id}", - "https://piped.video/watch?v=${ytTrack.id}", + "https://piped.video/watch?v=${sourceInfo.id}", + "https://piped.video/watch?v=${sourceInfo.id}", maxLines: 2, overflow: TextOverflow.ellipsis, ), context.l10n.channel: Hyperlink( - ytTrack.channelName, - "https://youtube.com${ytTrack.channelName}", + sourceInfo.artist, + sourceInfo.artistUrl, maxLines: 2, overflow: TextOverflow.ellipsis, ), - context.l10n.likes: - PrimitiveUtils.toReadableNumber(ytTrack.likes.toDouble()), - context.l10n.dislikes: - PrimitiveUtils.toReadableNumber(ytTrack.dislikes.toDouble()), - context.l10n.views: - PrimitiveUtils.toReadableNumber(ytTrack.views.toDouble()), context.l10n.streamUrl: Hyperlink( - (track as SpotubeTrack).ytUri, - (track as SpotubeTrack).ytUri, + (track as SourcedTrack).url, + (track as SourcedTrack).url, maxLines: 2, overflow: TextOverflow.ellipsis, ), diff --git a/lib/components/shared/track_table/track_options.dart b/lib/components/shared/track_table/track_options.dart index 96bd8b60..b0633d34 100644 --- a/lib/components/shared/track_table/track_options.dart +++ b/lib/components/shared/track_table/track_options.dart @@ -110,7 +110,7 @@ class TrackOptions extends HookConsumerWidget { ]); final progressNotifier = useMemoized(() { - final spotubeTrack = downloadManager.mapToSpotubeTrack(track); + final spotubeTrack = downloadManager.mapToSourcedTrack(track); if (spotubeTrack == null) return null; return downloadManager.getProgressNotifier(spotubeTrack); }); diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index 14a4f1a9..003662f5 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -60,7 +60,7 @@ class TracksTableView extends HookConsumerWidget { ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); final apiType = - ref.watch(userPreferencesProvider.select((s) => s.youtubeApiType)); + ref.watch(userPreferencesProvider.select((s) => s.audioSource)); const tableHeadStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 16); final selected = useState>([]); @@ -195,7 +195,7 @@ class TracksTableView extends HookConsumerWidget { switch (action) { case "download": { - final confirmed = apiType == YoutubeApiType.piped || + final confirmed = apiType == AudioSource.piped || await showDialog( context: context, builder: (context) { diff --git a/lib/main.dart b/lib/main.dart index f46f02c1..5d7ae2a7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,8 +18,8 @@ import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.da import 'package:spotube/hooks/configurators/use_get_storage_perms.dart'; import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/models/matched_track.dart'; import 'package:spotube/models/skip_segment.dart'; +import 'package:spotube/models/source_match.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -71,16 +71,18 @@ Future main(List rawArgs) async { cacheDir: hiveCacheDir, connectivity: FlQueryInternetConnectionCheckerAdapter(), ); - Hive.registerAdapter(MatchedTrackAdapter()); + Hive.registerAdapter(SkipSegmentAdapter()); - Hive.registerAdapter(SearchModeAdapter()); + + Hive.registerAdapter(SourceMatchAdapter()); + Hive.registerAdapter(SourceTypeAdapter()); // Cache versioning entities with Adapter - MatchedTrack.version = 'v1'; + SourceMatch.version = 'v1'; SkipSegment.version = 'v1'; - await Hive.openLazyBox( - MatchedTrack.boxName, + await Hive.openLazyBox( + SourceMatch.boxName, path: hiveCacheDir, ); await Hive.openLazyBox( diff --git a/lib/models/current_playlist.dart b/lib/models/current_playlist.dart index 1c3f8e16..53ea2799 100644 --- a/lib/models/current_playlist.dart +++ b/lib/models/current_playlist.dart @@ -1,6 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/extensions/track.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; class CurrentPlaylist { List? _tempTrack; @@ -18,13 +19,13 @@ class CurrentPlaylist { this.isLocal = false, }); - static CurrentPlaylist fromJson(Map map) { + static CurrentPlaylist fromJson(Map map, Ref ref) { return CurrentPlaylist( id: map["id"], tracks: List.castFrom(map["tracks"] .map( (track) => map["isLocal"] == true - ? SpotubeTrack.fromJson(track) + ? SourcedTrack.fromJson(track, ref: ref) : Track.fromJson(track), ) .toList()), @@ -66,7 +67,7 @@ class CurrentPlaylist { "name": name, "tracks": tracks .map((track) => - track is SpotubeTrack ? track.toJson() : track.toJson()) + track is SourcedTrack ? track.toJson() : track.toJson()) .toList(), "thumbnail": thumbnail, "isLocal": isLocal, diff --git a/lib/models/matched_track.dart b/lib/models/matched_track.dart deleted file mode 100644 index b7cc0a3f..00000000 --- a/lib/models/matched_track.dart +++ /dev/null @@ -1,69 +0,0 @@ -import "package:hive/hive.dart"; -part "matched_track.g.dart"; - -@HiveType(typeId: 1) -class MatchedTrack { - @HiveField(0) - String youtubeId; - @HiveField(1) - String spotifyId; - @HiveField(2) - SearchMode searchMode; - - String? id; - DateTime? createdAt; - - bool get isSynced => id != null; - - static String version = 'v1'; - static final boxName = "oss.krtirtho.spotube.matched_tracks.$version"; - - static LazyBox get box => Hive.lazyBox(boxName); - - MatchedTrack({ - required this.youtubeId, - required this.spotifyId, - required this.searchMode, - this.id, - this.createdAt, - }); - - factory MatchedTrack.fromJson(Map json) { - return MatchedTrack( - searchMode: SearchMode.fromString(json["searchMode"]), - youtubeId: json["youtube_id"], - spotifyId: json["spotify_id"], - id: json["id"], - createdAt: DateTime.parse(json["created_at"]), - ); - } - - Map toJson() { - return { - "youtube_id": youtubeId, - "spotify_id": spotifyId, - "id": id, - "searchMode": searchMode.name, - "created_at": createdAt?.toString() - }..removeWhere((key, value) => value == null); - } -} - -@HiveType(typeId: 4) -enum SearchMode { - @HiveField(0) - youtube._internal('YouTube'), - @HiveField(1) - youtubeMusic._internal('YouTube Music'); - - final String label; - - const SearchMode._internal(this.label); - - factory SearchMode.fromString(String value) { - return SearchMode.values.firstWhere( - (element) => element.name == value, - orElse: () => SearchMode.youtube, - ); - } -} diff --git a/lib/models/matched_track.g.dart b/lib/models/matched_track.g.dart deleted file mode 100644 index dd166e77..00000000 --- a/lib/models/matched_track.g.dart +++ /dev/null @@ -1,86 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'matched_track.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class MatchedTrackAdapter extends TypeAdapter { - @override - final int typeId = 1; - - @override - MatchedTrack read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return MatchedTrack( - youtubeId: fields[0] as String, - spotifyId: fields[1] as String, - searchMode: fields[2] as SearchMode, - ); - } - - @override - void write(BinaryWriter writer, MatchedTrack obj) { - writer - ..writeByte(3) - ..writeByte(0) - ..write(obj.youtubeId) - ..writeByte(1) - ..write(obj.spotifyId) - ..writeByte(2) - ..write(obj.searchMode); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is MatchedTrackAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SearchModeAdapter extends TypeAdapter { - @override - final int typeId = 4; - - @override - SearchMode read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return SearchMode.youtube; - case 1: - return SearchMode.youtubeMusic; - default: - return SearchMode.youtube; - } - } - - @override - void write(BinaryWriter writer, SearchMode obj) { - switch (obj) { - case SearchMode.youtube: - writer.writeByte(0); - break; - case SearchMode.youtubeMusic: - writer.writeByte(1); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SearchModeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/lib/models/source_match.dart b/lib/models/source_match.dart new file mode 100644 index 00000000..57a9f963 --- /dev/null +++ b/lib/models/source_match.dart @@ -0,0 +1,54 @@ +import 'package:hive/hive.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'source_match.g.dart'; + +@JsonEnum() +@HiveType(typeId: 5) +enum SourceType { + @HiveField(0) + youtube._("YouTube"), + + @HiveField(1) + youtubeMusic._("YouTube Music"), + + @HiveField(2) + jiosaavn._("JioSaavn"); + + final String label; + + const SourceType._(this.label); +} + +@JsonSerializable() +@HiveType(typeId: 6) +class SourceMatch { + @HiveField(0) + String id; + + @HiveField(1) + String sourceId; + + @HiveField(2) + SourceType sourceType; + + @HiveField(3) + DateTime createdAt; + + SourceMatch({ + required this.id, + required this.sourceId, + required this.sourceType, + required this.createdAt, + }); + + factory SourceMatch.fromJson(Map json) => + _$SourceMatchFromJson(json); + + Map toJson() => _$SourceMatchToJson(this); + + static String version = 'v1'; + static final boxName = "oss.krtirtho.spotube.source_matches.$version"; + + static LazyBox get box => Hive.lazyBox(boxName); +} diff --git a/lib/models/source_match.g.dart b/lib/models/source_match.g.dart new file mode 100644 index 00000000..11f34bf3 --- /dev/null +++ b/lib/models/source_match.g.dart @@ -0,0 +1,119 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'source_match.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class SourceMatchAdapter extends TypeAdapter { + @override + final int typeId = 6; + + @override + SourceMatch read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return SourceMatch( + id: fields[0] as String, + sourceId: fields[1] as String, + sourceType: fields[2] as SourceType, + createdAt: fields[3] as DateTime, + ); + } + + @override + void write(BinaryWriter writer, SourceMatch obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.sourceId) + ..writeByte(2) + ..write(obj.sourceType) + ..writeByte(3) + ..write(obj.createdAt); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SourceMatchAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class SourceTypeAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + SourceType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return SourceType.youtube; + case 1: + return SourceType.youtubeMusic; + case 2: + return SourceType.jiosaavn; + default: + return SourceType.youtube; + } + } + + @override + void write(BinaryWriter writer, SourceType obj) { + switch (obj) { + case SourceType.youtube: + writer.writeByte(0); + break; + case SourceType.youtubeMusic: + writer.writeByte(1); + break; + case SourceType.jiosaavn: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SourceTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SourceMatch _$SourceMatchFromJson(Map json) => SourceMatch( + id: json['id'] as String, + sourceId: json['sourceId'] as String, + sourceType: $enumDecode(_$SourceTypeEnumMap, json['sourceType']), + createdAt: DateTime.parse(json['createdAt'] as String), + ); + +Map _$SourceMatchToJson(SourceMatch instance) => + { + 'id': instance.id, + 'sourceId': instance.sourceId, + 'sourceType': _$SourceTypeEnumMap[instance.sourceType]!, + 'createdAt': instance.createdAt.toIso8601String(), + }; + +const _$SourceTypeEnumMap = { + SourceType.youtube: 'youtube', + SourceType.youtubeMusic: 'youtubeMusic', + SourceType.jiosaavn: 'jiosaavn', +}; diff --git a/lib/models/spotube_track.dart b/lib/models/spotube_track.dart deleted file mode 100644 index 67b09ad8..00000000 --- a/lib/models/spotube_track.dart +++ /dev/null @@ -1,274 +0,0 @@ -import 'dart:async'; - -import 'package:spotify/spotify.dart'; -import 'package:spotube/extensions/track.dart'; -import 'package:spotube/models/matched_track.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; -import 'package:spotube/services/youtube/youtube.dart'; -import 'package:spotube/utils/service_utils.dart'; -import 'package:collection/collection.dart'; - -final officialMusicRegex = RegExp( - r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)", - caseSensitive: false, -); - -class TrackNotFoundException implements Exception { - factory TrackNotFoundException(Track track) { - throw Exception("Failed to find any results for ${track.name}"); - } -} - -class SpotubeTrack extends Track { - final YoutubeVideoInfo ytTrack; - final String ytUri; - final MusicCodec codec; - - final List siblings; - - SpotubeTrack( - this.ytTrack, - this.ytUri, - this.siblings, - this.codec, - ) : super(); - - SpotubeTrack.fromTrack({ - required Track track, - required this.ytTrack, - required this.ytUri, - required this.siblings, - required this.codec, - }) : super() { - album = track.album; - artists = track.artists; - availableMarkets = track.availableMarkets; - discNumber = track.discNumber; - durationMs = track.durationMs; - explicit = track.explicit; - externalIds = track.externalIds; - externalUrls = track.externalUrls; - href = track.href; - id = track.id; - isPlayable = track.isPlayable; - linkedFrom = track.linkedFrom; - name = track.name; - popularity = track.popularity; - previewUrl = track.previewUrl; - trackNumber = track.trackNumber; - type = track.type; - uri = track.uri; - } - - static Future> fetchSiblings( - Track track, - YoutubeEndpoints client, - ) async { - final artists = (track.artists ?? []) - .map((ar) => ar.name) - .toList() - .whereNotNull() - .toList(); - - final title = ServiceUtils.getTitle( - track.name!, - artists: artists, - onlyCleanArtist: true, - ).trim(); - - final query = "$title - ${artists.join(", ")}"; - final List siblings = await client.search(query).then( - (res) { - final isYoutubeApi = - client.preferences.youtubeApiType == YoutubeApiType.youtube; - final siblings = isYoutubeApi || - client.preferences.searchMode == SearchMode.youtube - ? ServiceUtils.onlyContainsEnglish(query) - ? res - : res - .sorted((a, b) => b.views.compareTo(a.views)) - .map((sibling) { - int score = 0; - - for (final artist in artists) { - final isSameChannelArtist = - sibling.channelName.toLowerCase() == - artist.toLowerCase(); - final channelContainsArtist = sibling.channelName - .toLowerCase() - .contains(artist.toLowerCase()); - - if (isSameChannelArtist || channelContainsArtist) { - score += 1; - } - - final titleContainsArtist = sibling.title - .toLowerCase() - .contains(artist.toLowerCase()); - - if (titleContainsArtist) { - score += 1; - } - } - - final titleContainsTrackName = sibling.title - .toLowerCase() - .contains(track.name!.toLowerCase()); - - final hasOfficialFlag = officialMusicRegex - .hasMatch(sibling.title.toLowerCase()); - - if (titleContainsTrackName) { - score += 3; - } - - if (hasOfficialFlag) { - score += 1; - } - - if (hasOfficialFlag && titleContainsTrackName) { - score += 2; - } - - return (sibling: sibling, score: score); - }) - .sorted((a, b) => b.score.compareTo(a.score)) - .map((e) => e.sibling) - : res.sorted((a, b) => b.views.compareTo(a.views)).where((item) { - return artists.any( - (artist) => - artist.toLowerCase() == item.channelName.toLowerCase(), - ); - }); - - return siblings.take(10).toList(); - }, - ); - - return siblings; - } - - static Future fetchFromTrack( - Track track, - YoutubeEndpoints client, - MusicCodec codec, - ) async { - final matchedCachedTrack = await MatchedTrack.box.get(track.id!); - var siblings = []; - YoutubeVideoInfo ytVideo; - String ytStreamUrl; - if (matchedCachedTrack != null && - matchedCachedTrack.searchMode == client.preferences.searchMode) { - (ytVideo, ytStreamUrl) = await client.video( - matchedCachedTrack.youtubeId, matchedCachedTrack.searchMode, codec); - } else { - siblings = await fetchSiblings(track, client); - if (siblings.isEmpty) { - throw TrackNotFoundException(track); - } - (ytVideo, ytStreamUrl) = await client.video( - siblings.first.id, - siblings.first.searchMode, - codec, - ); - - await MatchedTrack.box.put( - track.id!, - MatchedTrack( - youtubeId: ytVideo.id, - spotifyId: track.id!, - searchMode: siblings.first.searchMode, - ), - ); - } - - return SpotubeTrack.fromTrack( - track: track, - ytTrack: ytVideo, - ytUri: ytStreamUrl, - siblings: siblings, - codec: codec, - ); - } - - Future swappedCopy( - YoutubeVideoInfo video, - YoutubeEndpoints client, - ) async { - // sibling tracks that were manually searched and swapped - final isStepSibling = siblings.none((element) => element.id == video.id); - - final (ytVideo, ytStreamUrl) = await client.video( - video.id, - siblings.first.searchMode, - // siblings are always swapped when streaming - client.preferences.streamMusicCodec, - ); - - if (!isStepSibling) { - await MatchedTrack.box.put( - id!, - MatchedTrack( - youtubeId: video.id, - spotifyId: id!, - searchMode: siblings.first.searchMode, - ), - ); - } - - return SpotubeTrack.fromTrack( - track: this, - ytTrack: ytVideo, - ytUri: ytStreamUrl, - siblings: [ - video, - ...siblings.where((element) => element.id != video.id), - ], - codec: client.preferences.streamMusicCodec, - ); - } - - static SpotubeTrack fromJson(Map map) { - return SpotubeTrack.fromTrack( - track: Track.fromJson(map), - ytTrack: YoutubeVideoInfo.fromJson(map["ytTrack"]), - ytUri: map["ytUri"], - siblings: List.castFrom>(map["siblings"]) - .map((sibling) => YoutubeVideoInfo.fromJson(sibling)) - .toList(), - codec: MusicCodec.values.firstWhere( - (element) => element.name == map["codec"], - orElse: () => MusicCodec.m4a, - ), - ); - } - - Future populatedCopy(YoutubeEndpoints client) async { - if (this.siblings.isNotEmpty) return this; - - final siblings = await fetchSiblings( - this, - client, - ); - - return SpotubeTrack.fromTrack( - track: this, - ytTrack: ytTrack, - ytUri: ytUri, - siblings: siblings, - codec: codec, - ); - } - - Map toJson() { - return { - // super values - ...TrackJson.trackToJson(this), - // this values - "ytTrack": ytTrack.toJson(), - "ytUri": ytUri, - "siblings": siblings.map((sibling) => sibling.toJson()).toList(), - "codec": codec.name, - }; - } -} diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index a585c9e5..5674e721 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -8,9 +8,9 @@ import 'package:spotube/components/shared/track_table/track_collection_view/trac import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_view.dart'; import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -68,7 +68,7 @@ class AlbumPage extends HookConsumerWidget { () => tracksSnapshot.data?.any((s) => s.id! == playlist.activeTrack?.id!) == true && - playlist.activeTrack is SpotubeTrack, + playlist.activeTrack is SourcedTrack, [playlist.activeTrack, tracksSnapshot.data], ); diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 1623195b..6a3ec9b9 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -11,9 +11,9 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -59,7 +59,7 @@ class PlaylistView extends HookConsumerWidget { tracksSnapshot.data ?.any((s) => s.id! == proxyPlaylist.activeTrack?.id!) == true && - proxyPlaylist.activeTrack is SpotubeTrack, + proxyPlaylist.activeTrack is SourcedTrack, [proxyPlaylist.activeTrack, tracksSnapshot.data], ); diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 5e000231..a0316b33 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -8,10 +8,10 @@ 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/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; class SettingsPlaybackSection extends HookConsumerWidget { const SettingsPlaybackSection({Key? key}) : super(key: key); @@ -25,17 +25,21 @@ class SettingsPlaybackSection extends HookConsumerWidget { return SectionCardWithHeading( heading: context.l10n.playback, children: [ - AdaptiveSelectTile( + AdaptiveSelectTile( secondary: const Icon(SpotubeIcons.audioQuality), title: Text(context.l10n.audio_quality), value: preferences.audioQuality, options: [ DropdownMenuItem( - value: AudioQuality.high, + value: SourceQualities.high, child: Text(context.l10n.high), ), DropdownMenuItem( - value: AudioQuality.low, + value: SourceQualities.medium, + child: Text(context.l10n.medium), + ), + DropdownMenuItem( + value: SourceQualities.low, child: Text(context.l10n.low), ), ], @@ -45,11 +49,11 @@ class SettingsPlaybackSection extends HookConsumerWidget { } }, ), - AdaptiveSelectTile( + AdaptiveSelectTile( secondary: const Icon(SpotubeIcons.api), title: Text(context.l10n.youtube_api_type), - value: preferences.youtubeApiType, - options: YoutubeApiType.values + value: preferences.audioSource, + options: AudioSource.values .map((e) => DropdownMenuItem( value: e, child: Text(e.label), @@ -57,12 +61,12 @@ class SettingsPlaybackSection extends HookConsumerWidget { .toList(), onChanged: (value) { if (value == null) return; - preferencesNotifier.setYoutubeApiType(value); + preferencesNotifier.setAudioSource(value); }, ), AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: preferences.youtubeApiType == YoutubeApiType.youtube + child: preferences.audioSource != AudioSource.piped ? const SizedBox.shrink() : Consumer(builder: (context, ref, child) { final instanceList = ref.watch(pipedInstancesFutureProvider); @@ -129,7 +133,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { ), AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: preferences.youtubeApiType == YoutubeApiType.youtube + child: preferences.audioSource != AudioSource.piped ? const SizedBox.shrink() : AdaptiveSelectTile( secondary: const Icon(SpotubeIcons.search), @@ -149,17 +153,18 @@ class SettingsPlaybackSection extends HookConsumerWidget { ), AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: preferences.searchMode == SearchMode.youtubeMusic && - preferences.youtubeApiType == YoutubeApiType.piped - ? const SizedBox.shrink() - : SwitchListTile( + child: preferences.searchMode == SearchMode.youtube && + (preferences.audioSource == AudioSource.piped || + preferences.audioSource == AudioSource.youtube) + ? SwitchListTile( secondary: const Icon(SpotubeIcons.skip), title: Text(context.l10n.skip_non_music), value: preferences.skipNonMusic, onChanged: (state) { preferencesNotifier.setSkipNonMusic(state); }, - ), + ) + : const SizedBox.shrink(), ), ListTile( leading: const Icon(SpotubeIcons.playlistRemove), @@ -176,44 +181,46 @@ class SettingsPlaybackSection extends HookConsumerWidget { value: preferences.normalizeAudio, onChanged: preferencesNotifier.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; - preferencesNotifier.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; - preferencesNotifier.setDownloadMusicCodec(value); - }, - ), + if (preferences.audioSource != AudioSource.jiosaavn) + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.stream), + title: Text(context.l10n.streaming_music_codec), + value: preferences.streamMusicCodec, + showValueWhenUnfolded: false, + options: SourceCodecs.values + .map((e) => DropdownMenuItem( + value: e, + child: Text( + e.label, + style: theme.textTheme.labelMedium, + ), + )) + .toList(), + onChanged: (value) { + if (value == null) return; + preferencesNotifier.setStreamMusicCodec(value); + }, + ), + if (preferences.audioSource != AudioSource.jiosaavn) + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.file), + title: Text(context.l10n.download_music_codec), + value: preferences.downloadMusicCodec, + showValueWhenUnfolded: false, + options: SourceCodecs.values + .map((e) => DropdownMenuItem( + value: e, + child: Text( + e.label, + style: theme.textTheme.labelMedium, + ), + )) + .toList(), + onChanged: (value) { + if (value == null) return; + preferencesNotifier.setDownloadMusicCodec(value); + }, + ), ], ); } diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index 889641f4..691a1385 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -9,25 +9,23 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:metadata_god/metadata_god.dart'; import 'package:path/path.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; -import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/services/download_manager/download_manager.dart'; -import 'package:spotube/services/youtube/youtube.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class DownloadManagerProvider extends ChangeNotifier { DownloadManagerProvider({required this.ref}) - : $history = {}, + : $history = {}, $backHistory = {}, dl = DownloadManager() { dl.statusStream.listen((event) async { final (:request, :status) = event; final track = $history.firstWhereOrNull( - (element) => element.ytUri == request.url, + (element) => element.url == request.url, ); if (track == null) return; @@ -45,7 +43,7 @@ class DownloadManagerProvider extends ChangeNotifier { //? WebA audiotagging is not supported yet //? Although in future by converting weba to opus & then tagging it //? is possible using vorbis comments - downloadCodec == MusicCodec.weba) return; + downloadCodec == SourceCodecs.weba) return; final file = File(request.path); @@ -91,10 +89,9 @@ class DownloadManagerProvider extends ChangeNotifier { final Ref ref; - YoutubeEndpoints get yt => ref.read(youtubeProvider); String get downloadDirectory => ref.read(userPreferencesProvider.select((s) => s.downloadLocation)); - MusicCodec get downloadCodec => + SourceCodecs get downloadCodec => ref.read(userPreferencesProvider.select((s) => s.downloadMusicCodec)); int get $downloadCount => dl @@ -107,7 +104,7 @@ class DownloadManagerProvider extends ChangeNotifier { ) .length; - final Set $history; + final Set $history; // these are the tracks which metadata hasn't been fetched yet final Set $backHistory; final DownloadManager dl; @@ -144,9 +141,9 @@ class DownloadManagerProvider extends ChangeNotifier { bool isActive(Track track) { if ($backHistory.contains(track)) return true; - final spotubeTrack = mapToSpotubeTrack(track); + final sourcedTrack = mapToSourcedTrack(track); - if (spotubeTrack == null) return false; + if (sourcedTrack == null) return false; return dl .getAllDownloads() @@ -157,7 +154,7 @@ class DownloadManagerProvider extends ChangeNotifier { download.status.value == DownloadStatus.queued, ) .map((e) => e.request.url) - .contains(spotubeTrack.ytUri); + .contains(sourcedTrack.getUrlOfCodec(downloadCodec)); } /// For singular downloads @@ -173,21 +170,27 @@ class DownloadManagerProvider extends ChangeNotifier { await oldFile.rename("$savePath.old"); } - if (track is SpotubeTrack && track.codec == downloadCodec) { - final downloadTask = await dl.addDownload(track.ytUri, savePath); + if (track is SourcedTrack && track.codec == downloadCodec) { + final downloadTask = + await dl.addDownload(track.getUrlOfCodec(downloadCodec), savePath); if (downloadTask != null) { $history.add(track); } } else { $backHistory.add(track); - final spotubeTrack = - await SpotubeTrack.fetchFromTrack(track, yt, downloadCodec).then((d) { + final sourcedTrack = await SourcedTrack.fetchFromTrack( + ref: ref, + track: track, + ).then((d) { $backHistory.remove(track); return d; }); - final downloadTask = await dl.addDownload(spotubeTrack.ytUri, savePath); + final downloadTask = await dl.addDownload( + sourcedTrack.getUrlOfCodec(downloadCodec), + savePath, + ); if (downloadTask != null) { - $history.add(spotubeTrack); + $history.add(sourcedTrack); } } @@ -196,7 +199,7 @@ class DownloadManagerProvider extends ChangeNotifier { Future batchAddToQueue(List tracks) async { $backHistory.addAll( - tracks.where((element) => element is! SpotubeTrack), + tracks.where((element) => element is! SourcedTrack), ); notifyListeners(); for (final track in tracks) { @@ -216,25 +219,25 @@ class DownloadManagerProvider extends ChangeNotifier { } } - Future removeFromQueue(SpotubeTrack track) async { - await dl.removeDownload(track.ytUri); + Future removeFromQueue(SourcedTrack track) async { + await dl.removeDownload(track.getUrlOfCodec(downloadCodec)); $history.remove(track); } - Future pause(SpotubeTrack track) { - return dl.pauseDownload(track.ytUri); + Future pause(SourcedTrack track) { + return dl.pauseDownload(track.getUrlOfCodec(downloadCodec)); } - Future resume(SpotubeTrack track) { - return dl.resumeDownload(track.ytUri); + Future resume(SourcedTrack track) { + return dl.resumeDownload(track.getUrlOfCodec(downloadCodec)); } - Future retry(SpotubeTrack track) { + Future retry(SourcedTrack track) { return addToQueue(track); } - void cancel(SpotubeTrack track) { - dl.cancelDownload(track.ytUri); + void cancel(SourcedTrack track) { + dl.cancelDownload(track.getUrlOfCodec(downloadCodec)); } void cancelAll() { @@ -244,20 +247,20 @@ class DownloadManagerProvider extends ChangeNotifier { } } - SpotubeTrack? mapToSpotubeTrack(Track track) { - if (track is SpotubeTrack) { + SourcedTrack? mapToSourcedTrack(Track track) { + if (track is SourcedTrack) { return track; } else { return $history.firstWhereOrNull((element) => element.id == track.id); } } - ValueNotifier? getStatusNotifier(SpotubeTrack track) { - return dl.getDownload(track.ytUri)?.status; + ValueNotifier? getStatusNotifier(SourcedTrack track) { + return dl.getDownload(track.getUrlOfCodec(downloadCodec))?.status; } - ValueNotifier? getProgressNotifier(SpotubeTrack track) { - return dl.getDownload(track.ytUri)?.progress; + ValueNotifier? getProgressNotifier(SourcedTrack track) { + return dl.getDownload(track.getUrlOfCodec(downloadCodec))?.progress; } } diff --git a/lib/provider/piped_instances_provider.dart b/lib/provider/piped_instances_provider.dart index 290ad2c4..264b7048 100644 --- a/lib/provider/piped_instances_provider.dart +++ b/lib/provider/piped_instances_provider.dart @@ -1,10 +1,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:piped_client/piped_client.dart'; -import 'package:spotube/provider/youtube_provider.dart'; +import 'package:spotube/services/sourced_track/sources/piped.dart'; final pipedInstancesFutureProvider = FutureProvider>( (ref) async { - final youtube = ref.watch(youtubeProvider); - return await youtube.piped?.instanceList() ?? []; + final pipedClient = ref.watch(pipedProvider); + + return await pipedClient.instanceList(); }, ); diff --git a/lib/provider/proxy_playlist/next_fetcher_mixin.dart b/lib/provider/proxy_playlist/next_fetcher_mixin.dart index b447f1ef..1d2cfde8 100644 --- a/lib/provider/proxy_playlist/next_fetcher_mixin.dart +++ b/lib/provider/proxy_playlist/next_fetcher_mixin.dart @@ -3,36 +3,30 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/models/matched_track.dart'; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; -import 'package:spotube/services/supabase.dart'; -import 'package:spotube/services/youtube/youtube.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; final logger = getLogger("NextFetcherMixin"); mixin NextFetcher on StateNotifier { - Future> fetchTracks( - UserPreferences preferences, - YoutubeEndpoints youtube, { + Future> fetchTracks( + Ref ref, { int count = 3, int offset = 0, }) async { - /// get [count] [state.tracks] that are not [SpotubeTrack] and [LocalTrack] + /// get [count] [state.tracks] that are not [SourcedTrack] and [LocalTrack] final bareTracks = state.tracks .skip(offset) - .where((element) => element is! SpotubeTrack && element is! LocalTrack) + .where((element) => element is! SourcedTrack && element is! LocalTrack) .take(count); /// fetch [bareTracks] one by one with 100ms delay final fetchedTracks = await Future.wait( bareTracks.mapIndexed((i, track) async { - final future = SpotubeTrack.fetchFromTrack( - track, - youtube, - preferences.streamMusicCodec, + final future = SourcedTrack.fetchFromTrack( + ref: ref, + track: track, ); if (i == 0) { return await future; @@ -47,9 +41,9 @@ mixin NextFetcher on StateNotifier { return fetchedTracks; } - /// Merges List of [SpotubeTrack]s with [Track]s and outputs a mixed List + /// Merges List of [SourcedTrack]s with [Track]s and outputs a mixed List Set mergeTracks( - Iterable fetchTracks, + Iterable fetchTracks, Iterable tracks, ) { return tracks.map((track) { @@ -80,12 +74,12 @@ mixin NextFetcher on StateNotifier { /// Returns appropriate Media source for [Track] /// - /// * If [Track] is [SpotubeTrack] then return [SpotubeTrack.ytUri] + /// * If [Track] is [SourcedTrack] then return [SourcedTrack.ytUri] /// * If [Track] is [LocalTrack] then return [LocalTrack.path] /// * If [Track] is [Track] then return [Track.id] with [isUnPlayable] source String makeAppropriateSource(Track track) { - if (track is SpotubeTrack) { - return track.ytUri; + if (track is SourcedTrack) { + return track.url; } else if (track is LocalTrack) { return track.path; } else { @@ -103,7 +97,7 @@ mixin NextFetcher on StateNotifier { final track = state.tracks.firstWhereOrNull( (track) => trackToUnplayableSource(track) == source || - (track is SpotubeTrack && track.ytUri == source) || + (track is SourcedTrack && track.url == source) || (track is LocalTrack && track.path == source), ); return track; @@ -111,23 +105,4 @@ mixin NextFetcher on StateNotifier { .whereNotNull() .toList(); } - - /// This method must be called after any playback operation as - /// it can increase the latency - Future storeTrack(Track track, SpotubeTrack spotubeTrack) async { - try { - if (track is! SpotubeTrack) { - await supabase.insertTrack( - MatchedTrack( - youtubeId: spotubeTrack.ytTrack.id, - spotifyId: spotubeTrack.id!, - searchMode: spotubeTrack.ytTrack.searchMode, - ), - ); - } - } catch (e, stackTrace) { - logger.e(e.toString()); - logger.t(stackTrace); - } - } } diff --git a/lib/provider/proxy_playlist/proxy_playlist.dart b/lib/provider/proxy_playlist/proxy_playlist.dart index e5dfa7e8..026b3403 100644 --- a/lib/provider/proxy_playlist/proxy_playlist.dart +++ b/lib/provider/proxy_playlist/proxy_playlist.dart @@ -1,8 +1,9 @@ import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/models/spotube_track.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; class ProxyPlaylist { final Set tracks; @@ -11,11 +12,14 @@ class ProxyPlaylist { ProxyPlaylist(this.tracks, [this.active, this.collections = const {}]); - factory ProxyPlaylist.fromJson(Map json) { + factory ProxyPlaylist.fromJson( + Map json, + Ref ref, + ) { return ProxyPlaylist( List.castFrom>( json['tracks'] ?? >[], - ).map(_makeAppropriateTrack).toSet(), + ).map((t) => _makeAppropriateTrack(t, ref)).toSet(), json['active'] as int?, json['collections'] == null ? {} @@ -28,7 +32,7 @@ class ProxyPlaylist { bool get isFetching => activeTrack != null && - activeTrack is! SpotubeTrack && + activeTrack is! SourcedTrack && activeTrack is! LocalTrack; bool containsCollection(String collection) { @@ -44,9 +48,9 @@ class ProxyPlaylist { return tracks.every(containsTrack); } - static Track _makeAppropriateTrack(Map track) { + static Track _makeAppropriateTrack(Map track, Ref ref) { if (track.containsKey("ytUri")) { - return SpotubeTrack.fromJson(track); + return SourcedTrack.fromJson(track, ref: ref); } else if (track.containsKey("path")) { return LocalTrack.fromJson(track); } else { @@ -59,7 +63,7 @@ class ProxyPlaylist { static Map _makeAppropriateTrackJson(Track track) { return switch (track.runtimeType) { LocalTrack => track.toJson(), - SpotubeTrack => track.toJson(), + SourcedTrack => track.toJson(), _ => track.toJson(), }; } diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 50024661..bd3934a7 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -12,9 +12,10 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/models/matched_track.dart'; + import 'package:spotube/models/skip_segment.dart'; -import 'package:spotube/models/spotube_track.dart'; +import 'package:spotube/models/source_match.dart'; + import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/next_fetcher_mixin.dart'; @@ -22,17 +23,20 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; -import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/audio_services.dart'; -import 'package:spotube/services/youtube/youtube.dart'; +import 'package:spotube/services/sourced_track/exceptions.dart'; +import 'package:spotube/services/sourced_track/models/source_info.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/services/supabase.dart'; + import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; /// Things implemented: /// * [x] Sponsor-Block skip -/// * [x] Prefetch next track as [SpotubeTrack] on 80% of current track -/// * [x] Mixed Queue containing both [SpotubeTrack] and [LocalTrack] +/// * [x] Prefetch next track as [SourcedTrack] on 80% of current track +/// * [x] Mixed Queue containing both [SourcedTrack] and [LocalTrack] /// * [x] Modification of the Queue /// * [x] Add track at the end /// * [x] Add track at the beginning @@ -56,7 +60,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier); UserPreferences get preferences => ref.read(userPreferencesProvider); - YoutubeEndpoints get youtube => ref.read(youtubeProvider); ProxyPlaylist get playlist => state; BlackListNotifier get blacklist => ref.read(BlackListNotifier.provider.notifier); @@ -168,11 +171,11 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier return; } try { - final isYTMusicMode = - preferences.youtubeApiType == YoutubeApiType.piped && - preferences.searchMode == SearchMode.youtubeMusic; + final isNotYTMode = preferences.audioSource != AudioSource.youtube || + (preferences.audioSource == AudioSource.piped && + preferences.searchMode == SearchMode.youtubeMusic); - if (isYTMusicMode || !preferences.skipNonMusic) return; + if (isNotYTMode || !preferences.skipNonMusic) return; final isNotSameSegmentId = currentSegments.value?.source != audioPlayer.currentSource; @@ -184,7 +187,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier currentSegments.value = ( source: audioPlayer.currentSource!, segments: await getAndCacheSkipSegments( - (state.activeTrack as SpotubeTrack).ytTrack.id, + (state.activeTrack as SourcedTrack).sourceInfo.id, ), ); } catch (e) { @@ -237,7 +240,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier }(); } - Future ensureSourcePlayable(String source) async { + Future ensureSourcePlayable(String source) async { if (isPlayable(source)) return null; final track = mapSourcesToTracks([source]).firstOrNull; @@ -247,17 +250,13 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } final nthFetchedTrack = switch (track.runtimeType) { - SpotubeTrack => track as SpotubeTrack, - _ => await SpotubeTrack.fetchFromTrack( - track, - youtube, - preferences.streamMusicCodec, - ), + SourcedTrack => track as SourcedTrack, + _ => await SourcedTrack.fetchFromTrack(ref: ref, track: track), }; await audioPlayer.replaceSource( source, - nthFetchedTrack.ytUri, + nthFetchedTrack.url, ); return nthFetchedTrack; @@ -335,15 +334,13 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier ); await notificationService.addTrack(indexTrack); } else { - final addableTrack = await SpotubeTrack.fetchFromTrack( - tracks.elementAtOrNull(initialIndex) ?? tracks.first, - youtube, - preferences.streamMusicCodec, + final addableTrack = await SourcedTrack.fetchFromTrack( + ref: ref, + track: tracks.elementAtOrNull(initialIndex) ?? tracks.first, ).catchError((e, stackTrace) { - return SpotubeTrack.fetchFromTrack( - tracks.elementAtOrNull(initialIndex + 1) ?? tracks.first, - youtube, - preferences.streamMusicCodec, + return SourcedTrack.fetchFromTrack( + ref: ref, + track: tracks.elementAtOrNull(initialIndex + 1) ?? tracks.first, ); }); @@ -437,9 +434,9 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } Future populateSibling() async { - if (state.activeTrack is SpotubeTrack) { + if (state.activeTrack is SourcedTrack) { final activeTrackWithSiblingsForSure = - await (state.activeTrack as SpotubeTrack).populatedCopy(youtube); + await (state.activeTrack as SourcedTrack).copyWithSibling(); state = state.copyWith( tracks: mergeTracks([activeTrackWithSiblingsForSure], state.tracks), @@ -449,11 +446,11 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } } - Future swapSibling(YoutubeVideoInfo video) async { - if (state.activeTrack is SpotubeTrack) { + Future swapSibling(SourceInfo sibling) async { + if (state.activeTrack is SourcedTrack) { await populateSibling(); final newTrack = - await (state.activeTrack as SpotubeTrack).swappedCopy(video, youtube); + await (state.activeTrack as SourcedTrack).swapWithSibling(sibling); if (newTrack == null) return; state = state.copyWith( tracks: mergeTracks([newTrack], state.tracks), @@ -564,7 +561,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier Future> getAndCacheSkipSegments(String id) async { if (!preferences.skipNonMusic || - (preferences.youtubeApiType == YoutubeApiType.piped && + (preferences.audioSource == AudioSource.piped && preferences.searchMode == SearchMode.youtubeMusic)) return []; try { @@ -628,6 +625,30 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } } + /// This method must be called after any playback operation as + /// it can increase the latency + Future storeTrack(Track track, SourcedTrack sourcedTrack) async { + try { + if (track is! SourcedTrack) { + await supabase.insertTrack( + SourceMatch( + id: sourcedTrack.id!, + createdAt: DateTime.now(), + sourceId: sourcedTrack.sourceInfo.id, + sourceType: preferences.audioSource == AudioSource.jiosaavn + ? SourceType.jiosaavn + : preferences.searchMode == SearchMode.youtube + ? SourceType.youtube + : SourceType.youtubeMusic, + ), + ); + } + } catch (e, stackTrace) { + logger.e(e.toString()); + logger.t(stackTrace); + } + } + @override set state(state) { super.state = state; @@ -652,7 +673,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier @override FutureOr fromJson(Map json) { - return ProxyPlaylist.fromJson(json); + return ProxyPlaylist.fromJson(json, ref); } @override diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index db4b73dc..88a0df2e 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -6,11 +6,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; -import 'package:spotube/models/matched_track.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/platform.dart'; @@ -26,11 +26,11 @@ class UserPreferencesNotifier extends PersistedStateNotifier { state = UserPreferences.withDefaults(); } - void setStreamMusicCodec(MusicCodec codec) { + void setStreamMusicCodec(SourceCodecs codec) { state = state.copyWith(streamMusicCodec: codec); } - void setDownloadMusicCodec(MusicCodec codec) { + void setDownloadMusicCodec(SourceCodecs codec) { state = state.copyWith(downloadMusicCodec: codec); } @@ -60,7 +60,7 @@ class UserPreferencesNotifier extends PersistedStateNotifier { state = state.copyWith(checkUpdate: check); } - void setAudioQuality(AudioQuality quality) { + void setAudioQuality(SourceQualities quality) { state = state.copyWith(audioQuality: quality); } @@ -97,8 +97,8 @@ class UserPreferencesNotifier extends PersistedStateNotifier { state = state.copyWith(skipNonMusic: skip); } - void setYoutubeApiType(YoutubeApiType type) { - state = state.copyWith(youtubeApiType: type); + void setAudioSource(AudioSource type) { + state = state.copyWith(audioSource: type); } void setSystemTitleBar(bool isSystemTitleBar) { diff --git a/lib/provider/user_preferences/user_preferences_state.dart b/lib/provider/user_preferences/user_preferences_state.dart index ff98fa8e..b3d7fe8a 100644 --- a/lib/provider/user_preferences/user_preferences_state.dart +++ b/lib/provider/user_preferences/user_preferences_state.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; -import 'package:spotube/models/matched_track.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; part 'user_preferences_state.g.dart'; @@ -15,12 +15,6 @@ enum LayoutMode { adaptive, } -@JsonEnum() -enum AudioQuality { - high, - low, -} - @JsonEnum() enum CloseBehavior { minimizeToTray, @@ -28,9 +22,10 @@ enum CloseBehavior { } @JsonEnum() -enum YoutubeApiType { +enum AudioSource { youtube, - piped; + piped, + jiosaavn; String get label => name[0].toUpperCase() + name.substring(1); } @@ -44,13 +39,27 @@ enum MusicCodec { const MusicCodec._(this.label); } +@JsonEnum() +enum SearchMode { + youtube._("YouTube"), + youtubeMusic._("YouTube Music"); + + final String label; + + const SearchMode._(this.label); + + factory SearchMode.fromString(String key) { + return SearchMode.values.firstWhere((e) => e.name == key); + } +} + @JsonSerializable() final class UserPreferences { @JsonKey( - defaultValue: AudioQuality.high, - unknownEnumValue: AudioQuality.high, + defaultValue: SourceQualities.high, + unknownEnumValue: SourceQualities.high, ) - final AudioQuality audioQuality; + final SourceQualities audioQuality; @JsonKey(defaultValue: true) final bool albumColorSync; @@ -172,22 +181,22 @@ final class UserPreferences { final ThemeMode themeMode; @JsonKey( - defaultValue: YoutubeApiType.youtube, - unknownEnumValue: YoutubeApiType.youtube, + defaultValue: AudioSource.youtube, + unknownEnumValue: AudioSource.youtube, ) - final YoutubeApiType youtubeApiType; + final AudioSource audioSource; @JsonKey( - defaultValue: MusicCodec.weba, - unknownEnumValue: MusicCodec.weba, + defaultValue: SourceCodecs.weba, + unknownEnumValue: SourceCodecs.weba, ) - final MusicCodec streamMusicCodec; + final SourceCodecs streamMusicCodec; @JsonKey( - defaultValue: MusicCodec.m4a, - unknownEnumValue: MusicCodec.m4a, + defaultValue: SourceCodecs.m4a, + unknownEnumValue: SourceCodecs.m4a, ) - final MusicCodec downloadMusicCodec; + final SourceCodecs downloadMusicCodec; UserPreferences({ required this.audioQuality, @@ -207,7 +216,7 @@ final class UserPreferences { required this.downloadLocation, required this.pipedInstance, required this.themeMode, - required this.youtubeApiType, + required this.audioSource, required this.streamMusicCodec, required this.downloadMusicCodec, }); @@ -229,7 +238,7 @@ final class UserPreferences { SpotubeColor? accentColorScheme, bool? albumColorSync, bool? checkUpdate, - AudioQuality? audioQuality, + SourceQualities? audioQuality, String? downloadLocation, LayoutMode? layoutMode, CloseBehavior? closeBehavior, @@ -238,13 +247,13 @@ final class UserPreferences { String? pipedInstance, SearchMode? searchMode, bool? skipNonMusic, - YoutubeApiType? youtubeApiType, + AudioSource? audioSource, Market? recommendationMarket, bool? saveTrackLyrics, bool? amoledDarkTheme, bool? normalizeAudio, - MusicCodec? downloadMusicCodec, - MusicCodec? streamMusicCodec, + SourceCodecs? downloadMusicCodec, + SourceCodecs? streamMusicCodec, bool? systemTitleBar, }) { return UserPreferences( @@ -261,7 +270,7 @@ final class UserPreferences { pipedInstance: pipedInstance ?? this.pipedInstance, searchMode: searchMode ?? this.searchMode, skipNonMusic: skipNonMusic ?? this.skipNonMusic, - youtubeApiType: youtubeApiType ?? this.youtubeApiType, + audioSource: audioSource ?? this.audioSource, recommendationMarket: recommendationMarket ?? this.recommendationMarket, amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme, downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, diff --git a/lib/provider/user_preferences/user_preferences_state.g.dart b/lib/provider/user_preferences/user_preferences_state.g.dart index 9e3eeee9..54cd3aa2 100644 --- a/lib/provider/user_preferences/user_preferences_state.g.dart +++ b/lib/provider/user_preferences/user_preferences_state.g.dart @@ -9,9 +9,9 @@ part of 'user_preferences_state.dart'; UserPreferences _$UserPreferencesFromJson(Map json) => UserPreferences( audioQuality: $enumDecodeNullable( - _$AudioQualityEnumMap, json['audioQuality'], - unknownValue: AudioQuality.high) ?? - AudioQuality.high, + _$SourceQualitiesEnumMap, json['audioQuality'], + unknownValue: SourceQualities.high) ?? + SourceQualities.high, albumColorSync: json['albumColorSync'] as bool? ?? true, amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false, checkUpdate: json['checkUpdate'] as bool? ?? true, @@ -51,23 +51,23 @@ UserPreferences _$UserPreferencesFromJson(Map json) => themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode'], unknownValue: ThemeMode.system) ?? ThemeMode.system, - youtubeApiType: $enumDecodeNullable( - _$YoutubeApiTypeEnumMap, json['youtubeApiType'], - unknownValue: YoutubeApiType.youtube) ?? - YoutubeApiType.youtube, + audioSource: $enumDecodeNullable( + _$AudioSourceEnumMap, json['audioSource'], + unknownValue: AudioSource.youtube) ?? + AudioSource.youtube, streamMusicCodec: $enumDecodeNullable( - _$MusicCodecEnumMap, json['streamMusicCodec'], - unknownValue: MusicCodec.weba) ?? - MusicCodec.weba, + _$SourceCodecsEnumMap, json['streamMusicCodec'], + unknownValue: SourceCodecs.weba) ?? + SourceCodecs.weba, downloadMusicCodec: $enumDecodeNullable( - _$MusicCodecEnumMap, json['downloadMusicCodec'], - unknownValue: MusicCodec.m4a) ?? - MusicCodec.m4a, + _$SourceCodecsEnumMap, json['downloadMusicCodec'], + unknownValue: SourceCodecs.m4a) ?? + SourceCodecs.m4a, ); Map _$UserPreferencesToJson(UserPreferences instance) => { - 'audioQuality': _$AudioQualityEnumMap[instance.audioQuality]!, + 'audioQuality': _$SourceQualitiesEnumMap[instance.audioQuality]!, 'albumColorSync': instance.albumColorSync, 'amoledDarkTheme': instance.amoledDarkTheme, 'checkUpdate': instance.checkUpdate, @@ -85,14 +85,15 @@ Map _$UserPreferencesToJson(UserPreferences instance) => 'downloadLocation': instance.downloadLocation, 'pipedInstance': instance.pipedInstance, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, - 'youtubeApiType': _$YoutubeApiTypeEnumMap[instance.youtubeApiType]!, - 'streamMusicCodec': _$MusicCodecEnumMap[instance.streamMusicCodec]!, - 'downloadMusicCodec': _$MusicCodecEnumMap[instance.downloadMusicCodec]!, + 'audioSource': _$AudioSourceEnumMap[instance.audioSource]!, + 'streamMusicCodec': _$SourceCodecsEnumMap[instance.streamMusicCodec]!, + 'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!, }; -const _$AudioQualityEnumMap = { - AudioQuality.high: 'high', - AudioQuality.low: 'low', +const _$SourceQualitiesEnumMap = { + SourceQualities.high: 'high', + SourceQualities.medium: 'medium', + SourceQualities.low: 'low', }; const _$CloseBehaviorEnumMap = { @@ -370,12 +371,13 @@ const _$ThemeModeEnumMap = { ThemeMode.dark: 'dark', }; -const _$YoutubeApiTypeEnumMap = { - YoutubeApiType.youtube: 'youtube', - YoutubeApiType.piped: 'piped', +const _$AudioSourceEnumMap = { + AudioSource.youtube: 'youtube', + AudioSource.piped: 'piped', + AudioSource.jiosaavn: 'jiosaavn', }; -const _$MusicCodecEnumMap = { - MusicCodec.m4a: 'm4a', - MusicCodec.weba: 'weba', +const _$SourceCodecsEnumMap = { + SourceCodecs.m4a: 'm4a', + SourceCodecs.weba: 'weba', }; diff --git a/lib/provider/youtube_provider.dart b/lib/provider/youtube_provider.dart deleted file mode 100644 index 33e0496f..00000000 --- a/lib/provider/youtube_provider.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/youtube/youtube.dart'; - -final youtubeProvider = Provider((ref) { - final preferences = ref.watch(userPreferencesProvider); - return YoutubeEndpoints(preferences); -}); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index c944004c..b3957964 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -5,9 +5,9 @@ import 'dart:async'; import 'package:media_kit/media_kit.dart' as mk; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/services/audio_player/loop_mode.dart'; import 'package:spotube/services/audio_player/playback_state.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; part 'audio_players_streams_mixin.dart'; part 'audio_player_impl.dart'; diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart index 4576ce8d..2af94dd7 100644 --- a/lib/services/audio_player/audio_player_impl.dart +++ b/lib/services/audio_player/audio_player_impl.dart @@ -121,11 +121,13 @@ class SpotubeAudioPlayer extends AudioPlayerInterface // } } - List resolveTracksForSource(List tracks) { - return tracks.where((e) => sources.contains(e.ytUri)).toList(); + // TODO: Make sure audio player soruces are also + // TODO: changed when preferences sources are changed + List resolveTracksForSource(List tracks) { + return tracks.where((e) => sources.contains(e.url)).toList(); } - bool tracksExistsInPlaylist(List tracks) { + bool tracksExistsInPlaylist(List tracks) { return resolveTracksForSource(tracks).length == tracks.length; } diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index 645548fb..a6ecac3f 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -2,10 +2,10 @@ import 'package:audio_service/audio_service.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_services/mobile_audio_service.dart'; import 'package:spotube/services/audio_services/windows_audio_service.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class AudioServices { @@ -47,8 +47,8 @@ class AudioServices { album: track.album?.name ?? "", title: track.name!, artist: TypeConversionUtils.artists_X_String(track.artists ?? []), - duration: track is SpotubeTrack - ? track.ytTrack.duration + duration: track is SourcedTrack + ? track.sourceInfo.duration : Duration(milliseconds: track.durationMs ?? 0), artUri: Uri.parse(TypeConversionUtils.image_X_UrlString( track.album?.images ?? [], diff --git a/lib/services/audio_services/linux_audio_service.dart b/lib/services/audio_services/linux_audio_service.dart index bfe022d6..436627e6 100644 --- a/lib/services/audio_services/linux_audio_service.dart +++ b/lib/services/audio_services/linux_audio_service.dart @@ -3,13 +3,12 @@ import 'dart:io'; import 'package:dbus/dbus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/loop_mode.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:window_manager/window_manager.dart'; final dbus = DBusClient.session(); @@ -321,8 +320,8 @@ class _MprisMediaPlayer2Player extends DBusObject { ), "xesam:title": DBusString(playlist.activeTrack!.name!), "xesam:url": DBusString( - playlist.activeTrack is SpotubeTrack - ? (playlist.activeTrack as SpotubeTrack).ytUri + playlist.activeTrack is SourcedTrack + ? (playlist.activeTrack as SourcedTrack).url : playlist.activeTrack!.previewUrl ?? "", ), "xesam:genre": const DBusString("Unknown"), diff --git a/lib/services/queries/lyrics.dart b/lib/services/queries/lyrics.dart index b51016b4..faa5bdec 100644 --- a/lib/services/queries/lyrics.dart +++ b/lib/services/queries/lyrics.dart @@ -8,7 +8,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/map.dart'; import 'package:spotube/hooks/spotify/use_spotify_query.dart'; import 'package:spotube/models/lyrics.dart'; -import 'package:spotube/models/spotube_track.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:http/http.dart' as http; @@ -44,7 +44,7 @@ class LyricsQueries { return useQuery( "synced-lyrics/${track?.id}}", () async { - if (track == null || track is! SpotubeTrack) { + if (track == null || track is! SourcedTrack) { throw "No track currently"; } final timedLyrics = await ServiceUtils.getTimedLyrics(track); diff --git a/lib/services/sourced_track/enums.dart b/lib/services/sourced_track/enums.dart new file mode 100644 index 00000000..48ce1cbd --- /dev/null +++ b/lib/services/sourced_track/enums.dart @@ -0,0 +1,18 @@ +import 'package:spotube/services/sourced_track/models/source_info.dart'; +import 'package:spotube/services/sourced_track/models/source_map.dart'; + +enum SourceCodecs { + m4a._("M4a (Best for downloaded music)"), + weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"); + + final String label; + const SourceCodecs._(this.label); +} + +enum SourceQualities { + high, + medium, + low, +} + +typedef SiblingType = ({SourceInfo info, SourceMap? source}); diff --git a/lib/services/sourced_track/exceptions.dart b/lib/services/sourced_track/exceptions.dart new file mode 100644 index 00000000..517d6ba4 --- /dev/null +++ b/lib/services/sourced_track/exceptions.dart @@ -0,0 +1,7 @@ +import 'package:spotify/spotify.dart'; + +class TrackNotFoundException implements Exception { + factory TrackNotFoundException(Track track) { + throw Exception("Failed to find any results for ${track.name}"); + } +} diff --git a/lib/services/sourced_track/models/source_info.dart b/lib/services/sourced_track/models/source_info.dart new file mode 100644 index 00000000..4ba90355 --- /dev/null +++ b/lib/services/sourced_track/models/source_info.dart @@ -0,0 +1,33 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'source_info.g.dart'; + +@JsonSerializable() +class SourceInfo { + final String id; + final String title; + final String artist; + final String artistUrl; + final String? album; + + final String thumbnail; + final String pageUrl; + + final Duration duration; + + SourceInfo({ + required this.id, + required this.title, + required this.artist, + required this.thumbnail, + required this.pageUrl, + required this.duration, + required this.artistUrl, + this.album, + }); + + factory SourceInfo.fromJson(Map json) => + _$SourceInfoFromJson(json); + + Map toJson() => _$SourceInfoToJson(this); +} diff --git a/lib/services/sourced_track/models/source_info.g.dart b/lib/services/sourced_track/models/source_info.g.dart new file mode 100644 index 00000000..1ec9f75f --- /dev/null +++ b/lib/services/sourced_track/models/source_info.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'source_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SourceInfo _$SourceInfoFromJson(Map json) => SourceInfo( + id: json['id'] as String, + title: json['title'] as String, + artist: json['artist'] as String, + thumbnail: json['thumbnail'] as String, + pageUrl: json['pageUrl'] as String, + duration: Duration(microseconds: json['duration'] as int), + artistUrl: json['artistUrl'] as String, + album: json['album'] as String?, + ); + +Map _$SourceInfoToJson(SourceInfo instance) => + { + 'id': instance.id, + 'title': instance.title, + 'artist': instance.artist, + 'artistUrl': instance.artistUrl, + 'album': instance.album, + 'thumbnail': instance.thumbnail, + 'pageUrl': instance.pageUrl, + 'duration': instance.duration.inMicroseconds, + }; diff --git a/lib/services/sourced_track/models/source_map.dart b/lib/services/sourced_track/models/source_map.dart new file mode 100644 index 00000000..f99f95e4 --- /dev/null +++ b/lib/services/sourced_track/models/source_map.dart @@ -0,0 +1,58 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; + +part 'source_map.g.dart'; + +@JsonSerializable() +class SourceQualityMap { + final String high; + final String medium; + final String low; + + const SourceQualityMap({ + required this.high, + required this.medium, + required this.low, + }); + + factory SourceQualityMap.fromJson(Map json) => + _$SourceQualityMapFromJson(json); + + Map toJson() => _$SourceQualityMapToJson(this); + + operator [](SourceQualities key) { + switch (key) { + case SourceQualities.high: + return high; + case SourceQualities.medium: + return medium; + case SourceQualities.low: + return low; + } + } +} + +@JsonSerializable() +class SourceMap { + final SourceQualityMap? weba; + final SourceQualityMap? m4a; + + const SourceMap({ + this.weba, + this.m4a, + }); + + factory SourceMap.fromJson(Map json) => + _$SourceMapFromJson(json); + + Map toJson() => _$SourceMapToJson(this); + + operator [](SourceCodecs key) { + switch (key) { + case SourceCodecs.weba: + return weba; + case SourceCodecs.m4a: + return m4a; + } + } +} diff --git a/lib/services/sourced_track/models/source_map.g.dart b/lib/services/sourced_track/models/source_map.g.dart new file mode 100644 index 00000000..e1085aa8 --- /dev/null +++ b/lib/services/sourced_track/models/source_map.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'source_map.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SourceQualityMap _$SourceQualityMapFromJson(Map json) => + SourceQualityMap( + high: json['high'] as String, + medium: json['medium'] as String, + low: json['low'] as String, + ); + +Map _$SourceQualityMapToJson(SourceQualityMap instance) => + { + 'high': instance.high, + 'medium': instance.medium, + 'low': instance.low, + }; + +SourceMap _$SourceMapFromJson(Map json) => SourceMap( + weba: json['weba'] == null + ? null + : SourceQualityMap.fromJson(json['weba'] as Map), + m4a: json['m4a'] == null + ? null + : SourceQualityMap.fromJson(json['m4a'] as Map), + ); + +Map _$SourceMapToJson(SourceMap instance) => { + 'weba': instance.weba, + 'm4a': instance.m4a, + }; diff --git a/lib/services/sourced_track/models/video_info.dart b/lib/services/sourced_track/models/video_info.dart new file mode 100644 index 00000000..031a8943 --- /dev/null +++ b/lib/services/sourced_track/models/video_info.dart @@ -0,0 +1,114 @@ +import 'package:piped_client/piped_client.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; + +class YoutubeVideoInfo { + final SearchMode searchMode; + final String title; + final Duration duration; + final String thumbnailUrl; + final String id; + final int likes; + final int dislikes; + final int views; + final String channelName; + final String channelId; + final DateTime publishedAt; + + YoutubeVideoInfo({ + required this.searchMode, + required this.title, + required this.duration, + required this.thumbnailUrl, + required this.id, + required this.likes, + required this.dislikes, + required this.views, + required this.channelName, + required this.publishedAt, + required this.channelId, + }); + + YoutubeVideoInfo.fromJson(Map json) + : title = json['title'], + searchMode = SearchMode.fromString(json['searchMode']), + duration = Duration(seconds: json['duration']), + thumbnailUrl = json['thumbnailUrl'], + id = json['id'], + likes = json['likes'], + dislikes = json['dislikes'], + views = json['views'], + channelName = json['channelName'], + channelId = json['channelId'], + publishedAt = DateTime.tryParse(json['publishedAt']) ?? DateTime.now(); + + Map toJson() => { + 'title': title, + 'duration': duration.inSeconds, + 'thumbnailUrl': thumbnailUrl, + 'id': id, + 'likes': likes, + 'dislikes': dislikes, + 'views': views, + 'channelName': channelName, + 'channelId': channelId, + 'publishedAt': publishedAt.toIso8601String(), + 'searchMode': searchMode.name, + }; + + factory YoutubeVideoInfo.fromVideo(Video video) { + return YoutubeVideoInfo( + searchMode: SearchMode.youtube, + title: video.title, + duration: video.duration ?? Duration.zero, + thumbnailUrl: video.thumbnails.mediumResUrl, + id: video.id.value, + likes: video.engagement.likeCount ?? 0, + dislikes: video.engagement.dislikeCount ?? 0, + views: video.engagement.viewCount, + channelName: video.author, + channelId: '/c/${video.channelId.value}', + publishedAt: video.uploadDate ?? DateTime(2003, 9, 9), + ); + } + + factory YoutubeVideoInfo.fromSearchItemStream( + PipedSearchItemStream searchItem, + SearchMode searchMode, + ) { + return YoutubeVideoInfo( + searchMode: searchMode, + title: searchItem.title, + duration: searchItem.duration, + thumbnailUrl: searchItem.thumbnail, + id: searchItem.id, + likes: 0, + dislikes: 0, + views: searchItem.views, + channelName: searchItem.uploaderName, + channelId: searchItem.uploaderUrl ?? "", + publishedAt: searchItem.uploadedDate != null + ? DateTime.tryParse(searchItem.uploadedDate!) ?? DateTime(2003, 9, 9) + : DateTime(2003, 9, 9), + ); + } + + factory YoutubeVideoInfo.fromStreamResponse( + PipedStreamResponse stream, SearchMode searchMode) { + return YoutubeVideoInfo( + searchMode: searchMode, + title: stream.title, + duration: stream.duration, + thumbnailUrl: stream.thumbnailUrl, + id: stream.id, + likes: stream.likes, + dislikes: stream.dislikes, + views: stream.views, + channelName: stream.uploader, + publishedAt: stream.uploadedDate != null + ? DateTime.tryParse(stream.uploadedDate!) ?? DateTime(2003, 9, 9) + : DateTime(2003, 9, 9), + channelId: stream.uploaderUrl, + ); + } +} diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart new file mode 100644 index 00000000..d2dd6f59 --- /dev/null +++ b/lib/services/sourced_track/sourced_track.dart @@ -0,0 +1,171 @@ +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:spotube/services/sourced_track/models/source_info.dart'; +import 'package:spotube/services/sourced_track/models/source_map.dart'; +import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; +import 'package:spotube/services/sourced_track/sources/piped.dart'; +import 'package:spotube/services/sourced_track/sources/youtube.dart'; +import 'package:spotube/utils/service_utils.dart'; + +abstract class SourcedTrack extends Track { + final SourceMap source; + final List siblings; + final SourceInfo sourceInfo; + final Ref ref; + + SourcedTrack({ + required this.ref, + required this.source, + required this.siblings, + required this.sourceInfo, + required Track track, + }) { + id = track.id; + name = track.name; + artists = track.artists; + album = track.album; + durationMs = track.durationMs; + discNumber = track.discNumber; + explicit = track.explicit; + externalIds = track.externalIds; + href = track.href; + isPlayable = track.isPlayable; + linkedFrom = track.linkedFrom; + popularity = track.popularity; + previewUrl = track.previewUrl; + trackNumber = track.trackNumber; + type = track.type; + uri = track.uri; + } + + static SourcedTrack fromJson( + Map json, { + required Ref ref, + }) { + final preferences = ref.read(userPreferencesProvider); + + final sourceInfo = SourceInfo.fromJson(json); + final source = SourceMap.fromJson(json); + final track = Track.fromJson(json); + final siblings = (json["siblings"] as List) + .map((sibling) => SourceInfo.fromJson(sibling)) + .toList() + .cast(); + + return switch (preferences.audioSource) { + AudioSource.youtube => YoutubeSourcedTrack( + ref: ref, + source: source, + siblings: siblings, + sourceInfo: sourceInfo, + track: track, + ), + AudioSource.piped => PipedSourcedTrack( + ref: ref, + source: source, + siblings: siblings, + sourceInfo: sourceInfo, + track: track, + ), + AudioSource.jiosaavn => JioSaavnSourcedTrack( + ref: ref, + source: source, + siblings: siblings, + sourceInfo: sourceInfo, + track: track, + ), + }; + } + + static String getSearchTerm(Track track) { + final artists = (track.artists ?? []) + .map((ar) => ar.name) + .toList() + .whereNotNull() + .toList(); + + final title = ServiceUtils.getTitle( + track.name!, + artists: artists, + onlyCleanArtist: true, + ).trim(); + + return "$title - ${artists.join(", ")}"; + } + + static Future fetchFromTrack({ + required Track track, + required Ref ref, + }) async { + try { + final preferences = ref.read(userPreferencesProvider); + + return switch (preferences.audioSource) { + AudioSource.piped => + await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref), + AudioSource.youtube => + await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref), + AudioSource.jiosaavn => + await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref), + }; + } catch (e) { + print("Got error: $e"); + return YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref); + } + } + + static Future> fetchSiblings({ + required Track track, + required Ref ref, + }) { + final preferences = ref.read(userPreferencesProvider); + + return switch (preferences.audioSource) { + AudioSource.piped => + PipedSourcedTrack.fetchSiblings(track: track, ref: ref), + AudioSource.youtube => + YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref), + AudioSource.jiosaavn => + JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref), + }; + } + + Future copyWithSibling(); + + Future swapWithSibling(SourceInfo sibling); + + Future swapWithSiblingOfIndex(int index) { + return swapWithSibling(siblings[index]); + } + + String get url { + final preferences = ref.read(userPreferencesProvider); + + final codec = preferences.audioSource == AudioSource.jiosaavn + ? SourceCodecs.m4a + : preferences.streamMusicCodec; + + return getUrlOfCodec(codec); + } + + String getUrlOfCodec(SourceCodecs codec) { + final preferences = ref.read(userPreferencesProvider); + + return source[codec]?[preferences.audioQuality] ?? + // this will ensure playback doesn't break + source[codec == SourceCodecs.m4a ? SourceCodecs.weba : SourceCodecs.m4a] + [preferences.audioQuality]; + } + + SourceCodecs get codec { + final preferences = ref.read(userPreferencesProvider); + + return preferences.audioSource == AudioSource.jiosaavn + ? SourceCodecs.m4a + : preferences.streamMusicCodec; + } +} diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart new file mode 100644 index 00000000..b25eca3b --- /dev/null +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -0,0 +1,159 @@ +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/models/source_match.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:spotube/services/sourced_track/exceptions.dart'; +import 'package:spotube/services/sourced_track/models/source_info.dart'; +import 'package:spotube/services/sourced_track/models/source_map.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:jiosaavn/jiosaavn.dart'; + +final jiosaavnClient = JioSaavnClient(); + +class JioSaavnSourcedTrack extends SourcedTrack { + JioSaavnSourcedTrack({ + required super.ref, + required super.source, + required super.siblings, + required super.sourceInfo, + required super.track, + }); + + static Future fetchFromTrack({ + required Track track, + required Ref ref, + }) async { + final cachedSource = await SourceMatch.box.get(track.id); + + if (cachedSource == null || + cachedSource.sourceType != SourceType.jiosaavn) { + final siblings = await fetchSiblings(ref: ref, track: track); + + if (siblings.isEmpty) { + throw TrackNotFoundException(track); + } + + await SourceMatch.box.put( + track.id!, + SourceMatch( + id: track.id!, + sourceType: SourceType.jiosaavn, + createdAt: DateTime.now(), + sourceId: siblings.first.info.id, + ), + ); + + return JioSaavnSourcedTrack( + ref: ref, + siblings: siblings.map((s) => s.info).skip(1).toList(), + source: siblings.first.source!, + sourceInfo: siblings.first.info, + track: track, + ); + } + + final [item] = + await jiosaavnClient.songs.detailsById([cachedSource.sourceId]); + + final (:info, :source) = toSiblingType(item); + + return JioSaavnSourcedTrack( + ref: ref, + siblings: [], + source: source!, + sourceInfo: info, + track: track, + ); + } + + static SiblingType toSiblingType(SongResponse result) { + final SiblingType sibling = ( + info: SourceInfo( + artist: [ + result.primaryArtists, + if (result.featuredArtists.isNotEmpty) ", ", + result.featuredArtists + ].join("").replaceAll("&", "&"), + artistUrl: + "https://www.jiosaavn.com/artist/${result.primaryArtistsId.split(",").firstOrNull ?? ""}", + duration: Duration(seconds: int.parse(result.duration)), + id: result.id, + pageUrl: result.url, + thumbnail: result.image?.last.link ?? "", + title: result.name!, + album: result.album.name, + ), + source: SourceMap( + m4a: SourceQualityMap( + high: result.downloadUrl! + .firstWhere((element) => element.quality == "320kbps") + .link, + medium: result.downloadUrl! + .firstWhere((element) => element.quality == "160kbps") + .link, + low: result.downloadUrl! + .firstWhere((element) => element.quality == "96kbps") + .link, + ), + ), + ); + + return sibling; + } + + static Future> fetchSiblings({ + required Track track, + required Ref ref, + }) async { + final query = SourcedTrack.getSearchTerm(track); + + final SongSearchResponse(:results) = + await jiosaavnClient.search.songs(query, limit: 20); + + return results.map(toSiblingType).toList(); + } + + @override + Future copyWithSibling() async { + if (siblings.isNotEmpty) { + return this; + } + final fetchedSiblings = await fetchSiblings(ref: ref, track: this); + + return JioSaavnSourcedTrack( + ref: ref, + siblings: fetchedSiblings + .where((s) => s.info.id != sourceInfo.id) + .map((s) => s.info) + .toList(), + source: source, + sourceInfo: sourceInfo, + track: this, + ); + } + + @override + Future swapWithSibling(SourceInfo sibling) async { + if (sibling.id == sourceInfo.id || + siblings.none((s) => s.id == sibling.id)) { + return null; + } + + final newSourceInfo = siblings.firstWhere((s) => s.id == sibling.id); + final newSiblings = siblings.where((s) => s.id != sibling.id).toList() + ..insert(0, sourceInfo); + + final [item] = await jiosaavnClient.songs.detailsById([newSourceInfo.id]); + + final (:info, :source) = toSiblingType(item); + + return JioSaavnSourcedTrack( + ref: ref, + siblings: newSiblings, + source: source!, + sourceInfo: info, + track: this, + ); + } +} diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart new file mode 100644 index 00000000..0778a7cf --- /dev/null +++ b/lib/services/sourced_track/sources/piped.dart @@ -0,0 +1,257 @@ +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:piped_client/piped_client.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/models/source_match.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:spotube/services/sourced_track/exceptions.dart'; +import 'package:spotube/services/sourced_track/models/source_info.dart'; +import 'package:spotube/services/sourced_track/models/source_map.dart'; +import 'package:spotube/services/sourced_track/models/video_info.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/services/sourced_track/sources/youtube.dart'; +import 'package:spotube/utils/service_utils.dart'; + +final pipedProvider = Provider( + (ref) { + final instance = + ref.watch(userPreferencesProvider.select((s) => s.pipedInstance)); + return PipedClient(instance: instance); + }, +); + +class PipedSourcedTrack extends SourcedTrack { + PipedSourcedTrack({ + required super.ref, + required super.source, + required super.siblings, + required super.sourceInfo, + required super.track, + }); + + static Future fetchFromTrack({ + required Track track, + required Ref ref, + }) async { + final cachedSource = await SourceMatch.box.get(track.id); + final preferences = ref.read(userPreferencesProvider); + final pipedClient = ref.read(pipedProvider); + + if (cachedSource == null) { + final siblings = await fetchSiblings(ref: ref, track: track); + if (siblings.isEmpty) { + throw TrackNotFoundException(track); + } + + await SourceMatch.box.put( + track.id!, + SourceMatch( + id: track.id!, + sourceType: preferences.searchMode == SearchMode.youtube + ? SourceType.youtube + : SourceType.youtubeMusic, + createdAt: DateTime.now(), + sourceId: siblings.first.info.id, + ), + ); + + return PipedSourcedTrack( + ref: ref, + siblings: siblings.map((s) => s.info).skip(1).toList(), + source: siblings.first.source as SourceMap, + sourceInfo: siblings.first.info, + track: track, + ); + } else { + final manifest = await pipedClient.streams(cachedSource.sourceId); + + return PipedSourcedTrack( + ref: ref, + siblings: [], + source: toSourceMap(manifest), + sourceInfo: SourceInfo( + id: manifest.id, + artist: manifest.uploader, + artistUrl: manifest.uploaderUrl, + pageUrl: "https://www.youtube.com/watch?v=${manifest.id}", + thumbnail: manifest.thumbnailUrl, + title: manifest.title, + duration: manifest.duration, + album: null, + ), + track: track, + ); + } + } + + static SourceMap toSourceMap(PipedStreamResponse manifest) { + final m4a = manifest.audioStreams + .where((audio) => audio.format == PipedAudioStreamFormat.m4a) + .sorted((a, b) => a.bitrate.compareTo(b.bitrate)); + + final weba = manifest.audioStreams + .where((audio) => audio.format == PipedAudioStreamFormat.webm) + .sorted((a, b) => a.bitrate.compareTo(b.bitrate)); + + return SourceMap( + m4a: SourceQualityMap( + high: m4a.first.url.toString(), + medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(), + low: m4a.last.url.toString(), + ), + weba: SourceQualityMap( + high: weba.first.url.toString(), + medium: + (weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1]).url.toString(), + low: weba.last.url.toString(), + ), + ); + } + + static Future toSiblingType( + int index, + YoutubeVideoInfo item, + PipedClient pipedClient, + ) async { + SourceMap? sourceMap; + if (index == 0) { + final manifest = await pipedClient.streams(item.id); + sourceMap = toSourceMap(manifest); + } + + final SiblingType sibling = ( + info: SourceInfo( + id: item.id, + artist: item.channelName, + artistUrl: "https://www.youtube.com/${item.channelId}", + pageUrl: "https://www.youtube.com/watch?v=${item.id}", + thumbnail: item.thumbnailUrl, + title: item.title, + duration: item.duration, + album: null, + ), + source: sourceMap, + ); + + return sibling; + } + + static Future> fetchSiblings({ + required Track track, + required Ref ref, + }) async { + final pipedClient = ref.read(pipedProvider); + final preference = ref.read(userPreferencesProvider); + final query = SourcedTrack.getSearchTerm(track); + + final PipedSearchResult(items: searchResults) = await pipedClient.search( + query, + preference.searchMode == SearchMode.youtube + ? PipedFilter.video + : PipedFilter.musicSongs, + ); + + final isYouTubeMusic = preference.searchMode == SearchMode.youtubeMusic; + + if (isYouTubeMusic) { + final artists = (track.artists ?? []) + .map((ar) => ar.name) + .toList() + .whereNotNull() + .toList(); + + return await Future.wait( + searchResults + .map( + (result) => YoutubeVideoInfo.fromSearchItemStream( + result as PipedSearchItemStream, + preference.searchMode, + ), + ) + .sorted((a, b) => b.views.compareTo(a.views)) + .where( + (item) => artists.any( + (artist) => + artist.toLowerCase() == item.channelName.toLowerCase(), + ), + ) + .mapIndexed((i, r) => toSiblingType(i, r, pipedClient)), + ); + } + + if (ServiceUtils.onlyContainsEnglish(query)) { + return await Future.wait( + searchResults + .whereType() + .map( + (result) => YoutubeVideoInfo.fromSearchItemStream( + result, + preference.searchMode, + ), + ) + .mapIndexed((i, r) => toSiblingType(i, r, pipedClient)), + ); + } + + final rankedSiblings = YoutubeSourcedTrack.rankResults( + searchResults + .map( + (result) => YoutubeVideoInfo.fromSearchItemStream( + result as PipedSearchItemStream, + preference.searchMode, + ), + ) + .toList(), + track, + ); + + return await Future.wait( + rankedSiblings.mapIndexed((i, r) => toSiblingType(i, r, pipedClient)), + ); + } + + @override + Future copyWithSibling() async { + if (siblings.isNotEmpty) { + return this; + } + final fetchedSiblings = await fetchSiblings(ref: ref, track: this); + + return PipedSourcedTrack( + ref: ref, + siblings: fetchedSiblings + .where((s) => s.info.id != sourceInfo.id) + .map((s) => s.info) + .toList(), + source: source, + sourceInfo: sourceInfo, + track: this, + ); + } + + @override + Future swapWithSibling(SourceInfo sibling) async { + if (sibling.id == sourceInfo.id || + siblings.none((s) => s.id == sibling.id)) { + return null; + } + + final newSourceInfo = siblings.firstWhere((s) => s.id == sibling.id); + final newSiblings = siblings.where((s) => s.id != sibling.id).toList() + ..insert(0, sourceInfo); + + final pipedClient = ref.read(pipedProvider); + + final manifest = await pipedClient.streams(newSourceInfo.id); + + return PipedSourcedTrack( + ref: ref, + siblings: newSiblings, + source: toSourceMap(manifest), + sourceInfo: newSourceInfo, + track: this, + ); + } +} diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart new file mode 100644 index 00000000..096de2d4 --- /dev/null +++ b/lib/services/sourced_track/sources/youtube.dart @@ -0,0 +1,256 @@ +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/models/source_match.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:spotube/services/sourced_track/exceptions.dart'; +import 'package:spotube/services/sourced_track/models/source_info.dart'; +import 'package:spotube/services/sourced_track/models/source_map.dart'; +import 'package:spotube/services/sourced_track/models/video_info.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/utils/service_utils.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; + +final youtubeClient = YoutubeExplode(); +final officialMusicRegex = RegExp( + r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)", + caseSensitive: false, +); + +class YoutubeSourcedTrack extends SourcedTrack { + YoutubeSourcedTrack({ + required super.source, + required super.siblings, + required super.sourceInfo, + required super.track, + required super.ref, + }); + + static Future fetchFromTrack({ + required Track track, + required Ref ref, + }) async { + final cachedSource = await SourceMatch.box.get(track.id); + + if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) { + final siblings = await fetchSiblings(ref: ref, track: track); + if (siblings.isEmpty) { + throw TrackNotFoundException(track); + } + + await SourceMatch.box.put( + track.id!, + SourceMatch( + id: track.id!, + sourceType: SourceType.youtube, + createdAt: DateTime.now(), + sourceId: siblings.first.info.id, + ), + ); + + return YoutubeSourcedTrack( + ref: ref, + siblings: siblings.map((s) => s.info).skip(1).toList(), + source: siblings.first.source as SourceMap, + sourceInfo: siblings.first.info, + track: track, + ); + } + final item = await youtubeClient.videos.get(cachedSource.sourceId); + final manifest = await youtubeClient.videos.streamsClient.getManifest( + cachedSource.sourceId, + ); + return YoutubeSourcedTrack( + ref: ref, + siblings: [], + source: toSourceMap(manifest), + sourceInfo: SourceInfo( + id: item.id.value, + artist: item.author, + artistUrl: "https://www.youtube.com/channel/${item.channelId}", + pageUrl: item.url, + thumbnail: item.thumbnails.highResUrl, + title: item.title, + duration: item.duration ?? Duration.zero, + album: null, + ), + track: track, + ); + } + + static SourceMap toSourceMap(StreamManifest manifest) { + final m4a = manifest.audioOnly + .where((audio) => audio.codec.mimeType == "audio/mp4") + .sortByBitrate(); + + final weba = manifest.audioOnly + .where((audio) => audio.codec.mimeType == "audio/webm") + .sortByBitrate(); + + return SourceMap( + m4a: SourceQualityMap( + high: m4a.first.url.toString(), + medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(), + low: m4a.last.url.toString(), + ), + weba: SourceQualityMap( + high: weba.first.url.toString(), + medium: + (weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1]).url.toString(), + low: weba.last.url.toString(), + ), + ); + } + + static Future toSiblingType( + int index, + YoutubeVideoInfo item, + ) async { + SourceMap? sourceMap; + if (index == 0) { + final manifest = + await youtubeClient.videos.streamsClient.getManifest(item.id); + sourceMap = toSourceMap(manifest); + } + + final SiblingType sibling = ( + info: SourceInfo( + id: item.id, + artist: item.channelName, + artistUrl: "https://www.youtube.com/channel/${item.channelId}", + pageUrl: "https://www.youtube.com/watch?v=${item.id}", + thumbnail: item.thumbnailUrl, + title: item.title, + duration: item.duration, + album: null, + ), + source: sourceMap, + ); + + return sibling; + } + + static List rankResults( + List results, Track track) { + final artists = (track.artists ?? []) + .map((ar) => ar.name) + .toList() + .whereNotNull() + .toList(); + + return results + .sorted((a, b) => b.views.compareTo(a.views)) + .map((sibling) { + int score = 0; + + for (final artist in artists) { + final isSameChannelArtist = + sibling.channelName.toLowerCase() == artist.toLowerCase(); + final channelContainsArtist = sibling.channelName + .toLowerCase() + .contains(artist.toLowerCase()); + + if (isSameChannelArtist || channelContainsArtist) { + score += 1; + } + + final titleContainsArtist = + sibling.title.toLowerCase().contains(artist.toLowerCase()); + + if (titleContainsArtist) { + score += 1; + } + } + + final titleContainsTrackName = + sibling.title.toLowerCase().contains(track.name!.toLowerCase()); + + final hasOfficialFlag = + officialMusicRegex.hasMatch(sibling.title.toLowerCase()); + + if (titleContainsTrackName) { + score += 3; + } + + if (hasOfficialFlag) { + score += 1; + } + + if (hasOfficialFlag && titleContainsTrackName) { + score += 2; + } + + return (sibling: sibling, score: score); + }) + .sorted((a, b) => b.score.compareTo(a.score)) + .map((e) => e.sibling) + .toList(); + } + + static Future> fetchSiblings({ + required Track track, + required Ref ref, + }) async { + final query = SourcedTrack.getSearchTerm(track); + + final searchResults = await youtubeClient.search.search( + query, + filter: TypeFilters.video, + ); + + if (ServiceUtils.onlyContainsEnglish(query)) { + return await Future.wait(searchResults + .map(YoutubeVideoInfo.fromVideo) + .mapIndexed(toSiblingType)); + } + + final rankedSiblings = rankResults( + searchResults.map(YoutubeVideoInfo.fromVideo).toList(), + track, + ); + + return await Future.wait(rankedSiblings.mapIndexed(toSiblingType)); + } + + @override + Future swapWithSibling(SourceInfo sibling) async { + if (sibling.id == sourceInfo.id || + siblings.none((s) => s.id == sibling.id)) { + return null; + } + + final newSourceInfo = siblings.firstWhere((s) => s.id == sibling.id); + final newSiblings = siblings.where((s) => s.id != sibling.id).toList() + ..insert(0, sourceInfo); + + final manifest = + await youtubeClient.videos.streamsClient.getManifest(newSourceInfo.id); + + return YoutubeSourcedTrack( + ref: ref, + siblings: newSiblings, + source: toSourceMap(manifest), + sourceInfo: newSourceInfo, + track: this, + ); + } + + @override + Future copyWithSibling() async { + if (siblings.isNotEmpty) { + return this; + } + final fetchedSiblings = await fetchSiblings(ref: ref, track: this); + + return YoutubeSourcedTrack( + ref: ref, + siblings: fetchedSiblings + .where((s) => s.info.id != sourceInfo.id) + .map((s) => s.info) + .toList(), + source: source, + sourceInfo: sourceInfo, + track: this, + ); + } +} diff --git a/lib/services/supabase.dart b/lib/services/supabase.dart index d42d8eeb..ef3fa87c 100644 --- a/lib/services/supabase.dart +++ b/lib/services/supabase.dart @@ -1,5 +1,5 @@ import 'package:spotube/collections/env.dart'; -import 'package:spotube/models/matched_track.dart'; +import 'package:spotube/models/source_match.dart'; import 'package:supabase/supabase.dart'; class SupabaseService { @@ -8,7 +8,9 @@ class SupabaseService { Env.supabaseAnonKey ?? "", ); - Future insertTrack(MatchedTrack track) async { + Future insertTrack(SourceMatch track) async { + return null; + // TODO: Fix this await api.from("tracks").insert(track.toJson()); } } diff --git a/lib/services/youtube/youtube.dart b/lib/services/youtube/youtube.dart deleted file mode 100644 index 2b52864b..00000000 --- a/lib/services/youtube/youtube.dart +++ /dev/null @@ -1,248 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; -import 'package:piped_client/piped_client.dart'; -import 'package:spotube/collections/routes.dart'; -import 'package:spotube/components/shared/dialogs/piped_down_dialog.dart'; -import 'package:spotube/models/matched_track.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; -import 'package:spotube/utils/primitive_utils.dart'; -import 'package:youtube_explode_dart/youtube_explode_dart.dart'; - -class YoutubeVideoInfo { - final SearchMode searchMode; - final String title; - final Duration duration; - final String thumbnailUrl; - final String id; - final int likes; - final int dislikes; - final int views; - final String channelName; - final String channelId; - final DateTime publishedAt; - - YoutubeVideoInfo({ - required this.searchMode, - required this.title, - required this.duration, - required this.thumbnailUrl, - required this.id, - required this.likes, - required this.dislikes, - required this.views, - required this.channelName, - required this.publishedAt, - required this.channelId, - }); - - YoutubeVideoInfo.fromJson(Map json) - : title = json['title'], - searchMode = SearchMode.fromString(json['searchMode']), - duration = Duration(seconds: json['duration']), - thumbnailUrl = json['thumbnailUrl'], - id = json['id'], - likes = json['likes'], - dislikes = json['dislikes'], - views = json['views'], - channelName = json['channelName'], - channelId = json['channelId'], - publishedAt = DateTime.tryParse(json['publishedAt']) ?? DateTime.now(); - - Map toJson() => { - 'title': title, - 'duration': duration.inSeconds, - 'thumbnailUrl': thumbnailUrl, - 'id': id, - 'likes': likes, - 'dislikes': dislikes, - 'views': views, - 'channelName': channelName, - 'channelId': channelId, - 'publishedAt': publishedAt.toIso8601String(), - 'searchMode': searchMode.name, - }; - - factory YoutubeVideoInfo.fromVideo(Video video) { - return YoutubeVideoInfo( - searchMode: SearchMode.youtube, - title: video.title, - duration: video.duration ?? Duration.zero, - thumbnailUrl: video.thumbnails.mediumResUrl, - id: video.id.value, - likes: video.engagement.likeCount ?? 0, - dislikes: video.engagement.dislikeCount ?? 0, - views: video.engagement.viewCount, - channelName: video.author, - channelId: '/c/${video.channelId.value}', - publishedAt: video.uploadDate ?? DateTime(2003, 9, 9), - ); - } - - factory YoutubeVideoInfo.fromSearchItemStream( - PipedSearchItemStream searchItem, - SearchMode searchMode, - ) { - return YoutubeVideoInfo( - searchMode: searchMode, - title: searchItem.title, - duration: searchItem.duration, - thumbnailUrl: searchItem.thumbnail, - id: searchItem.id, - likes: 0, - dislikes: 0, - views: searchItem.views, - channelName: searchItem.uploaderName, - channelId: searchItem.uploaderUrl ?? "", - publishedAt: searchItem.uploadedDate != null - ? DateTime.tryParse(searchItem.uploadedDate!) ?? DateTime(2003, 9, 9) - : DateTime(2003, 9, 9), - ); - } - - factory YoutubeVideoInfo.fromStreamResponse( - PipedStreamResponse stream, SearchMode searchMode) { - return YoutubeVideoInfo( - searchMode: searchMode, - title: stream.title, - duration: stream.duration, - thumbnailUrl: stream.thumbnailUrl, - id: stream.id, - likes: stream.likes, - dislikes: stream.dislikes, - views: stream.views, - channelName: stream.uploader, - publishedAt: stream.uploadedDate != null - ? DateTime.tryParse(stream.uploadedDate!) ?? DateTime(2003, 9, 9) - : DateTime(2003, 9, 9), - channelId: stream.uploaderUrl, - ); - } -} - -class YoutubeEndpoints { - PipedClient? piped; - YoutubeExplode? youtube; - - final UserPreferences preferences; - - YoutubeEndpoints(this.preferences) { - switch (preferences.youtubeApiType) { - case YoutubeApiType.youtube: - youtube = YoutubeExplode(); - break; - case YoutubeApiType.piped: - piped = PipedClient(instance: preferences.pipedInstance); - break; - } - } - - Future showPipedErrorDialog(Exception e) async { - if (e is DioException && (e.response?.statusCode ?? 0) >= 500) { - final context = rootNavigatorKey?.currentContext; - if (context != null) { - await showDialog( - context: context, - builder: (context) => const PipedDownDialog(), - ); - } - } - } - - Future> search(String query) async { - if (youtube != null) { - final res = await youtube!.search( - query, - filter: TypeFilters.video, - ); - - return res.map(YoutubeVideoInfo.fromVideo).toList(); - } else { - try { - final res = await piped!.search( - query, - switch (preferences.searchMode) { - SearchMode.youtube => PipedFilter.video, - SearchMode.youtubeMusic => PipedFilter.musicSongs, - }, - ); - return res.items - .whereType() - .map( - (e) => YoutubeVideoInfo.fromSearchItemStream( - e, - preferences.searchMode, - ), - ) - .toList(); - } on Exception catch (e) { - await showPipedErrorDialog(e); - rethrow; - } - } - } - - String _pipedStreamResponseToStreamUrl( - PipedStreamResponse stream, - MusicCodec codec, - ) { - final pipedStreamFormat = switch (codec) { - MusicCodec.m4a => PipedAudioStreamFormat.m4a, - MusicCodec.weba => PipedAudioStreamFormat.webm, - }; - - return switch (preferences.audioQuality) { - AudioQuality.high => - stream.highestBitrateAudioStreamOfFormat(pipedStreamFormat)!.url, - AudioQuality.low => - stream.lowestBitrateAudioStreamOfFormat(pipedStreamFormat)!.url, - }; - } - - Future streamingUrl(String id, MusicCodec codec) async { - if (youtube != null) { - final res = await PrimitiveUtils.raceMultiple( - () => youtube!.videos.streams.getManifest(id), - ); - final audioOnlyManifests = res.audioOnly.where((info) { - return switch (codec) { - MusicCodec.m4a => info.codec.mimeType == "audio/mp4", - MusicCodec.weba => info.codec.mimeType == "audio/webm", - }; - }); - - return switch (preferences.audioQuality) { - AudioQuality.high => - audioOnlyManifests.withHighestBitrate().url.toString(), - AudioQuality.low => - audioOnlyManifests.sortByBitrate().last.url.toString(), - }; - } else { - return _pipedStreamResponseToStreamUrl(await piped!.streams(id), codec); - } - } - - Future<(YoutubeVideoInfo info, String streamingUrl)> video( - String id, - SearchMode searchMode, - MusicCodec codec, - ) async { - if (youtube != null) { - final res = await youtube!.videos.get(id); - return ( - YoutubeVideoInfo.fromVideo(res), - await streamingUrl(id, codec), - ); - } else { - try { - final res = await piped!.streams(id); - return ( - YoutubeVideoInfo.fromStreamResponse(res, searchMode), - _pipedStreamResponseToStreamUrl(res, codec), - ); - } on Exception catch (e) { - await showPipedErrorDialog(e); - rethrow; - } - } - } -} diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 0be1dd97..9e3b5893 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -8,7 +8,7 @@ import 'package:spotube/components/library/user_local_tracks.dart'; import 'package:spotube/models/logger.dart'; import 'package:http/http.dart' as http; import 'package:spotube/models/lyrics.dart'; -import 'package:spotube/models/spotube_track.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:collection/collection.dart'; @@ -171,7 +171,7 @@ abstract class ServiceUtils { static const baseUri = "https://www.rentanadviser.com/subtitles"; @Deprecated("In favor spotify lyrics api, this isn't needed anymore") - static Future getTimedLyrics(SpotubeTrack track) async { + static Future getTimedLyrics(SourcedTrack track) async { final artistNames = track.artists?.map((artist) => artist.name!).toList() ?? []; final query = getTitle( @@ -199,7 +199,7 @@ abstract class ServiceUtils { false; final hasTrackName = title.contains(track.name!.toLowerCase()); final isNotLive = !PrimitiveUtils.containsTextInBracket(title, "live"); - final exactYtMatch = title == track.ytTrack.title.toLowerCase(); + final exactYtMatch = title == track.sourceInfo.title.toLowerCase(); if (exactYtMatch) points = 7; for (final criteria in [hasTrackName, hasAllArtists, isNotLive]) { if (criteria) points++; diff --git a/pubspec.lock b/pubspec.lock index 39e92028..54f6d934 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -385,6 +385,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + dart_des: + dependency: transitive + description: + name: dart_des + sha256: "0a66afb8883368c824497fd2a1fd67bdb1a785965a3956728382c03d40747c33" + url: "https://pub.dev" + source: hosted + version: "1.0.2" dart_style: dependency: transitive description: @@ -1174,6 +1182,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + jiosaavn: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "8a7cda9b8b687cde28e0f7fcb10adb0d4fde1007" + url: "https://github.com/KRTirtho/jiosaavn.git" + source: git + version: "0.1.0" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 590aaae4..b3fd3c3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -106,6 +106,9 @@ dependencies: simple_icons: ^7.10.0 audio_service_mpris: ^0.1.0 file_picker: ^6.0.0 + jiosaavn: + git: + url: https://github.com/KRTirtho/jiosaavn.git draggable_scrollbar: git: url: https://github.com/thielepaul/flutter-draggable-scrollbar.git From 28a5d6bb3820ab0bd4007664f73d685f6e1d2c90 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 17 Nov 2023 13:14:25 +0600 Subject: [PATCH 047/131] feat: paginated playlist and album page --- lib/collections/routes.dart | 16 +- lib/components/album/album_card.dart | 79 ++-- lib/components/library/user_local_tracks.dart | 10 +- lib/components/player/player_queue.dart | 2 +- lib/components/playlist/playlist_card.dart | 48 ++- .../expandable_search/expandable_search.dart | 21 +- .../shimmers/shimmer_artist_profile.dart | 2 +- .../shared/shimmers/shimmer_track_tile.dart | 56 +-- .../track_collection_heading.dart | 229 ----------- .../track_collection_view.dart | 274 ------------- .../shared/track_table/tracks_table_view.dart | 368 ------------------ .../track_options.dart | 0 .../track_tile.dart | 2 +- .../sections/body/track_view_body.dart | 124 ++++++ .../body/track_view_body_headers.dart | 106 +++++ .../sections/body/track_view_options.dart | 131 +++++++ .../sections/body/use_is_user_playlist.dart | 18 + .../sections/header/flexible_header.dart | 142 +++++++ .../sections/header/header_actions.dart | 82 ++++ .../sections/header/header_buttons.dart | 137 +++++++ .../shared/tracks_view/track_view.dart | 44 +++ .../shared/tracks_view/track_view_props.dart | 102 +++++ .../tracks_view/track_view_provider.dart | 64 +++ lib/extensions/infinite_query.dart | 30 ++ lib/pages/album/album.dart | 192 +++------ lib/pages/artist/artist.dart | 2 +- lib/pages/home/genres.dart | 6 +- lib/pages/playlist/liked_playlist.dart | 45 +++ lib/pages/playlist/playlist.dart | 212 +++------- lib/pages/search/sections/tracks.dart | 2 +- lib/services/queries/album.dart | 48 ++- lib/services/queries/playlist.dart | 65 ++-- pubspec.lock | 16 + pubspec.yaml | 2 + 34 files changed, 1376 insertions(+), 1301 deletions(-) delete mode 100644 lib/components/shared/track_table/track_collection_view/track_collection_heading.dart delete mode 100644 lib/components/shared/track_table/track_collection_view/track_collection_view.dart delete mode 100644 lib/components/shared/track_table/tracks_table_view.dart rename lib/components/shared/{track_table => track_tile}/track_options.dart (100%) rename lib/components/shared/{track_table => track_tile}/track_tile.dart (99%) create mode 100644 lib/components/shared/tracks_view/sections/body/track_view_body.dart create mode 100644 lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart create mode 100644 lib/components/shared/tracks_view/sections/body/track_view_options.dart create mode 100644 lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart create mode 100644 lib/components/shared/tracks_view/sections/header/flexible_header.dart create mode 100644 lib/components/shared/tracks_view/sections/header/header_actions.dart create mode 100644 lib/components/shared/tracks_view/sections/header/header_buttons.dart create mode 100644 lib/components/shared/tracks_view/track_view.dart create mode 100644 lib/components/shared/tracks_view/track_view_props.dart create mode 100644 lib/components/shared/tracks_view/track_view_provider.dart create mode 100644 lib/extensions/infinite_query.dart create mode 100644 lib/pages/playlist/liked_playlist.dart diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 81ebb3e6..82597ddb 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -3,29 +3,29 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:spotify/spotify.dart' hide Search; +import 'package:spotube/pages/album/album.dart'; import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/lastfm_login/lastfm_login.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; +import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart'; import 'package:spotube/pages/lyrics/mini_lyrics.dart'; +import 'package:spotube/pages/playlist/liked_playlist.dart'; +import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/about.dart'; import 'package:spotube/pages/settings/logs.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/components/shared/spotube_page_route.dart'; -import 'package:spotube/pages/album/album.dart'; import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/pages/library/library.dart'; import 'package:spotube/pages/desktop_login/login_tutorial.dart'; import 'package:spotube/pages/desktop_login/desktop_login.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; -import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/pages/root/root_app.dart'; import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/pages/mobile_login/mobile_login.dart'; -import '../pages/library/playlist_generate/playlist_generate_result.dart'; - final rootNavigatorKey = Catcher2.navigatorKey; final shellRouteNavigatorKey = GlobalKey(); final router = GoRouter( @@ -104,7 +104,9 @@ final router = GoRouter( path: "/album/:id", pageBuilder: (context, state) { assert(state.extra is AlbumSimple); - return SpotubePage(child: AlbumPage(state.extra as AlbumSimple)); + return SpotubePage( + child: AlbumPage(album: state.extra as AlbumSimple), + ); }, ), GoRoute( @@ -119,7 +121,9 @@ final router = GoRouter( pageBuilder: (context, state) { assert(state.extra is PlaylistSimple); return SpotubePage( - child: PlaylistView(state.extra as PlaylistSimple), + child: state.pathParameters["id"] == "user-liked-tracks" + ? LikedPlaylistPage(playlist: state.extra as PlaylistSimple) + : PlaylistPage(playlist: state.extra as PlaylistSimple), ); }, ), diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index 945f8ecf..c7ae2f9a 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -4,9 +4,12 @@ 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/extensions/context.dart'; +import 'package:spotube/extensions/infinite_query.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'; +import 'package:spotube/services/queries/album.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -15,7 +18,7 @@ extension FormattedAlbumType on AlbumType { } class AlbumCard extends HookConsumerWidget { - final Album album; + final AlbumSimple album; const AlbumCard( this.album, { Key? key, @@ -27,7 +30,9 @@ class AlbumCard extends HookConsumerWidget { final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); + final queryClient = useQueryClient(); + bool isPlaylistPlaying = useMemoized( () => playlist.containsCollection(album.id!), [playlist, album.id], @@ -36,6 +41,34 @@ class AlbumCard extends HookConsumerWidget { final updating = useState(false); final spotify = ref.watch(spotifyProvider); + final scaffoldMessenger = ScaffoldMessenger.maybeOf(context); + + Future> fetchAllTrack() async { + if (album.tracks != null && album.tracks!.isNotEmpty) { + return album.tracks! + .map((track) => + TypeConversionUtils.simpleTrack_X_Track(track, album)) + .toList(); + } + final job = AlbumQueries.tracksOfJob(album.id!); + + final query = queryClient.createInfiniteQuery( + job.queryKey, + (page) => job.task(page, (spotify: spotify, album: album)), + initialPage: 0, + nextPage: job.nextPage, + ); + + return await query.fetchAllTracks( + getAllTracks: () async { + final res = await spotify.albums.tracks(album.id!).all(); + return res + .map((e) => TypeConversionUtils.simpleTrack_X_Track(e, album)) + .toList(); + }, + ); + } + return PlaybuttonCard( imageUrl: TypeConversionUtils.image_X_UrlString( album.images, @@ -54,20 +87,15 @@ class AlbumCard extends HookConsumerWidget { onPlaybuttonPressed: () async { updating.value = true; try { - if (isPlaylistPlaying && playing) { - return audioPlayer.pause(); - } else if (isPlaylistPlaying && !playing) { - return audioPlayer.resume(); + if (isPlaylistPlaying) { + return playing ? audioPlayer.pause() : audioPlayer.resume(); } - await playlistNotifier.load( - album.tracks - ?.map((e) => - TypeConversionUtils.simpleTrack_X_Track(e, album)) - .toList() ?? - [], - autoPlay: true, - ); + final fetchedTracks = await fetchAllTrack(); + + if (fetchedTracks.isEmpty) return; + + await playlistNotifier.load(fetchedTracks, autoPlay: true); playlistNotifier.addCollection(album.id!); } finally { updating.value = false; @@ -80,28 +108,16 @@ class AlbumCard extends HookConsumerWidget { updating.value = true; try { - final fetchedTracks = - await queryClient.fetchQuery, SpotifyApi>( - "album-tracks/${album.id}", - () { - return spotify.albums - .tracks(album.id!) - .all() - .then((value) => value.toList()); - }, - ).then( - (tracks) => tracks - ?.map( - (e) => TypeConversionUtils.simpleTrack_X_Track(e, album)) - .toList(), - ); + final fetchedTracks = await fetchAllTrack(); - if (fetchedTracks == null || fetchedTracks.isEmpty) return; + if (fetchedTracks.isEmpty) return; playlistNotifier.addTracks(fetchedTracks); playlistNotifier.addCollection(album.id!); if (context.mounted) { final snackbar = SnackBar( - content: Text("Added ${album.tracks?.length} tracks to queue"), + content: Text( + context.l10n.added_to_queue(fetchedTracks.length), + ), action: SnackBarAction( label: "Undo", onPressed: () { @@ -110,7 +126,8 @@ class AlbumCard extends HookConsumerWidget { }, ), ); - ScaffoldMessenger.maybeOf(context)?.showSnackBar(snackbar); + + scaffoldMessenger?.showSnackBar(snackbar); } } finally { updating.value = false; diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index 354d9fe6..cc8b10cf 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -18,7 +18,7 @@ import 'package:spotube/components/shared/expandable_search/expandable_search.da import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; -import 'package:spotube/components/shared/track_table/track_tile.dart'; +import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; @@ -199,7 +199,8 @@ class UserLocalTracks extends HookConsumerWidget { ), const Spacer(), ExpandableSearchButton( - isFiltering: isFiltering, + isFiltering: isFiltering.value, + onPressed: (value) => isFiltering.value = value, searchFocus: searchFocus, ), const SizedBox(width: 10), @@ -222,7 +223,8 @@ class UserLocalTracks extends HookConsumerWidget { ExpandableSearchField( searchController: searchController, searchFocus: searchFocus, - isFiltering: isFiltering, + isFiltering: isFiltering.value, + onChangeFiltering: (value) => isFiltering.value = value, ), trackSnapshot.when( data: (tracks) { @@ -284,7 +286,7 @@ class UserLocalTracks extends HookConsumerWidget { ); }, loading: () => - const Expanded(child: ShimmerTrackTile(noSliver: true)), + const Expanded(child: ShimmerTrackTileGroup(noSliver: true)), error: (error, stackTrace) => Text(error.toString() + stackTrace.toString()), ) diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index a6f69925..8142740c 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -11,7 +11,7 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/shared/track_table/track_tile.dart'; +import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index 0438e559..f429a0ab 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -4,6 +4,7 @@ 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/extensions/infinite_query.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'; @@ -23,7 +24,7 @@ class PlaylistCard extends HookConsumerWidget { final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final playing = useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; - final queryBowl = QueryClient.of(context); + final queryClient = QueryClient.of(context); final tracks = useState?>(null); bool isPlaylistPlaying = useMemoized( () => playlistQueue.containsCollection(playlist.id!), @@ -34,6 +35,31 @@ class PlaylistCard extends HookConsumerWidget { final spotify = ref.watch(spotifyProvider); final me = useQueries.user.me(ref); + Future> fetchAllTracks() async { + if (playlist.id == 'user-liked-tracks') { + return await queryClient.fetchQuery( + "user-liked-tracks", + () => useQueries.playlist.likedTracks(spotify), + ) ?? + []; + } + + final query = queryClient.createInfiniteQuery, dynamic, int>( + "playlist-tracks/${playlist.id}", + (page) => useQueries.playlist.tracksOf(page, spotify, playlist.id!), + initialPage: 0, + nextPage: useQueries.playlist.tracksOfQueryNextPage, + ); + + return await query.fetchAllTracks( + getAllTracks: () async { + final res = + await spotify.playlists.getTracksByPlaylistId(playlist.id!).all(); + return res.toList(); + }, + ); + } + return PlaybuttonCard( margin: const EdgeInsets.symmetric(horizontal: 10), title: playlist.name!, @@ -62,18 +88,7 @@ class PlaylistCard extends HookConsumerWidget { return audioPlayer.resume(); } - List fetchedTracks = playlist.id == 'user-liked-tracks' - ? await queryBowl.fetchQuery( - "user-liked-tracks", - () => useQueries.playlist.likedTracks(spotify, ref), - ) ?? - [] - : await queryBowl.fetchQuery( - "playlist-tracks/${playlist.id}", - () => useQueries.playlist - .tracksOf(playlist.id!, spotify, ref), - ) ?? - []; + List fetchedTracks = await fetchAllTracks(); if (fetchedTracks.isEmpty) return; @@ -90,11 +105,8 @@ class PlaylistCard extends HookConsumerWidget { updating.value = true; try { if (isPlaylistPlaying) return; - List fetchedTracks = await queryBowl.fetchQuery( - "playlist-tracks/${playlist.id}", - () => useQueries.playlist.tracksOf(playlist.id!, spotify, ref), - ) ?? - []; + + final fetchedTracks = await fetchAllTracks(); if (fetchedTracks.isEmpty) return; diff --git a/lib/components/shared/expandable_search/expandable_search.dart b/lib/components/shared/expandable_search/expandable_search.dart index 684e373e..75ac6841 100644 --- a/lib/components/shared/expandable_search/expandable_search.dart +++ b/lib/components/shared/expandable_search/expandable_search.dart @@ -4,13 +4,15 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/extensions/context.dart'; class ExpandableSearchField extends StatelessWidget { - final ValueNotifier isFiltering; + final bool isFiltering; + final ValueChanged onChangeFiltering; final TextEditingController searchController; final FocusNode searchFocus; const ExpandableSearchField({ Key? key, required this.isFiltering, + required this.onChangeFiltering, required this.searchController, required this.searchFocus, }) : super(key: key); @@ -19,17 +21,17 @@ class ExpandableSearchField extends StatelessWidget { Widget build(BuildContext context) { return AnimatedOpacity( duration: const Duration(milliseconds: 200), - opacity: isFiltering.value ? 1 : 0, + opacity: isFiltering ? 1 : 0, child: AnimatedSize( duration: const Duration(milliseconds: 200), child: SizedBox( - height: isFiltering.value ? 50 : 0, + height: isFiltering ? 50 : 0, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: CallbackShortcuts( bindings: { LogicalKeySet(LogicalKeyboardKey.escape): () { - isFiltering.value = false; + onChangeFiltering(false); searchController.clear(); searchFocus.unfocus(); } @@ -52,7 +54,7 @@ class ExpandableSearchField extends StatelessWidget { } class ExpandableSearchButton extends StatelessWidget { - final ValueNotifier isFiltering; + final bool isFiltering; final FocusNode searchFocus; final Widget icon; final ValueChanged? onPressed; @@ -73,18 +75,17 @@ class ExpandableSearchButton extends StatelessWidget { icon: icon, style: IconButton.styleFrom( backgroundColor: - isFiltering.value ? theme.colorScheme.secondaryContainer : null, - foregroundColor: isFiltering.value ? theme.colorScheme.secondary : null, + isFiltering ? theme.colorScheme.secondaryContainer : null, + foregroundColor: isFiltering ? theme.colorScheme.secondary : null, minimumSize: const Size(25, 25), ), onPressed: () { - isFiltering.value = !isFiltering.value; - if (isFiltering.value) { + if (isFiltering) { searchFocus.requestFocus(); } else { searchFocus.unfocus(); } - onPressed?.call(isFiltering.value); + onPressed?.call(!isFiltering); }, ); } diff --git a/lib/components/shared/shimmers/shimmer_artist_profile.dart b/lib/components/shared/shimmers/shimmer_artist_profile.dart index d0b0288f..75e50cd0 100644 --- a/lib/components/shared/shimmers/shimmer_artist_profile.dart +++ b/lib/components/shared/shimmers/shimmer_artist_profile.dart @@ -50,7 +50,7 @@ class ShimmerArtistProfile extends HookWidget { ), ), const SizedBox(width: 10), - const Flexible(child: ShimmerTrackTile(noSliver: true)), + const Flexible(child: ShimmerTrackTileGroup(noSliver: true)), ], ); } diff --git a/lib/components/shared/shimmers/shimmer_track_tile.dart b/lib/components/shared/shimmers/shimmer_track_tile.dart index 070b2f09..dcb634ed 100644 --- a/lib/components/shared/shimmers/shimmer_track_tile.dart +++ b/lib/components/shared/shimmers/shimmer_track_tile.dart @@ -70,8 +70,7 @@ class ShimmerTrackTilePainter extends CustomPainter { } class ShimmerTrackTile extends StatelessWidget { - final bool noSliver; - const ShimmerTrackTile({super.key, this.noSliver = false}); + const ShimmerTrackTile({super.key}); @override Widget build(BuildContext context) { @@ -82,39 +81,42 @@ class ShimmerTrackTile extends StatelessWidget { shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300], ); + return Padding( + padding: const EdgeInsets.only(bottom: 8.0, left: 8, right: 8), + child: CustomPaint( + size: const Size(double.infinity, 60), + painter: ShimmerTrackTilePainter( + background: shimmerTheme.shimmerBackgroundColor ?? + theme.scaffoldBackgroundColor, + foreground: shimmerTheme.shimmerColor ?? theme.cardColor, + ), + ), + ); + } +} + +class ShimmerTrackTileGroup extends StatelessWidget { + final bool noSliver; + final int count; + const ShimmerTrackTileGroup({ + super.key, + this.noSliver = false, + this.count = 5, + }); + + @override + Widget build(BuildContext context) { if (noSliver) { return ListView.builder( itemCount: 5, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0, left: 8, right: 8), - child: CustomPaint( - size: const Size(double.infinity, 60), - painter: ShimmerTrackTilePainter( - background: shimmerTheme.shimmerBackgroundColor ?? - theme.scaffoldBackgroundColor, - foreground: shimmerTheme.shimmerColor ?? theme.cardColor, - ), - ), - ); - }, + itemBuilder: (context, index) => const ShimmerTrackTile(), ); } return SliverList( delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) => Padding( - padding: const EdgeInsets.only(bottom: 8.0, left: 8, right: 8), - child: CustomPaint( - size: const Size(double.infinity, 60), - painter: ShimmerTrackTilePainter( - background: shimmerTheme.shimmerBackgroundColor ?? - theme.scaffoldBackgroundColor, - foreground: shimmerTheme.shimmerColor ?? theme.cardColor, - ), - ), - ), - childCount: 5, + (BuildContext context, int index) => const ShimmerTrackTile(), + childCount: count, ), ); } diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart deleted file mode 100644 index 6436f7cd..00000000 --- a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart +++ /dev/null @@ -1,229 +0,0 @@ -import 'dart:ui'; - -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:fl_query/fl_query.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:palette_generator/palette_generator.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/collections/assets.gen.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/album/album_card.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; -import 'package:spotube/components/shared/playbutton_card.dart'; -import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/extensions/context.dart'; - -enum PlayButtonState { - playing, - notPlaying, - loading, -} - -class TrackCollectionHeading extends HookConsumerWidget { - final String title; - final String? description; - final String titleImage; - final List buttons; - final AlbumSimple? album; - final Query, T> tracksSnapshot; - final PlayButtonState playingState; - final void Function([Track? currentTrack]) onPlay; - final void Function([Track? currentTrack]) onShuffledPlay; - final PaletteColor? color; - - const TrackCollectionHeading({ - Key? key, - required this.title, - required this.titleImage, - required this.buttons, - required this.tracksSnapshot, - required this.playingState, - required this.onPlay, - required this.onShuffledPlay, - required this.color, - this.description, - this.album, - }) : super(key: key); - - @override - Widget build(BuildContext context, ref) { - final theme = Theme.of(context); - - final cleanDescription = useDescription(description); - - return LayoutBuilder( - builder: (context, constrains) { - return DecoratedBox( - decoration: BoxDecoration( - image: DecorationImage( - image: UniversalImage.imageProvider(titleImage), - fit: BoxFit.cover, - ), - ), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.black45, - theme.colorScheme.surface, - ], - begin: const FractionalOffset(0, 0), - end: const FractionalOffset(0, 1), - tileMode: TileMode.clamp, - ), - ), - child: Material( - type: MaterialType.transparency, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - child: SafeArea( - child: Flex( - direction: constrains.mdAndDown - ? Axis.vertical - : Axis.horizontal, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - path: titleImage, - placeholder: Assets.albumPlaceholder.path, - ), - ), - ), - const SizedBox(width: 10, height: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: constrains.mdAndDown ? 400 : 300, - ), - child: AutoSizeText( - title, - style: theme.textTheme.titleLarge!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w600, - ), - maxLines: 2, - minFontSize: 16, - overflow: TextOverflow.ellipsis, - ), - ), - if (album != null) - Text( - "${album?.albumType?.formatted} • ${context.l10n.released} • ${DateTime.tryParse( - album?.releaseDate ?? "", - )?.year}", - style: theme.textTheme.titleMedium!.copyWith( - color: Colors.white, - fontWeight: FontWeight.normal, - ), - ), - if (cleanDescription != null) - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: constrains.mdAndDown ? 400 : 300, - ), - child: AutoSizeText( - cleanDescription, - style: const TextStyle(color: Colors.white), - maxLines: 2, - overflow: TextOverflow.fade, - minFontSize: 14, - ), - ), - const SizedBox(height: 10), - IconTheme( - data: theme.iconTheme.copyWith( - color: Colors.white, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: buttons, - ), - ), - const SizedBox(height: 10), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: constrains.mdAndDown ? 400 : 300, - ), - child: Row( - mainAxisSize: constrains.smAndUp - ? MainAxisSize.min - : MainAxisSize.min, - children: [ - Expanded( - child: FilledButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - ), - label: Text(context.l10n.shuffle), - icon: const Icon(SpotubeIcons.shuffle), - onPressed: tracksSnapshot.data == null || - playingState == - PlayButtonState.playing - ? null - : onShuffledPlay, - ), - ), - const SizedBox(width: 10), - Expanded( - child: FilledButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: color?.color, - foregroundColor: color?.bodyTextColor, - ), - onPressed: tracksSnapshot.data != null || - playingState == - PlayButtonState.loading - ? onPlay - : null, - icon: switch (playingState) { - PlayButtonState.playing => - const Icon(SpotubeIcons.pause), - PlayButtonState.notPlaying => - const Icon(SpotubeIcons.play), - PlayButtonState.loading => - const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: .7, - ), - ), - }, - label: Text( - playingState == PlayButtonState.playing - ? context.l10n.stop - : context.l10n.play, - ), - ), - ), - ], - ), - ), - ], - ) - ], - ), - ), - ), - ), - ), - ), - ); - }, - ); - } -} diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart deleted file mode 100644 index f211a521..00000000 --- a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart +++ /dev/null @@ -1,274 +0,0 @@ -import 'dart:async'; - -import 'package:fl_query/fl_query.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/playlist/playlist_create_dialog.dart'; -import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; -import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_heading.dart'; -import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; -import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; -import 'package:spotube/hooks/utils/use_palette_color.dart'; -import 'package:spotube/models/logger.dart'; -import 'package:flutter/material.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/utils/platform.dart'; -import 'package:spotube/utils/type_conversion_utils.dart'; - -class TrackCollectionView extends HookConsumerWidget { - final logger = getLogger(TrackCollectionView); - final String id; - final String title; - final String? description; - final Query, T> tracksSnapshot; - final String titleImage; - final PlayButtonState playingState; - final Future Function([Track? currentTrack]) onPlay; - final void Function([Track? currentTrack]) onShuffledPlay; - final void Function() onAddToQueue; - final void Function() onShare; - final Widget? heartBtn; - final AlbumSimple? album; - - final bool showShare; - final bool isOwned; - final bool bottomSpace; - - final String routePath; - TrackCollectionView({ - required this.title, - required this.id, - required this.tracksSnapshot, - required this.titleImage, - required this.playingState, - required this.onPlay, - required this.onShuffledPlay, - required this.onAddToQueue, - required this.onShare, - required this.routePath, - this.heartBtn, - this.album, - this.description, - this.showShare = true, - this.isOwned = false, - this.bottomSpace = false, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context, ref) { - final theme = Theme.of(context); - final auth = ref.watch(AuthenticationNotifier.provider); - - final color = usePaletteGenerator(titleImage).dominantColor; - - final List buttons = [ - if (showShare) - IconButton( - icon: const Icon(SpotubeIcons.share), - onPressed: onShare, - ), - if (isOwned) - IconButton( - icon: const Icon(SpotubeIcons.edit), - onPressed: () { - showDialog( - context: context, - builder: (context) { - return PlaylistCreateDialog(playlistId: id); - }, - ); - }, - ), - if (heartBtn != null && auth != null) heartBtn!, - IconButton( - onPressed: playingState == PlayButtonState.playing - ? null - : tracksSnapshot.data != null - ? onAddToQueue - : null, - icon: const Icon( - SpotubeIcons.queueAdd, - ), - ), - ]; - - final controller = useScrollController(); - - final collapsed = useState(false); - - useCustomStatusBarColor( - Colors.transparent, - GoRouterState.of(context).matchedLocation == routePath, - ); - - useEffect(() { - listener() { - if (controller.position.pixels >= 390 && !collapsed.value) { - collapsed.value = true; - } else if (controller.position.pixels < 390 && collapsed.value) { - collapsed.value = false; - } - } - - controller.addListener(listener); - - return () => controller.removeListener(listener); - }, [collapsed.value]); - - return Scaffold( - appBar: kIsDesktop - ? const PageWindowTitleBar( - backgroundColor: Colors.transparent, - foregroundColor: Colors.white, - leadingWidth: 400, - leading: Align( - alignment: Alignment.centerLeft, - child: BackButton(color: Colors.white), - ), - ) - : null, - extendBodyBehindAppBar: kIsDesktop, - body: RefreshIndicator( - onRefresh: () async { - await tracksSnapshot.refresh(); - }, - child: InterScrollbar( - controller: controller, - child: CustomScrollView( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverAppBar( - actions: [ - AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: collapsed.value ? 1 : 0, - child: Row( - mainAxisSize: MainAxisSize.min, - children: buttons, - ), - ), - AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: collapsed.value ? 1 : 0, - child: IconButton( - tooltip: context.l10n.shuffle, - icon: const Icon(SpotubeIcons.shuffle), - onPressed: playingState == PlayButtonState.playing - ? null - : onShuffledPlay, - ), - ), - AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: collapsed.value ? 1 : 0, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - shape: const CircleBorder(), - backgroundColor: theme.colorScheme.inversePrimary, - ), - onPressed: tracksSnapshot.data != null ? onPlay : null, - child: switch (playingState) { - PlayButtonState.playing => - const Icon(SpotubeIcons.pause), - PlayButtonState.notPlaying => - const Icon(SpotubeIcons.play), - PlayButtonState.loading => const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: .7, - ), - ), - }, - ), - ), - ], - floating: false, - pinned: true, - expandedHeight: 400, - automaticallyImplyLeading: kIsMobile, - leading: - kIsMobile ? const BackButton(color: Colors.white) : null, - iconTheme: IconThemeData(color: color?.titleTextColor), - primary: true, - backgroundColor: color?.color.withOpacity(.8), - title: collapsed.value - ? Text( - title, - style: theme.textTheme.titleMedium!.copyWith( - color: color?.titleTextColor, - fontWeight: FontWeight.w600, - ), - ) - : null, - centerTitle: true, - flexibleSpace: FlexibleSpaceBar( - background: TrackCollectionHeading( - color: color, - title: title, - description: description, - titleImage: titleImage, - playingState: playingState, - onPlay: onPlay, - onShuffledPlay: onShuffledPlay, - tracksSnapshot: tracksSnapshot, - buttons: buttons, - album: album, - ), - ), - ), - HookBuilder( - builder: (context) { - if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) { - return const ShimmerTrackTile(); - } else if (tracksSnapshot.hasError) { - return SliverToBoxAdapter( - child: Text( - context.l10n.error(tracksSnapshot.error ?? ""), - ), - ); - } - - return TracksTableView( - (tracksSnapshot.data ?? []).map( - (track) { - if (track is Track) { - return track; - } else { - return TypeConversionUtils.simpleTrack_X_Track( - track, - album!, - ); - } - }, - ).toList(), - onTrackPlayButtonPressed: onPlay, - playlistId: id, - userPlaylist: isOwned, - onFiltering: () { - // scroll the flexible space - // to allow more space for search results - controller.animateTo( - 330, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - ); - }, - ); - }, - ) - ], - ), - ), - )); - } -} diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart deleted file mode 100644 index 003662f5..00000000 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ /dev/null @@ -1,368 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fuzzywuzzy/fuzzywuzzy.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import 'package:spotify/spotify.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; -import 'package:spotube/components/shared/dialogs/confirm_download_dialog.dart'; -import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart'; -import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; -import 'package:spotube/components/shared/fallbacks/not_found.dart'; -import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; -import 'package:spotube/components/shared/track_table/track_tile.dart'; -import 'package:spotube/components/library/user_local_tracks.dart'; -import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/extensions/context.dart'; - -import 'package:spotube/provider/download_manager_provider.dart'; -import 'package:spotube/provider/blacklist_provider.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; -import 'package:spotube/utils/service_utils.dart'; - -final trackCollectionSortState = - StateProvider.family((ref, _) => SortBy.none); - -class TracksTableView extends HookConsumerWidget { - final Future Function(Track currentTrack)? onTrackPlayButtonPressed; - final List tracks; - final bool userPlaylist; - final String? playlistId; - final bool isSliver; - - final Widget? heading; - - final VoidCallback? onFiltering; - - const TracksTableView( - this.tracks, { - Key? key, - this.onTrackPlayButtonPressed, - this.onFiltering, - this.userPlaylist = false, - this.playlistId, - this.heading, - this.isSliver = true, - }) : super(key: key); - - @override - Widget build(context, ref) { - final mediaQuery = MediaQuery.of(context); - - ref.watch(ProxyPlaylistNotifier.provider); - final playback = ref.watch(ProxyPlaylistNotifier.notifier); - ref.watch(downloadManagerProvider); - final downloader = ref.watch(downloadManagerProvider.notifier); - final apiType = - ref.watch(userPreferencesProvider.select((s) => s.audioSource)); - const tableHeadStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 16); - - final selected = useState>([]); - final showCheck = useState(false); - final sortBy = ref.watch(trackCollectionSortState(playlistId ?? '')); - - final isFiltering = useState(false); - - final searchController = useTextEditingController(); - final searchFocus = useFocusNode(); - - final controller = useScrollController(); - - // this will trigger update on each change in searchController - useValueListenable(searchController); - - final filteredTracks = useMemoized(() { - if (searchController.text.isEmpty) { - return tracks; - } - return tracks - .map((e) => (weightedRatio(e.name!, searchController.text), e)) - .sorted((a, b) => b.$1.compareTo(a.$1)) - .where((e) => e.$1 > 50) - .map((e) => e.$2) - .toList(); - }, [tracks, searchController.text]); - - final sortedTracks = useMemoized( - () { - return ServiceUtils.sortTracks(filteredTracks, sortBy); - }, - [filteredTracks, sortBy], - ); - - final selectedTracks = useMemoized( - () => sortedTracks.where( - (track) => selected.value.contains(track.id), - ), - [sortedTracks], - ); - - final children = tracks.isEmpty - ? [const NotFound(vertical: true)] - : [ - if (heading != null) heading!, - LayoutBuilder(builder: (context, constrains) { - return Row( - children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (child, animation) { - return FadeTransition( - opacity: animation, - child: ScaleTransition( - scale: animation, - child: child, - ), - ); - }, - child: showCheck.value - ? Checkbox( - value: selected.value.length == sortedTracks.length, - onChanged: (checked) { - if (!showCheck.value) showCheck.value = true; - if (checked == true) { - selected.value = - sortedTracks.map((s) => s.id!).toList(); - } else { - selected.value = []; - showCheck.value = false; - } - }, - ) - : constrains.mdAndUp - ? const SizedBox(width: 32) - : const SizedBox(width: 16), - ), - Expanded( - flex: 7, - child: Row( - children: [ - Text( - context.l10n.title, - style: tableHeadStyle, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - // used alignment of this table-head - if (constrains.mdAndUp) - Expanded( - flex: 3, - child: Row( - children: [ - Text( - context.l10n.album, - overflow: TextOverflow.ellipsis, - style: tableHeadStyle, - ), - ], - ), - ), - SortTracksDropdown( - value: sortBy, - onChanged: (value) { - ref - .read(trackCollectionSortState(playlistId ?? '') - .notifier) - .state = value; - }, - ), - ExpandableSearchButton( - isFiltering: isFiltering, - searchFocus: searchFocus, - onPressed: (value) { - if (isFiltering.value) { - onFiltering?.call(); - } - }, - ), - AdaptivePopSheetList( - tooltip: context.l10n.more_actions, - headings: [ - Text( - context.l10n.more_actions, - style: tableHeadStyle, - ), - ], - onSelected: (action) async { - switch (action) { - case "download": - { - final confirmed = apiType == AudioSource.piped || - await showDialog( - context: context, - builder: (context) { - return const ConfirmDownloadDialog(); - }, - ); - if (confirmed != true) return; - await downloader - .batchAddToQueue(selectedTracks.toList()); - if (context.mounted) { - selected.value = []; - showCheck.value = false; - } - break; - } - case "add-to-playlist": - { - if (context.mounted) { - await showDialog( - context: context, - builder: (context) { - return PlaylistAddTrackDialog( - tracks: selectedTracks.toList(), - ); - }, - ); - } - break; - } - case "play-next": - { - playback.addTracksAtFirst(selectedTracks); - if (playlistId != null) { - playback.addCollection(playlistId!); - } - selected.value = []; - showCheck.value = false; - break; - } - case "add-to-queue": - { - playback.addTracks(selectedTracks); - if (playlistId != null) { - playback.addCollection(playlistId!); - } - selected.value = []; - showCheck.value = false; - break; - } - default: - } - }, - icon: const Icon(SpotubeIcons.moreVertical), - children: [ - PopSheetEntry( - value: "download", - leading: const Icon(SpotubeIcons.download), - enabled: selectedTracks.isNotEmpty, - title: Text( - context.l10n.download_count(selectedTracks.length), - ), - ), - if (!userPlaylist) - PopSheetEntry( - value: "add-to-playlist", - leading: const Icon(SpotubeIcons.playlistAdd), - enabled: selectedTracks.isNotEmpty, - title: Text( - context.l10n - .add_count_to_playlist(selectedTracks.length), - ), - ), - PopSheetEntry( - enabled: selectedTracks.isNotEmpty, - value: "add-to-queue", - leading: const Icon(SpotubeIcons.queueAdd), - title: Text( - context.l10n - .add_count_to_queue(selectedTracks.length), - ), - ), - PopSheetEntry( - enabled: selectedTracks.isNotEmpty, - value: "play-next", - leading: const Icon(SpotubeIcons.lightning), - title: Text( - context.l10n.play_count_next(selectedTracks.length), - ), - ), - ], - ), - const SizedBox(width: 10), - ], - ); - }), - ExpandableSearchField( - isFiltering: isFiltering, - searchController: searchController, - searchFocus: searchFocus, - ), - ...sortedTracks.mapIndexed((i, track) { - return TrackTile( - index: i, - track: track, - selected: selected.value.contains(track.id), - userPlaylist: userPlaylist, - playlistId: playlistId, - onTap: () async { - if (showCheck.value) { - final alreadyChecked = selected.value.contains(track.id); - if (alreadyChecked) { - selected.value = - selected.value.where((id) => id != track.id).toList(); - } else { - selected.value = [...selected.value, track.id!]; - } - } else { - final isBlackListed = ref.read( - BlackListNotifier.provider.select( - (blacklist) => blacklist.contains( - BlacklistedElement.track(track.id!, track.name!), - ), - ), - ); - if (isBlackListed) return; - await onTrackPlayButtonPressed?.call(track); - } - }, - onLongPress: () { - if (showCheck.value) return; - showCheck.value = true; - selected.value = [...selected.value, track.id!]; - }, - onChanged: !showCheck.value - ? null - : (value) { - if (value == null) return; - if (value) { - selected.value = [...selected.value, track.id!]; - } else { - selected.value = selected.value - .where((id) => id != track.id) - .toList(); - } - }, - ); - }), - // extra space for mobile devices where keyboard takes half of the screen - if (isFiltering.value) - SizedBox( - height: mediaQuery.size.height * .75, //75% of the screen - ), - ]; - - if (isSliver) { - return SliverSafeArea( - top: false, - sliver: SliverList( - delegate: SliverChildListDelegate(children), - ), - ); - } - return SafeArea( - child: ListView( - controller: controller, - children: children, - ), - ); - } -} diff --git a/lib/components/shared/track_table/track_options.dart b/lib/components/shared/track_tile/track_options.dart similarity index 100% rename from lib/components/shared/track_table/track_options.dart rename to lib/components/shared/track_tile/track_options.dart diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_tile/track_tile.dart similarity index 99% rename from lib/components/shared/track_table/track_tile.dart rename to lib/components/shared/track_tile/track_tile.dart index 4980f96b..6d4e236a 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_tile/track_tile.dart @@ -9,7 +9,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/hover_builder.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/links/link_text.dart'; -import 'package:spotube/components/shared/track_table/track_options.dart'; +import 'package:spotube/components/shared/track_tile/track_options.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/models/local_track.dart'; diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/shared/tracks_view/sections/body/track_view_body.dart new file mode 100644 index 00000000..486e4405 --- /dev/null +++ b/lib/components/shared/tracks_view/sections/body/track_view_body.dart @@ -0,0 +1,124 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; +import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; +import 'package:spotube/components/shared/track_tile/track_tile.dart'; +import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body_headers.dart'; +import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_provider.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/utils/service_utils.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; + +class TrackViewBodySection extends HookConsumerWidget { + const TrackViewBodySection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final playlist = ref.watch(ProxyPlaylistNotifier.provider); + final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); + final props = InheritedTrackView.of(context); + + final trackViewState = ref.watch(trackViewProvider(props.tracks)); + + final searchController = useTextEditingController(); + final searchFocus = useFocusNode(); + + useValueListenable(searchController); + final searchQuery = searchController.text; + + final isFiltering = useState(false); + + final tracks = useMemoized(() { + List filteredTracks; + if (searchQuery.isEmpty) { + filteredTracks = props.tracks; + } else { + filteredTracks = props.tracks + .map((e) => (weightedRatio(e.name!, searchQuery), e)) + .sorted((a, b) => b.$1.compareTo(a.$1)) + .where((e) => e.$1 > 50) + .map((e) => e.$2) + .toList(); + } + return ServiceUtils.sortTracks(filteredTracks, trackViewState.sortBy); + }, [trackViewState.sortBy, searchQuery, props.tracks]); + + final isUserPlaylist = useIsUserPlaylist(ref, props.collectionId); + + final isActive = playlist.collections.contains(props.collectionId); + + return SliverMainAxisGroup( + slivers: [ + SliverToBoxAdapter( + child: TrackViewBodyHeaders( + isFiltering: isFiltering, + searchFocus: searchFocus, + ), + ), + const SliverGap(8), + SliverToBoxAdapter( + child: ExpandableSearchField( + isFiltering: isFiltering.value, + onChangeFiltering: (value) { + isFiltering.value = value; + }, + searchController: searchController, + searchFocus: searchFocus, + ), + ), + SliverSafeArea( + top: false, + sliver: SliverInfiniteList( + itemCount: tracks.length, + onFetchData: props.pagination.onFetchMore, + isLoading: props.pagination.isLoading, + hasReachedMax: !props.pagination.hasNextPage, + loadingBuilder: (context) => const ShimmerTrackTile(), + itemBuilder: (context, index) { + final track = tracks[index]; + return TrackTile( + track: track, + index: index, + selected: trackViewState.selectedTrackIds.contains(track.id!), + playlistId: props.collectionId, + userPlaylist: isUserPlaylist, + onChanged: !trackViewState.isSelecting + ? null + : (value) { + trackViewState.toggleTrackSelection(track.id!); + }, + onLongPress: () { + trackViewState.selectTrack(track.id!); + }, + onTap: () async { + if (trackViewState.isSelecting) { + trackViewState.toggleTrackSelection(track.id!); + return; + } + + if (isActive || playlist.tracks.contains(track)) { + await playlistNotifier.jumpToTrack(track); + } else { + await playlistNotifier.load( + props.tracks, + initialIndex: index, + autoPlay: true, + ); + playlistNotifier.addCollection(props.collectionId); + } + }, + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart b/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart new file mode 100644 index 00000000..57d8b296 --- /dev/null +++ b/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; +import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; +import 'package:spotube/components/shared/tracks_view/sections/body/track_view_options.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_provider.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; + +class TrackViewBodyHeaders extends HookConsumerWidget { + final ValueNotifier isFiltering; + final FocusNode searchFocus; + + const TrackViewBodyHeaders({ + Key? key, + required this.isFiltering, + required this.searchFocus, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final ThemeData(:textTheme) = Theme.of(context); + final props = InheritedTrackView.of(context); + final trackViewState = ref.watch(trackViewProvider(props.tracks)); + return LayoutBuilder( + builder: (context, constrains) { + return Row( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: ScaleTransition( + scale: animation, + child: child, + ), + ); + }, + child: trackViewState.isSelecting + ? Checkbox( + value: trackViewState.hasSelectedAll, + onChanged: (checked) { + if (checked == true) { + trackViewState.selectAll(); + } else { + trackViewState.deselectAll(); + } + }, + ) + : constrains.mdAndUp + ? const SizedBox(width: 32) + : const SizedBox(width: 16), + ), + Expanded( + flex: 7, + child: Row( + children: [ + Text( + context.l10n.title, + style: textTheme.bodyLarge, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + // used alignment of this table-head + if (constrains.mdAndUp) + Expanded( + flex: 3, + child: Row( + children: [ + Text( + context.l10n.album, + overflow: TextOverflow.ellipsis, + style: textTheme.bodyLarge, + ), + ], + ), + ), + SortTracksDropdown( + value: trackViewState.sortBy, + onChanged: (value) { + trackViewState.sort(value); + }, + ), + ExpandableSearchButton( + isFiltering: isFiltering.value, + searchFocus: searchFocus, + onPressed: (value) { + isFiltering.value = value; + if (value) { + searchFocus.requestFocus(); + } else { + searchFocus.unfocus(); + } + }, + ), + const TrackViewBodyOptions(), + ], + ); + }, + ); + } +} diff --git a/lib/components/shared/tracks_view/sections/body/track_view_options.dart b/lib/components/shared/tracks_view/sections/body/track_view_options.dart new file mode 100644 index 00000000..4fcd0a59 --- /dev/null +++ b/lib/components/shared/tracks_view/sections/body/track_view_options.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; +import 'package:spotube/components/shared/dialogs/confirm_download_dialog.dart'; +import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_provider.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/download_manager_provider.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:spotube/services/queries/queries.dart'; + +class TrackViewBodyOptions extends HookConsumerWidget { + const TrackViewBodyOptions({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final props = InheritedTrackView.of(context); + final ThemeData(:textTheme) = Theme.of(context); + + ref.watch(downloadManagerProvider); + final downloader = ref.watch(downloadManagerProvider.notifier); + final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); + final audioSource = + ref.watch(userPreferencesProvider.select((s) => s.audioSource)); + + final trackViewState = ref.watch(trackViewProvider(props.tracks)); + final selectedTracks = trackViewState.selectedTracks; + + final userPlaylists = useQueries.playlist.ofMineAll(ref); + + final isUserPlaylist = + userPlaylists.data?.any((e) => e.id == props.collectionId) ?? false; + + return AdaptivePopSheetList( + tooltip: context.l10n.more_actions, + headings: [ + Text( + context.l10n.more_actions, + style: textTheme.bodyLarge, + ), + ], + onSelected: (action) async { + switch (action) { + case "download": + { + final confirmed = audioSource == AudioSource.piped || + await showDialog( + context: context, + builder: (context) { + return const ConfirmDownloadDialog(); + }, + ); + if (confirmed != true) return; + await downloader.batchAddToQueue(selectedTracks); + trackViewState.deselectAll(); + break; + } + case "add-to-playlist": + { + if (context.mounted) { + await showDialog( + context: context, + builder: (context) { + return PlaylistAddTrackDialog( + tracks: selectedTracks.toList(), + ); + }, + ); + } + break; + } + case "play-next": + { + playlistNotifier.addTracksAtFirst(selectedTracks); + playlistNotifier.addCollection(props.collectionId); + trackViewState.deselectAll(); + break; + } + case "add-to-queue": + { + playlistNotifier.addTracks(selectedTracks); + playlistNotifier.addCollection(props.collectionId); + trackViewState.deselectAll(); + break; + } + default: + } + }, + icon: const Icon(SpotubeIcons.moreVertical), + children: [ + PopSheetEntry( + value: "download", + leading: const Icon(SpotubeIcons.download), + enabled: selectedTracks.isNotEmpty, + title: Text( + context.l10n.download_count(selectedTracks.length), + ), + ), + if (!isUserPlaylist) + PopSheetEntry( + value: "add-to-playlist", + leading: const Icon(SpotubeIcons.playlistAdd), + enabled: selectedTracks.isNotEmpty, + title: Text( + context.l10n.add_count_to_playlist(selectedTracks.length), + ), + ), + PopSheetEntry( + enabled: selectedTracks.isNotEmpty, + value: "add-to-queue", + leading: const Icon(SpotubeIcons.queueAdd), + title: Text( + context.l10n.add_count_to_queue(selectedTracks.length), + ), + ), + PopSheetEntry( + enabled: selectedTracks.isNotEmpty, + value: "play-next", + leading: const Icon(SpotubeIcons.lightning), + title: Text( + context.l10n.play_count_next(selectedTracks.length), + ), + ), + ], + ); + } +} diff --git a/lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart b/lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart new file mode 100644 index 00000000..ca3c6706 --- /dev/null +++ b/lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart @@ -0,0 +1,18 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/services/queries/queries.dart'; + +bool useIsUserPlaylist(WidgetRef ref, String playlistId) { + final userPlaylistsQuery = useQueries.playlist.ofMineAll(ref); + final me = useQueries.user.me(ref); + + return useMemoized( + () => + userPlaylistsQuery.data?.any((e) => + e.id == playlistId && + me.data != null && + e.owner?.id == me.data?.id) ?? + false, + [userPlaylistsQuery.data, playlistId, me.data], + ); +} diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/shared/tracks_view/sections/header/flexible_header.dart new file mode 100644 index 00000000..e63161fa --- /dev/null +++ b/lib/components/shared/tracks_view/sections/header/flexible_header.dart @@ -0,0 +1,142 @@ +import 'dart:ui'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/assets.gen.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/playbutton_card.dart'; +import 'package:spotube/components/shared/tracks_view/sections/header/header_actions.dart'; +import 'package:spotube/components/shared/tracks_view/sections/header/header_buttons.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:gap/gap.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/hooks/utils/use_palette_color.dart'; + +class TrackViewFlexHeader extends HookConsumerWidget { + const TrackViewFlexHeader({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final props = InheritedTrackView.of(context); + final ThemeData(:colorScheme, :textTheme, :iconTheme) = Theme.of(context); + final defaultTextStyle = DefaultTextStyle.of(context); + final mediaQuery = MediaQuery.of(context); + + final description = useDescription(props.description); + + final palette = usePaletteColor(props.image, ref); + + return IconTheme( + data: iconTheme.copyWith(color: palette.bodyTextColor), + child: SliverLayoutBuilder( + builder: (context, constrains) { + final isExpanded = constrains.scrollOffset < 350; + + final headingStyle = (mediaQuery.mdAndDown + ? textTheme.headlineSmall + : textTheme.headlineMedium) + ?.copyWith( + color: palette.bodyTextColor, + ); + return SliverAppBar( + iconTheme: iconTheme.copyWith( + color: palette.bodyTextColor, + size: 16, + ), + actions: isExpanded + ? [] + : [ + const TrackViewHeaderActions(), + TrackViewHeaderButtons(compact: true, color: palette), + ], + floating: false, + pinned: true, + expandedHeight: 400, + automaticallyImplyLeading: false, + backgroundColor: palette.color, + title: isExpanded ? null : Text(props.title, style: headingStyle), + flexibleSpace: FlexibleSpaceBar( + background: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + image: DecorationImage( + image: CachedNetworkImageProvider(props.image), + fit: BoxFit.cover, + ), + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black45, + colorScheme.surface, + ], + begin: const FractionalOffset(0, 0), + end: const FractionalOffset(0, 1), + tileMode: TileMode.clamp, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flex( + direction: mediaQuery.mdAndDown + ? Axis.vertical + : Axis.horizontal, + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: props.image, + width: 200, + height: 200, + placeholder: Assets.albumPlaceholder.path, + ), + ), + const Gap(20), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: mediaQuery.mdAndDown + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + Text(props.title, style: headingStyle), + const SizedBox(height: 10), + if (description != null) + Text( + description, + style: defaultTextStyle.style.copyWith( + color: palette.bodyTextColor, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Gap(10), + const TrackViewHeaderActions(), + const Gap(10), + TrackViewHeaderButtons(color: palette), + ], + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/components/shared/tracks_view/sections/header/header_actions.dart b/lib/components/shared/tracks_view/sections/header/header_actions.dart new file mode 100644 index 00000000..954f266d --- /dev/null +++ b/lib/components/shared/tracks_view/sections/header/header_actions.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/heart_button.dart'; +import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; + +class TrackViewHeaderActions extends HookConsumerWidget { + const TrackViewHeaderActions({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final props = InheritedTrackView.of(context); + + final playlist = ref.watch(ProxyPlaylistNotifier.provider); + final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); + + final isActive = playlist.collections.contains(props.collectionId); + + final isUserPlaylist = useIsUserPlaylist(ref, props.collectionId); + + final scaffoldMessenger = ScaffoldMessenger.of(context); + + final auth = ref.watch(AuthenticationNotifier.provider); + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + tooltip: context.l10n.share, + icon: const Icon(SpotubeIcons.share), + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: props.shareUrl), + ); + + scaffoldMessenger.showSnackBar( + SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + "Copied ${props.shareUrl} to clipboard", + textAlign: TextAlign.center, + ), + ), + ); + }, + ), + IconButton( + icon: const Icon(SpotubeIcons.queueAdd), + tooltip: context.l10n.add_to_queue, + onPressed: isActive || props.tracks.isEmpty + ? null + : () async { + final tracks = await props.pagination.onFetchAll(); + await playlistNotifier.addTracks(tracks); + playlistNotifier.addCollection(props.collectionId); + }, + ), + if (props.onHeart != null && auth != null) + HeartButton( + isLiked: props.isLiked, + icon: isUserPlaylist ? SpotubeIcons.trash : null, + tooltip: props.isLiked + ? context.l10n.remove_from_favorites + : context.l10n.save_as_favorite, + onPressed: () { + props.onHeart?.call(); + if (isUserPlaylist) { + context.pop(); + } + }, + ), + ], + ); + } +} diff --git a/lib/components/shared/tracks_view/sections/header/header_buttons.dart b/lib/components/shared/tracks_view/sections/header/header_buttons.dart new file mode 100644 index 00000000..c006ec08 --- /dev/null +++ b/lib/components/shared/tracks_view/sections/header/header_buttons.dart @@ -0,0 +1,137 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:palette_generator/palette_generator.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; + +class TrackViewHeaderButtons extends HookConsumerWidget { + final PaletteColor color; + final bool compact; + const TrackViewHeaderButtons({ + Key? key, + required this.color, + this.compact = false, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final props = InheritedTrackView.of(context); + final playlist = ref.watch(ProxyPlaylistNotifier.provider); + final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); + + final isActive = playlist.collections.contains(props.collectionId); + + final isLoading = useState(false); + + const progressIndicator = Center( + child: SizedBox.square( + dimension: 20, + child: CircularProgressIndicator(strokeWidth: .8), + ), + ); + + void onShuffle() async { + try { + isLoading.value = true; + + final allTracks = await props.pagination.onFetchAll(); + + await playlistNotifier.load( + allTracks, + autoPlay: true, + initialIndex: Random().nextInt(allTracks.length), + ); + await audioPlayer.setShuffle(true); + playlistNotifier.addCollection(props.collectionId); + } finally { + isLoading.value = false; + } + } + + void onPlay() async { + try { + isLoading.value = true; + + final allTracks = await props.pagination.onFetchAll(); + + await playlistNotifier.load(allTracks, autoPlay: true); + playlistNotifier.addCollection(props.collectionId); + } finally { + isLoading.value = false; + } + } + + if (compact) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isActive && !isLoading.value) + IconButton( + icon: const Icon(SpotubeIcons.shuffle), + onPressed: props.tracks.isEmpty ? null : onShuffle, + ), + const Gap(10), + IconButton.filledTonal( + icon: isActive + ? const Icon(SpotubeIcons.pause) + : isLoading.value + ? progressIndicator + : const Icon(SpotubeIcons.play), + onPressed: isActive || props.tracks.isEmpty || isLoading.value + ? null + : onPlay, + ), + const Gap(10), + ], + ); + } + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: isActive || isLoading.value ? 0 : 1, + child: AnimatedSize( + duration: const Duration(milliseconds: 300), + child: SizedBox.square( + dimension: isActive || isLoading.value ? 0 : null, + child: FilledButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + label: Text(context.l10n.shuffle), + icon: const Icon(SpotubeIcons.shuffle), + onPressed: props.tracks.isEmpty ? null : onShuffle, + ), + ), + ), + ), + const Gap(10), + FilledButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: color.color, + foregroundColor: color.bodyTextColor, + ), + onPressed: isActive || props.tracks.isEmpty || isLoading.value + ? null + : onPlay, + icon: isActive + ? const Icon(SpotubeIcons.pause) + : isLoading.value + ? progressIndicator + : const Icon(SpotubeIcons.play), + label: Text(context.l10n.play), + ), + ], + ); + } +} diff --git a/lib/components/shared/tracks_view/track_view.dart b/lib/components/shared/tracks_view/track_view.dart new file mode 100644 index 00000000..a65bcff1 --- /dev/null +++ b/lib/components/shared/tracks_view/track_view.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:sliver_tools/sliver_tools.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; +import 'package:spotube/components/shared/tracks_view/sections/header/flexible_header.dart'; +import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; + +class TrackView extends HookConsumerWidget { + const TrackView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final props = InheritedTrackView.of(context); + + return Scaffold( + appBar: DesktopTools.platform.isDesktop + ? const PageWindowTitleBar( + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + leadingWidth: 400, + leading: Align( + alignment: Alignment.centerLeft, + child: BackButton(color: Colors.white), + ), + ) + : null, + extendBodyBehindAppBar: true, + body: CustomScrollView( + slivers: [ + const TrackViewFlexHeader(), + SliverAnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: props.tracks.isEmpty + ? const ShimmerTrackTileGroup() + : const TrackViewBodySection(), + ), + ], + ), + ); + } +} diff --git a/lib/components/shared/tracks_view/track_view_props.dart b/lib/components/shared/tracks_view/track_view_props.dart new file mode 100644 index 00000000..59c05db2 --- /dev/null +++ b/lib/components/shared/tracks_view/track_view_props.dart @@ -0,0 +1,102 @@ +import 'package:fl_query/fl_query.dart'; +import 'package:flutter/material.dart' hide Page; +import 'package:spotify/spotify.dart'; + +class PaginationProps { + final bool hasNextPage; + final bool isLoading; + final VoidCallback onFetchMore; + final Future> Function() onFetchAll; + + const PaginationProps({ + required this.hasNextPage, + required this.isLoading, + required this.onFetchMore, + required this.onFetchAll, + }); + + factory PaginationProps.fromQuery( + InfiniteQuery, dynamic, int> query, { + required Future> Function() onFetchAll, + }) { + return PaginationProps( + hasNextPage: query.hasNextPage, + isLoading: query.isLoadingNextPage, + onFetchMore: query.fetchNext, + onFetchAll: onFetchAll, + ); + } + + @override + operator ==(Object other) { + return other is PaginationProps && + other.hasNextPage == hasNextPage && + other.isLoading == isLoading && + other.onFetchMore == onFetchMore && + other.onFetchAll == onFetchAll; + } + + @override + int get hashCode => + super.hashCode ^ + hasNextPage.hashCode ^ + isLoading.hashCode ^ + onFetchMore.hashCode ^ + onFetchAll.hashCode; +} + +class InheritedTrackView extends InheritedWidget { + final String collectionId; + final String title; + final String? description; + final String image; + final String routePath; + final List tracks; + final PaginationProps pagination; + final bool isLiked; + final String shareUrl; + + // events + final VoidCallback? onHeart; // if null heart button will hidden + + const InheritedTrackView({ + super.key, + required super.child, + required this.collectionId, + required this.title, + this.description, + required this.image, + required this.tracks, + required this.pagination, + required this.routePath, + required this.shareUrl, + this.isLiked = false, + this.onHeart, + }); + + @override + bool updateShouldNotify(InheritedTrackView oldWidget) { + return oldWidget.title != title || + oldWidget.description != description || + oldWidget.image != image || + oldWidget.tracks != tracks || + oldWidget.pagination != pagination || + oldWidget.isLiked != isLiked || + oldWidget.onHeart != onHeart || + oldWidget.shareUrl != shareUrl || + oldWidget.routePath != routePath || + oldWidget.collectionId != collectionId || + oldWidget.child != child; + } + + static InheritedTrackView of(BuildContext context) { + final widget = + context.dependOnInheritedWidgetOfExactType(); + if (widget == null) { + throw Exception( + 'InheritedTrackView not found. Make sure to wrap [TrackView] with [InheritedTrackView]', + ); + } + return widget; + } +} diff --git a/lib/components/shared/tracks_view/track_view_provider.dart b/lib/components/shared/tracks_view/track_view_provider.dart new file mode 100644 index 00000000..14dc1136 --- /dev/null +++ b/lib/components/shared/tracks_view/track_view_provider.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/library/user_local_tracks.dart'; + +class TrackViewNotifier extends ChangeNotifier { + List tracks; + List selectedTrackIds; + SortBy sortBy; + String? searchQuery; + + TrackViewNotifier( + this.tracks, { + this.selectedTrackIds = const [], + this.sortBy = SortBy.none, + this.searchQuery, + }); + + bool get isSelecting => selectedTrackIds.isNotEmpty; + + bool get hasSelectedAll => + selectedTrackIds.length == tracks.length && tracks.isNotEmpty; + + List get selectedTracks => + tracks.where((e) => selectedTrackIds.contains(e.id)).toList(); + + void selectTrack(String trackId) { + selectedTrackIds = [...selectedTrackIds, trackId]; + notifyListeners(); + } + + void unselectTrack(String trackId) { + selectedTrackIds = selectedTrackIds.where((e) => e != trackId).toList(); + notifyListeners(); + } + + void toggleTrackSelection(String trackId) { + if (selectedTrackIds.contains(trackId)) { + unselectTrack(trackId); + } else { + selectTrack(trackId); + } + } + + void selectAll() { + selectedTrackIds = tracks.map((e) => e.id!).toList(); + notifyListeners(); + } + + void deselectAll() { + selectedTrackIds = []; + notifyListeners(); + } + + void sort(SortBy sortBy) { + this.sortBy = sortBy; + notifyListeners(); + } +} + +final trackViewProvider = ChangeNotifierProvider.autoDispose + .family>((ref, tracks) { + return TrackViewNotifier(tracks); +}); diff --git a/lib/extensions/infinite_query.dart b/lib/extensions/infinite_query.dart new file mode 100644 index 00000000..86a2aaa6 --- /dev/null +++ b/lib/extensions/infinite_query.dart @@ -0,0 +1,30 @@ +import 'package:fl_query/fl_query.dart'; +import 'package:spotify/spotify.dart'; + +extension FetchAllTracks on InfiniteQuery, dynamic, int> { + Future> fetchAllTracks({ + required Future> Function() getAllTracks, + }) async { + if (!hasNextPage) { + return pages.expand((page) => page).toList(); + } + final tracks = await getAllTracks(); + final pagedTracks = tracks.fold( + >{}, + (acc, element) { + final index = acc.length; + final groupIndex = index ~/ 20; + final group = acc[groupIndex] ?? []; + group.add(element); + acc[groupIndex] = group; + return acc; + }, + ); + + for (final group in pagedTracks.entries) { + setPageData(group.key, group.value); + } + + return tracks.toList(); + } +} diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index 5674e721..72f9a9af 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -1,157 +1,79 @@ +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/shared/heart_button.dart'; -import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_heading.dart'; -import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_view.dart'; -import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; -import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/components/shared/tracks_view/track_view.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/extensions/infinite_query.dart'; +import 'package:spotube/provider/spotify_provider.dart'; +import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/services/queries/queries.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class AlbumPage extends HookConsumerWidget { final AlbumSimple album; - const AlbumPage(this.album, {Key? key}) : super(key: key); - - Future playPlaylist( - List tracks, - WidgetRef ref, { - Track? currentTrack, - }) async { - final playlist = ref.read(ProxyPlaylistNotifier.provider); - final playback = ref.read(ProxyPlaylistNotifier.notifier); - final sortBy = ref.read(trackCollectionSortState(album.id!)); - final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy); - currentTrack ??= sortedTracks.first; - final isAlbumPlaying = playlist.containsTracks(tracks); - if (!isAlbumPlaying) { - playback.addCollection(album.id!); // for enabling loading indicator - await playback.load( - sortedTracks, - initialIndex: sortedTracks.indexWhere((s) => s.id == currentTrack?.id), - ); - playback.addCollection(album.id!); - } else if (isAlbumPlaying && - currentTrack.id != null && - currentTrack.id != playlist.activeTrack?.id) { - await playback.jumpToTrack(currentTrack); - } - } + const AlbumPage({ + Key? key, + required this.album, + }) : super(key: key); @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(ProxyPlaylistNotifier.provider); - final playback = ref.watch(ProxyPlaylistNotifier.notifier); + final spotify = ref.watch(spotifyProvider); + final tracksQuery = useQueries.album.tracksOf(ref, album); - final tracksSnapshot = useQueries.album.tracksOf(ref, album.id!); + final tracks = useMemoized(() { + return tracksQuery.pages.expand((element) => element).toList(); + }, [tracksQuery.pages]); - final albumArt = useMemoized( - () => TypeConversionUtils.image_X_UrlString( - album.images, - placeholder: ImagePlaceholder.albumArt, - ), - [album.images]); + final client = useQueryClient(); - final mediaQuery = MediaQuery.of(context); + final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!); + final isLiked = albumIsSaved.data ?? false; - final isAlbumPlaying = useMemoized( - () => playlist.collections.contains(album.id!), - [playlist, album], - ); - - final albumTrackPlaying = useMemoized( - () => - tracksSnapshot.data?.any((s) => s.id! == playlist.activeTrack?.id!) == - true && - playlist.activeTrack is SourcedTrack, - [playlist.activeTrack, tracksSnapshot.data], - ); - - return TrackCollectionView( - id: album.id!, - playingState: isAlbumPlaying && albumTrackPlaying - ? PlayButtonState.playing - : isAlbumPlaying && !albumTrackPlaying - ? PlayButtonState.loading - : PlayButtonState.notPlaying, - title: album.name!, - titleImage: albumArt, - tracksSnapshot: tracksSnapshot, - album: album, - routePath: "/album/${album.id}", - bottomSpace: mediaQuery.mdAndDown, - onPlay: ([track]) async { - if (tracksSnapshot.hasData) { - if (!isAlbumPlaying) { - await playPlaylist( - tracksSnapshot.data! - .map((track) => - TypeConversionUtils.simpleTrack_X_Track(track, album)) - .toList(), - ref, - ); - } else if (isAlbumPlaying && track != null) { - await playPlaylist( - tracksSnapshot.data! - .map((track) => - TypeConversionUtils.simpleTrack_X_Track(track, album)) - .toList(), - currentTrack: track, - ref, - ); - } else { - await playback - .removeTracks(tracksSnapshot.data!.map((track) => track.id!)); - } - } + final toggleAlbumLike = useMutations.album.toggleFavorite( + ref, + album.id!, + refreshQueries: [albumIsSaved.key], + onData: (_, __) async { + await client.refreshInfiniteQueryAllPages("current-user-albums"); }, - onAddToQueue: () { - if (tracksSnapshot.hasData && !isAlbumPlaying) { - playback.addTracks( - tracksSnapshot.data! + ); + + return InheritedTrackView( + collectionId: album.id!, + image: TypeConversionUtils.image_X_UrlString( + album.images, + placeholder: ImagePlaceholder.albumArt, + ), + title: album.name!, + description: + "${context.l10n.released} • ${album.releaseDate} • ${album.artists!.first.name}", + tracks: tracks, + pagination: PaginationProps.fromQuery( + tracksQuery, + onFetchAll: () { + return tracksQuery.fetchAllTracks(getAllTracks: () async { + final res = await spotify.albums.tracks(album.id!).all(); + + return res .map((track) => TypeConversionUtils.simpleTrack_X_Track(track, album)) - .toList(), - ); - playback.addCollection(album.id!); - } - }, - onShare: () { - Clipboard.setData( - ClipboardData(text: "https://open.spotify.com/album/${album.id}"), - ); - }, - heartBtn: AlbumHeartButton(album: album), - onShuffledPlay: ([track]) { - // Shuffle the tracks (create a copy of playlist) - if (tracksSnapshot.hasData) { - final tracks = tracksSnapshot.data! - .map((track) => - TypeConversionUtils.simpleTrack_X_Track(track, album)) - .toList() - ..shuffle(); - if (!isAlbumPlaying) { - playPlaylist( - tracks, - ref, - ); - } else if (isAlbumPlaying && track != null) { - playPlaylist( - tracks, - ref, - currentTrack: track, - ); - } else { - // TODO: Disable ability to stop playback from playlist/album - // playback.stop(); - } - } - }, + .toList(); + }); + }, + ), + routePath: "/album/${album.id}", + shareUrl: album.externalUrls!.spotify!, + isLiked: isLiked, + onHeart: albumIsSaved.hasData + ? () { + toggleAlbumLike.mutate(isLiked); + } + : null, + child: const TrackView(), ); } } diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 299bf9f5..8b57c2a8 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -10,7 +10,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/shimmers/shimmer_artist_profile.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/track_tile/track_tile.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/artist/artist_album_list.dart'; import 'package:spotube/components/artist/artist_card.dart'; diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index b3904e2e..54fb6786 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -70,7 +70,8 @@ class GenrePage extends HookConsumerWidget { child: Column( children: [ ExpandableSearchField( - isFiltering: isFiltering, + isFiltering: isFiltering.value, + onChangeFiltering: (value) => isFiltering.value = value, searchController: searchController, searchFocus: searchFocus, ), @@ -103,10 +104,11 @@ class GenrePage extends HookConsumerWidget { top: 0, right: 10, child: ExpandableSearchButton( - isFiltering: isFiltering, + isFiltering: isFiltering.value, searchFocus: searchFocus, icon: const Icon(SpotubeIcons.search), onPressed: (value) { + isFiltering.value = value; if (isFiltering.value) { scrollController.animateTo( 0, diff --git a/lib/pages/playlist/liked_playlist.dart b/lib/pages/playlist/liked_playlist.dart new file mode 100644 index 00000000..1f252ed4 --- /dev/null +++ b/lib/pages/playlist/liked_playlist.dart @@ -0,0 +1,45 @@ +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/shared/tracks_view/track_view.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +class LikedPlaylistPage extends HookConsumerWidget { + final PlaylistSimple playlist; + const LikedPlaylistPage({ + Key? key, + required this.playlist, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final likedTracks = useQueries.playlist.likedTracksQuery(ref); + final tracks = likedTracks.data ?? []; + + return InheritedTrackView( + collectionId: playlist.id!, + image: TypeConversionUtils.image_X_UrlString( + playlist.images, + placeholder: ImagePlaceholder.collection, + ), + pagination: PaginationProps( + hasNextPage: false, + isLoading: false, + onFetchMore: () {}, + onFetchAll: () async { + return tracks.toList(); + }, + ), + title: playlist.name!, + description: playlist.description, + tracks: tracks, + routePath: '/playlist/${playlist.id}', + isLiked: false, + shareUrl: "", + onHeart: null, + child: const TrackView(), + ); + } +} diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 6a3ec9b9..ab39b225 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -1,178 +1,82 @@ -import 'package:flutter/services.dart'; +import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/heart_button.dart'; -import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_heading.dart'; -import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_view.dart'; -import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; -import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/models/logger.dart'; -import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/components/shared/tracks_view/track_view.dart'; +import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; +import 'package:spotube/extensions/infinite_query.dart'; +import 'package:spotube/provider/spotify_provider.dart'; +import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/services/queries/queries.dart'; -import 'package:spotube/services/sourced_track/sourced_track.dart'; - -import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -class PlaylistView extends HookConsumerWidget { - final logger = getLogger(PlaylistView); - final PlaylistSimple playlistSimple; - PlaylistView(this.playlistSimple, {Key? key}) : super(key: key); +class PlaylistPage extends HookConsumerWidget { + final PlaylistSimple playlist; + const PlaylistPage({ + Key? key, + required this.playlist, + }) : super(key: key); @override Widget build(BuildContext context, ref) { - final proxyPlaylist = ref.watch(ProxyPlaylistNotifier.provider); - final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); + final spotify = ref.watch(spotifyProvider); + final tracksQuery = useQueries.playlist.tracksOfQuery(ref, playlist.id!); - final mediaQuery = MediaQuery.of(context); - - final meSnapshot = useQueries.user.me(ref); - - final playlistQuery = useQueries.playlist.byId(ref, playlistSimple.id!); - final playlist = playlistQuery.data ?? playlistSimple; - - final playlistTrackSnapshot = - useQueries.playlist.tracksOfQuery(ref, playlist.id!); - final likedTracksSnapshot = useQueries.playlist.likedTracksQuery(ref); - final tracksSnapshot = playlist.id! == "user-liked-tracks" - ? likedTracksSnapshot - : playlistTrackSnapshot; - - final isPlaylistPlaying = useMemoized( - () => proxyPlaylist.collections.contains(playlist.id!), - [proxyPlaylist, playlist], + final tracks = useMemoized( + () { + return tracksQuery.pages.expand((page) => page).toList(); + }, + [tracksQuery.pages], ); - final titleImage = useMemoized( - () => TypeConversionUtils.image_X_UrlString( - playlist.images, - placeholder: ImagePlaceholder.collection, - ), - [playlist.images]); + final me = useQueries.user.me(ref); - final playlistTrackPlaying = useMemoized( - () => - tracksSnapshot.data - ?.any((s) => s.id! == proxyPlaylist.activeTrack?.id!) == - true && - proxyPlaylist.activeTrack is SourcedTrack, - [proxyPlaylist.activeTrack, tracksSnapshot.data], + final isLikedQuery = useQueries.playlist.doesUserFollow( + ref, + playlist.id!, + me.data?.id ?? '', ); - final playPlaylist = useCallback(( - List tracks, - WidgetRef ref, { - Track? currentTrack, - }) async { - final playback = ref.read(ProxyPlaylistNotifier.notifier); - final sortBy = ref.read(trackCollectionSortState(playlist.id!)); - final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy); - currentTrack ??= sortedTracks.first; - final isPlaylistPlaying = proxyPlaylist.containsTracks(tracks); - if (!isPlaylistPlaying) { - playback.addCollection(playlist.id!); // for enabling loading indicator - await playback.load( - sortedTracks, - initialIndex: - sortedTracks.indexWhere((s) => s.id == currentTrack?.id), - autoPlay: true, - ); - playback.addCollection(playlist.id!); - } else if (isPlaylistPlaying && - currentTrack.id != null && - currentTrack.id != proxyPlaylist.activeTrack?.id) { - await playback.jumpToTrack(currentTrack); - } - }, [proxyPlaylist, playlist]); + final togglePlaylistLike = useMutations.playlist.toggleFavorite( + ref, + playlist.id!, + refreshQueries: [ + isLikedQuery.key, + ], + ); - final ownPlaylist = - playlist.owner?.id != null && playlist.owner?.id == meSnapshot.data?.id; - - return TrackCollectionView( - id: playlist.id!, - playingState: isPlaylistPlaying && playlistTrackPlaying - ? PlayButtonState.playing - : isPlaylistPlaying && !playlistTrackPlaying - ? PlayButtonState.loading - : PlayButtonState.notPlaying, - title: playlist.name!, - titleImage: titleImage, - tracksSnapshot: tracksSnapshot, - description: playlist.description, - isOwned: ownPlaylist, - onPlay: ([track]) async { - if (tracksSnapshot.hasData) { - if (!isPlaylistPlaying || (isPlaylistPlaying && track != null)) { - await playPlaylist( - tracksSnapshot.data!, - ref, - currentTrack: track, - ); - } else { - await playlistNotifier - .removeTracks(tracksSnapshot.data!.map((e) => e.id!)); - } - } - }, - onAddToQueue: () { - if (tracksSnapshot.hasData && !isPlaylistPlaying) { - playlistNotifier.addTracks(tracksSnapshot.data!); - playlistNotifier.addCollection(playlist.id!); - } - }, - bottomSpace: mediaQuery.mdAndDown, - showShare: playlist.id != "user-liked-tracks", - routePath: "/playlist/${playlist.id}", - onShare: () { - final data = "https://open.spotify.com/playlist/${playlist.id}"; - Clipboard.setData( - ClipboardData(text: data), - ).then((_) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - "Copied $data to clipboard", - textAlign: TextAlign.center, - ), - ), + return InheritedTrackView( + collectionId: playlist.id!, + image: TypeConversionUtils.image_X_UrlString( + playlist.images, + placeholder: ImagePlaceholder.collection, + ), + pagination: PaginationProps.fromQuery( + tracksQuery, + onFetchAll: () async { + return tracksQuery.fetchAllTracks( + getAllTracks: () async { + final res = await spotify.playlists + .getTracksByPlaylistId(playlist.id!) + .all(); + return res.toList(); + }, ); - }); - }, - heartBtn: PlaylistHeartButton( - playlist: playlist, - icon: ownPlaylist ? SpotubeIcons.trash : null, - onData: (data) { - GoRouter.of(context).pop(); }, ), - onShuffledPlay: ([track]) { - final tracks = [...?tracksSnapshot.data]..shuffle(); - - if (tracksSnapshot.hasData) { - if (!isPlaylistPlaying) { - playPlaylist( - tracks, - ref, - currentTrack: track, - ); - } else if (isPlaylistPlaying && track != null) { - playPlaylist( - tracks, - ref, - currentTrack: track, - ); - } else { - // TODO: Remove the ability to stop the playlist - // playlistNotifier.stop(); - } + title: playlist.name!, + description: playlist.description, + tracks: tracks, + routePath: '/playlist/${playlist.id}', + isLiked: isLikedQuery.data ?? false, + shareUrl: playlist.externalUrls?.spotify ?? "", + onHeart: () async { + if (!isLikedQuery.hasData || togglePlaylistLike.isMutating) { + return; } + await togglePlaylistLike.mutate(isLikedQuery.data!); }, + child: const TrackView(), ); } } diff --git a/lib/pages/search/sections/tracks.dart b/lib/pages/search/sections/tracks.dart index 59c6a4e1..e77cd8f2 100644 --- a/lib/pages/search/sections/tracks.dart +++ b/lib/pages/search/sections/tracks.dart @@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/dialogs/prompt_dialog.dart'; -import 'package:spotube/components/shared/track_table/track_tile.dart'; +import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; diff --git a/lib/services/queries/album.dart b/lib/services/queries/album.dart index 546b3d15..0cc10256 100644 --- a/lib/services/queries/album.dart +++ b/lib/services/queries/album.dart @@ -1,10 +1,13 @@ import 'package:catcher_2/catcher_2.dart'; import 'package:fl_query/fl_query.dart'; +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; import 'package:spotube/hooks/spotify/use_spotify_query.dart'; +import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; class AlbumQueries { const AlbumQueries(); @@ -27,19 +30,42 @@ class AlbumQueries { ); } - Query, dynamic> tracksOf( + static final tracksOfJob = InfiniteQueryJob.withVariableKey< + List, + dynamic, + int, + ({ + SpotifyApi spotify, + AlbumSimple album, + })>( + baseQueryKey: "album-tracks", + initialPage: 0, + task: (albumId, page, args) async { + final res = + await args!.spotify.albums.tracks(albumId).getPage(20, page * 20); + return res.items + ?.map((track) => + TypeConversionUtils.simpleTrack_X_Track(track, args.album)) + .toList() ?? + []; + }, + nextPage: (lastPage, lastPageData) { + if (lastPageData.length < 20) { + return null; + } + return lastPage + 1; + }, + ); + + InfiniteQuery, dynamic, int> tracksOf( WidgetRef ref, - String albumId, + AlbumSimple album, ) { - return useSpotifyQuery, dynamic>( - "album-tracks/$albumId", - (spotify) { - return spotify.albums - .getTracks(albumId) - .all() - .then((value) => value.toList()); - }, - ref: ref, + final spotify = ref.watch(spotifyProvider); + + return useInfiniteQueryJob( + job: tracksOfJob(album.id!), + args: (spotify: spotify, album: album), ); } diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index 2c6c38be..836f9d72 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -166,17 +166,14 @@ class PlaylistQueries { ); } - Future> likedTracks( - SpotifyApi spotify, - WidgetRef ref, - ) async { + Future> likedTracks(SpotifyApi spotify) async { final tracks = await spotify.tracks.me.saved.all(); return tracks.map((e) => e.track!).toList(); } Query, dynamic> likedTracksQuery(WidgetRef ref) { - final query = useCallback((spotify) => likedTracks(spotify, ref), []); + final query = useCallback((spotify) => likedTracks(spotify), []); final context = useContext(); return useSpotifyQuery, dynamic>( @@ -201,28 +198,6 @@ class PlaylistQueries { ); } - Future> tracksOf( - String playlistId, - SpotifyApi spotify, - WidgetRef ref, - ) async { - if (playlistId == "user-liked-tracks") return []; - return spotify.playlists.getTracksByPlaylistId(playlistId).all().then( - (value) => value.where((track) => track.id != null).toList(), - ); - } - - Query, dynamic> tracksOfQuery( - WidgetRef ref, - String playlistId, - ) { - return useSpotifyQuery, dynamic>( - "playlist-tracks/$playlistId", - (spotify) => tracksOf(playlistId, spotify, ref), - ref: ref, - ); - } - Query byId(WidgetRef ref, String id) { return useSpotifyQuery( "playlist/$id", @@ -233,6 +208,42 @@ class PlaylistQueries { ); } + Future> tracksOf( + int pageParam, + SpotifyApi spotify, + String playlistId, + ) async { + try { + final playlists = await spotify.playlists + .getTracksByPlaylistId(playlistId) + .getPage(20, pageParam * 20); + return playlists.items?.toList() ?? []; + } catch (e, stack) { + Catcher2.reportCheckedError(e, stack); + rethrow; + } + } + + int? tracksOfQueryNextPage(int lastPage, List lastPageData) { + if (lastPageData.length < 20) { + return null; + } + return lastPage + 1; + } + + InfiniteQuery, dynamic, int> tracksOfQuery( + WidgetRef ref, + String playlistId, + ) { + return useSpotifyInfiniteQuery, dynamic, int>( + "playlist-tracks/$playlistId", + (page, spotify) => tracksOf(page, spotify, playlistId), + initialPage: 0, + nextPage: tracksOfQueryNextPage, + ref: ref, + ); + } + InfiniteQuery, dynamic, int> featured( WidgetRef ref, ) { diff --git a/pubspec.lock b/pubspec.lock index 54f6d934..2a8dbb71 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -969,6 +969,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.6" + gap: + dependency: "direct main" + description: + name: gap + sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d + url: "https://pub.dev" + source: hosted + version: "3.0.1" glob: dependency: transitive description: @@ -1861,6 +1869,14 @@ packages: description: flutter source: sdk version: "0.0.99" + sliver_tools: + dependency: "direct main" + description: + name: sliver_tools + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 + url: "https://pub.dev" + source: hosted + version: "0.2.12" smtc_windows: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b3fd3c3e..4d31085c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -114,6 +114,8 @@ dependencies: url: https://github.com/thielepaul/flutter-draggable-scrollbar.git ref: cfd570035bf393de541d32e9b28808b5d7e602df very_good_infinite_list: ^0.7.1 + gap: ^3.0.1 + sliver_tools: ^0.2.12 dev_dependencies: build_runner: ^2.3.2 From 1b087c6eb37ff21a8b8576f713c212500286d058 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 17 Nov 2023 13:23:31 +0600 Subject: [PATCH 048/131] chore: hide empty description --- .../shared/tracks_view/sections/header/flexible_header.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/shared/tracks_view/sections/header/flexible_header.dart index e63161fa..26c8a526 100644 --- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart +++ b/lib/components/shared/tracks_view/sections/header/flexible_header.dart @@ -110,7 +110,8 @@ class TrackViewFlexHeader extends HookConsumerWidget { children: [ Text(props.title, style: headingStyle), const SizedBox(height: 10), - if (description != null) + if (description != null && + description.isNotEmpty) Text( description, style: defaultTextStyle.style.copyWith( From fc4a39e9f3cba550f9fd06775b6bd40ee10ca09b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 17 Nov 2023 13:44:00 +0600 Subject: [PATCH 049/131] chore: fix safearea of flexible header --- .../sections/header/flexible_header.dart | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/shared/tracks_view/sections/header/flexible_header.dart index 26c8a526..57089975 100644 --- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart +++ b/lib/components/shared/tracks_view/sections/header/flexible_header.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; @@ -52,8 +53,8 @@ class TrackViewFlexHeader extends HookConsumerWidget { ], floating: false, pinned: true, - expandedHeight: 400, - automaticallyImplyLeading: false, + expandedHeight: 450, + automaticallyImplyLeading: DesktopTools.platform.isMobile, backgroundColor: palette.color, title: isExpanded ? null : Text(props.title, style: headingStyle), flexibleSpace: FlexibleSpaceBar( @@ -79,56 +80,58 @@ class TrackViewFlexHeader extends HookConsumerWidget { tileMode: TileMode.clamp, ), ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flex( - direction: mediaQuery.mdAndDown - ? Axis.vertical - : Axis.horizontal, - mainAxisSize: MainAxisSize.min, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - path: props.image, - width: 200, - height: 200, - placeholder: Assets.albumPlaceholder.path, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flex( + direction: mediaQuery.mdAndDown + ? Axis.vertical + : Axis.horizontal, + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: props.image, + width: 200, + height: 200, + placeholder: Assets.albumPlaceholder.path, + ), ), - ), - const Gap(20), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: mediaQuery.mdAndDown - ? CrossAxisAlignment.center - : CrossAxisAlignment.start, - children: [ - Text(props.title, style: headingStyle), - const SizedBox(height: 10), - if (description != null && - description.isNotEmpty) - Text( - description, - style: defaultTextStyle.style.copyWith( - color: palette.bodyTextColor, + const Gap(20), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: mediaQuery.mdAndDown + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + Text(props.title, style: headingStyle), + const SizedBox(height: 10), + if (description != null && + description.isNotEmpty) + Text( + description, + style: defaultTextStyle.style.copyWith( + color: palette.bodyTextColor, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const Gap(10), - const TrackViewHeaderActions(), - const Gap(10), - TrackViewHeaderButtons(color: palette), - ], - ), - ], - ), - ], + const Gap(10), + const TrackViewHeaderActions(), + const Gap(10), + TrackViewHeaderButtons(color: palette), + ], + ), + ], + ), + ], + ), ), ), ), From ed63032a8281fe4420db69c0d67b0e4b888fd265 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 17 Nov 2023 22:41:40 +0600 Subject: [PATCH 050/131] cd: fix distutils not found for macos --- .github/workflows/spotube-release-binary.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index d461e296..ab42dcb9 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -319,6 +319,7 @@ jobs: - name: Package Macos App run: | + python3 -m pip install setuptools npm install -g appdmg mkdir -p build/${{ env.BUILD_VERSION }} appdmg appdmg.json build/Spotube-macos-universal.dmg From 03b5f08102a0837cd41bc52fe200aa15c339236a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 21 Nov 2023 20:03:22 +0600 Subject: [PATCH 051/131] chore: remove supabase dep --- .circleci/config.yml | 2 - .env.example | 3 - lib/collections/env.dart | 6 - .../proxy_playlist/next_fetcher_mixin.dart | 22 ---- .../proxy_playlist_provider.dart | 30 ----- lib/services/supabase.dart | 16 --- pubspec.lock | 106 +++--------------- pubspec.yaml | 1 - windows/flutter/CMakeLists.txt | 7 +- 9 files changed, 23 insertions(+), 170 deletions(-) delete mode 100644 lib/services/supabase.dart diff --git a/.circleci/config.yml b/.circleci/config.yml index a5c71033..a55310ce 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -82,8 +82,6 @@ jobs: name: Generate .env file command: | echo "SPOTIFY_SECRETS=${SPOTIFY_SECRETS}" >> .env - echo "SUPABASE_URL=${SUPABASE_URL}" >> .env - echo "SUPABASE_API_KEY=${SUPABASE_API_KEY}" >> .env - run: name: Replace Version in files diff --git a/.env.example b/.env.example index 67d1be8e..22abd24b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,3 @@ -SUPABASE_URL= -SUPABASE_API_KEY= - # The format: # SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2 SPOTIFY_SECRETS= diff --git a/lib/collections/env.dart b/lib/collections/env.dart index 1b9de3de..3923435b 100644 --- a/lib/collections/env.dart +++ b/lib/collections/env.dart @@ -4,12 +4,6 @@ part 'env.g.dart'; @Envied(obfuscate: true, requireEnvFile: true, path: ".env") abstract class Env { - @EnviedField(varName: 'SUPABASE_URL') - static final String? supabaseUrl = _Env.supabaseUrl; - - @EnviedField(varName: 'SUPABASE_API_KEY') - static final String? supabaseAnonKey = _Env.supabaseAnonKey; - @EnviedField(varName: 'SPOTIFY_SECRETS') static final String rawSpotifySecrets = _Env.rawSpotifySecrets; diff --git a/lib/provider/proxy_playlist/next_fetcher_mixin.dart b/lib/provider/proxy_playlist/next_fetcher_mixin.dart index 61b86d8c..fa7fdfe8 100644 --- a/lib/provider/proxy_playlist/next_fetcher_mixin.dart +++ b/lib/provider/proxy_playlist/next_fetcher_mixin.dart @@ -1,14 +1,11 @@ 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'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/models/matched_track.dart'; import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; -import 'package:spotube/services/supabase.dart'; import 'package:spotube/services/youtube/youtube.dart'; final logger = getLogger("NextFetcherMixin"); @@ -112,23 +109,4 @@ mixin NextFetcher on StateNotifier { .whereNotNull() .toList(); } - - /// This method must be called after any playback operation as - /// it can increase the latency - Future storeTrack(Track track, SpotubeTrack spotubeTrack) async { - try { - if (track is! SpotubeTrack) { - await supabase.insertTrack( - MatchedTrack( - youtubeId: spotubeTrack.ytTrack.id, - spotifyId: spotubeTrack.id!, - searchMode: spotubeTrack.ytTrack.searchMode, - ), - ); - } - } catch (e, stackTrace) { - logger.e(e.toString()); - logger.t(stackTrace); - } - } } diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 685a9942..69537979 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -138,13 +138,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier if (track != null) { state = state.copyWith(tracks: mergeTracks([track], state.tracks)); } - - if (oldTrack != null && track != null) { - await storeTrack( - oldTrack, - track, - ); - } } catch (e, stackTrace) { // Removing tracks that were not found to avoid queue interruption // TODO: Add a flag to enable/disable skip not found tracks @@ -332,10 +325,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier collections: {}, ); await notificationService.addTrack(addableTrack); - await storeTrack( - tracks.elementAt(initialIndex), - addableTrack, - ); } await audioPlayer.openPlaylist( @@ -365,13 +354,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier if (oldTrack != null || track != null) { await notificationService.addTrack(track ?? oldTrack!); } - - if (oldTrack != null && track != null) { - await storeTrack( - oldTrack, - track, - ); - } } Future jumpToTrack(Track track) async { @@ -474,12 +456,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier if (oldTrack != null || track != null) { await notificationService.addTrack(track ?? oldTrack!); } - if (oldTrack != null && track != null) { - await storeTrack( - oldTrack, - track, - ); - } } Future previous() async { @@ -505,12 +481,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier if (oldTrack != null || track != null) { await notificationService.addTrack(track ?? oldTrack!); } - if (oldTrack != null && track != null) { - await storeTrack( - oldTrack, - track, - ); - } } Future stop() async { diff --git a/lib/services/supabase.dart b/lib/services/supabase.dart deleted file mode 100644 index d42d8eeb..00000000 --- a/lib/services/supabase.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:spotube/collections/env.dart'; -import 'package:spotube/models/matched_track.dart'; -import 'package:supabase/supabase.dart'; - -class SupabaseService { - static final api = SupabaseClient( - Env.supabaseUrl ?? "", - Env.supabaseAnonKey ?? "", - ); - - Future insertTrack(MatchedTrack track) async { - await api.from("tracks").insert(track.toJson()); - } -} - -final supabase = SupabaseService(); diff --git a/pubspec.lock b/pubspec.lock index 3dbc3cbf..093a133b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -325,10 +325,10 @@ packages: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" color: dependency: transitive description: @@ -931,14 +931,6 @@ packages: description: flutter source: sdk version: "0.0.0" - functions_client: - dependency: transitive - description: - name: functions_client - sha256: "3b157b4d3ae9e38614fd80fab76d1ef1e0e39ff3412a45de2651f27cecb9d2d2" - url: "https://pub.dev" - source: hosted - version: "1.3.2" fuzzywuzzy: dependency: "direct main" description: @@ -971,14 +963,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" - gotrue: - dependency: transitive - description: - name: gotrue - sha256: af61c5c6a2374d9032b7e4b388de0bb0442f4bedc56372d5382c1ef61c85f1f3 - url: "https://pub.dev" - source: hosted - version: "1.12.1" graphs: dependency: transitive description: @@ -1192,14 +1176,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.2" - jwt_decode: - dependency: transitive - description: - name: jwt_decode - sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb - url: "https://pub.dev" - source: hosted - version: "0.3.1" lints: dependency: transitive description: @@ -1324,10 +1300,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" metadata_god: dependency: "direct main" description: @@ -1540,10 +1516,10 @@ packages: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" plugin_platform_interface: dependency: transitive description: @@ -1576,14 +1552,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.8+2" - postgrest: - dependency: transitive - description: - name: postgrest - sha256: d6cc0f60c7dc761f84d1c6d11d9e02b3ad90399bd84639a28c1c024adbaa9bde - url: "https://pub.dev" - source: hosted - version: "1.5.0" process: dependency: transitive description: @@ -1648,22 +1616,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" - realtime_client: - dependency: transitive - description: - name: realtime_client - sha256: b4b7bb293417dafc73943ed639209b2dcb796db8495e56bba29a4e26fadef5cd - url: "https://pub.dev" - source: hosted - version: "1.2.1" - retry: - dependency: transitive - description: - name: retry - sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" - url: "https://pub.dev" - source: hosted - version: "3.1.2" riverpod: dependency: transitive description: @@ -1890,10 +1842,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -1902,22 +1854,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2+1" - storage_client: - dependency: transitive - description: - name: storage_client - sha256: "4bf2fc76f09c3698f0ba3f1a44d567995796f6aef76501f194631d0c03752ab7" - url: "https://pub.dev" - source: hosted - version: "1.5.2" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1942,14 +1886,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" - supabase: - dependency: "direct main" - description: - name: supabase - sha256: "4bfa8f673b39c036ed82829a2ddc462dcacfc36fe168b680664ab954c7d91ccd" - url: "https://pub.dev" - source: hosted - version: "1.11.3" sync_http: dependency: transitive description: @@ -2002,10 +1938,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" time: dependency: transitive description: @@ -2178,10 +2114,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "11.7.1" + version: "11.10.0" watcher: dependency: transitive description: @@ -2194,10 +2130,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -2271,14 +2207,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - yet_another_json_isolate: - dependency: transitive - description: - name: yet_another_json_isolate - sha256: "86fad76026c4241a32831d6c7febd8f9bded5019e2cd36c5b148499808d8307d" - url: "https://pub.dev" - source: hosted - version: "1.1.1" youtube_explode_dart: dependency: "direct main" description: @@ -2288,5 +2216,5 @@ packages: source: hosted version: "2.0.2" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 04f2d8b8..f8e36ada 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -98,7 +98,6 @@ dependencies: smtc_windows: ^0.1.1 spotify: ^0.12.0 stroke_text: ^0.0.2 - supabase: ^1.9.9 system_theme: ^2.1.0 titlebar_buttons: ^1.0.0 url_launcher: ^6.1.7 diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index b2e4bd8d..4f2af69b 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -9,6 +9,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +96,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS From 75c0c4fff4352531827ecaf4b8c4ac6cb44fd149 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 21 Nov 2023 20:11:08 +0600 Subject: [PATCH 052/131] Merge branch 'master' into dev --- .circleci/config.yml | 2 - .env.example | 3 - README.md | 2 +- lib/collections/env.dart | 6 - .../proxy_playlist_provider.dart | 59 ---------- lib/services/supabase.dart | 18 --- pubspec.lock | 106 +++--------------- pubspec.yaml | 1 - windows/flutter/CMakeLists.txt | 7 +- 9 files changed, 24 insertions(+), 180 deletions(-) delete mode 100644 lib/services/supabase.dart diff --git a/.circleci/config.yml b/.circleci/config.yml index a5c71033..a55310ce 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -82,8 +82,6 @@ jobs: name: Generate .env file command: | echo "SPOTIFY_SECRETS=${SPOTIFY_SECRETS}" >> .env - echo "SUPABASE_URL=${SUPABASE_URL}" >> .env - echo "SUPABASE_API_KEY=${SUPABASE_API_KEY}" >> .env - run: name: Replace Version in files diff --git a/.env.example b/.env.example index 67d1be8e..22abd24b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,3 @@ -SUPABASE_URL= -SUPABASE_API_KEY= - # The format: # SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2 SPOTIFY_SECRETS= 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: Debian/Ubuntu Download -

Then run: sudo apt install Spotube-linux-x86_64.deb

+

Then run: sudo apt install ./Spotube-linux-x86_64.deb

diff --git a/lib/collections/env.dart b/lib/collections/env.dart index 8086ada7..28941201 100644 --- a/lib/collections/env.dart +++ b/lib/collections/env.dart @@ -5,12 +5,6 @@ part 'env.g.dart'; @Envied(obfuscate: true, requireEnvFile: true, path: ".env") abstract class Env { - @EnviedField(varName: 'SUPABASE_URL') - static final String? supabaseUrl = _Env.supabaseUrl; - - @EnviedField(varName: 'SUPABASE_API_KEY') - static final String? supabaseAnonKey = _Env.supabaseAnonKey; - @EnviedField(varName: 'SPOTIFY_SECRETS') static final String rawSpotifySecrets = _Env.rawSpotifySecrets; diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index bd3934a7..258f1d9e 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -14,7 +14,6 @@ import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/models/skip_segment.dart'; -import 'package:spotube/models/source_match.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/palette_provider.dart'; @@ -28,7 +27,6 @@ import 'package:spotube/services/audio_services/audio_services.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; -import 'package:spotube/services/supabase.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -134,21 +132,11 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier try { isPreSearching.value = true; - final oldTrack = - mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; - final track = await ensureSourcePlayable(audioPlayer.nextSource!); if (track != null) { state = state.copyWith(tracks: mergeTracks([track], state.tracks)); } - - if (oldTrack != null && track != null) { - await storeTrack( - oldTrack, - track, - ); - } } catch (e, stackTrace) { // Removing tracks that were not found to avoid queue interruption // TODO: Add a flag to enable/disable skip not found tracks @@ -350,10 +338,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier collections: {}, ); await notificationService.addTrack(addableTrack); - await storeTrack( - tracks.elementAt(initialIndex), - addableTrack, - ); } await audioPlayer.openPlaylist( @@ -383,13 +367,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier if (oldTrack != null || track != null) { await notificationService.addTrack(track ?? oldTrack!); } - - if (oldTrack != null && track != null) { - await storeTrack( - oldTrack, - track, - ); - } } Future jumpToTrack(Track track) async { @@ -492,12 +469,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier if (oldTrack != null || track != null) { await notificationService.addTrack(track ?? oldTrack!); } - if (oldTrack != null && track != null) { - await storeTrack( - oldTrack, - track, - ); - } } Future previous() async { @@ -523,12 +494,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier if (oldTrack != null || track != null) { await notificationService.addTrack(track ?? oldTrack!); } - if (oldTrack != null && track != null) { - await storeTrack( - oldTrack, - track, - ); - } } Future stop() async { @@ -625,30 +590,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } } - /// This method must be called after any playback operation as - /// it can increase the latency - Future storeTrack(Track track, SourcedTrack sourcedTrack) async { - try { - if (track is! SourcedTrack) { - await supabase.insertTrack( - SourceMatch( - id: sourcedTrack.id!, - createdAt: DateTime.now(), - sourceId: sourcedTrack.sourceInfo.id, - sourceType: preferences.audioSource == AudioSource.jiosaavn - ? SourceType.jiosaavn - : preferences.searchMode == SearchMode.youtube - ? SourceType.youtube - : SourceType.youtubeMusic, - ), - ); - } - } catch (e, stackTrace) { - logger.e(e.toString()); - logger.t(stackTrace); - } - } - @override set state(state) { super.state = state; diff --git a/lib/services/supabase.dart b/lib/services/supabase.dart deleted file mode 100644 index ef3fa87c..00000000 --- a/lib/services/supabase.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:spotube/collections/env.dart'; -import 'package:spotube/models/source_match.dart'; -import 'package:supabase/supabase.dart'; - -class SupabaseService { - static final api = SupabaseClient( - Env.supabaseUrl ?? "", - Env.supabaseAnonKey ?? "", - ); - - Future insertTrack(SourceMatch track) async { - return null; - // TODO: Fix this - await api.from("tracks").insert(track.toJson()); - } -} - -final supabase = SupabaseService(); diff --git a/pubspec.lock b/pubspec.lock index 2a8dbb71..8826c439 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -325,10 +325,10 @@ packages: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" color: dependency: transitive description: @@ -953,14 +953,6 @@ packages: description: flutter source: sdk version: "0.0.0" - functions_client: - dependency: transitive - description: - name: functions_client - sha256: "3b157b4d3ae9e38614fd80fab76d1ef1e0e39ff3412a45de2651f27cecb9d2d2" - url: "https://pub.dev" - source: hosted - version: "1.3.2" fuzzywuzzy: dependency: "direct main" description: @@ -1001,14 +993,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" - gotrue: - dependency: transitive - description: - name: gotrue - sha256: af61c5c6a2374d9032b7e4b388de0bb0442f4bedc56372d5382c1ef61c85f1f3 - url: "https://pub.dev" - source: hosted - version: "1.12.1" graphs: dependency: transitive description: @@ -1231,14 +1215,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.2" - jwt_decode: - dependency: transitive - description: - name: jwt_decode - sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb - url: "https://pub.dev" - source: hosted - version: "0.3.1" lints: dependency: transitive description: @@ -1363,10 +1339,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" metadata_god: dependency: "direct main" description: @@ -1579,10 +1555,10 @@ packages: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" plugin_platform_interface: dependency: transitive description: @@ -1615,14 +1591,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.8+2" - postgrest: - dependency: transitive - description: - name: postgrest - sha256: d6cc0f60c7dc761f84d1c6d11d9e02b3ad90399bd84639a28c1c024adbaa9bde - url: "https://pub.dev" - source: hosted - version: "1.5.0" process: dependency: transitive description: @@ -1687,22 +1655,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" - realtime_client: - dependency: transitive - description: - name: realtime_client - sha256: b4b7bb293417dafc73943ed639209b2dcb796db8495e56bba29a4e26fadef5cd - url: "https://pub.dev" - source: hosted - version: "1.2.1" - retry: - dependency: transitive - description: - name: retry - sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" - url: "https://pub.dev" - source: hosted - version: "3.1.2" riverpod: dependency: transitive description: @@ -1937,10 +1889,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -1949,22 +1901,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2+1" - storage_client: - dependency: transitive - description: - name: storage_client - sha256: "4bf2fc76f09c3698f0ba3f1a44d567995796f6aef76501f194631d0c03752ab7" - url: "https://pub.dev" - source: hosted - version: "1.5.2" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1989,14 +1933,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" - supabase: - dependency: "direct main" - description: - name: supabase - sha256: "4bfa8f673b39c036ed82829a2ddc462dcacfc36fe168b680664ab954c7d91ccd" - url: "https://pub.dev" - source: hosted - version: "1.11.3" sync_http: dependency: transitive description: @@ -2049,10 +1985,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" time: dependency: transitive description: @@ -2233,10 +2169,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "11.7.1" + version: "11.10.0" watcher: dependency: transitive description: @@ -2249,10 +2185,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -2326,14 +2262,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - yet_another_json_isolate: - dependency: transitive - description: - name: yet_another_json_isolate - sha256: "86fad76026c4241a32831d6c7febd8f9bded5019e2cd36c5b148499808d8307d" - url: "https://pub.dev" - source: hosted - version: "1.1.1" youtube_explode_dart: dependency: "direct main" description: @@ -2343,5 +2271,5 @@ packages: source: hosted version: "2.0.2" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4d31085c..5a88c39a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,7 +89,6 @@ dependencies: smtc_windows: ^0.1.1 spotify: ^0.12.0 stroke_text: ^0.0.2 - supabase: ^1.9.9 system_theme: ^2.1.0 titlebar_buttons: ^1.0.0 url_launcher: ^6.1.7 diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index b2e4bd8d..4f2af69b 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -9,6 +9,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +96,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS From 7818574356d0fb8ff567e1f6a83fd0b6f2ee7c8a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 21 Nov 2023 21:23:08 +0600 Subject: [PATCH 053/131] fix(windows): media control not working #641 --- lib/services/audio_services/windows_audio_service.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/services/audio_services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart index 4481140b..fde88145 100644 --- a/lib/services/audio_services/windows_audio_service.dart +++ b/lib/services/audio_services/windows_audio_service.dart @@ -51,11 +51,9 @@ class WindowsAudioService { break; case AudioPlaybackState.stopped: await smtc.setPlaybackStatus(PlaybackStatus.Stopped); - await smtc.disableSmtc(); break; case AudioPlaybackState.completed: await smtc.setPlaybackStatus(PlaybackStatus.Changing); - await smtc.disableSmtc(); break; default: break; From dcbb1568337969841acc0abe0e7185ee5e4c3590 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 21 Nov 2023 22:56:45 +0600 Subject: [PATCH 054/131] feat(mini_player): show/hide lyrics #851 --- lib/collections/spotube_icons.dart | 1 + lib/pages/lyrics/mini_lyrics.dart | 83 +++++++++++++++++++---------- lib/pages/lyrics/synced_lyrics.dart | 2 +- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 5c769498..78cbb52c 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -40,6 +40,7 @@ abstract class SpotubeIcons { static const trash = FeatherIcons.trash2; static const clock = FeatherIcons.clock; static const lyrics = Icons.lyrics_rounded; + static const lyricsOff = Icons.lyrics_outlined; static const logout = FeatherIcons.logOut; static const login = FeatherIcons.logIn; static const dashboard = FeatherIcons.grid; diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index be32dbc9..2cf73728 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; @@ -32,6 +33,7 @@ class MiniLyricsPage extends HookConsumerWidget { final areaActive = useState(false); final hoverMode = useState(true); + final showLyrics = useState(true); useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -82,17 +84,41 @@ class MiniLyricsPage extends HookConsumerWidget { child: Sidebar.brandLogo(), ), const Spacer(), - SizedBox( - height: 30, - child: TabBar( - tabs: [ - Tab(text: context.l10n.synced), - Tab(text: context.l10n.plain), - ], - isScrollable: true, + if (showLyrics.value) + SizedBox( + height: 30, + child: TabBar( + tabs: [ + Tab(text: context.l10n.synced), + Tab(text: context.l10n.plain), + ], + isScrollable: true, + ), ), - ), const Spacer(), + IconButton( + tooltip: context.l10n.lyrics, + icon: showLyrics.value + ? const Icon(SpotubeIcons.lyrics) + : const Icon(SpotubeIcons.lyricsOff), + style: ButtonStyle( + foregroundColor: showLyrics.value + ? MaterialStateProperty.all( + theme.colorScheme.primary) + : null, + ), + onPressed: () async { + showLyrics.value = !showLyrics.value; + areaActive.value = true; + hoverMode.value = false; + + await DesktopTools.window.setSize( + showLyrics.value + ? const Size(400, 500) + : const Size(400, 150), + ); + }, + ), IconButton( tooltip: context.l10n.show_hide_ui_on_hover, icon: hoverMode.value @@ -105,9 +131,7 @@ class MiniLyricsPage extends HookConsumerWidget { : null, ), onPressed: () async { - if (!hoverMode.value == true) { - areaActive.value = true; - } + areaActive.value = true; hoverMode.value = !hoverMode.value; }, ), @@ -150,22 +174,25 @@ class MiniLyricsPage extends HookConsumerWidget { playlistQueue.activeTrack!.name!, style: theme.textTheme.titleMedium, ), - Expanded( - child: TabBarView( - children: [ - SyncedLyrics( - palette: PaletteColor(theme.colorScheme.background, 0), - isModal: true, - defaultTextZoom: 65, - ), - PlainLyrics( - palette: PaletteColor(theme.colorScheme.background, 0), - isModal: true, - defaultTextZoom: 65, - ), - ], - ), - ), + if (showLyrics.value) + Expanded( + child: TabBarView( + children: [ + SyncedLyrics( + palette: PaletteColor(theme.colorScheme.background, 0), + isModal: true, + defaultTextZoom: 65, + ), + PlainLyrics( + palette: PaletteColor(theme.colorScheme.background, 0), + isModal: true, + defaultTextZoom: 65, + ), + ], + ), + ) + else + const Gap(20), AnimatedCrossFade( crossFadeState: areaActive.value ? CrossFadeState.showFirst diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 8147915f..36a9f316 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -112,7 +112,7 @@ class SyncedLyrics extends HookConsumerWidget { final lyricSlice = lyricValue.lyrics[index]; final isActive = lyricSlice.time.inSeconds == currentTime; - if (isActive && isUnSyncLyric == true) { + if (isActive) { controller.scrollToIndex( index, preferPosition: AutoScrollPosition.middle, From 98aff8f3b94af6d1d3137df8aee36b9e8d5007f9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 21 Nov 2023 23:13:57 +0600 Subject: [PATCH 055/131] chore: fix jiosaavn exact match --- lib/services/sourced_track/sourced_track.dart | 1 - .../sourced_track/sources/jiosaavn.dart | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index d2dd6f59..3ceafbf7 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -113,7 +113,6 @@ abstract class SourcedTrack extends Track { await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref), }; } catch (e) { - print("Got error: $e"); return YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref); } } diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart index b25eca3b..01c041ad 100644 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -111,7 +111,24 @@ class JioSaavnSourcedTrack extends SourcedTrack { final SongSearchResponse(:results) = await jiosaavnClient.search.songs(query, limit: 20); - return results.map(toSiblingType).toList(); + final trackArtistNames = track.artists?.map((ar) => ar.name).toList(); + return results + .where( + (s) { + final sameName = s.name?.replaceAll("&", "&") == track.name; + final artistNames = + "${s.primaryArtists}${s.featuredArtists.isNotEmpty ? ", " : ""}${s.featuredArtists}" + .replaceAll("&", "&"); + final sameArtists = artistNames.split(", ").any( + (artist) => + trackArtistNames?.any((ar) => artist == ar) ?? false, + ); + + return sameName && sameArtists; + }, + ) + .map(toSiblingType) + .toList(); } @override From 92deb0cc6ac8ad781377fb99e1f49dec1519a716 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 22 Nov 2023 08:55:42 +0600 Subject: [PATCH 056/131] chore: fix playbutton not playing anything --- .../tracks_view/sections/header/flexible_header.dart | 3 +++ .../tracks_view/sections/header/header_buttons.dart | 12 ++++++------ lib/extensions/infinite_query.dart | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/shared/tracks_view/sections/header/flexible_header.dart index 57089975..7c469654 100644 --- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart +++ b/lib/components/shared/tracks_view/sections/header/flexible_header.dart @@ -119,6 +119,9 @@ class TrackViewFlexHeader extends HookConsumerWidget { style: defaultTextStyle.style.copyWith( color: palette.bodyTextColor, ), + textAlign: mediaQuery.mdAndDown + ? TextAlign.center + : TextAlign.start, maxLines: 2, overflow: TextOverflow.ellipsis, ), diff --git a/lib/components/shared/tracks_view/sections/header/header_buttons.dart b/lib/components/shared/tracks_view/sections/header/header_buttons.dart index c006ec08..bae47f12 100644 --- a/lib/components/shared/tracks_view/sections/header/header_buttons.dart +++ b/lib/components/shared/tracks_view/sections/header/header_buttons.dart @@ -105,9 +105,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget { dimension: isActive || isLoading.value ? 0 : null, child: FilledButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - ), + backgroundColor: Colors.white, + foregroundColor: Colors.black, + minimumSize: const Size(150, 40)), label: Text(context.l10n.shuffle), icon: const Icon(SpotubeIcons.shuffle), onPressed: props.tracks.isEmpty ? null : onShuffle, @@ -118,9 +118,9 @@ class TrackViewHeaderButtons extends HookConsumerWidget { const Gap(10), FilledButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: color.color, - foregroundColor: color.bodyTextColor, - ), + backgroundColor: color.color, + foregroundColor: color.bodyTextColor, + minimumSize: const Size(150, 40)), onPressed: isActive || props.tracks.isEmpty || isLoading.value ? null : onPlay, diff --git a/lib/extensions/infinite_query.dart b/lib/extensions/infinite_query.dart index 86a2aaa6..90dcf73e 100644 --- a/lib/extensions/infinite_query.dart +++ b/lib/extensions/infinite_query.dart @@ -5,7 +5,7 @@ extension FetchAllTracks on InfiniteQuery, dynamic, int> { Future> fetchAllTracks({ required Future> Function() getAllTracks, }) async { - if (!hasNextPage) { + if (pages.isNotEmpty && !hasNextPage) { return pages.expand((page) => page).toList(); } final tracks = await getAllTracks(); From 88b8785cb86a19900f3a867b044c1ccb2fe400bb Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 22 Nov 2023 09:32:05 +0600 Subject: [PATCH 057/131] feat: discord RPC integration #98 --- lib/collections/env.dart | 2 + lib/main.dart | 5 +++ .../proxy_playlist_provider.dart | 8 ++++ lib/services/discord/discord.dart | 44 +++++++++++++++++++ linux/flutter/generated_plugin_registrant.cc | 4 ++ linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 9 ++++ pubspec.yaml | 3 ++ .../flutter/generated_plugin_registrant.cc | 3 ++ windows/flutter/generated_plugins.cmake | 1 + 10 files changed, 80 insertions(+) create mode 100644 lib/services/discord/discord.dart diff --git a/lib/collections/env.dart b/lib/collections/env.dart index 28941201..50fe1e6a 100644 --- a/lib/collections/env.dart +++ b/lib/collections/env.dart @@ -27,4 +27,6 @@ abstract class Env { static bool get enableUpdateChecker => DesktopTools.platform.isFlatpak || _enableUpdateChecker == "1"; + + static String discordAppId = "1176718791388975124"; } diff --git a/lib/main.dart b/lib/main.dart index 5d7ae2a7..7bb96543 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:catcher_2/catcher_2.dart'; +import 'package:dart_discord_rpc/dart_discord_rpc.dart'; import 'package:device_preview/device_preview.dart'; import 'package:fl_query/fl_query.dart'; import 'package:flutter/foundation.dart'; @@ -63,6 +64,10 @@ Future main(List rawArgs) async { MetadataGod.initialize(); } + if (DesktopTools.platform.isWindows || DesktopTools.platform.isLinux) { + DiscordRPC.initialize(); + } + final hiveCacheDir = kIsWeb ? null : (await getApplicationSupportDirectory()).path; diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 258f1d9e..89bb8a6c 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -24,6 +24,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/audio_services.dart'; +import 'package:spotube/services/discord/discord.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; @@ -92,6 +93,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } notificationService.addTrack(newActiveTrack); + discord.updatePresence(newActiveTrack); state = state.copyWith( active: state.tracks .toList() @@ -321,6 +323,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier collections: {}, ); await notificationService.addTrack(indexTrack); + discord.updatePresence(indexTrack); } else { final addableTrack = await SourcedTrack.fetchFromTrack( ref: ref, @@ -338,6 +341,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier collections: {}, ); await notificationService.addTrack(addableTrack); + discord.updatePresence(addableTrack); } await audioPlayer.openPlaylist( @@ -366,6 +370,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier if (oldTrack != null || track != null) { await notificationService.addTrack(track ?? oldTrack!); + discord.updatePresence(track ?? oldTrack!); } } @@ -468,6 +473,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier if (oldTrack != null || track != null) { await notificationService.addTrack(track ?? oldTrack!); + discord.updatePresence(track ?? oldTrack!); } } @@ -493,12 +499,14 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier await audioPlayer.skipToPrevious(); if (oldTrack != null || track != null) { await notificationService.addTrack(track ?? oldTrack!); + discord.updatePresence(track ?? oldTrack!); } } Future stop() async { state = ProxyPlaylist({}); await audioPlayer.stop(); + discord.clear(); } Future updatePalette() async { diff --git a/lib/services/discord/discord.dart b/lib/services/discord/discord.dart new file mode 100644 index 00000000..2a40e388 --- /dev/null +++ b/lib/services/discord/discord.dart @@ -0,0 +1,44 @@ +import 'package:dart_discord_rpc/dart_discord_rpc.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/env.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +class Discord { + final DiscordRPC? discordRPC; + + Discord() + : discordRPC = + DesktopTools.platform.isWindows || DesktopTools.platform.isLinux + ? DiscordRPC(applicationId: Env.discordAppId) + : null { + discordRPC?.start(autoRegister: true); + } + + void updatePresence(Track track) { + clear(); + final artistNames = + TypeConversionUtils.artists_X_String(track.artists ?? []); + discordRPC?.updatePresence( + DiscordPresence( + details: "Song: ${track.name} by $artistNames", + state: "Vibing in Music", + startTimeStamp: DateTime.now().millisecondsSinceEpoch, + largeImageKey: "spotube-logo-foreground", + largeImageText: "Spotube", + smallImageKey: "spotube-logo-foreground", + smallImageText: "Spotube", + ), + ); + } + + void clear() { + discordRPC?.clearPresence(); + } + + void shutdown() { + discordRPC?.shutDown(); + } +} + +final discord = Discord(); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d455dc02..a07f7f9b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -18,6 +19,9 @@ #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dart_discord_rpc_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DartDiscordRpcPlugin"); + dart_discord_rpc_plugin_register_with_registrar(dart_discord_rpc_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 22319e92..97d541b3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dart_discord_rpc file_selector_linux flutter_secure_storage_linux local_notifier diff --git a/pubspec.lock b/pubspec.lock index 8826c439..19b52a8d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -393,6 +393,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + dart_discord_rpc: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "4d05017838ebeadcdb832e1893fabad1506fddba" + url: "https://github.com/Tommypop2/dart_discord_rpc.git" + source: git + version: "0.0.3" dart_style: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5a88c39a..eb26c94f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -115,6 +115,9 @@ dependencies: very_good_infinite_list: ^0.7.1 gap: ^3.0.1 sliver_tools: ^0.2.12 + dart_discord_rpc: + git: + url: https://github.com/Tommypop2/dart_discord_rpc.git dev_dependencies: build_runner: ^2.3.2 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ff25c4e3..b9c6a481 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -19,6 +20,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DartDiscordRpcPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DartDiscordRpcPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0a5ab976..5cd55ff3 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dart_discord_rpc file_selector_windows flutter_secure_storage_windows local_notifier From 7d05c40dc0d04208b059f2483c1e4de199c8b51d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 22 Nov 2023 10:02:11 +0600 Subject: [PATCH 058/131] fix: use CustomScrollView for personalized page --- lib/extensions/string.dart | 11 +++ lib/pages/home/personalized.dart | 95 +++++++++++-------- .../sourced_track/sources/jiosaavn.dart | 15 +-- pubspec.lock | 8 ++ pubspec.yaml | 1 + 5 files changed, 85 insertions(+), 45 deletions(-) create mode 100644 lib/extensions/string.dart diff --git a/lib/extensions/string.dart b/lib/extensions/string.dart new file mode 100644 index 00000000..b7ab7514 --- /dev/null +++ b/lib/extensions/string.dart @@ -0,0 +1,11 @@ +import 'package:html_unescape/html_unescape.dart'; + +final htmlEscape = HtmlUnescape(); + +extension UnescapeHtml on String { + String unescapeHtml() => htmlEscape.convert(this); +} + +extension NullableUnescapeHtml on String? { + String? unescapeHtml() => this == null ? null : htmlEscape.convert(this!); +} diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index 16cfc3a8..7fbd27ae 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -46,47 +46,64 @@ class PersonalizedPage extends HookConsumerWidget { [newReleases.pages], ); - return ListView( + return CustomScrollView( controller: controller, - children: [ - if (!featuredPlaylistsQuery.hasPageData && - !featuredPlaylistsQuery.isLoadingNextPage) - const ShimmerCategories() - else - HorizontalPlaybuttonCardView( - items: playlists.toList(), - title: Text(context.l10n.featured), - isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage, - hasNextPage: featuredPlaylistsQuery.hasNextPage, - onFetchMore: featuredPlaylistsQuery.fetchNext, + slivers: [ + SliverList.list( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: !featuredPlaylistsQuery.hasPageData && + !featuredPlaylistsQuery.isLoadingNextPage + ? const ShimmerCategories() + : HorizontalPlaybuttonCardView( + items: playlists.toList(), + title: Text(context.l10n.featured), + isLoadingNextPage: + featuredPlaylistsQuery.isLoadingNextPage, + hasNextPage: featuredPlaylistsQuery.hasNextPage, + onFetchMore: featuredPlaylistsQuery.fetchNext, + ), + ), + if (auth != null) + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: newReleases.hasPageData && + userArtistsQuery.hasData && + !newReleases.isLoadingNextPage + ? HorizontalPlaybuttonCardView( + items: albums, + title: Text(context.l10n.new_releases), + isLoadingNextPage: newReleases.isLoadingNextPage, + hasNextPage: newReleases.hasNextPage, + onFetchMore: newReleases.fetchNext, + ) + : const ShimmerCategories(), + ), + ], + ), + SliverSafeArea( + sliver: SliverList.builder( + itemCount: madeForUser.data?["content"]?["items"]?.length ?? 0, + itemBuilder: (context, index) { + final item = madeForUser.data?["content"]?["items"]?[index]; + 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, + isLoadingNextPage: false, + onFetchMore: () {}, + ); + }, ), - if (auth != null && - newReleases.hasPageData && - userArtistsQuery.hasData && - !newReleases.isLoadingNextPage) - HorizontalPlaybuttonCardView( - items: albums, - title: Text(context.l10n.new_releases), - isLoadingNextPage: newReleases.isLoadingNextPage, - 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, - isLoadingNextPage: false, - onFetchMore: () {}, - ); - }) + ), ], ); } diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart index 01c041ad..a447b0c1 100644 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -8,6 +8,7 @@ import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/models/source_map.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:jiosaavn/jiosaavn.dart'; +import 'package:spotube/extensions/string.dart'; final jiosaavnClient = JioSaavnClient(); @@ -74,14 +75,14 @@ class JioSaavnSourcedTrack extends SourcedTrack { result.primaryArtists, if (result.featuredArtists.isNotEmpty) ", ", result.featuredArtists - ].join("").replaceAll("&", "&"), + ].join("").unescapeHtml(), artistUrl: "https://www.jiosaavn.com/artist/${result.primaryArtistsId.split(",").firstOrNull ?? ""}", duration: Duration(seconds: int.parse(result.duration)), id: result.id, pageUrl: result.url, thumbnail: result.image?.last.link ?? "", - title: result.name!, + title: result.name!.unescapeHtml(), album: result.album.name, ), source: SourceMap( @@ -115,10 +116,12 @@ class JioSaavnSourcedTrack extends SourcedTrack { return results .where( (s) { - final sameName = s.name?.replaceAll("&", "&") == track.name; - final artistNames = - "${s.primaryArtists}${s.featuredArtists.isNotEmpty ? ", " : ""}${s.featuredArtists}" - .replaceAll("&", "&"); + final sameName = s.name?.unescapeHtml() == track.name; + final artistNames = [ + s.primaryArtists, + if (s.featuredArtists.isNotEmpty) ", ", + s.featuredArtists + ].join("").unescapeHtml(); final sameArtists = artistNames.split(", ").any( (artist) => trackArtistNames?.any((ar) => artist == ar) ?? false, diff --git a/pubspec.lock b/pubspec.lock index 19b52a8d..6c822604 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1058,6 +1058,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.4" + html_unescape: + dependency: "direct main" + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index eb26c94f..6a33d294 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -118,6 +118,7 @@ dependencies: dart_discord_rpc: git: url: https://github.com/Tommypop2/dart_discord_rpc.git + html_unescape: ^2.0.0 dev_dependencies: build_runner: ^2.3.2 From 4511a0bd006d8004dcac50e056b397bf2763720d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 22 Nov 2023 10:49:00 +0600 Subject: [PATCH 059/131] chore: bring back edit user playlist button --- lib/components/shared/playbutton_card.dart | 18 ------------------ .../sections/header/header_actions.dart | 16 ++++++++++++++++ lib/services/mutations/playlist.dart | 4 +--- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index 4fef72c0..d9c48640 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -63,26 +63,8 @@ class PlaybuttonCard extends HookWidget { others: 15, ); - final textsHeight = useState( - (textsKey.currentContext?.findRenderObject() as RenderBox?) - ?.size - .height ?? - 110.00, - ); - final cleanDescription = useDescription(description); - useEffect(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - textsHeight.value = - (textsKey.currentContext?.findRenderObject() as RenderBox?) - ?.size - .height ?? - textsHeight.value; - }); - return null; - }, [textsKey]); - return Container( constraints: BoxConstraints(maxWidth: size), margin: margin, diff --git a/lib/components/shared/tracks_view/sections/header/header_actions.dart b/lib/components/shared/tracks_view/sections/header/header_actions.dart index 954f266d..b050c199 100644 --- a/lib/components/shared/tracks_view/sections/header/header_actions.dart +++ b/lib/components/shared/tracks_view/sections/header/header_actions.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; @@ -76,6 +77,21 @@ class TrackViewHeaderActions extends HookConsumerWidget { } }, ), + if (isUserPlaylist) + IconButton( + icon: const Icon(SpotubeIcons.edit), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return PlaylistCreateDialog( + playlistId: props.collectionId, + trackIds: props.tracks.map((e) => e.id!).toList(), + ); + }, + ); + }, + ), ], ); } diff --git a/lib/services/mutations/playlist.dart b/lib/services/mutations/playlist.dart index a88e8512..077fff06 100644 --- a/lib/services/mutations/playlist.dart +++ b/lib/services/mutations/playlist.dart @@ -131,10 +131,8 @@ class PlaylistMutations { ); } }, - refreshQueries: [ - "playlist/$playlistId", - ], refreshInfiniteQueries: [ + "playlist/$playlistId", "current-user-playlists", ], ref: ref, From 2a698865567883271471ace9a44123bbfd8fcd2f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 22 Nov 2023 20:23:12 +0600 Subject: [PATCH 060/131] feat(artist): modularize page and add wikipedia section --- lib/collections/spotube_icons.dart | 1 + lib/components/library/user_playlists.dart | 49 +- lib/extensions/color.dart | 28 ++ lib/extensions/constrains.dart | 24 + lib/pages/artist/artist.dart | 475 ++---------------- lib/pages/artist/section/footer.dart | 93 ++++ lib/pages/artist/section/header.dart | 257 ++++++++++ lib/pages/artist/section/related_artists.dart | 49 ++ lib/pages/artist/section/top_tracks.dart | 126 +++++ lib/services/queries/artist.dart | 29 +- lib/services/wikipedia/wikipedia.dart | 3 + pubspec.lock | 11 +- pubspec.yaml | 3 + 13 files changed, 693 insertions(+), 455 deletions(-) create mode 100644 lib/extensions/color.dart create mode 100644 lib/pages/artist/section/footer.dart create mode 100644 lib/pages/artist/section/header.dart create mode 100644 lib/pages/artist/section/related_artists.dart create mode 100644 lib/pages/artist/section/top_tracks.dart create mode 100644 lib/services/wikipedia/wikipedia.dart diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 78cbb52c..d00775c7 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -107,4 +107,5 @@ abstract class SpotubeIcons { static const eye = FeatherIcons.eye; static const noEye = FeatherIcons.eyeOff; static const normalize = FeatherIcons.barChart2; + static const wikipedia = SimpleIcons.wikipedia; } diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 0102a3c7..f7736ca7 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -14,6 +14,7 @@ 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'; import 'package:spotube/components/shared/waypoint.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -120,31 +121,33 @@ class UserPlaylists extends HookConsumerWidget { 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(); + SliverLayoutBuilder(builder: (context, constrains) { + return SliverGrid.builder( + itemCount: playlists.length + 1, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: constrains.smAndDown ? 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/extensions/color.dart b/lib/extensions/color.dart new file mode 100644 index 00000000..68cd8ef7 --- /dev/null +++ b/lib/extensions/color.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +extension ColorAlterer on Color { + Color darken(double amount) { + assert(amount >= 0 && amount <= 1); + final hsl = HSLColor.fromColor(this); + final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + return hslDark.toColor(); + } + + Color lighten(double amount) { + assert(amount >= 0 && amount <= 1); + final hsl = HSLColor.fromColor(this); + final hslLight = + hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); + return hslLight.toColor(); + } + + bool isLight() { + final luminance = computeLuminance(); + return luminance > 0.5; + } + + bool isDark() { + final luminance = computeLuminance(); + return luminance <= 0.5; + } +} diff --git a/lib/extensions/constrains.dart b/lib/extensions/constrains.dart index 85c84ca9..1177f5ac 100644 --- a/lib/extensions/constrains.dart +++ b/lib/extensions/constrains.dart @@ -1,3 +1,4 @@ +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; // ignore: constant_identifier_names @@ -9,6 +10,29 @@ const Breakpoints = ( xl: 1280.0, ); +extension SliverBreakpoints on SliverConstraints { + bool get isXs => crossAxisExtent <= Breakpoints.xs; + bool get isSm => + crossAxisExtent > Breakpoints.xs && crossAxisExtent <= Breakpoints.sm; + bool get isMd => + crossAxisExtent > Breakpoints.sm && crossAxisExtent <= Breakpoints.md; + bool get isLg => + crossAxisExtent > Breakpoints.md && crossAxisExtent <= Breakpoints.lg; + bool get isXl => + crossAxisExtent > Breakpoints.lg && crossAxisExtent <= Breakpoints.xl; + bool get is2Xl => crossAxisExtent > Breakpoints.xl; + + bool get smAndUp => isSm || isMd || isLg || isXl || is2Xl; + bool get mdAndUp => isMd || isLg || isXl || is2Xl; + bool get lgAndUp => isLg || isXl || is2Xl; + bool get xlAndUp => isXl || is2Xl; + + bool get smAndDown => isXs || isSm; + bool get mdAndDown => isXs || isSm || isMd; + bool get lgAndDown => isXs || isSm || isMd || isLg; + bool get xlAndDown => isXs || isSm || isMd || isLg || isXl; +} + extension ContainerBreakpoints on BoxConstraints { bool get isXs => biggest.width <= Breakpoints.xs; bool get isSm => diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 8b57c2a8..693e825b 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -1,32 +1,19 @@ -import 'package:collection/collection.dart'; -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_artist_profile.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/shared/track_tile/track_tile.dart'; -import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/artist/artist_album_list.dart'; -import 'package:spotube/components/artist/artist_card.dart'; -import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/components/shared/shimmers/shimmer_artist_profile.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/models/logger.dart'; -import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/provider/blacklist_provider.dart'; -import 'package:spotube/provider/spotify_provider.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/pages/artist/section/footer.dart'; +import 'package:spotube/pages/artist/section/header.dart'; +import 'package:spotube/pages/artist/section/related_artists.dart'; +import 'package:spotube/pages/artist/section/top_tracks.dart'; import 'package:spotube/services/queries/queries.dart'; -import 'package:spotube/utils/primitive_utils.dart'; -import 'package:spotube/utils/type_conversion_utils.dart'; - class ArtistPage extends HookConsumerWidget { final String artistId; final logger = getLogger(ArtistPage); @@ -34,427 +21,61 @@ class ArtistPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - SpotifyApi spotify = ref.watch(spotifyProvider); - final parentScrollController = useScrollController(); + final scrollController = useScrollController(); final theme = Theme.of(context); - final scaffoldMessenger = ScaffoldMessenger.of(context); - final textTheme = theme.textTheme; - final chipTextVariant = useBreakpointValue( - xs: textTheme.bodySmall, - sm: textTheme.bodySmall, - md: textTheme.bodyMedium, - lg: textTheme.bodyLarge, - xl: textTheme.titleSmall, - xxl: textTheme.titleMedium, - ); - final mediaQuery = MediaQuery.of(context); - - final avatarWidth = useBreakpointValue( - xs: mediaQuery.size.width * 0.50, - sm: mediaQuery.size.width * 0.50, - md: mediaQuery.size.width * 0.40, - lg: mediaQuery.size.width * 0.18, - xl: mediaQuery.size.width * 0.18, - xxl: mediaQuery.size.width * 0.18, - ); - - final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); - final playlist = ref.watch(ProxyPlaylistNotifier.provider); - - final auth = ref.watch(AuthenticationNotifier.provider); - - final queryClient = useQueryClient(); + final artistQuery = useQueries.artist.get(ref, artistId); return SafeArea( bottom: false, child: Scaffold( appBar: const PageWindowTitleBar( leading: BackButton(), + backgroundColor: Colors.transparent, ), - body: HookBuilder( - builder: (context) { - final artistsQuery = useQueries.artist.get(ref, artistId); - - if (artistsQuery.isLoading || !artistsQuery.hasData) { - return const ShimmerArtistProfile(); - } else if (artistsQuery.hasError) { - return Center( - child: Text(artistsQuery.error.toString()), - ); - } - - final data = artistsQuery.data!; - - final blacklist = ref.watch(BlackListNotifier.provider); - final isBlackListed = blacklist.contains( - BlacklistedElement.artist(artistId, data.name!), - ); - - return InterScrollbar( - controller: parentScrollController, - child: SingleChildScrollView( - controller: parentScrollController, + extendBodyBehindAppBar: true, + body: Builder(builder: (context) { + if (artistQuery.isLoading || !artistQuery.hasData) { + const ShimmerArtistProfile(); + } else if (artistQuery.hasError) { + return Center(child: Text(artistQuery.error.toString())); + } + return CustomScrollView( + controller: scrollController, + slivers: [ + SliverToBoxAdapter( child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runAlignment: WrapAlignment.center, - children: [ - const SizedBox(width: 50), - Padding( - padding: const EdgeInsets.all(16), - child: CircleAvatar( - radius: avatarWidth, - backgroundImage: UniversalImage.imageProvider( - TypeConversionUtils.image_X_UrlString( - data.images, - placeholder: ImagePlaceholder.artist, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: Colors.blue, - borderRadius: - BorderRadius.circular(50)), - child: Text( - data.type!.toUpperCase(), - style: chipTextVariant.copyWith( - color: Colors.white, - ), - ), - ), - if (isBlackListed) ...[ - const SizedBox(width: 5), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: Colors.red[400], - borderRadius: - BorderRadius.circular(50)), - child: Text( - context.l10n.blacklisted, - style: chipTextVariant.copyWith( - color: Colors.white, - ), - ), - ), - ] - ], - ), - Text( - data.name!, - style: mediaQuery.smAndDown - ? textTheme.headlineSmall - : textTheme.headlineMedium, - ), - Text( - context.l10n.followers( - PrimitiveUtils.toReadableNumber( - data.followers!.total!.toDouble(), - ), - ), - style: textTheme.bodyMedium?.copyWith( - fontWeight: mediaQuery.mdAndUp - ? FontWeight.bold - : null, - ), - ), - const SizedBox(height: 20), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (auth != null) - HookBuilder( - builder: (context) { - final isFollowingQuery = useQueries - .artist - .doIFollow(ref, artistId); - - final followUnfollow = - useCallback(() async { - try { - isFollowingQuery.data! - ? await spotify.me.unfollow( - FollowingType.artist, - [artistId], - ) - : await spotify.me.follow( - FollowingType.artist, - [artistId], - ); - await isFollowingQuery.refresh(); - - queryClient - .refreshInfiniteQueryAllPages( - "user-following-artists"); - } finally { - queryClient.refreshQuery( - "user-follows-artists-query/$artistId", - ); - } - }, [isFollowingQuery]); - - if (isFollowingQuery.isLoading || - !isFollowingQuery.hasData) { - return const SizedBox( - height: 20, - width: 20, - child: - CircularProgressIndicator(), - ); - } - - if (isFollowingQuery.data!) { - return OutlinedButton( - onPressed: followUnfollow, - child: - Text(context.l10n.following), - ); - } - - return FilledButton( - onPressed: followUnfollow, - child: Text(context.l10n.follow), - ); - }, - ), - const SizedBox(width: 5), - IconButton( - tooltip: - context.l10n.add_artist_to_blacklist, - icon: Icon( - SpotubeIcons.userRemove, - color: !isBlackListed - ? Colors.red[400] - : Colors.white, - ), - style: IconButton.styleFrom( - backgroundColor: isBlackListed - ? Colors.red[400] - : null, - ), - onPressed: () async { - if (isBlackListed) { - ref - .read(BlackListNotifier - .provider.notifier) - .remove( - BlacklistedElement.artist( - data.id!, data.name!), - ); - } else { - ref - .read(BlackListNotifier - .provider.notifier) - .add( - BlacklistedElement.artist( - data.id!, data.name!), - ); - } - }, - ), - IconButton( - icon: const Icon(SpotubeIcons.share), - onPressed: () async { - if (data.externalUrls?.spotify != - null) { - await Clipboard.setData( - ClipboardData( - text: data.externalUrls!.spotify!, - ), - ); - } - - if (!context.mounted) return; - - scaffoldMessenger.showSnackBar( - SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - context.l10n.artist_url_copied, - textAlign: TextAlign.center, - ), - ), - ); - }, - ) - ], - ) - ], - ), - ), - ], - ), - const SizedBox(height: 50), - HookBuilder( - builder: (context) { - final topTracksQuery = useQueries.artist.topTracksOf( - ref, - artistId, - ); - - final isPlaylistPlaying = playlist.containsTracks( - topTracksQuery.data ?? [], - ); - - if (topTracksQuery.isLoading || - !topTracksQuery.hasData) { - return const CircularProgressIndicator(); - } else if (topTracksQuery.hasError) { - return Center( - child: Text(topTracksQuery.error.toString()), - ); - } - - final topTracks = topTracksQuery.data!; - - void playPlaylist(List tracks, - {Track? currentTrack}) async { - currentTrack ??= tracks.first; - if (!isPlaylistPlaying) { - playlistNotifier.load( - tracks, - initialIndex: tracks.indexWhere( - (s) => s.id == currentTrack?.id), - autoPlay: true, - ); - } else if (isPlaylistPlaying && - currentTrack.id != null && - currentTrack.id != playlist.activeTrack?.id) { - await playlistNotifier.jumpToTrack(currentTrack); - } - } - - return Column( - children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.l10n.top_tracks, - style: theme.textTheme.headlineSmall, - ), - ), - if (!isPlaylistPlaying) - IconButton( - icon: const Icon( - SpotubeIcons.queueAdd, - ), - onPressed: () { - playlistNotifier - .addTracks(topTracks.toList()); - scaffoldMessenger.showSnackBar( - SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - context.l10n.added_to_queue( - topTracks.length, - ), - textAlign: TextAlign.center, - ), - ), - ); - }, - ), - const SizedBox(width: 5), - IconButton( - icon: Icon( - isPlaylistPlaying - ? SpotubeIcons.stop - : SpotubeIcons.play, - color: Colors.white, - ), - style: IconButton.styleFrom( - backgroundColor: - theme.colorScheme.primary, - ), - onPressed: () => - playPlaylist(topTracks.toList()), - ) - ], - ), - ...topTracks.mapIndexed((i, track) { - return TrackTile( - index: i, - track: track, - onTap: () async { - playPlaylist( - topTracks.toList(), - currentTrack: track, - ); - }, - ); - }), - ], - ); - }, - ), - const SizedBox(height: 50), - ArtistAlbumList(artistId), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - context.l10n.fans_also_like, - style: theme.textTheme.headlineSmall, - ), - ), - const SizedBox(height: 10), - HookBuilder( - builder: (context) { - final relatedArtists = - useQueries.artist.relatedArtistsOf( - ref, - artistId, - ); - - if (relatedArtists.isLoading || - !relatedArtists.hasData) { - return const CircularProgressIndicator(); - } else if (relatedArtists.hasError) { - return Center( - child: Text(relatedArtists.error.toString()), - ); - } - - return Center( - child: Wrap( - spacing: 20, - runSpacing: 20, - children: relatedArtists.data! - .map((artist) => ArtistCard(artist)) - .toList(), - ), - ); - }, - ), - ], + bottom: false, + child: ArtistPageHeader(artistId: artistId), + ), + ), + const SliverGap(50), + ArtistPageTopTracks(artistId: artistId), + const SliverGap(50), + SliverToBoxAdapter(child: ArtistAlbumList(artistId)), + const SliverGap(20), + SliverPadding( + padding: const EdgeInsets.all(8.0), + sliver: SliverToBoxAdapter( + child: Text( + context.l10n.fans_also_like, + style: theme.textTheme.headlineSmall, ), ), ), - ); - }, - ), + SliverSafeArea( + sliver: ArtistPageRelatedArtists(artistId: artistId), + ), + if (artistQuery.data != null) + SliverSafeArea( + top: false, + sliver: SliverToBoxAdapter( + child: ArtistPageFooter(artist: artistQuery.data!), + ), + ), + ], + ); + }), ), ); } diff --git a/lib/pages/artist/section/footer.dart b/lib/pages/artist/section/footer.dart new file mode 100644 index 00000000..3c0db8a5 --- /dev/null +++ b/lib/pages/artist/section/footer.dart @@ -0,0 +1,93 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class ArtistPageFooter extends HookConsumerWidget { + final Artist artist; + const ArtistPageFooter({Key? key, required this.artist}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final ThemeData(:textTheme) = Theme.of(context); + final mediaQuery = MediaQuery.of(context); + + final artistImage = TypeConversionUtils.image_X_UrlString( + artist.images, + placeholder: ImagePlaceholder.artist, + ); + final summary = useQueries.artist.wikipediaSummary(artist); + if (summary.hasError || !summary.hasData) return const SizedBox.shrink(); + return Container( + margin: const EdgeInsets.all(16), + padding: mediaQuery.smAndDown + ? const EdgeInsets.all(20) + : const EdgeInsets.all(30), + constraints: const BoxConstraints(minHeight: 300), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + image: DecorationImage( + colorFilter: ColorFilter.mode( + Colors.black.withOpacity(0.5), + BlendMode.darken, + ), + image: UniversalImage.imageProvider( + summary.data!.originalimage?.source_ ?? artistImage, + height: summary.data!.originalimage?.height.toDouble(), + width: summary.data!.originalimage?.width.toDouble(), + ), + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + alignment: Alignment.center, + child: RichText( + text: TextSpan( + style: textTheme.bodyLarge?.copyWith( + color: Colors.white, + ), + children: [ + // icon + const WidgetSpan( + child: Icon( + SpotubeIcons.wikipedia, + color: Colors.white, + size: 30, + ), + ), + TextSpan( + text: " Wikipedia", + style: textTheme.titleLarge?.copyWith( + color: Colors.white, + ), + ), + const TextSpan(text: '\n\n'), + TextSpan( + text: summary.data!.extract, + ), + TextSpan( + text: '\n...read more at wikipedia', + style: textTheme.bodyLarge?.copyWith( + color: Colors.lightBlue[300], + decoration: TextDecoration.underline, + decorationColor: Colors.lightBlue[300], + ), + recognizer: TapGestureRecognizer() + ..onTap = () async { + await launchUrlString( + "http://en.wikipedia.org/wiki?curid=${summary.data?.pageid}", + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart new file mode 100644 index 00000000..9fc9d78e --- /dev/null +++ b/lib/pages/artist/section/header.dart @@ -0,0 +1,257 @@ +import 'package:fl_query_hooks/fl_query_hooks.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; +import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/blacklist_provider.dart'; +import 'package:spotube/provider/spotify_provider.dart'; +import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/utils/primitive_utils.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +class ArtistPageHeader extends HookConsumerWidget { + final String artistId; + const ArtistPageHeader({Key? key, required this.artistId}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final queryClient = useQueryClient(); + final artistQuery = useQueries.artist.get(ref, artistId); + final artist = artistQuery.data; + + final scaffoldMessenger = ScaffoldMessenger.of(context); + final mediaQuery = MediaQuery.of(context); + final theme = Theme.of(context); + final ThemeData(:textTheme) = theme; + + final chipTextVariant = useBreakpointValue( + xs: textTheme.bodySmall, + sm: textTheme.bodySmall, + md: textTheme.bodyMedium, + lg: textTheme.bodyLarge, + xl: textTheme.titleSmall, + xxl: textTheme.titleMedium, + ); + + if (artist == null) { + return const SizedBox.shrink(); + } + + final spotify = ref.read(spotifyProvider); + final auth = ref.watch(AuthenticationNotifier.provider); + final blacklist = ref.watch(BlackListNotifier.provider); + final isBlackListed = blacklist.contains( + BlacklistedElement.artist(artistId, artist.name!), + ); + + final image = TypeConversionUtils.image_X_UrlString( + artist.images, + placeholder: ImagePlaceholder.artist, + ); + + return LayoutBuilder( + builder: (context, constrains) { + return Center( + child: Flex( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: constrains.smAndDown + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, + direction: constrains.smAndDown ? Axis.vertical : Axis.horizontal, + children: [ + DecoratedBox( + decoration: BoxDecoration( + boxShadow: kElevationToShadow[2], + borderRadius: BorderRadius.circular(35), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(35), + child: UniversalImage( + path: image, + width: 250, + height: 250, + fit: BoxFit.cover, + ), + ), + ), + const Gap(20), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(50)), + child: Text( + artist.type!.toUpperCase(), + style: chipTextVariant.copyWith( + color: Colors.white, + ), + ), + ), + if (isBlackListed) ...[ + const SizedBox(width: 5), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.red[400], + borderRadius: BorderRadius.circular(50)), + child: Text( + context.l10n.blacklisted, + style: chipTextVariant.copyWith( + color: Colors.white, + ), + ), + ), + ] + ], + ), + Text( + artist.name!, + style: mediaQuery.smAndDown + ? textTheme.headlineSmall + : textTheme.headlineMedium, + ), + Text( + context.l10n.followers( + PrimitiveUtils.toReadableNumber( + artist.followers!.total!.toDouble(), + ), + ), + style: textTheme.bodyMedium?.copyWith( + fontWeight: mediaQuery.mdAndUp ? FontWeight.bold : null, + ), + ), + const Gap(20), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (auth != null) + HookBuilder( + builder: (context) { + final isFollowingQuery = + useQueries.artist.doIFollow(ref, artistId); + + final followUnfollow = useCallback(() async { + try { + isFollowingQuery.data! + ? await spotify.me.unfollow( + FollowingType.artist, + [artistId], + ) + : await spotify.me.follow( + FollowingType.artist, + [artistId], + ); + await isFollowingQuery.refresh(); + + queryClient.refreshInfiniteQueryAllPages( + "user-following-artists"); + } finally { + queryClient.refreshQuery( + "user-follows-artists-query/$artistId", + ); + } + }, [isFollowingQuery]); + + if (isFollowingQuery.isLoading || + !isFollowingQuery.hasData) { + return const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ); + } + + if (isFollowingQuery.data!) { + return OutlinedButton( + onPressed: followUnfollow, + child: Text(context.l10n.following), + ); + } + + return FilledButton( + onPressed: followUnfollow, + child: Text(context.l10n.follow), + ); + }, + ), + const SizedBox(width: 5), + IconButton( + tooltip: context.l10n.add_artist_to_blacklist, + icon: Icon( + SpotubeIcons.userRemove, + color: + !isBlackListed ? Colors.red[400] : Colors.white, + ), + style: IconButton.styleFrom( + backgroundColor: + isBlackListed ? Colors.red[400] : null, + ), + onPressed: () async { + if (isBlackListed) { + ref + .read(BlackListNotifier.provider.notifier) + .remove( + BlacklistedElement.artist( + artist.id!, artist.name!), + ); + } else { + ref.read(BlackListNotifier.provider.notifier).add( + BlacklistedElement.artist( + artist.id!, artist.name!), + ); + } + }, + ), + IconButton( + icon: const Icon(SpotubeIcons.share), + onPressed: () async { + if (artist.externalUrls?.spotify != null) { + await Clipboard.setData( + ClipboardData( + text: artist.externalUrls!.spotify!, + ), + ); + } + + if (!context.mounted) return; + + scaffoldMessenger.showSnackBar( + SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + context.l10n.artist_url_copied, + textAlign: TextAlign.center, + ), + ), + ); + }, + ) + ], + ) + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/artist/section/related_artists.dart b/lib/pages/artist/section/related_artists.dart new file mode 100644 index 00000000..2938c084 --- /dev/null +++ b/lib/pages/artist/section/related_artists.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/artist/artist_card.dart'; +import 'package:spotube/services/queries/queries.dart'; + +class ArtistPageRelatedArtists extends HookConsumerWidget { + final String artistId; + const ArtistPageRelatedArtists({ + Key? key, + required this.artistId, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final relatedArtists = useQueries.artist.relatedArtistsOf( + ref, + artistId, + ); + + if (relatedArtists.isLoading || !relatedArtists.hasData) { + return const SliverToBoxAdapter( + child: Center(child: CircularProgressIndicator())); + } else if (relatedArtists.hasError) { + return SliverToBoxAdapter( + child: Center( + child: Text(relatedArtists.error.toString()), + ), + ); + } + + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + sliver: SliverGrid.builder( + itemCount: relatedArtists.data!.length, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: 250, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 0.8, + ), + itemBuilder: (context, index) { + final artist = relatedArtists.data!.elementAt(index); + return ArtistCard(artist); + }, + ), + ); + } +} diff --git a/lib/pages/artist/section/top_tracks.dart b/lib/pages/artist/section/top_tracks.dart new file mode 100644 index 00000000..9e3e4054 --- /dev/null +++ b/lib/pages/artist/section/top_tracks.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/track_tile/track_tile.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/services/queries/queries.dart'; + +class ArtistPageTopTracks extends HookConsumerWidget { + final String artistId; + const ArtistPageTopTracks({Key? key, required this.artistId}) + : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final theme = Theme.of(context); + final scaffoldMessenger = ScaffoldMessenger.of(context); + + final playlist = ref.watch(ProxyPlaylistNotifier.provider); + final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); + final topTracksQuery = useQueries.artist.topTracksOf( + ref, + artistId, + ); + + final isPlaylistPlaying = playlist.containsTracks( + topTracksQuery.data ?? [], + ); + + if (topTracksQuery.isLoading || !topTracksQuery.hasData) { + return const SliverToBoxAdapter( + child: Center(child: CircularProgressIndicator()), + ); + } else if (topTracksQuery.hasError) { + return SliverToBoxAdapter( + child: Center( + child: Text(topTracksQuery.error.toString()), + ), + ); + } + + final topTracks = topTracksQuery.data!; + + void playPlaylist(List tracks, {Track? currentTrack}) async { + currentTrack ??= tracks.first; + if (!isPlaylistPlaying) { + playlistNotifier.load( + tracks, + initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id), + autoPlay: true, + ); + } else if (isPlaylistPlaying && + currentTrack.id != null && + currentTrack.id != playlist.activeTrack?.id) { + await playlistNotifier.jumpToTrack(currentTrack); + } + } + + return SliverMainAxisGroup( + slivers: [ + SliverToBoxAdapter( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + context.l10n.top_tracks, + style: theme.textTheme.headlineSmall, + ), + ), + if (!isPlaylistPlaying) + IconButton( + icon: const Icon( + SpotubeIcons.queueAdd, + ), + onPressed: () { + playlistNotifier.addTracks(topTracks.toList()); + scaffoldMessenger.showSnackBar( + SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + context.l10n.added_to_queue( + topTracks.length, + ), + textAlign: TextAlign.center, + ), + ), + ); + }, + ), + const SizedBox(width: 5), + IconButton( + icon: Icon( + isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, + color: Colors.white, + ), + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + ), + onPressed: () => playPlaylist(topTracks.toList()), + ) + ], + ), + ), + SliverList.builder( + itemCount: topTracks.length, + itemBuilder: (context, index) { + final track = topTracks.elementAt(index); + return TrackTile( + index: index, + track: track, + onTap: () async { + playPlaylist( + topTracks.toList(), + currentTrack: track, + ); + }, + ); + }, + ), + ], + ); + } +} diff --git a/lib/services/queries/artist.dart b/lib/services/queries/artist.dart index 7501d619..1b939c82 100644 --- a/lib/services/queries/artist.dart +++ b/lib/services/queries/artist.dart @@ -1,8 +1,12 @@ import 'package:fl_query/fl_query.dart'; +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; import 'package:spotube/hooks/spotify/use_spotify_query.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/wikipedia/wikipedia.dart'; +import 'package:wikipedia_api/wikipedia_api.dart'; class ArtistQueries { const ArtistQueries(); @@ -72,11 +76,11 @@ class ArtistQueries { return useSpotifyQuery( "user-follows-artists-query/$artist", (spotify) async { - final result = await spotify.me.isFollowing( + final result = await spotify.me.checkFollowing( FollowingType.artist, [artist], ); - return result.first; + return result[artist]; }, ref: ref, ); @@ -86,10 +90,12 @@ class ArtistQueries { WidgetRef ref, String artist, ) { + final preferences = ref.watch(userPreferencesProvider); return useSpotifyQuery, dynamic>( "artist-top-track-query/$artist", (spotify) { - return spotify.artists.getTopTracks(artist, "US"); + return spotify.artists + .topTracks(artist, preferences.recommendationMarket); }, ref: ref, ); @@ -122,9 +128,24 @@ class ArtistQueries { return useSpotifyQuery, dynamic>( "artist-related-artist-query/$artist", (spotify) { - return spotify.artists.getRelatedArtists(artist); + return spotify.artists.relatedArtists(artist); }, ref: ref, ); } + + Query wikipediaSummary(ArtistSimple artist) { + return useQuery( + "artist-wikipedia-query/${artist.id}", + () async { + final query = artist.name!.replaceAll(" ", "_"); + final res = await wikipedia.pageContent.pageSummaryTitleGet(query); + if (res?.type != "standard") { + return await wikipedia.pageContent + .pageSummaryTitleGet("${query}_(singer)"); + } + return res; + }, + ); + } } diff --git a/lib/services/wikipedia/wikipedia.dart b/lib/services/wikipedia/wikipedia.dart new file mode 100644 index 00000000..b571f30f --- /dev/null +++ b/lib/services/wikipedia/wikipedia.dart @@ -0,0 +1,3 @@ +import 'package:wikipedia_api/wikipedia_api.dart'; + +final wikipedia = WikipediaApi(); diff --git a/pubspec.lock b/pubspec.lock index 6c822604..414297c5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2222,6 +2222,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + wikipedia_api: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: cb7590a3d76b25f16ad3f7147ae6603350777a00 + url: "https://github.com/KRTirtho/wikipedia_api.git" + source: git + version: "0.1.0" win32: dependency: transitive description: @@ -2288,5 +2297,5 @@ packages: source: hosted version: "2.0.2" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6a33d294..fd99e841 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -119,6 +119,9 @@ dependencies: git: url: https://github.com/Tommypop2/dart_discord_rpc.git html_unescape: ^2.0.0 + wikipedia_api: + git: + url: https://github.com/KRTirtho/wikipedia_api.git dev_dependencies: build_runner: ^2.3.2 From 42dd4d68e7ae46c5bfb95f1e065f82fa9e8f5781 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 22 Nov 2023 21:09:29 +0600 Subject: [PATCH 061/131] chore: update deps --- pubspec.lock | 18 ++++++++---------- pubspec.yaml | 8 ++------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 414297c5..06ca8202 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1194,11 +1194,10 @@ packages: jiosaavn: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: "8a7cda9b8b687cde28e0f7fcb10adb0d4fde1007" - url: "https://github.com/KRTirtho/jiosaavn.git" - source: git + name: jiosaavn + sha256: d32b4f43f26488f942f5d7d19d748a1f2664ae3d41ff9c7d50eeb81705174bd2 + url: "https://pub.dev" + source: hosted version: "0.1.0" js: dependency: transitive @@ -2225,11 +2224,10 @@ packages: wikipedia_api: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: cb7590a3d76b25f16ad3f7147ae6603350777a00 - url: "https://github.com/KRTirtho/wikipedia_api.git" - source: git + name: wikipedia_api + sha256: "8bae02778c40e0c09ea237b7c1952c99a33a19ccbe31545e03c807fdc7c56ec6" + url: "https://pub.dev" + source: hosted version: "0.1.0" win32: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index fd99e841..1ad8fd9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,9 +105,7 @@ dependencies: simple_icons: ^7.10.0 audio_service_mpris: ^0.1.0 file_picker: ^6.0.0 - jiosaavn: - git: - url: https://github.com/KRTirtho/jiosaavn.git + jiosaavn: ^0.1.0 draggable_scrollbar: git: url: https://github.com/thielepaul/flutter-draggable-scrollbar.git @@ -119,9 +117,7 @@ dependencies: git: url: https://github.com/Tommypop2/dart_discord_rpc.git html_unescape: ^2.0.0 - wikipedia_api: - git: - url: https://github.com/KRTirtho/wikipedia_api.git + wikipedia_api: ^0.1.0 dev_dependencies: build_runner: ^2.3.2 From 82593f1d654f26549965ad0e9c59d798897138e4 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 22 Nov 2023 21:54:56 +0600 Subject: [PATCH 062/131] cd: increment flutter version --- .fvm/fvm_config.json | 2 +- .github/workflows/spotube-release-binary.yml | 2 +- CONTRIBUTION.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index ba129cfd..f1f9ceed 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.10.0", + "flutterSdkVersion": "3.16.0", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index ab42dcb9..46555de5 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -26,7 +26,7 @@ on: default: true env: - FLUTTER_VERSION: '3.13.2' + FLUTTER_VERSION: '3.16.0' jobs: windows: diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 11206e6d..b2823e62 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -119,7 +119,7 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/KRTirt Do the following: -- Download the latest Flutter SDK (>=3.10.0) & enable desktop support +- Download the latest Flutter SDK (>=3.16.0) & enable desktop support - Install Development dependencies in linux - Debian (>=12/Bookworm)/Ubuntu ```bash From 64080ef2731cef4e370096d2ffe9e917ea06f0c7 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 24 Nov 2023 20:49:25 +0600 Subject: [PATCH 063/131] cd: fix flutter_distributor for windows not working (temporary) --- .github/workflows/spotube-release-binary.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 46555de5..62dbcace 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -33,6 +33,11 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: KRTirtho/flutter_distributor + path: flutter_distributor + ref: fix-windows-build - uses: subosito/flutter-action@v2.10.0 with: cache: true @@ -74,9 +79,10 @@ jobs: - name: Build Windows Executable run: | - dart pub global activate flutter_distributor + dart pub global activate melos + cd flutter_distributor && melos bs && cd .. make innoinstall - flutter_distributor package --platform=windows --targets=exe --skip-clean + dart run ./flutter_distributor/packages/flutter_distributor/bin/main.dart package --platform=windows --targets=exe --skip-clean mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe - name: Create Chocolatey Package and set hash From 722dd86810ea076c0e540ff5cd108fb5f2df2a0f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 27 Nov 2023 19:34:18 +0600 Subject: [PATCH 064/131] chore: track view play not working properly --- .../sections/body/track_view_body.dart | 18 +- lib/extensions/infinite_query.dart | 26 +- lib/pages/artist/section/footer.dart | 6 +- .../playlist_generate/playlist_generate.dart | 513 +++++++++--------- lib/pages/playlist/playlist.dart | 2 +- 5 files changed, 296 insertions(+), 269 deletions(-) diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/shared/tracks_view/sections/body/track_view_body.dart index 486e4405..1c3ba3fb 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_body.dart @@ -25,8 +25,6 @@ class TrackViewBodySection extends HookConsumerWidget { final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final props = InheritedTrackView.of(context); - final trackViewState = ref.watch(trackViewProvider(props.tracks)); - final searchController = useTextEditingController(); final searchFocus = useFocusNode(); @@ -35,12 +33,19 @@ class TrackViewBodySection extends HookConsumerWidget { final isFiltering = useState(false); + final uniqTracks = useMemoized(() { + final trackIds = props.tracks.map((e) => e.id).toSet(); + return props.tracks.where((e) => trackIds.remove(e.id)).toList(); + }, [props.tracks]); + + final trackViewState = ref.watch(trackViewProvider(uniqTracks)); + final tracks = useMemoized(() { List filteredTracks; if (searchQuery.isEmpty) { - filteredTracks = props.tracks; + filteredTracks = uniqTracks; } else { - filteredTracks = props.tracks + filteredTracks = uniqTracks .map((e) => (weightedRatio(e.name!, searchQuery), e)) .sorted((a, b) => b.$1.compareTo(a.$1)) .where((e) => e.$1 > 50) @@ -48,7 +53,7 @@ class TrackViewBodySection extends HookConsumerWidget { .toList(); } return ServiceUtils.sortTracks(filteredTracks, trackViewState.sortBy); - }, [trackViewState.sortBy, searchQuery, props.tracks]); + }, [trackViewState.sortBy, searchQuery, uniqTracks]); final isUserPlaylist = useIsUserPlaylist(ref, props.collectionId); @@ -106,8 +111,9 @@ class TrackViewBodySection extends HookConsumerWidget { if (isActive || playlist.tracks.contains(track)) { await playlistNotifier.jumpToTrack(track); } else { + final tracks = await props.pagination.onFetchAll(); await playlistNotifier.load( - props.tracks, + tracks, initialIndex: index, autoPlay: true, ); diff --git a/lib/extensions/infinite_query.dart b/lib/extensions/infinite_query.dart index 90dcf73e..2181ab3c 100644 --- a/lib/extensions/infinite_query.dart +++ b/lib/extensions/infinite_query.dart @@ -9,17 +9,21 @@ extension FetchAllTracks on InfiniteQuery, dynamic, int> { return pages.expand((page) => page).toList(); } final tracks = await getAllTracks(); - final pagedTracks = tracks.fold( - >{}, - (acc, element) { - final index = acc.length; - final groupIndex = index ~/ 20; - final group = acc[groupIndex] ?? []; - group.add(element); - acc[groupIndex] = group; - return acc; - }, - ); + + final numOfPages = (tracks.length / 20).round(); + + final Map> pagedTracks = {}; + + for (var i = 0; i < numOfPages; i++) { + if (i == numOfPages - 1) { + final pageTracks = tracks.sublist(i * 20); + pagedTracks[i] = pageTracks; + break; + } + + final pageTracks = tracks.sublist(i * 20, (i + 1) * 20); + pagedTracks[i] = pageTracks; + } for (final group in pagedTracks.entries) { setPageData(group.key, group.value); diff --git a/lib/pages/artist/section/footer.dart b/lib/pages/artist/section/footer.dart index 3c0db8a5..b01ef705 100644 --- a/lib/pages/artist/section/footer.dart +++ b/lib/pages/artist/section/footer.dart @@ -38,9 +38,9 @@ class ArtistPageFooter extends HookConsumerWidget { BlendMode.darken, ), image: UniversalImage.imageProvider( - summary.data!.originalimage?.source_ ?? artistImage, - height: summary.data!.originalimage?.height.toDouble(), - width: summary.data!.originalimage?.width.toDouble(), + summary.data!.thumbnail?.source_ ?? artistImage, + height: summary.data!.thumbnail?.height.toDouble(), + width: summary.data!.thumbnail?.width.toDouble(), ), fit: BoxFit.cover, alignment: Alignment.center, diff --git a/lib/pages/library/playlist_generate/playlist_generate.dart b/lib/pages/library/playlist_generate/playlist_generate.dart index 4b8dddaf..802b28d3 100644 --- a/lib/pages/library/playlist_generate/playlist_generate.dart +++ b/lib/pages/library/playlist_generate/playlist_generate.dart @@ -242,267 +242,284 @@ class PlaylistGeneratorPage extends HookConsumerWidget { }, ); + final controller = useScrollController(); + return Scaffold( appBar: PageWindowTitleBar( leading: const BackButton(), title: Text(context.l10n.generate_playlist), centerTitle: true, ), - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: Breakpoints.lg), - child: SliderTheme( - data: const SliderThemeData( - overlayShape: RoundSliderOverlayShape(), - ), - child: SafeArea( - child: LayoutBuilder(builder: (context, constrains) { - return ListView( - padding: const EdgeInsets.all(16), - children: [ - ValueListenableBuilder( - valueListenable: limit, - builder: (context, value, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.number_of_tracks_generate, - style: textTheme.titleMedium, - ), - Row( + body: Scrollbar( + controller: controller, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: Breakpoints.lg), + child: SliderTheme( + data: const SliderThemeData( + overlayShape: RoundSliderOverlayShape(), + ), + child: SafeArea( + child: LayoutBuilder(builder: (context, constrains) { + return ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith(scrollbars: false), + child: ListView( + controller: controller, + padding: const EdgeInsets.all(16), + children: [ + ValueListenableBuilder( + valueListenable: limit, + builder: (context, value, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 40, - height: 40, - alignment: Alignment.center, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - shape: BoxShape.circle, - ), - child: Text( - value.round().toString(), - style: textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.primaryContainer, - ), - ), + Text( + context.l10n.number_of_tracks_generate, + style: textTheme.titleMedium, ), - Expanded( - child: Slider( - value: value.toDouble(), - min: 10, - max: 100, - divisions: 9, - label: value.round().toString(), - onChanged: (value) { - limit.value = value.round(); - }, - ), + Row( + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + shape: BoxShape.circle, + ), + child: Text( + value.round().toString(), + style: textTheme.bodyLarge?.copyWith( + color: theme + .colorScheme.primaryContainer, + ), + ), + ), + Expanded( + child: Slider( + value: value.toDouble(), + min: 10, + max: 100, + divisions: 9, + label: value.round().toString(), + onChanged: (value) { + limit.value = value.round(); + }, + ), + ) + ], ) ], - ) - ], - ); - }, - ), - const SizedBox(height: 16), - if (constrains.mdAndUp) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: countrySelector, - ), - const SizedBox(width: 16), - Expanded( - child: genreSelector, - ), + ); + }, + ), + const SizedBox(height: 16), + if (constrains.mdAndUp) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: countrySelector, + ), + const SizedBox(width: 16), + Expanded( + child: genreSelector, + ), + ], + ) + else ...[ + countrySelector, + const SizedBox(height: 16), + genreSelector, ], - ) - else ...[ - countrySelector, - const SizedBox(height: 16), - genreSelector, - ], - const SizedBox(height: 16), - if (constrains.mdAndUp) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: artistAutoComplete, - ), - const SizedBox(width: 16), - Expanded( - child: tracksAutocomplete, - ), + const SizedBox(height: 16), + if (constrains.mdAndUp) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: artistAutoComplete, + ), + const SizedBox(width: 16), + Expanded( + child: tracksAutocomplete, + ), + ], + ) + else ...[ + artistAutoComplete, + const SizedBox(height: 16), + tracksAutocomplete, ], - ) - else ...[ - artistAutoComplete, - const SizedBox(height: 16), - tracksAutocomplete, - ], - const SizedBox(height: 16), - RecommendationAttributeDials( - title: Text(context.l10n.acousticness), - values: acousticness.value, - onChanged: (value) { - acousticness.value = value; - }, + const SizedBox(height: 16), + RecommendationAttributeDials( + title: Text(context.l10n.acousticness), + values: acousticness.value, + onChanged: (value) { + acousticness.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.danceability), + values: danceability.value, + onChanged: (value) { + danceability.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.energy), + values: energy.value, + onChanged: (value) { + energy.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.instrumentalness), + values: instrumentalness.value, + onChanged: (value) { + instrumentalness.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.liveness), + values: liveness.value, + onChanged: (value) { + liveness.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.loudness), + values: loudness.value, + onChanged: (value) { + loudness.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.speechiness), + values: speechiness.value, + onChanged: (value) { + speechiness.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.valence), + values: valence.value, + onChanged: (value) { + valence.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.popularity), + values: popularity.value, + base: 100, + onChanged: (value) { + popularity.value = value; + }, + ), + RecommendationAttributeDials( + title: Text(context.l10n.key), + values: key.value, + base: 11, + onChanged: (value) { + key.value = value; + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.duration), + values: ( + max: durationMs.value.max / 1000, + target: durationMs.value.target / 1000, + min: durationMs.value.min / 1000, + ), + onChanged: (value) { + durationMs.value = ( + max: value.max * 1000, + target: value.target * 1000, + min: value.min * 1000, + ); + }, + presets: { + context.l10n.short: (min: 50, target: 90, max: 120), + context.l10n.medium: ( + min: 120, + target: 180, + max: 200 + ), + context.l10n.long: (min: 480, target: 560, max: 640) + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.tempo), + values: tempo.value, + onChanged: (value) { + tempo.value = value; + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.mode), + values: mode.value, + onChanged: (value) { + mode.value = value; + }, + ), + RecommendationAttributeFields( + title: Text(context.l10n.time_signature), + values: timeSignature.value, + onChanged: (value) { + timeSignature.value = value; + }, + ), + const SizedBox(height: 20), + FilledButton.icon( + icon: const Icon(SpotubeIcons.magic), + label: Text(context.l10n.generate_playlist), + onPressed: artists.value.isEmpty && + tracks.value.isEmpty && + genres.value.isEmpty + ? null + : () { + final PlaylistGenerateResultRouteState + routeState = ( + seeds: ( + artists: artists.value + .map((a) => a.id!) + .toList(), + tracks: tracks.value + .map((t) => t.id!) + .toList(), + genres: genres.value + ), + market: market.value, + limit: limit.value, + parameters: ( + acousticness: acousticness.value, + danceability: danceability.value, + energy: energy.value, + instrumentalness: instrumentalness.value, + liveness: liveness.value, + loudness: loudness.value, + speechiness: speechiness.value, + valence: valence.value, + popularity: popularity.value, + key: key.value, + duration_ms: durationMs.value, + tempo: tempo.value, + mode: mode.value, + time_signature: timeSignature.value, + ) + ); + GoRouter.of(context).push( + "/library/generate/result", + extra: routeState, + ); + }, + ), + ], ), - RecommendationAttributeDials( - title: Text(context.l10n.danceability), - values: danceability.value, - onChanged: (value) { - danceability.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.energy), - values: energy.value, - onChanged: (value) { - energy.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.instrumentalness), - values: instrumentalness.value, - onChanged: (value) { - instrumentalness.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.liveness), - values: liveness.value, - onChanged: (value) { - liveness.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.loudness), - values: loudness.value, - onChanged: (value) { - loudness.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.speechiness), - values: speechiness.value, - onChanged: (value) { - speechiness.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.valence), - values: valence.value, - onChanged: (value) { - valence.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.popularity), - values: popularity.value, - base: 100, - onChanged: (value) { - popularity.value = value; - }, - ), - RecommendationAttributeDials( - title: Text(context.l10n.key), - values: key.value, - base: 11, - onChanged: (value) { - key.value = value; - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.duration), - values: ( - max: durationMs.value.max / 1000, - target: durationMs.value.target / 1000, - min: durationMs.value.min / 1000, - ), - onChanged: (value) { - durationMs.value = ( - max: value.max * 1000, - target: value.target * 1000, - min: value.min * 1000, - ); - }, - presets: { - context.l10n.short: (min: 50, target: 90, max: 120), - context.l10n.medium: (min: 120, target: 180, max: 200), - context.l10n.long: (min: 480, target: 560, max: 640) - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.tempo), - values: tempo.value, - onChanged: (value) { - tempo.value = value; - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.mode), - values: mode.value, - onChanged: (value) { - mode.value = value; - }, - ), - RecommendationAttributeFields( - title: Text(context.l10n.time_signature), - values: timeSignature.value, - onChanged: (value) { - timeSignature.value = value; - }, - ), - const SizedBox(height: 20), - FilledButton.icon( - icon: const Icon(SpotubeIcons.magic), - label: Text(context.l10n.generate_playlist), - onPressed: artists.value.isEmpty && - tracks.value.isEmpty && - genres.value.isEmpty - ? null - : () { - final PlaylistGenerateResultRouteState - routeState = ( - seeds: ( - artists: - artists.value.map((a) => a.id!).toList(), - tracks: - tracks.value.map((t) => t.id!).toList(), - genres: genres.value - ), - market: market.value, - limit: limit.value, - parameters: ( - acousticness: acousticness.value, - danceability: danceability.value, - energy: energy.value, - instrumentalness: instrumentalness.value, - liveness: liveness.value, - loudness: loudness.value, - speechiness: speechiness.value, - valence: valence.value, - popularity: popularity.value, - key: key.value, - duration_ms: durationMs.value, - tempo: tempo.value, - mode: mode.value, - time_signature: timeSignature.value, - ) - ); - GoRouter.of(context).push( - "/library/generate/result", - extra: routeState, - ); - }, - ), - ], - ); - }), + ); + }), + ), ), ), ), diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index ab39b225..29601a09 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -53,7 +53,7 @@ class PlaylistPage extends HookConsumerWidget { ), pagination: PaginationProps.fromQuery( tracksQuery, - onFetchAll: () async { + onFetchAll: () { return tracksQuery.fetchAllTracks( getAllTracks: () async { final res = await spotify.playlists From ee8229020b3b03fc074b316db4b322af13b807bd Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 27 Nov 2023 19:39:53 +0600 Subject: [PATCH 065/131] fix: settings page scrollbar position --- lib/pages/search/search.dart | 5 +++-- lib/pages/settings/settings.dart | 15 ++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index b19162fa..f4a78d4f 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -114,8 +114,9 @@ class SearchPage extends HookConsumerWidget { ), color: theme.scaffoldBackgroundColor, child: TextField( - autofocus: - queries.none((s) => s.hasPageData && !s.hasPageError), + autofocus: queries + .none((s) => s.hasPageData && !s.hasPageError) && + !kIsMobile, decoration: InputDecoration( prefixIcon: const Icon(SpotubeIcons.search), hintText: "${context.l10n.search}...", diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 842d5240..f773b809 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -30,12 +30,13 @@ class SettingsPage extends HookConsumerWidget { title: Text(context.l10n.settings), centerTitle: true, ), - body: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: Container( - constraints: const BoxConstraints(maxWidth: 1366), + body: Scrollbar( + controller: controller, + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1366), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(scrollbars: false), child: ListView( controller: controller, children: [ @@ -59,7 +60,7 @@ class SettingsPage extends HookConsumerWidget { ), ), ), - ], + ), ), ), ); From cd31798870208bffef4d2ac9727c9e683237475b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 27 Nov 2023 20:10:56 +0600 Subject: [PATCH 066/131] chore: get ready for release --- .github/workflows/spotube-release-binary.yml | 2 +- CHANGELOG.md | 38 ++++++++++++++++++++ README.md | 32 +++++++++++------ bin/gen-credits.dart | 26 +++++++++----- pubspec.yaml | 2 +- 5 files changed, 79 insertions(+), 21 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 62dbcace..d57cc0e8 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -4,7 +4,7 @@ on: inputs: version: description: Version to release (x.x.x) - default: 3.2.0 + default: 3.3.0 required: true channel: type: choice diff --git a/CHANGELOG.md b/CHANGELOG.md index 3710d812..dbdf1326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [3.3.0](https://github.com/KRTirtho/spotube/compare/v3.2.0...v3.3.0) (2023-11-27) + + +### Features + +* Add JioSaavn as audio source ([#881](https://github.com/KRTirtho/spotube/issues/881)) ([14069cd](https://github.com/KRTirtho/spotube/commit/14069cd4fe08597c8d9aa0810270fb4c386c1d55)) +* **android:** better quick scroll/drag to scroll implementation ([2e2c44f](https://github.com/KRTirtho/spotube/commit/2e2c44f0afef69bf9bc485db97d45127a0847c8e)) +* **artist:** modularize page and add wikipedia section ([2a69886](https://github.com/KRTirtho/spotube/commit/2a698865567883271471ace9a44123bbfd8fcd2f)) +* discord RPC integration [#98](https://github.com/KRTirtho/spotube/issues/98) ([88b8785](https://github.com/KRTirtho/spotube/commit/88b8785cb86a19900f3a867b044c1ccb2fe400bb)) +* **mini_player:** show/hide lyrics [#851](https://github.com/KRTirtho/spotube/issues/851) ([dcbb156](https://github.com/KRTirtho/spotube/commit/dcbb1568337969841acc0abe0e7185ee5e4c3590)) +* paginated playlist and album page ([28a5d6b](https://github.com/KRTirtho/spotube/commit/28a5d6bb3820ab0bd4007664f73d685f6e1d2c90)) +* **translations:** add Turkish translations ([0c22469](https://github.com/KRTirtho/spotube/commit/0c22469503f32dbbf1a5d31419c1b76c699fa966)) + + +### Bug Fixes + +* 0:00 media duration in queue after application restart [#782](https://github.com/KRTirtho/spotube/issues/782) ([83c0b49](https://github.com/KRTirtho/spotube/commit/83c0b49da962d9f3d40de9525f90f0b320e8f7b8)) +* Add to Playlist Dialog memory leak [#817](https://github.com/KRTirtho/spotube/issues/817) ([fed36ec](https://github.com/KRTirtho/spotube/commit/fed36ecdd81e8a0f8358693eff0a6233dea32e5d)) +* **album_card:** show loading state during adding track to queue/play ([5633367](https://github.com/KRTirtho/spotube/commit/5633367397812148f6d712d06e97a4f84033f968)) +* alternative track source safearea overflow [#876](https://github.com/KRTirtho/spotube/issues/876) ([7b72a90](https://github.com/KRTirtho/spotube/commit/7b72a90bc65b541cbe2e24ef2234524b522ad71d)) +* android invalid download location Download not starting or not explaining error [#720](https://github.com/KRTirtho/spotube/issues/720) ([d056dbf](https://github.com/KRTirtho/spotube/commit/d056dbf9eeef7033dbc012d0c05800063e820042)) +* changed settings are not persisting after force stop [#821](https://github.com/KRTirtho/spotube/issues/821) ([e29a38d](https://github.com/KRTirtho/spotube/commit/e29a38dfa43ddf7a38046d1d40424f01dbe62261)) +* check for unsynced lyrics and error handling for timed lyrics query ([1d77556](https://github.com/KRTirtho/spotube/commit/1d77556157d158600f29cf2ea5f26c567607dec7)) +* **genres:** lag while scrolling ([dc980b0](https://github.com/KRTirtho/spotube/commit/dc980b024edad3132e72cbb2f0087297a4b76469)) +* infinite list disappearing for a moment everytime new page is fetched ([1334a62](https://github.com/KRTirtho/spotube/commit/1334a62aaea31f97031b3ebf455e94c583f37314)) +* last track of queue keeps repeating [#718](https://github.com/KRTirtho/spotube/issues/718) ([58e5698](https://github.com/KRTirtho/spotube/commit/58e569864dddd74c3064624998dfc184046e97eb)) +* Navigating to settings, redirects to home page [#812](https://github.com/KRTirtho/spotube/issues/812) ([da04f06](https://github.com/KRTirtho/spotube/commit/da04f068f9b7effff8d50cb5714d93ea80c22b7f)) +* new releases section flickering on scroll glitch ([ee94b7c](https://github.com/KRTirtho/spotube/commit/ee94b7cbb24e0f0bc22a6d49c830d4055aa02895)) +* **playbutton_card:** annoying animation ([574406d](https://github.com/KRTirtho/spotube/commit/574406dd5fc410914b27e7fce374323696845012)) +* scrobbling not working for first track or single track ([0a6b54d](https://github.com/KRTirtho/spotube/commit/0a6b54da367345b73fe6e954f1d9368d9f9ead71)) +* settings page scrollbar position ([ee82290](https://github.com/KRTirtho/spotube/commit/ee8229020b3b03fc074b316db4b322af13b807bd)) +* shuffle doesn't move active track to top ([4956bf3](https://github.com/KRTirtho/spotube/commit/4956bf367baae39c88b5de7c6c136513a14f8ad2)) +* spotube doesn't exit properly, hangs in infinite loop [#768](https://github.com/KRTirtho/spotube/issues/768) ([353ca79](https://github.com/KRTirtho/spotube/commit/353ca79be334077c3ac27b4f64e8b4b15eca7175)) +* trim login field padding ([286ef83](https://github.com/KRTirtho/spotube/commit/286ef83e8ec516db70019398d9e3e724437a4172)) +* use CustomScrollView for personalized page ([7d05c40](https://github.com/KRTirtho/spotube/commit/7d05c40dc0d04208b059f2483c1e4de199c8b51d)) +* user_playlists layout, track tile index, ([487c2ed](https://github.com/KRTirtho/spotube/commit/487c2ed6bdc4af33006ba52532eb4eaaa261dceb)) +* **windows:** media control not working [#641](https://github.com/KRTirtho/spotube/issues/641) ([7818574](https://github.com/KRTirtho/spotube/commit/7818574356d0fb8ff567e1f6a83fd0b6f2ee7c8a)) + ## [3.2.0](https://github.com/KRTirtho/spotube/compare/v3.1.2...v3.2.0) (2023-10-16) diff --git a/README.md b/README.md index d82af783..498c45de 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Spotube Logo An open source, cross-platform Spotify client compatible across multiple platforms
-utilizing Spotify's data API and YouTube (or Piped.video) as an audio source,
+utilizing Spotify's data API and YouTube (or Piped.video or JioSaavn) as an audio source,
eliminating the need for Spotify Premium Btw it's not another Electron app😉 @@ -184,19 +184,23 @@ If you are concerned, you can [read the reason of choosing this license](https:/
-

[Click to show] 🙏 Library/Plugin/Framework Credits

+

[Click to show] 🙏 Services/Package/Plugin Credits

+### Services 1. [Flutter](https://flutter.dev) - Flutter transforms the app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase 1. [Spotify API](https://developer.spotify.com/documentation/web-api) - The Spotify Web API is a RESTful API that provides access to Spotify data 1. [Piped](https://piped-docs.kavin.rocks/) - Piped is a privacy friendly alternative YouTube frontend, which is efficient and scalable by design. 1. [YouTube](https://youtube.com/) - YouTube is an American online video-sharing platform headquartered in San Bruno, California. Three former PayPal employees—Chad Hurley, Steve Chen, and Jawed Karim—created the service in February 2005 +1. [JioSaavn](https://www.jiosaavn.com) - JioSaavn is an Indian online music streaming service and a digital distributor of Bollywood, English and other regional Indian music across the world. Since it was founded in 2007 as Saavn, the company has acquired rights to over 5 crore (50 million) music tracks in 15 languages 1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution 1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users 1. [Flatpak](https://flatpak.org) - Flatpak is a utility for software deployment and package management for Linux 1. [SponsorBlock](https://sponsor.ajay.app) - SponsorBlock is an open-source crowdsourced browser extension and open API for skipping sponsor segments in YouTube videos. 1. [Inno Setup](https://jrsoftware.org/isinfo.php) - Inno Setup is a free installer for Windows programs by Jordan Russell and Martijn Laan 1. [F-Droid](https://f-droid.org) - F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device + +### Dependencies 1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options. 1. [async](https://pub.dev/packages/async) - Utility functions and classes related to the 'dart:async' library. 1. [audio_service](https://pub.dev/packages/audio_service) - Flutter plugin to play audio in the background while the screen is off. @@ -216,7 +220,10 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration. 1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times. 1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI. -1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft. +1. [fl_query](https://fl-query.krtirtho.dev) - Asynchronous data caching, refetching & invalidation library for Flutter +1. [fl_query_hooks](https://fl-query.krtirtho.dev) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter +1. [fl_query_devtools](https://fl-query.krtirtho.dev) - Devtools support for Fl-Query +1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft. 1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite. 1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices. 1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability. @@ -227,7 +234,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android. 1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files. 1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets -1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs! +1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs! 1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling. 1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more 1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256. @@ -244,10 +251,10 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms. 1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files 1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents. -1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. +1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. 1. [palette_generator](https://pub.dev/packages/palette_generator) - Flutter package for generating palette colors from a source image. 1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web. -1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. +1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. 1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. 1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video 1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area. @@ -258,7 +265,6 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet. 1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API. 1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget -1. [supabase](https://supabase.com) - A dart client for Supabase. This client makes it simple for developers to build secure and scalable products. 1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS 1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos. 1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. @@ -269,6 +275,13 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key. 1. [simple_icons](https://jlnrrg.github.io/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands. 1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification. +1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support. +1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com +1. [very_good_infinite_list](https://github.com/VeryGoodOpenSource/very_good_infinite_list) - A library for easily displaying paginated data, created by Very Good Ventures. Great for activity feeds, news feeds, and more. +1. [gap](https://github.com/letsar/gap) - Flutter widgets for easily adding gaps inside Flex widgets such as Columns and Rows or scrolling views. +1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework +1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References. +1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter 1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation. 1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied. 1. [flutter_distributor](https://distributor.leanflutter.org) - A complete tool for packaging and publishing your Flutter apps. @@ -279,12 +292,11 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes. 1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information. 1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting. -1. [fl_query](https://fl-query.vercel.app) - Asynchronous data caching, refetching & invalidation library for Flutter -1. [fl_query_hooks](https://fl-query.vercel.app) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter -1. [fl_query_devtools](https://fl-query.vercel.app) - Devtools support for Fl-Query 1. [flutter_desktop_tools](https://github.com/KRTirtho/flutter_desktop_tools) - Essential collection of tools for flutter desktop app development 1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark. 1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter. +1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item. +1. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games.

© Copyright Spotube 2023

diff --git a/bin/gen-credits.dart b/bin/gen-credits.dart index 43e1e53d..f8975335 100644 --- a/bin/gen-credits.dart +++ b/bin/gen-credits.dart @@ -1,7 +1,7 @@ +import 'dart:developer'; import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:path/path.dart'; import 'package:http/http.dart'; import 'package:html/parser.dart'; import 'package:pub_api_client/pub_api_client.dart'; @@ -33,15 +33,20 @@ void main() async { final gitDeps = gitDepsList.map( (d) { + final uri = Uri.parse( + d.value.url.toString().replaceAll('.git', ''), + ); return MapEntry( d.key, - join( - d.value.url.toString().replaceAll('.git', ''), - 'raw', - d.value.ref ?? 'main', - d.value.path ?? '', - 'pubspec.yaml', - ), + uri.replace( + pathSegments: [ + ...uri.pathSegments, + 'raw', + d.value.ref ?? 'main', + d.value.path ?? '', + 'pubspec.yaml', + ], + ).toString(), ); }, ).toList(); @@ -55,7 +60,10 @@ void main() async { } catch (e) { final document = parse(res.body); final pre = document.querySelector('pre'); - if (pre == null) rethrow; + if (pre == null) { + log(d.toString()); + rethrow; + } return Pubspec.parse(pre.text); } } diff --git a/pubspec.yaml b/pubspec.yaml index 1ad8fd9f..ba758cbf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El publish_to: "none" -version: 3.2.0+25 +version: 3.3.0+26 homepage: https://spotube.krtirtho.dev repository: https://github.com/KRTirtho/spotube From 96021e1a49d22bd25fd052c122f49f439c2bea43 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 27 Nov 2023 20:38:50 +0600 Subject: [PATCH 067/131] fix: "Add () to Playlist" option not showing in favorited playlists #904 --- .../dialogs/playlist_add_track_dialog.dart | 8 +++++-- .../shared/track_tile/track_options.dart | 6 ++++- .../sections/body/track_view_body.dart | 3 +-- .../body/track_view_body_headers.dart | 24 ++++++++----------- .../sections/body/track_view_options.dart | 22 +++++++---------- lib/main.dart | 2 +- .../playlist_generate_result.dart | 1 + lib/themes/theme.dart | 3 +++ 8 files changed, 35 insertions(+), 34 deletions(-) diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/shared/dialogs/playlist_add_track_dialog.dart index aadcd9d6..1c8e5aaa 100644 --- a/lib/components/shared/dialogs/playlist_add_track_dialog.dart +++ b/lib/components/shared/dialogs/playlist_add_track_dialog.dart @@ -11,9 +11,12 @@ import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class PlaylistAddTrackDialog extends HookConsumerWidget { + /// The id of the playlist this dialog was opened from + final String? openFromPlaylist; final List tracks; const PlaylistAddTrackDialog({ required this.tracks, + required this.openFromPlaylist, Key? key, }) : super(key: key); @@ -30,11 +33,12 @@ class PlaylistAddTrackDialog extends HookConsumerWidget { ?.where( (playlist) => playlist.owner?.id != null && - playlist.owner!.id == me.data?.id, + playlist.owner!.id == me.data?.id && + playlist.id != openFromPlaylist, ) .toList() ?? [], - [userPlaylists.data, me.data?.id], + [userPlaylists.data, me.data?.id, openFromPlaylist], ); final playlistsCheck = useState({}); diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/shared/track_tile/track_options.dart index b0633d34..9a587be6 100644 --- a/lib/components/shared/track_tile/track_options.dart +++ b/lib/components/shared/track_tile/track_options.dart @@ -64,11 +64,15 @@ class TrackOptions extends HookConsumerWidget { }); } - void actionAddToPlaylist(BuildContext context, Track track) { + void actionAddToPlaylist( + BuildContext context, + Track track, + ) { showDialog( context: context, builder: (context) => PlaylistAddTrackDialog( tracks: [track], + openFromPlaylist: playlistId, ), ); } diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/shared/tracks_view/sections/body/track_view_body.dart index 1c3ba3fb..d77a3e6f 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_body.dart @@ -24,6 +24,7 @@ class TrackViewBodySection extends HookConsumerWidget { final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final props = InheritedTrackView.of(context); + final trackViewState = ref.watch(trackViewProvider(props.tracks)); final searchController = useTextEditingController(); final searchFocus = useFocusNode(); @@ -38,8 +39,6 @@ class TrackViewBodySection extends HookConsumerWidget { return props.tracks.where((e) => trackIds.remove(e.id)).toList(); }, [props.tracks]); - final trackViewState = ref.watch(trackViewProvider(uniqTracks)); - final tracks = useMemoized(() { List filteredTracks; if (searchQuery.isEmpty) { diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart b/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart index 57d8b296..7e4522a0 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart @@ -38,20 +38,16 @@ class TrackViewBodyHeaders extends HookConsumerWidget { ), ); }, - child: trackViewState.isSelecting - ? Checkbox( - value: trackViewState.hasSelectedAll, - onChanged: (checked) { - if (checked == true) { - trackViewState.selectAll(); - } else { - trackViewState.deselectAll(); - } - }, - ) - : constrains.mdAndUp - ? const SizedBox(width: 32) - : const SizedBox(width: 16), + child: Checkbox( + value: trackViewState.hasSelectedAll, + onChanged: (checked) { + if (checked == true) { + trackViewState.selectAll(); + } else { + trackViewState.deselectAll(); + } + }, + ), ), Expanded( flex: 7, diff --git a/lib/components/shared/tracks_view/sections/body/track_view_options.dart b/lib/components/shared/tracks_view/sections/body/track_view_options.dart index 4fcd0a59..583c9107 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_options.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_options.dart @@ -11,7 +11,6 @@ import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; -import 'package:spotube/services/queries/queries.dart'; class TrackViewBodyOptions extends HookConsumerWidget { const TrackViewBodyOptions({Key? key}) : super(key: key); @@ -30,11 +29,6 @@ class TrackViewBodyOptions extends HookConsumerWidget { final trackViewState = ref.watch(trackViewProvider(props.tracks)); final selectedTracks = trackViewState.selectedTracks; - final userPlaylists = useQueries.playlist.ofMineAll(ref); - - final isUserPlaylist = - userPlaylists.data?.any((e) => e.id == props.collectionId) ?? false; - return AdaptivePopSheetList( tooltip: context.l10n.more_actions, headings: [ @@ -66,6 +60,7 @@ class TrackViewBodyOptions extends HookConsumerWidget { context: context, builder: (context) { return PlaylistAddTrackDialog( + openFromPlaylist: props.collectionId, tracks: selectedTracks.toList(), ); }, @@ -100,15 +95,14 @@ class TrackViewBodyOptions extends HookConsumerWidget { context.l10n.download_count(selectedTracks.length), ), ), - if (!isUserPlaylist) - PopSheetEntry( - value: "add-to-playlist", - leading: const Icon(SpotubeIcons.playlistAdd), - enabled: selectedTracks.isNotEmpty, - title: Text( - context.l10n.add_count_to_playlist(selectedTracks.length), - ), + PopSheetEntry( + value: "add-to-playlist", + leading: const Icon(SpotubeIcons.playlistAdd), + enabled: selectedTracks.isNotEmpty, + title: Text( + context.l10n.add_count_to_playlist(selectedTracks.length), ), + ), PopSheetEntry( enabled: selectedTracks.isNotEmpty, value: "add-to-queue", diff --git a/lib/main.dart b/lib/main.dart index 7bb96543..c68b6bc6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -201,7 +201,7 @@ class SpotubeState extends ConsumerState { Brightness.dark, isAmoledTheme, ), - [paletteColor, accentMaterialColor, isAmoledTheme], + ); return MaterialApp.router( diff --git a/lib/pages/library/playlist_generate/playlist_generate_result.dart b/lib/pages/library/playlist_generate/playlist_generate_result.dart index 015685f1..f751b65b 100644 --- a/lib/pages/library/playlist_generate/playlist_generate_result.dart +++ b/lib/pages/library/playlist_generate/playlist_generate_result.dart @@ -163,6 +163,7 @@ class PlaylistGenerateResultPage extends HookConsumerWidget { context: context, builder: (context) => PlaylistAddTrackDialog( + openFromPlaylist: null, tracks: selectedTracks.value .map( (e) => generatedPlaylist.data! diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index 8c968e1b..9a5e473f 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -71,5 +71,8 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { scrollbarTheme: const ScrollbarThemeData( thickness: MaterialStatePropertyAll(14), ), + checkboxTheme: CheckboxThemeData( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + ), ); } From cdd94529969bfc647f55bb85313ac20ff7358c77 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 27 Nov 2023 20:41:14 +0600 Subject: [PATCH 068/131] chore: update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbdf1326..1544f055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file. See [standa ### Bug Fixes +* "Add () to Playlist" option not showing in favorited playlists [#904](https://github.com/KRTirtho/spotube/issues/904) ([96021e1](https://github.com/KRTirtho/spotube/commit/96021e1a49d22bd25fd052c122f49f439c2bea43)) * 0:00 media duration in queue after application restart [#782](https://github.com/KRTirtho/spotube/issues/782) ([83c0b49](https://github.com/KRTirtho/spotube/commit/83c0b49da962d9f3d40de9525f90f0b320e8f7b8)) * Add to Playlist Dialog memory leak [#817](https://github.com/KRTirtho/spotube/issues/817) ([fed36ec](https://github.com/KRTirtho/spotube/commit/fed36ecdd81e8a0f8358693eff0a6233dea32e5d)) * **album_card:** show loading state during adding track to queue/play ([5633367](https://github.com/KRTirtho/spotube/commit/5633367397812148f6d712d06e97a4f84033f968)) From a7b9398708ede865dc2c25fb791c8e98eeff7a38 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 2 Dec 2023 21:57:05 +0600 Subject: [PATCH 069/131] fix: metadata not getting added for YouTube tracks #916 and Wrong duration of downloaded tracks #912 --- lib/provider/download_manager_provider.dart | 2 +- lib/utils/type_conversion_utils.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index 691a1385..dc538938 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -25,7 +25,7 @@ class DownloadManagerProvider extends ChangeNotifier { final (:request, :status) = event; final track = $history.firstWhereOrNull( - (element) => element.url == request.url, + (element) => element.getUrlOfCodec(downloadCodec) == request.url, ); if (track == null) return; diff --git a/lib/utils/type_conversion_utils.dart b/lib/utils/type_conversion_utils.dart index a805272c..662b611c 100644 --- a/lib/utils/type_conversion_utils.dart +++ b/lib/utils/type_conversion_utils.dart @@ -147,7 +147,7 @@ abstract class TypeConversionUtils { track.name = metadata?.title ?? basenameWithoutExtension(file.path); track.type = "track"; track.uri = file.path; - track.durationMs = (metadata?.durationMs?.toInt() ?? 0) * 1000; + track.durationMs = (metadata?.durationMs?.toInt() ?? 0); return track; } From 840e014f2b18f193d040baef0e0cd595088a4a84 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 2 Dec 2023 22:05:35 +0600 Subject: [PATCH 070/131] fix: amoled mode and color scheme can't be changed --- lib/main.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c68b6bc6..24ba81b4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -184,7 +184,6 @@ class SpotubeState extends ConsumerState { /// For enabling hot reload for audio player if (!kDebugMode) return; audioPlayer.dispose(); - // youtube.close(); }; }, []); @@ -201,7 +200,7 @@ class SpotubeState extends ConsumerState { Brightness.dark, isAmoledTheme, ), - + [paletteColor, accentMaterialColor, isAmoledTheme], ); return MaterialApp.router( From 2a87a0fe281fde9e99ae8b4c79d6f1ac23703dd0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 2 Dec 2023 22:17:53 +0600 Subject: [PATCH 071/131] chore: change API Type to Audio Source --- lib/l10n/app_ar.arb | 2 +- lib/l10n/app_bn.arb | 2 +- lib/l10n/app_ca.arb | 2 +- lib/l10n/app_de.arb | 2 +- lib/l10n/app_en.arb | 2 +- lib/l10n/app_es.arb | 2 +- lib/l10n/app_fa.arb | 2 +- lib/l10n/app_fr.arb | 2 +- lib/l10n/app_hi.arb | 2 +- lib/l10n/app_ja.arb | 2 +- lib/l10n/app_pl.arb | 2 +- lib/l10n/app_pt.arb | 2 +- lib/l10n/app_ru.arb | 2 +- lib/l10n/app_tr.arb | 2 +- lib/l10n/app_uk.arb | 2 +- lib/l10n/app_zh.arb | 2 +- lib/pages/settings/sections/playback.dart | 2 +- lib/provider/proxy_playlist/proxy_playlist_provider.dart | 6 ++++-- 18 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index f587710c..b74564a4 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -251,7 +251,7 @@ "developers": "المطورون", "not_logged_in": "لم تقم بتسجيل الدخول", "search_mode": "وضع البحث", - "youtube_api_type": "نوع الـAPI", + "audio_source": "مصدر الصوت", "ok": "حسسناً", "failed_to_encrypt": "فشل في التشفير", "encryption_failed_warning": "يستخدم Spotube التشفير لتخزين بياناتك بشكل آمن. لكنها فشلت في القيام بذلك. لذلك سيعود الأمر إلى التخزين غير الآمن\nإذا كنت تستخدم Linux، فيرجى التأكد من تثبيت أي خدمة سرية (gnome-keyring، kde-wallet، keepassxc، إلخ)", diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 02402179..59985b24 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -249,7 +249,7 @@ "developers": "ডেভেলপার", "not_logged_in": "আপনি লগইন করা নেই", "search_mode": "অনুসন্ধান মোড", - "youtube_api_type": "API প্রকার", + "audio_source": "অডিও উৎস", "ok": "ঠিক আছে", "failed_to_encrypt": "এনক্রিপ্ট করা ব্যর্থ হয়েছে", "encryption_failed_warning": "Spotube আপনার তথ্যগুলি নিরাপদভাবে স্টোর করতে এনক্রিপশন ব্যবহার করে। কিন্তু এটি ব্যর্থ হয়েছে। তাই এটি অনিরাপদ স্টোরে ফলফল হবে\nযদি আপনি Linux ব্যবহার করেন, তবে দয়া করে নিশ্চিত হউন যে আপনার কোনও সিক্রেট-সার্ভিস gnome-keyring, kde-wallet, keepassxc ইত্যাদি ইনস্টল করা আছে", diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index 81a11082..1d1bb5e4 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -249,7 +249,7 @@ "developers": "Desenvolupadors", "not_logged_in": "No ha iniciat sesió", "search_mode": "Mode de cerca", - "youtube_api_type": "Tipus d'API de YouTube", + "audio_source": "Font d'àudio", "ok": "OK", "failed_to_encrypt": "Error al xifrar", "encryption_failed_warning": "Spotube utilitza el xifrado per emmagatzemar les seves dades de forma segura. Però ha fallat. Per tant, tornarà a un emmagatzament no segur\nSi estè utilizant Linux, asseguri's de tenir instal·lats els serveis secrets com gnome-keyring, kde-wallet i keepassxc", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 339a8d65..8b7b2593 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -249,7 +249,7 @@ "developers": "Entwickler", "not_logged_in": "Sie sind nicht angemeldet", "search_mode": "Suchmodus", - "youtube_api_type": "API-Typ", + "audio_source": "Audioquelle", "ok": "OK", "failed_to_encrypt": "Verschlüsselung fehlgeschlagen", "encryption_failed_warning": "Spotube verwendet Verschlüsselung, um Ihre Daten sicher zu speichern. Dies ist jedoch fehlgeschlagen. Daher wird es auf unsichere Speicherung zurückgreifen\nWenn Sie Linux verwenden, stellen Sie bitte sicher, dass Sie Secret-Services wie gnome-keyring, kde-wallet und keepassxc installiert haben", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 730f51ea..2ce5b8ca 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -251,7 +251,7 @@ "developers": "Developers", "not_logged_in": "You're not logged in", "search_mode": "Search Mode", - "youtube_api_type": "API Type", + "audio_source": "Audio Source", "ok": "Ok", "failed_to_encrypt": "Failed to encrypt", "encryption_failed_warning": "Spotube uses encryption to securely store your data. But failed to do so. So it'll fallback to insecure storage\nIf you're using linux, please make sure you've any secret-service (gnome-keyring, kde-wallet, keepassxc etc) installed", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index f617705e..d90ec972 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -249,7 +249,7 @@ "developers": "Desarrolladores", "not_logged_in": "No has iniciado sesión", "search_mode": "Modo de búsqueda", - "youtube_api_type": "Tipo de API de YouTube", + "audio_source": "Fuente de audio", "ok": "OK", "failed_to_encrypt": "Error al cifrar", "encryption_failed_warning": "Spotube utiliza el cifrado para almacenar sus datos de forma segura. Pero ha fallado. Por lo tanto, volverá a un almacenamiento no seguro\nSi está utilizando Linux, asegúrese de tener instalados servicios secretos como gnome-keyring, kde-wallet y keepassxc", diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 5454b13b..a8bb9f8c 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -251,7 +251,7 @@ "developers": "توسعه دهنده ها", "not_logged_in": "شما وارد نشده اید ", "search_mode": "حالت جستجو", - "youtube_api_type": "API نوع", + "audio_source": "منبع صدا", "ok": "باشد", "failed_to_encrypt": "رمز گذاری نشده", "encryption_failed_warning": "Spotube از رمزگذاری برای ذخیره ایمن داده های شما استفاده می کند. اما موفق به انجام این کار نشد. بنابراین به فضای ذخیره‌سازی ناامن تبدیل می‌شود\nاگر از لینوکس استفاده می‌کنید، لطفاً مطمئن شوید که سرویس مخفی (gnome-keyring، kde-wallet، keepassxc و غیره) را نصب کرده‌اید.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index fbe5c335..06015964 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -249,7 +249,7 @@ "developers": "Développeurs", "not_logged_in": "Vous n'êtes pas connecté(e)", "search_mode": "Mode de recherche", - "youtube_api_type": "Type d'API", + "audio_source": "Source audio", "ok": "OK", "failed_to_encrypt": "Échec de la cryptage", "encryption_failed_warning": "Spotube utilise le cryptage pour stocker vos données en toute sécurité. Mais cela a échoué. Il basculera donc vers un stockage non sécurisé\nSi vous utilisez Linux, assurez-vous d'avoir installé des services secrets tels que gnome-keyring, kde-wallet et keepassxc", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index d33f41dc..adbc6c06 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -249,7 +249,7 @@ "developers": "डेवलपर्स", "not_logged_in": "आप लॉग इन नहीं हैं", "search_mode": "खोज मोड", - "youtube_api_type": "API प्रकार", + "audio_source": "ऑडियो स्रोत", "ok": "ठीक है", "failed_to_encrypt": "एन्क्रिप्ट करने में विफल रहा", "encryption_failed_warning": "Spotube आपके डेटा को सुरक्षित रूप से स्टोर करने के लिए एन्क्रिप्शन का उपयोग करता है। लेकिन इसमें विफल रहा। इसलिए, यह असुरक्षित स्टोरेज पर फॉलबैक करेगा\nयदि आप Linux का उपयोग कर रहे हैं, तो कृपया सुनिश्चित करें कि आपके पास gnome-keyring, kde-wallet, keepassxc आदि जैसी कोई सीक्रेट-सर्विस इंस्टॉल की गई है", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 50c9369f..a288cf0e 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -249,7 +249,7 @@ "developers": "開発", "not_logged_in": "ログインしていません", "search_mode": "検索モード", - "youtube_api_type": "APIの種類", + "audio_source": "音声ソース", "ok": "分かりました", "failed_to_encrypt": "暗号化に失敗しました", "encryption_failed_warning": "Spotubeはデータを安全に保存するために暗号化を使用しています。しかし、失敗しました。したがって、安全でないストレージにフォールバックします\nLinuxを使用している場合は、gnome-keyring、kde-wallet、keepassxcなどのシークレットサービスがインストールされていることを確認してください", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 1a946615..8c6147ad 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -249,7 +249,7 @@ "developers": "Developerzy", "not_logged_in": "Nie jesteś zalogowany", "search_mode": "Tryb szukania", - "youtube_api_type": "Typ API", + "audio_source": "Źródło dźwięku", "ok": "Ok", "failed_to_encrypt": "Nie można zaszyfrować :(", "encryption_failed_warning": "Spotube używa szyfrowania do bezpiecznego przechowywania danych. Ale nie udało się tego zrobić. Więc powróci do niezabezpieczonego przechowywania\nJeśli używasz Linuksa, upewnij się, że masz zainstalowane jakieś usługi do szyfrowania (gnome-keyring, kde-wallet, keepassxc itp.)", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 97df3db3..46c0a88e 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -249,7 +249,7 @@ "developers": "Desenvolvedores", "not_logged_in": "Você não está logado", "search_mode": "Modo de Busca", - "youtube_api_type": "Tipo de API", + "audio_source": "Fonte de Áudio", "ok": "Ok", "failed_to_encrypt": "Falha ao criptografar", "encryption_failed_warning": "O Spotube usa criptografia para armazenar seus dados com segurança, mas falhou em fazê-lo. Portanto, ele voltará para o armazenamento não seguro.\nSe você estiver usando o Linux, certifique-se de ter algum serviço secreto (gnome-keyring, kde-wallet, keepassxc, etc.) instalado", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 098e73c7..b1964f5f 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -249,7 +249,7 @@ "developers": "Разработчики", "not_logged_in": "Вы не выполнили вход", "search_mode": "Режим поиска", - "youtube_api_type": "Тип API", + "audio_source": "Источник аудио", "ok": "Ок", "failed_to_encrypt": "Не удалось зашифровать", "encryption_failed_warning": "Spotube использует шифрование для безопасного хранения ваших данных. Однако в этом случае произошла ошибка. Поэтому будет использовано небезопасное хранилище.\nЕсли вы используете Linux, убедитесь, что у вас установлен какой-либо инструмент для работы с секретами (gnome-keyring, kde-wallet, keepassxc и т.д.)", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 1d72ec85..fa0ea587 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -251,7 +251,7 @@ "developers": "Geliştiriciler", "not_logged_in": "Giriş yapmadınız", "search_mode": "Arama Modu", - "youtube_api_type": "API Türü", + "audio_source": "Ses Kaynağı", "ok": "Tamam", "failed_to_encrypt": "Şifreleme başarısız oldu", "encryption_failed_warning": "Spotube, verilerinizi güvenli bir şekilde depolamak için şifreleme kullanır. Ancak bunu başaramadı. Bu nedenle güvensiz bir depolamaya geri dönecektir. Linux kullanıyorsanız, lütfen gnome-keyring, kde-wallet, keepassxc vb. gibi bir güvenlik hizmetinizin kurulu olduğundan emin olun.", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index fa0877d1..22fc341e 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -251,7 +251,7 @@ "developers": "Розробники", "not_logged_in": "Ви не ввійшли в обліковий запис", "search_mode": "Режим пошуку", - "youtube_api_type": "Тип API", + "audio_source": "Джерело аудіо", "ok": "Гаразд", "failed_to_encrypt": "Не вдалося зашифрувати", "encryption_failed_warning": "Spotube використовує шифрування для безпечного зберігання ваших даних. Але не вдалося цього зробити. Тому він перейде до небезпечного зберігання\nЯкщо ви використовуєте Linux, переконайтеся, що у вас встановлено будь-який секретний сервіс (gnome-keyring, kde-wallet, keepassxc тощо)", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 9936c812..325216fa 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -249,7 +249,7 @@ "developers": "开发者", "not_logged_in": "你尚未登录", "search_mode": "搜索模式", - "youtube_api_type": "API 类型", + "audio_source": "音频源", "ok": "确定", "failed_to_encrypt": "加密失败", "encryption_failed_warning": "Spotube使用加密来安全地存储您的数据。但是失败了。因此,它将回退到不安全的存储\n如果您使用Linux,请确保已安装gnome-keyring、kde-wallet和keepassxc等秘密服务", diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index a0316b33..d36e0713 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -51,7 +51,7 @@ class SettingsPlaybackSection extends HookConsumerWidget { ), AdaptiveSelectTile( secondary: const Icon(SpotubeIcons.api), - title: Text(context.l10n.youtube_api_type), + title: Text(context.l10n.audio_source), value: preferences.audioSource, options: AudioSource.values .map((e) => DropdownMenuItem( diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 89bb8a6c..84943993 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -28,6 +28,8 @@ import 'package:spotube/services/discord/discord.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/services/sourced_track/sources/piped.dart'; +import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -161,8 +163,8 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier return; } try { - final isNotYTMode = preferences.audioSource != AudioSource.youtube || - (preferences.audioSource == AudioSource.piped && + final isNotYTMode = state.activeTrack is! YoutubeSourcedTrack && + (state.activeTrack is PipedSourcedTrack && preferences.searchMode == SearchMode.youtubeMusic); if (isNotYTMode || !preferences.skipNonMusic) return; From f86d5449168068e338f769d7f504d2146b86dc79 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 2 Dec 2023 22:20:22 +0600 Subject: [PATCH 072/131] fix: Incorrect "Artist" label/heading on Search Results Page #920 --- lib/pages/search/sections/artists.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/search/sections/artists.dart b/lib/pages/search/sections/artists.dart index b736bf13..fe4459d6 100644 --- a/lib/pages/search/sections/artists.dart +++ b/lib/pages/search/sections/artists.dart @@ -32,7 +32,7 @@ class SearchArtistsSection extends HookConsumerWidget { hasNextPage: query.hasNextPage, items: artists, onFetchMore: query.fetchNext, - title: Text(context.l10n.albums), + title: Text(context.l10n.artists), ); } } From b0beeca0cbaf810fae27832cff98cfda95715050 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 2 Dec 2023 22:41:52 +0600 Subject: [PATCH 073/131] feat: Add Go to Album option in track option #917 --- .../shared/track_tile/track_options.dart | 21 ++++++- lib/l10n/app_en.arb | 3 +- untranslated_messages.json | 62 ++++++++++++++++++- 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/shared/track_tile/track_options.dart index 9a587be6..8405d6ea 100644 --- a/lib/components/shared/track_tile/track_options.dart +++ b/lib/components/shared/track_tile/track_options.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -12,6 +13,7 @@ import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart import 'package:spotube/components/shared/dialogs/track_details_dialog.dart'; import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/authentication_provider.dart'; @@ -22,6 +24,7 @@ import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; enum TrackOptionValue { + album, share, addToPlaylist, addToQueue, @@ -79,9 +82,12 @@ class TrackOptions extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final scaffoldMessenger = ScaffoldMessenger.of(context); + final mediaQuery = MediaQuery.of(context); + final router = GoRouter.of(context); + final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playback = ref.watch(ProxyPlaylistNotifier.notifier); - final scaffoldMessenger = ScaffoldMessenger.of(context); final auth = ref.watch(AuthenticationNotifier.provider); ref.watch(downloadManagerProvider); final downloadManager = ref.watch(downloadManagerProvider.notifier); @@ -122,6 +128,12 @@ class TrackOptions extends HookConsumerWidget { final adaptivePopSheetList = AdaptivePopSheetList( onSelected: (value) async { switch (value) { + case TrackOptionValue.album: + await router.push( + '/album/${track.album!.id}', + extra: track.album!, + ); + break; case TrackOptionValue.delete: await File((track as LocalTrack).path).delete(); ref.refresh(localTracksProvider); @@ -233,6 +245,13 @@ class TrackOptions extends HookConsumerWidget { ) ], _ => [ + if (mediaQuery.smAndDown) + PopSheetEntry( + value: TrackOptionValue.album, + leading: const Icon(SpotubeIcons.album), + title: Text(context.l10n.go_to_album), + subtitle: Text(track.album!.name!), + ), if (!playlist.containsTrack(track)) ...[ PopSheetEntry( value: TrackOptionValue.addToQueue, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2ce5b8ca..5aded7d5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -279,5 +279,6 @@ "password": "Password", "login": "Login", "login_with_your_lastfm": "Login with your Last.fm account", - "scrobble_to_lastfm": "Scrobble to Last.fm" + "scrobble_to_lastfm": "Scrobble to Last.fm", + "go_to_album": "Go to Album" } \ No newline at end of file diff --git a/untranslated_messages.json b/untranslated_messages.json index 9e26dfee..0130c162 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1 +1,61 @@ -{} \ No newline at end of file +{ + "ar": [ + "go_to_album" + ], + + "bn": [ + "go_to_album" + ], + + "ca": [ + "go_to_album" + ], + + "de": [ + "go_to_album" + ], + + "es": [ + "go_to_album" + ], + + "fa": [ + "go_to_album" + ], + + "fr": [ + "go_to_album" + ], + + "hi": [ + "go_to_album" + ], + + "ja": [ + "go_to_album" + ], + + "pl": [ + "go_to_album" + ], + + "pt": [ + "go_to_album" + ], + + "ru": [ + "go_to_album" + ], + + "tr": [ + "go_to_album" + ], + + "uk": [ + "go_to_album" + ], + + "zh": [ + "go_to_album" + ] +} From 5f1df5a87d8fb7980b52cf57b7b6bedea57a1269 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 2 Dec 2023 22:49:29 +0600 Subject: [PATCH 074/131] fix: Playlist refresh not working #915 --- .../shared/tracks_view/track_view.dart | 23 +++++++++++-------- .../shared/tracks_view/track_view_props.dart | 9 ++++++-- lib/pages/playlist/liked_playlist.dart | 3 +++ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/components/shared/tracks_view/track_view.dart b/lib/components/shared/tracks_view/track_view.dart index a65bcff1..217aaed4 100644 --- a/lib/components/shared/tracks_view/track_view.dart +++ b/lib/components/shared/tracks_view/track_view.dart @@ -28,16 +28,19 @@ class TrackView extends HookConsumerWidget { ) : null, extendBodyBehindAppBar: true, - body: CustomScrollView( - slivers: [ - const TrackViewFlexHeader(), - SliverAnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: props.tracks.isEmpty - ? const ShimmerTrackTileGroup() - : const TrackViewBodySection(), - ), - ], + body: RefreshIndicator( + onRefresh: props.pagination.onRefresh, + child: CustomScrollView( + slivers: [ + const TrackViewFlexHeader(), + SliverAnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: props.tracks.isEmpty + ? const ShimmerTrackTileGroup() + : const TrackViewBodySection(), + ), + ], + ), ), ); } diff --git a/lib/components/shared/tracks_view/track_view_props.dart b/lib/components/shared/tracks_view/track_view_props.dart index 59c05db2..1c6c7647 100644 --- a/lib/components/shared/tracks_view/track_view_props.dart +++ b/lib/components/shared/tracks_view/track_view_props.dart @@ -6,6 +6,7 @@ class PaginationProps { final bool hasNextPage; final bool isLoading; final VoidCallback onFetchMore; + final Future Function() onRefresh; final Future> Function() onFetchAll; const PaginationProps({ @@ -13,6 +14,7 @@ class PaginationProps { required this.isLoading, required this.onFetchMore, required this.onFetchAll, + required this.onRefresh, }); factory PaginationProps.fromQuery( @@ -24,6 +26,7 @@ class PaginationProps { isLoading: query.isLoadingNextPage, onFetchMore: query.fetchNext, onFetchAll: onFetchAll, + onRefresh: query.refreshAll, ); } @@ -33,7 +36,8 @@ class PaginationProps { other.hasNextPage == hasNextPage && other.isLoading == isLoading && other.onFetchMore == onFetchMore && - other.onFetchAll == onFetchAll; + other.onFetchAll == onFetchAll && + other.onRefresh == onRefresh; } @override @@ -42,7 +46,8 @@ class PaginationProps { hasNextPage.hashCode ^ isLoading.hashCode ^ onFetchMore.hashCode ^ - onFetchAll.hashCode; + onFetchAll.hashCode ^ + onRefresh.hashCode; } class InheritedTrackView extends InheritedWidget { diff --git a/lib/pages/playlist/liked_playlist.dart b/lib/pages/playlist/liked_playlist.dart index 1f252ed4..5972a303 100644 --- a/lib/pages/playlist/liked_playlist.dart +++ b/lib/pages/playlist/liked_playlist.dart @@ -31,6 +31,9 @@ class LikedPlaylistPage extends HookConsumerWidget { onFetchAll: () async { return tracks.toList(); }, + onRefresh: () async { + await likedTracks.refresh(); + }, ), title: playlist.name!, description: playlist.description, From bb8f250f5f351c1a353791b77b25b9de7586191f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 2 Dec 2023 23:18:07 +0600 Subject: [PATCH 075/131] fix: doesn't minimize to tray when system title bar close button is used #866 --- .../shared/page_window_title_bar.dart | 17 +- .../configurators/use_close_behavior.dart | 32 +++ .../configurators/use_window_listener.dart | 197 ++++++++++++++++++ lib/main.dart | 6 + 4 files changed, 236 insertions(+), 16 deletions(-) create mode 100644 lib/hooks/configurators/use_close_behavior.dart create mode 100644 lib/hooks/configurators/use_window_listener.dart diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart index 43435f7d..d8e20184 100644 --- a/lib/components/shared/page_window_title_bar.dart +++ b/lib/components/shared/page_window_title_bar.dart @@ -11,16 +11,6 @@ import 'dart:io' show Platform, exit; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:local_notifier/local_notifier.dart'; -final closeNotification = DesktopTools.createNotification( - title: 'Spotube', - message: 'Running in background. Minimized to System Tray', - actions: [ - LocalNotificationAction(text: 'Close The App'), - ], -)?..onClickAction = (value) { - exit(0); - }; - class PageWindowTitleBar extends StatefulHookConsumerWidget implements PreferredSizeWidget { final Widget? leading; @@ -113,12 +103,7 @@ class WindowTitleBarButtons extends HookConsumerWidget { const type = ThemeType.auto; Future onClose() async { - if (preferences.closeBehavior == CloseBehavior.close) { - exit(0); - } else { - await DesktopTools.window.hide(); - await closeNotification?.show(); - } + await DesktopTools.window.close(); } useEffect(() { diff --git a/lib/hooks/configurators/use_close_behavior.dart b/lib/hooks/configurators/use_close_behavior.dart new file mode 100644 index 00000000..05c03fff --- /dev/null +++ b/lib/hooks/configurators/use_close_behavior.dart @@ -0,0 +1,32 @@ +import 'dart:io'; + +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/hooks/configurators/use_window_listener.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; +import 'package:local_notifier/local_notifier.dart'; + +final closeNotification = DesktopTools.createNotification( + title: 'Spotube', + message: 'Running in background. Minimized to System Tray', + actions: [ + LocalNotificationAction(text: 'Close The App'), + ], +)?..onClickAction = (value) { + exit(0); + }; + +void useCloseBehavior(WidgetRef ref) { + useWindowListener( + onWindowClose: () async { + final preferences = ref.read(userPreferencesProvider); + if (preferences.closeBehavior == CloseBehavior.minimizeToTray) { + await DesktopTools.window.hide(); + closeNotification?.show(); + } else { + exit(0); + } + }, + ); +} diff --git a/lib/hooks/configurators/use_window_listener.dart b/lib/hooks/configurators/use_window_listener.dart new file mode 100644 index 00000000..b91ad413 --- /dev/null +++ b/lib/hooks/configurators/use_window_listener.dart @@ -0,0 +1,197 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class CallbackWindowListener implements WindowListener { + final VoidCallback? _onWindowClose; + final VoidCallback? _onWindowFocus; + final VoidCallback? _onWindowBlur; + final VoidCallback? _onWindowMaximize; + final VoidCallback? _onWindowUnmaximize; + final VoidCallback? _onWindowMinimize; + final VoidCallback? _onWindowRestore; + final VoidCallback? _onWindowResize; + final VoidCallback? _onWindowResized; + final VoidCallback? _onWindowMove; + final VoidCallback? _onWindowMoved; + final VoidCallback? _onWindowEnterFullScreen; + final VoidCallback? _onWindowLeaveFullScreen; + final VoidCallback? _onWindowDocked; + final VoidCallback? _onWindowUndocked; + final VoidCallback? _onWindowEvent; + + const CallbackWindowListener({ + VoidCallback? onWindowClose, + VoidCallback? onWindowFocus, + VoidCallback? onWindowBlur, + VoidCallback? onWindowMaximize, + VoidCallback? onWindowUnmaximize, + VoidCallback? onWindowMinimize, + VoidCallback? onWindowRestore, + VoidCallback? onWindowResize, + VoidCallback? onWindowResized, + VoidCallback? onWindowMove, + VoidCallback? onWindowMoved, + VoidCallback? onWindowEnterFullScreen, + VoidCallback? onWindowLeaveFullScreen, + VoidCallback? onWindowDocked, + VoidCallback? onWindowUndocked, + VoidCallback? onWindowEvent, + }) : _onWindowClose = onWindowClose, + _onWindowFocus = onWindowFocus, + _onWindowBlur = onWindowBlur, + _onWindowMaximize = onWindowMaximize, + _onWindowUnmaximize = onWindowUnmaximize, + _onWindowMinimize = onWindowMinimize, + _onWindowRestore = onWindowRestore, + _onWindowResize = onWindowResize, + _onWindowResized = onWindowResized, + _onWindowMove = onWindowMove, + _onWindowMoved = onWindowMoved, + _onWindowEnterFullScreen = onWindowEnterFullScreen, + _onWindowLeaveFullScreen = onWindowLeaveFullScreen, + _onWindowDocked = onWindowDocked, + _onWindowUndocked = onWindowUndocked, + _onWindowEvent = onWindowEvent; + + @override + void onWindowBlur() { + return _onWindowBlur?.call(); + } + + @override + void onWindowClose() { + return _onWindowClose?.call(); + } + + @override + void onWindowDocked() { + return _onWindowDocked?.call(); + } + + @override + void onWindowEnterFullScreen() { + return _onWindowEnterFullScreen?.call(); + } + + @override + void onWindowEvent(String eventName) { + return _onWindowEvent?.call(); + } + + @override + void onWindowFocus() { + return _onWindowFocus?.call(); + } + + @override + void onWindowLeaveFullScreen() { + return _onWindowLeaveFullScreen?.call(); + } + + @override + void onWindowMaximize() { + return _onWindowMaximize?.call(); + } + + @override + void onWindowMinimize() { + return _onWindowMinimize?.call(); + } + + @override + void onWindowMove() { + return _onWindowMove?.call(); + } + + @override + void onWindowMoved() { + return _onWindowMoved?.call(); + } + + @override + void onWindowResize() { + return _onWindowResize?.call(); + } + + @override + void onWindowResized() { + return _onWindowResized?.call(); + } + + @override + void onWindowRestore() { + return _onWindowRestore?.call(); + } + + @override + void onWindowUndocked() { + return _onWindowUndocked?.call(); + } + + @override + void onWindowUnmaximize() { + return _onWindowUnmaximize?.call(); + } +} + +void useWindowListener({ + VoidCallback? onWindowClose, + VoidCallback? onWindowFocus, + VoidCallback? onWindowBlur, + VoidCallback? onWindowMaximize, + VoidCallback? onWindowUnmaximize, + VoidCallback? onWindowMinimize, + VoidCallback? onWindowRestore, + VoidCallback? onWindowResize, + VoidCallback? onWindowResized, + VoidCallback? onWindowMove, + VoidCallback? onWindowMoved, + VoidCallback? onWindowEnterFullScreen, + VoidCallback? onWindowLeaveFullScreen, + VoidCallback? onWindowDocked, + VoidCallback? onWindowUndocked, + VoidCallback? onWindowEvent, +}) { + useEffect(() { + final listener = CallbackWindowListener( + onWindowClose: onWindowClose, + onWindowFocus: onWindowFocus, + onWindowBlur: onWindowBlur, + onWindowMaximize: onWindowMaximize, + onWindowUnmaximize: onWindowUnmaximize, + onWindowMinimize: onWindowMinimize, + onWindowRestore: onWindowRestore, + onWindowResize: onWindowResize, + onWindowResized: onWindowResized, + onWindowMove: onWindowMove, + onWindowMoved: onWindowMoved, + onWindowEnterFullScreen: onWindowEnterFullScreen, + onWindowLeaveFullScreen: onWindowLeaveFullScreen, + onWindowDocked: onWindowDocked, + onWindowUndocked: onWindowUndocked, + onWindowEvent: onWindowEvent, + ); + DesktopTools.window.addListener(listener); + return () { + DesktopTools.window.removeListener(listener); + }; + }, [ + onWindowClose, + onWindowFocus, + onWindowBlur, + onWindowMaximize, + onWindowUnmaximize, + onWindowMinimize, + onWindowRestore, + onWindowResize, + onWindowResized, + onWindowMove, + onWindowMoved, + onWindowEnterFullScreen, + onWindowLeaveFullScreen, + onWindowDocked, + onWindowUndocked, + onWindowEvent, + ]); +} diff --git a/lib/main.dart b/lib/main.dart index 24ba81b4..91ec789d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'package:metadata_god/metadata_god.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/intents.dart'; +import 'package:spotube/hooks/configurators/use_close_behavior.dart'; import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart'; import 'package:spotube/hooks/configurators/use_get_storage_perms.dart'; import 'package:spotube/l10n/l10n.dart'; @@ -49,6 +50,10 @@ Future main(List rawArgs) async { await FlutterDisplayMode.setHighRefreshRate(); } + if (DesktopTools.platform.isDesktop) { + await DesktopTools.window.setPreventClose(true); + } + await DesktopTools.ensureInitialized( DesktopWindowOptions( hideTitleBar: true, @@ -177,6 +182,7 @@ class SpotubeState extends ConsumerState { ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); useInitSysTray(ref); + useCloseBehavior(ref); useEffect(() { FlutterNativeSplash.remove(); From 5b659ce8c974f611fc04509b4cc45329bd8f1120 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 2 Dec 2023 23:27:33 +0600 Subject: [PATCH 076/131] cd: upgrade flutter_distributor --- .github/workflows/spotube-release-binary.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index d57cc0e8..e4d2e860 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -33,11 +33,6 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v4 - - uses: actions/checkout@v4 - with: - repository: KRTirtho/flutter_distributor - path: flutter_distributor - ref: fix-windows-build - uses: subosito/flutter-action@v2.10.0 with: cache: true @@ -79,10 +74,9 @@ jobs: - name: Build Windows Executable run: | - dart pub global activate melos - cd flutter_distributor && melos bs && cd .. + dart pub global activate flutter_distributor make innoinstall - dart run ./flutter_distributor/packages/flutter_distributor/bin/main.dart package --platform=windows --targets=exe --skip-clean + flutter_distributor package --platform=windows --targets=exe --skip-clean mv dist/**/spotube-*-windows-setup.exe dist/Spotube-windows-x86_64-setup.exe - name: Create Chocolatey Package and set hash From 2ceb6a8e53d68c32d97d5b195add03cd6e1334a2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 3 Dec 2023 23:00:25 +0600 Subject: [PATCH 077/131] cd: remove appimage --- .github/workflows/spotube-release-binary.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index e4d2e860..f5cc65ee 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -163,7 +163,6 @@ jobs: dart pub global activate flutter_distributor alias dpkg-deb="dpkg-deb --Zxz" flutter_distributor package --platform=linux --targets=deb - flutter_distributor package --platform=linux --targets=appimage flutter_distributor package --platform=linux --targets=rpm - name: Create tar.xz (stable) @@ -179,7 +178,6 @@ jobs: mv build/spotube-linux-*-x86_64.tar.xz dist/ mv dist/**/spotube-*-linux.deb dist/Spotube-linux-x86_64.deb mv dist/**/spotube-*-linux.rpm dist/Spotube-linux-x86_64.rpm - mv dist/**/spotube-*-linux.AppImage dist/Spotube-linux-x86_64.AppImage - uses: actions/upload-artifact@v3 @@ -187,7 +185,6 @@ jobs: if-no-files-found: error name: Spotube-Release-Binaries path: | - dist/Spotube-linux-x86_64.AppImage dist/Spotube-linux-x86_64.deb dist/Spotube-linux-x86_64.rpm dist/spotube-linux-${{ env.BUILD_VERSION }}-x86_64.tar.xz From b92583d0df7b8dee0d121cd2bb666b14c77d8c86 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 4 Dec 2023 22:20:47 +0600 Subject: [PATCH 078/131] feat: improve loading animations --- lib/collections/fake.dart | 161 ++++++++++++++ lib/components/artist/artist_card.dart | 15 +- lib/components/library/user_albums.dart | 53 ++--- lib/components/library/user_artists.dart | 21 +- lib/components/library/user_local_tracks.dart | 61 +++-- lib/components/library/user_playlists.dart | 18 +- .../horizontal_playbutton_card_view.dart | 60 +++-- lib/components/shared/playbutton_card.dart | 37 ++-- .../shimmers/shimmer_artist_profile.dart | 57 ----- .../shared/shimmers/shimmer_categories.dart | 53 ----- .../shared/shimmers/shimmer_lyrics.dart | 81 +++---- .../shimmers/shimmer_playbutton_card.dart | 119 ---------- .../shared/shimmers/shimmer_track_tile.dart | 123 ----------- .../sections/body/track_view_body.dart | 20 +- .../shared/tracks_view/track_view.dart | 11 +- lib/pages/artist/artist.dart | 67 +++--- lib/pages/artist/section/header.dart | 208 +++++++++--------- lib/pages/artist/section/top_tracks.dart | 19 +- lib/pages/home/genres.dart | 32 ++- lib/pages/home/personalized.dart | 52 ++--- lib/pages/lyrics/synced_lyrics.dart | 6 +- pubspec.lock | 8 + pubspec.yaml | 1 + 23 files changed, 583 insertions(+), 700 deletions(-) create mode 100644 lib/collections/fake.dart delete mode 100644 lib/components/shared/shimmers/shimmer_artist_profile.dart delete mode 100644 lib/components/shared/shimmers/shimmer_categories.dart delete mode 100644 lib/components/shared/shimmers/shimmer_playbutton_card.dart delete mode 100644 lib/components/shared/shimmers/shimmer_track_tile.dart diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart new file mode 100644 index 00000000..a02e8587 --- /dev/null +++ b/lib/collections/fake.dart @@ -0,0 +1,161 @@ +import 'package:spotify/spotify.dart'; +import 'package:spotube/extensions/track.dart'; + +abstract class FakeData { + static final Image image = Image() + ..height = 1 + ..width = 1 + ..url = "url"; + + static final Followers followers = Followers() + ..href = "text" + ..total = 1; + + static final Artist artist = Artist() + ..id = "1" + ..name = "Wow artist Good!" + ..images = [image] + ..popularity = 1 + ..type = "type" + ..uri = "uri" + ..externalUrls = externalUrls + ..genres = ["genre"] + ..href = "text" + ..followers = followers; + + static final externalIds = ExternalIds() + ..isrc = "text" + ..ean = "text" + ..upc = "text"; + + static final externalUrls = ExternalUrls()..spotify = "text"; + + static final Album album = Album() + ..id = "1" + ..genres = ["genre"] + ..label = "label" + ..popularity = 1 + ..albumType = AlbumType.album + ..artists = [artist] + ..availableMarkets = [Market.BD] + ..externalUrls = externalUrls + ..href = "text" + ..images = [image] + ..name = "Another good album" + ..releaseDate = "2021-01-01" + ..releaseDatePrecision = DatePrecision.day + ..tracks = [track] + ..type = "type" + ..uri = "uri" + ..externalIds = externalIds + ..copyrights = [ + Copyright() + ..type = CopyrightType.C + ..text = "text", + ]; + + static final ArtistSimple artistSimple = ArtistSimple() + ..id = "1" + ..name = "What an artist" + ..type = "type" + ..uri = "uri" + ..externalUrls = externalUrls; + + static final AlbumSimple albumSimple = AlbumSimple() + ..id = "1" + ..albumType = AlbumType.album + ..artists = [artistSimple] + ..availableMarkets = [Market.BD] + ..externalUrls = externalUrls + ..href = "text" + ..images = [image] + ..name = "A good album" + ..releaseDate = "2021-01-01" + ..releaseDatePrecision = DatePrecision.day + ..type = "type" + ..uri = "uri"; + + static final Track track = Track() + ..id = "1" + ..artists = [artist, artist, artist] + ..album = albumSimple + ..availableMarkets = [Market.BD] + ..discNumber = 1 + ..durationMs = 50000 + ..explicit = false + ..externalUrls = externalUrls + ..href = "text" + ..name = "A Track Name" + ..popularity = 1 + ..previewUrl = "url" + ..trackNumber = 1 + ..type = "type" + ..uri = "uri" + ..isPlayable = true + ..explicit = false + ..linkedFrom = trackLink; + + static final TrackLink trackLink = TrackLink() + ..id = "1" + ..type = "type" + ..uri = "uri" + ..externalUrls = {"spotify": "text"} + ..href = "text"; + + static final Paging paging = Paging() + ..href = "text" + ..itemsNative = [track.toJson()] + ..limit = 1 + ..next = "text" + ..offset = 1 + ..previous = "text" + ..total = 1; + + static final User user = User() + ..id = "1" + ..displayName = "Your Name" + ..birthdate = "2021-01-01" + ..country = Market.BD + ..email = "test@email.com" + ..followers = followers + ..href = "text" + ..images = [image] + ..type = "type" + ..uri = "uri"; + + static final TracksLink tracksLink = TracksLink() + ..href = "text" + ..total = 1; + + static final Playlist playlist = Playlist() + ..id = "1" + ..collaborative = false + ..description = "A very good playlist description" + ..externalUrls = externalUrls + ..followers = followers + ..href = "text" + ..images = [image] + ..name = "A good playlist" + ..owner = user + ..public = true + ..snapshotId = "text" + ..tracks = paging + ..tracksLink = tracksLink + ..type = "type" + ..uri = "uri"; + + static final PlaylistSimple playlistSimple = PlaylistSimple() + ..id = "1" + ..collaborative = false + ..externalUrls = externalUrls + ..href = "text" + ..images = [image] + ..name = "A good playlist" + ..owner = user + ..public = true + ..snapshotId = "text" + ..tracksLink = tracksLink + ..type = "type" + ..description = "A very good playlist description" + ..uri = "uri"; +} diff --git a/lib/components/artist/artist_card.dart b/lib/components/artist/artist_card.dart index 434b90ad..3526e88f 100644 --- a/lib/components/artist/artist_card.dart +++ b/lib/components/artist/artist_card.dart @@ -1,6 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; @@ -91,12 +92,14 @@ class ArtistCard extends HookConsumerWidget { decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(50)), - child: Text( - context.l10n.artist, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, + child: Skeleton.ignore( + child: Text( + context.l10n.artist, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), ), ), diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index ccde43f9..49243870 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -3,12 +3,13 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/album/album_card.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/context.dart'; @@ -82,30 +83,32 @@ class UserAlbums extends HookConsumerWidget { child: SingleChildScrollView( padding: const EdgeInsets.all(8.0), controller: controller, - child: Wrap( - runSpacing: 20, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - if (albums.isEmpty) - Container( - alignment: Alignment.topLeft, - padding: const EdgeInsets.all(16.0), - child: const ShimmerPlaybuttonCard(count: 4), - ), - for (final album in albums) - AlbumCard( - TypeConversionUtils.simpleAlbum_X_Album(album), - ), - if (albumsQuery.hasNextPage) - Waypoint( - controller: controller, - isGrid: true, - onTouchEdge: albumsQuery.fetchNext, - child: const ShimmerPlaybuttonCard(count: 1), - ) - ], + child: Skeletonizer( + enabled: albums.isEmpty, + child: Wrap( + runSpacing: 20, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (albums.isEmpty) + ...List.generate( + 10, + (index) => AlbumCard(FakeData.album), + ), + for (final album in albums) + AlbumCard( + TypeConversionUtils.simpleAlbum_X_Album(album), + ), + if (albums.isNotEmpty && albumsQuery.hasNextPage) + Waypoint( + controller: controller, + isGrid: true, + onTouchEdge: albumsQuery.fetchNext, + child: AlbumCard(FakeData.album), + ) + ], + ), ), ), ), diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index 881451b0..7269d7eb 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -3,6 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; @@ -87,12 +89,19 @@ class UserArtists extends HookConsumerWidget { width: double.infinity, child: SafeArea( child: Center( - child: Wrap( - spacing: 15, - runSpacing: 5, - children: filteredArtists - .mapIndexed((index, artist) => ArtistCard(artist)) - .toList(), + child: Skeletonizer( + enabled: artistQuery.isLoading, + child: Wrap( + spacing: 15, + runSpacing: 5, + children: artistQuery.isLoading + ? List.generate( + 10, (index) => ArtistCard(FakeData.artist)) + : filteredArtists + .mapIndexed( + (index, artist) => ArtistCard(artist)) + .toList(), + ), ), ), ), diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index cc8b10cf..fcaada9e 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -11,12 +11,13 @@ import 'package:metadata_god/metadata_god.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; @@ -261,32 +262,48 @@ class UserLocalTracks extends HookConsumerWidget { }, child: InterScrollbar( controller: controller, - child: ListView.builder( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: filteredTracks.length, - itemBuilder: (context, index) { - final track = filteredTracks[index]; - return TrackTile( - index: index, - track: track, - userPlaylist: false, - onTap: () async { - await playLocalTracks( - ref, - sortedTracks, - currentTrack: track, - ); - }, - ); - }, + child: Skeletonizer( + enabled: trackSnapshot.isLoading, + child: ListView.builder( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: + trackSnapshot.isLoading ? 5 : filteredTracks.length, + itemBuilder: (context, index) { + if (trackSnapshot.isLoading) { + return TrackTile(track: FakeData.track, index: index); + } + + final track = filteredTracks[index]; + return TrackTile( + index: index, + track: track, + userPlaylist: false, + onTap: () async { + await playLocalTracks( + ref, + sortedTracks, + currentTrack: track, + ); + }, + ); + }, + ), ), ), ), ); }, - loading: () => - const Expanded(child: ShimmerTrackTileGroup(noSliver: true)), + loading: () => Expanded( + child: Skeletonizer( + enabled: true, + child: ListView.builder( + itemCount: 5, + itemBuilder: (context, index) => + TrackTile(track: FakeData.track, index: index), + ), + ), + ), error: (error, stackTrace) => Text(error.toString() + stackTrace.toString()), ) diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index f7736ca7..a65c6d0e 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -1,16 +1,16 @@ 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'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/fake.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'; import 'package:spotube/components/shared/waypoint.dart'; @@ -123,7 +123,7 @@ class UserPlaylists extends HookConsumerWidget { ), SliverLayoutBuilder(builder: (context, constrains) { return SliverGrid.builder( - itemCount: playlists.length + 1, + itemCount: playlists.isEmpty ? 6 : playlists.length + 1, gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 200, mainAxisExtent: constrains.smAndDown ? 225 : 250, @@ -131,7 +131,7 @@ class UserPlaylists extends HookConsumerWidget { mainAxisSpacing: 8, ), itemBuilder: (context, index) { - if (index == playlists.length) { + if (playlists.isNotEmpty && index == playlists.length) { if (!playlistsQuery.hasNextPage) { return const SizedBox.shrink(); } @@ -140,11 +140,17 @@ class UserPlaylists extends HookConsumerWidget { controller: controller, isGrid: true, onTouchEdge: playlistsQuery.fetchNext, - child: const ShimmerPlaybuttonCard(count: 1), + child: Skeletonizer( + enabled: true, + child: PlaylistCard(FakeData.playlistSimple), + ), ); } - return PlaylistCard(playlists[index]); + return PlaylistCard( + playlists.elementAtOrNull(index) ?? + FakeData.playlistSimple, + ); }, ); }) 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 index dca77233..d00e5c4b 100644 --- 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 @@ -2,11 +2,12 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/fake.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/hooks/utils/use_breakpoint_value.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; @@ -61,30 +62,41 @@ class HorizontalPlaybuttonCardView extends HookWidget { PointerDeviceKind.mouse, }, ), - child: InfiniteList( - scrollController: scrollController, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(vertical: 8.0), - itemCount: items.length, - onFetchData: onFetchMore, - loadingBuilder: (context) => const ShimmerPlaybuttonCard(), - emptyBuilder: (context) => - const ShimmerPlaybuttonCard(count: 5), - isLoading: isLoadingNextPage, - hasReachedMax: !hasNextPage, - itemBuilder: (context, index) { - final item = items[index]; + child: items.isEmpty + ? ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: 5, + itemBuilder: (context, index) { + return AlbumCard(FakeData.albumSimple); + }, + ) + : InfiniteList( + scrollController: scrollController, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(vertical: 8.0), + itemCount: items.length, + onFetchData: onFetchMore, + loadingBuilder: (context) => Skeletonizer( + enabled: true, + child: AlbumCard(FakeData.albumSimple), + ), + isLoading: isLoadingNextPage, + hasReachedMax: !hasNextPage, + itemBuilder: (context, index) { + 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(), - }; - }), + 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/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index d9c48640..60db648b 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -1,6 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -146,14 +147,16 @@ class PlaybuttonCard extends HookWidget { mainAxisSize: MainAxisSize.min, children: [ if (!isPlaying) - IconButton( - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.background, - foregroundColor: theme.colorScheme.primary, - minimumSize: const Size.square(10), + Skeleton.keep( + child: 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, ), - icon: const Icon(SpotubeIcons.queueAdd), - onPressed: isLoading ? null : onAddToQueuePressed, ), const SizedBox(height: 5), IconButton( @@ -162,15 +165,17 @@ class PlaybuttonCard extends HookWidget { 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), + icon: Skeleton.keep( + child: 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, ), ], diff --git a/lib/components/shared/shimmers/shimmer_artist_profile.dart b/lib/components/shared/shimmers/shimmer_artist_profile.dart deleted file mode 100644 index 75e50cd0..00000000 --- a/lib/components/shared/shimmers/shimmer_artist_profile.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; - -import 'package:skeleton_text/skeleton_text.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; -import 'package:spotube/extensions/theme.dart'; -import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; - -class ShimmerArtistProfile extends HookWidget { - const ShimmerArtistProfile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final shimmerTheme = ShimmerColorTheme( - shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200], - shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300], - ); - final shimmerColor = shimmerTheme.shimmerColor ?? Colors.white; - final shimmerBackgroundColor = - shimmerTheme.shimmerBackgroundColor ?? Colors.grey; - - final avatarWidth = useBreakpointValue( - xs: MediaQuery.of(context).size.width * 0.80, - sm: MediaQuery.of(context).size.width * 0.80, - md: MediaQuery.of(context).size.width * 0.50, - lg: MediaQuery.of(context).size.width * 0.30, - xl: MediaQuery.of(context).size.width * 0.30, - xxl: MediaQuery.of(context).size.width * 0.30, - ) ?? - 0; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(20), - child: SkeletonAnimation( - shimmerColor: shimmerColor, - borderRadius: BorderRadius.circular(avatarWidth), - shimmerDuration: 1000, - child: Container( - width: avatarWidth, - height: avatarWidth, - decoration: BoxDecoration( - color: shimmerBackgroundColor, - borderRadius: BorderRadius.circular(avatarWidth), - ), - ), - ), - ), - const SizedBox(width: 10), - const Flexible(child: ShimmerTrackTileGroup(noSliver: true)), - ], - ); - } -} diff --git a/lib/components/shared/shimmers/shimmer_categories.dart b/lib/components/shared/shimmers/shimmer_categories.dart deleted file mode 100644 index 9bc773da..00000000 --- a/lib/components/shared/shimmers/shimmer_categories.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; - -import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; -import 'package:spotube/extensions/theme.dart'; -import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; - -class ShimmerCategories extends HookWidget { - const ShimmerCategories({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final shimmerTheme = ShimmerColorTheme( - shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200], - ); - final shimmerBackgroundColor = - shimmerTheme.shimmerBackgroundColor ?? Colors.grey; - - final shimmerCount = useBreakpointValue( - xs: 2, - sm: 2, - md: 3, - lg: 3, - xl: 6, - xxl: 8, - ); - - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.only(left: 15), - height: 10, - width: 100, - decoration: BoxDecoration( - color: shimmerBackgroundColor, - borderRadius: BorderRadius.circular(10), - ), - ), - const SizedBox(height: 10), - Align( - alignment: Alignment.topLeft, - child: ShimmerPlaybuttonCard(count: shimmerCount), - ), - ], - ), - ); - } -} diff --git a/lib/components/shared/shimmers/shimmer_lyrics.dart b/lib/components/shared/shimmers/shimmer_lyrics.dart index b0fba340..b225c008 100644 --- a/lib/components/shared/shimmers/shimmer_lyrics.dart +++ b/lib/components/shared/shimmers/shimmer_lyrics.dart @@ -1,69 +1,38 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; -import 'package:skeleton_text/skeleton_text.dart'; -import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/extensions/theme.dart'; - -const widths = [20, 56, 89, 60, 25, 69]; +import 'package:skeletonizer/skeletonizer.dart'; class ShimmerLyrics extends HookWidget { const ShimmerLyrics({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final shimmerTheme = ShimmerColorTheme( - shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200], - shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300], - ); - final shimmerColor = shimmerTheme.shimmerColor ?? Colors.white; - final shimmerBackgroundColor = - shimmerTheme.shimmerBackgroundColor ?? Colors.grey; - - final mediaQuery = MediaQuery.of(context); - - return ListView.builder( - itemCount: 20, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - final widthsCp = [...widths]; - if (mediaQuery.isMd) { - widthsCp.removeLast(); - } - if (mediaQuery.smAndDown) { - widthsCp.removeLast(); - widthsCp.removeLast(); - } - widthsCp.shuffle(); - return Container( - margin: const EdgeInsets.symmetric(vertical: 5), - child: Row( + return Skeletonizer( + enabled: true, + child: ListView.builder( + itemCount: 30, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + final texts = [ + "Lorem ipsum", + "consectetur.", + "Sed", + "Sed non risus", + ]..shuffle(); + return Row( mainAxisAlignment: MainAxisAlignment.center, - children: widthsCp.map( - (width) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SkeletonAnimation( - shimmerColor: shimmerColor, - shimmerDuration: 1000, - child: Container( - height: 10, - width: width.toDouble(), - decoration: BoxDecoration( - color: shimmerBackgroundColor, - borderRadius: BorderRadius.circular(10), - ), - margin: const EdgeInsets.only(top: 10), - ), - ), - ); - }, - ).toList(), - ), - ); - }, + children: [ + for (final text in texts) ...[ + Text(text), + if (text != texts.last) const Gap(10), + ], + ], + ); + }, + ), ); } } diff --git a/lib/components/shared/shimmers/shimmer_playbutton_card.dart b/lib/components/shared/shimmers/shimmer_playbutton_card.dart deleted file mode 100644 index 2259c9b0..00000000 --- a/lib/components/shared/shimmers/shimmer_playbutton_card.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; - -import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; - -class ShimmerPlaybuttonCardPainter extends CustomPainter { - final Color background; - final Color foreground; - ShimmerPlaybuttonCardPainter({ - required this.background, - required this.foreground, - }); - - @override - void paint(Canvas canvas, Size size) { - const radius = Radius.circular(15); - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(0, 0, size.width, size.height), - radius, - ), - Paint()..color = background, - ); - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(8, 8, size.width - 16, size.height - 90), - radius, - ), - Paint()..color = foreground, - ); - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(12, size.height - 67, size.width / 2, 10), - radius, - ), - Paint()..color = foreground, - ); - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(12, size.height - 45, size.width - 24, 8), - radius, - ), - Paint()..color = foreground, - ); - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(12, size.height - 30, size.width * .4, 8), - radius, - ), - Paint()..color = foreground, - ); - - canvas.drawCircle( - Offset(size.width * .85, size.height * .50), - 17, - Paint()..color = background, - ); - - canvas.drawCircle( - Offset(size.width * .85, size.height * .67), - 17, - Paint()..color = background, - ); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return false; - } -} - -class ShimmerPlaybuttonCard extends HookWidget { - final int count; - - const ShimmerPlaybuttonCard({ - Key? key, - this.count = 1, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final Size size = useBreakpointValue( - xs: const Size(130, 200), - sm: const Size(130, 200), - md: const Size(150, 220), - others: const Size(170, 240), - ); - - final isDark = theme.brightness == Brightness.dark; - final bgColor = theme.colorScheme.surfaceVariant.withOpacity(.2); - final fgColor = Color.lerp( - theme.colorScheme.surfaceVariant, - isDark ? Colors.black : Colors.white, - .4, - ); - - return Wrap( - spacing: 20, - runSpacing: 20, - children: [ - for (var i = 0; i < count; i++) ...[ - CustomPaint( - size: size, - painter: ShimmerPlaybuttonCardPainter( - background: bgColor, - foreground: fgColor!, - ), - ), - ] - ], - ); - } -} diff --git a/lib/components/shared/shimmers/shimmer_track_tile.dart b/lib/components/shared/shimmers/shimmer_track_tile.dart deleted file mode 100644 index dcb634ed..00000000 --- a/lib/components/shared/shimmers/shimmer_track_tile.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:spotube/extensions/theme.dart'; - -class ShimmerTrackTilePainter extends CustomPainter { - final Color background; - final Color foreground; - ShimmerTrackTilePainter({ - required this.background, - required this.foreground, - }); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = background - ..style = PaintingStyle.fill; - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(0, 0, size.width, size.height), - const Radius.circular(5), - ), - paint, - ); - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(0, 0, size.height, size.height), - const Radius.circular(5), - ), - Paint()..color = foreground, - ); - - canvas.drawRRect( - RRect.fromRectAndRadius( - const Rect.fromLTWH(70, 10, 100, 10), - const Radius.circular(5), - ), - Paint()..color = foreground, - ); - - // draw Icons.play - const icon = Icons.play_arrow_outlined; - TextPainter textPainter = TextPainter(textDirection: TextDirection.rtl); - textPainter.text = TextSpan( - text: String.fromCharCode(icon.codePoint), - style: TextStyle( - fontSize: 40.0, - fontFamily: icon.fontFamily, - color: background, - ), - ); - textPainter.layout(); - textPainter.paint(canvas, const Offset(10, 10)); - - canvas.drawRRect( - RRect.fromRectAndRadius( - const Rect.fromLTWH(70, 30, 170, 7), - const Radius.circular(5), - ), - Paint()..color = foreground, - ); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return false; - } -} - -class ShimmerTrackTile extends StatelessWidget { - const ShimmerTrackTile({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - final shimmerTheme = ShimmerColorTheme( - shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200], - shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300], - ); - - return Padding( - padding: const EdgeInsets.only(bottom: 8.0, left: 8, right: 8), - child: CustomPaint( - size: const Size(double.infinity, 60), - painter: ShimmerTrackTilePainter( - background: shimmerTheme.shimmerBackgroundColor ?? - theme.scaffoldBackgroundColor, - foreground: shimmerTheme.shimmerColor ?? theme.cardColor, - ), - ), - ); - } -} - -class ShimmerTrackTileGroup extends StatelessWidget { - final bool noSliver; - final int count; - const ShimmerTrackTileGroup({ - super.key, - this.noSliver = false, - this.count = 5, - }); - - @override - Widget build(BuildContext context) { - if (noSliver) { - return ListView.builder( - itemCount: 5, - itemBuilder: (context, index) => const ShimmerTrackTile(), - ); - } - - return SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) => const ShimmerTrackTile(), - childCount: count, - ), - ); - } -} diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/shared/tracks_view/sections/body/track_view_body.dart index d77a3e6f..b7149cc2 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_body.dart @@ -4,9 +4,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/fake.dart'; import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body_headers.dart'; import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart'; @@ -84,7 +85,22 @@ class TrackViewBodySection extends HookConsumerWidget { onFetchData: props.pagination.onFetchMore, isLoading: props.pagination.isLoading, hasReachedMax: !props.pagination.hasNextPage, - loadingBuilder: (context) => const ShimmerTrackTile(), + loadingBuilder: (context) => Skeletonizer( + enabled: true, + child: TrackTile( + track: FakeData.track, + index: 0, + ), + ), + emptyBuilder: (context) => Skeletonizer( + enabled: true, + child: Column( + children: List.generate( + 10, + (index) => TrackTile(track: FakeData.track, index: index), + ), + ), + ), itemBuilder: (context, index) { final track = tracks[index]; return TrackTile( diff --git a/lib/components/shared/tracks_view/track_view.dart b/lib/components/shared/tracks_view/track_view.dart index 217aaed4..a1a2d48b 100644 --- a/lib/components/shared/tracks_view/track_view.dart +++ b/lib/components/shared/tracks_view/track_view.dart @@ -3,7 +3,6 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart'; import 'package:spotube/components/shared/tracks_view/sections/header/flexible_header.dart'; import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; @@ -30,14 +29,12 @@ class TrackView extends HookConsumerWidget { extendBodyBehindAppBar: true, body: RefreshIndicator( onRefresh: props.pagination.onRefresh, - child: CustomScrollView( + child: const CustomScrollView( slivers: [ - const TrackViewFlexHeader(), + TrackViewFlexHeader(), SliverAnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: props.tracks.isEmpty - ? const ShimmerTrackTileGroup() - : const TrackViewBodySection(), + duration: Duration(milliseconds: 500), + child: TrackViewBodySection(), ), ], ), diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 693e825b..92470397 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/artist/artist_album_list.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_artist_profile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/pages/artist/section/footer.dart'; @@ -35,45 +35,46 @@ class ArtistPage extends HookConsumerWidget { ), extendBodyBehindAppBar: true, body: Builder(builder: (context) { - if (artistQuery.isLoading || !artistQuery.hasData) { - const ShimmerArtistProfile(); - } else if (artistQuery.hasError) { + if (artistQuery.hasError) { return Center(child: Text(artistQuery.error.toString())); } - return CustomScrollView( - controller: scrollController, - slivers: [ - SliverToBoxAdapter( - child: SafeArea( - bottom: false, - child: ArtistPageHeader(artistId: artistId), - ), - ), - const SliverGap(50), - ArtistPageTopTracks(artistId: artistId), - const SliverGap(50), - SliverToBoxAdapter(child: ArtistAlbumList(artistId)), - const SliverGap(20), - SliverPadding( - padding: const EdgeInsets.all(8.0), - sliver: SliverToBoxAdapter( - child: Text( - context.l10n.fans_also_like, - style: theme.textTheme.headlineSmall, + return Skeletonizer( + enabled: artistQuery.isLoading, + child: CustomScrollView( + controller: scrollController, + slivers: [ + SliverToBoxAdapter( + child: SafeArea( + bottom: false, + child: ArtistPageHeader(artistId: artistId), ), ), - ), - SliverSafeArea( - sliver: ArtistPageRelatedArtists(artistId: artistId), - ), - if (artistQuery.data != null) - SliverSafeArea( - top: false, + const SliverGap(50), + ArtistPageTopTracks(artistId: artistId), + const SliverGap(50), + SliverToBoxAdapter(child: ArtistAlbumList(artistId)), + const SliverGap(20), + SliverPadding( + padding: const EdgeInsets.all(8.0), sliver: SliverToBoxAdapter( - child: ArtistPageFooter(artist: artistQuery.data!), + child: Text( + context.l10n.fans_also_like, + style: theme.textTheme.headlineSmall, + ), ), ), - ], + SliverSafeArea( + sliver: ArtistPageRelatedArtists(artistId: artistId), + ), + if (artistQuery.data != null) + SliverSafeArea( + top: false, + sliver: SliverToBoxAdapter( + child: ArtistPageFooter(artist: artistQuery.data!), + ), + ), + ], + ), ); }), ), diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index 9fc9d78e..7cee7a01 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -4,7 +4,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; @@ -25,7 +27,7 @@ class ArtistPageHeader extends HookConsumerWidget { Widget build(BuildContext context, ref) { final queryClient = useQueryClient(); final artistQuery = useQueries.artist.get(ref, artistId); - final artist = artistQuery.data; + final artist = artistQuery.data ?? FakeData.artist; final scaffoldMessenger = ScaffoldMessenger.of(context); final mediaQuery = MediaQuery.of(context); @@ -41,10 +43,6 @@ class ArtistPageHeader extends HookConsumerWidget { xxl: textTheme.titleMedium, ); - if (artist == null) { - return const SizedBox.shrink(); - } - final spotify = ref.read(spotifyProvider); final auth = ref.watch(AuthenticationNotifier.provider); final blacklist = ref.watch(BlackListNotifier.provider); @@ -96,10 +94,12 @@ class ArtistPageHeader extends HookConsumerWidget { decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(50)), - child: Text( - artist.type!.toUpperCase(), - style: chipTextVariant.copyWith( - color: Colors.white, + child: Skeleton.keep( + child: Text( + artist.type!.toUpperCase(), + style: chipTextVariant.copyWith( + color: Colors.white, + ), ), ), ), @@ -138,113 +138,115 @@ class ArtistPageHeader extends HookConsumerWidget { ), ), const Gap(20), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (auth != null) - HookBuilder( - builder: (context) { - final isFollowingQuery = - useQueries.artist.doIFollow(ref, artistId); + Skeleton.keep( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (auth != null) + HookBuilder( + builder: (context) { + final isFollowingQuery = + useQueries.artist.doIFollow(ref, artistId); - final followUnfollow = useCallback(() async { - try { - isFollowingQuery.data! - ? await spotify.me.unfollow( - FollowingType.artist, - [artistId], - ) - : await spotify.me.follow( - FollowingType.artist, - [artistId], - ); - await isFollowingQuery.refresh(); + final followUnfollow = useCallback(() async { + try { + isFollowingQuery.data! + ? await spotify.me.unfollow( + FollowingType.artist, + [artistId], + ) + : await spotify.me.follow( + FollowingType.artist, + [artistId], + ); + await isFollowingQuery.refresh(); - queryClient.refreshInfiniteQueryAllPages( - "user-following-artists"); - } finally { - queryClient.refreshQuery( - "user-follows-artists-query/$artistId", + queryClient.refreshInfiniteQueryAllPages( + "user-following-artists"); + } finally { + queryClient.refreshQuery( + "user-follows-artists-query/$artistId", + ); + } + }, [isFollowingQuery]); + + if (isFollowingQuery.isLoading || + !isFollowingQuery.hasData) { + return const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), ); } - }, [isFollowingQuery]); - if (isFollowingQuery.isLoading || - !isFollowingQuery.hasData) { - return const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ); - } + if (isFollowingQuery.data!) { + return OutlinedButton( + onPressed: followUnfollow, + child: Text(context.l10n.following), + ); + } - if (isFollowingQuery.data!) { - return OutlinedButton( + return FilledButton( onPressed: followUnfollow, - child: Text(context.l10n.following), + child: Text(context.l10n.follow), ); + }, + ), + const SizedBox(width: 5), + IconButton( + tooltip: context.l10n.add_artist_to_blacklist, + icon: Icon( + SpotubeIcons.userRemove, + color: + !isBlackListed ? Colors.red[400] : Colors.white, + ), + style: IconButton.styleFrom( + backgroundColor: + isBlackListed ? Colors.red[400] : null, + ), + onPressed: () async { + if (isBlackListed) { + ref + .read(BlackListNotifier.provider.notifier) + .remove( + BlacklistedElement.artist( + artist.id!, artist.name!), + ); + } else { + ref.read(BlackListNotifier.provider.notifier).add( + BlacklistedElement.artist( + artist.id!, artist.name!), + ); } - - return FilledButton( - onPressed: followUnfollow, - child: Text(context.l10n.follow), - ); }, ), - const SizedBox(width: 5), - IconButton( - tooltip: context.l10n.add_artist_to_blacklist, - icon: Icon( - SpotubeIcons.userRemove, - color: - !isBlackListed ? Colors.red[400] : Colors.white, - ), - style: IconButton.styleFrom( - backgroundColor: - isBlackListed ? Colors.red[400] : null, - ), - onPressed: () async { - if (isBlackListed) { - ref - .read(BlackListNotifier.provider.notifier) - .remove( - BlacklistedElement.artist( - artist.id!, artist.name!), - ); - } else { - ref.read(BlackListNotifier.provider.notifier).add( - BlacklistedElement.artist( - artist.id!, artist.name!), - ); - } - }, - ), - IconButton( - icon: const Icon(SpotubeIcons.share), - onPressed: () async { - if (artist.externalUrls?.spotify != null) { - await Clipboard.setData( - ClipboardData( - text: artist.externalUrls!.spotify!, + IconButton( + icon: const Icon(SpotubeIcons.share), + onPressed: () async { + if (artist.externalUrls?.spotify != null) { + await Clipboard.setData( + ClipboardData( + text: artist.externalUrls!.spotify!, + ), + ); + } + + if (!context.mounted) return; + + scaffoldMessenger.showSnackBar( + SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + context.l10n.artist_url_copied, + textAlign: TextAlign.center, + ), ), ); - } - - if (!context.mounted) return; - - scaffoldMessenger.showSnackBar( - SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - context.l10n.artist_url_copied, - textAlign: TextAlign.center, - ), - ), - ); - }, - ) - ], + }, + ) + ], + ), ) ], ), diff --git a/lib/pages/artist/section/top_tracks.dart b/lib/pages/artist/section/top_tracks.dart index 9e3e4054..771757b9 100644 --- a/lib/pages/artist/section/top_tracks.dart +++ b/lib/pages/artist/section/top_tracks.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; @@ -28,11 +30,7 @@ class ArtistPageTopTracks extends HookConsumerWidget { topTracksQuery.data ?? [], ); - if (topTracksQuery.isLoading || !topTracksQuery.hasData) { - return const SliverToBoxAdapter( - child: Center(child: CircularProgressIndicator()), - ); - } else if (topTracksQuery.hasError) { + if (topTracksQuery.hasError) { return SliverToBoxAdapter( child: Center( child: Text(topTracksQuery.error.toString()), @@ -40,7 +38,8 @@ class ArtistPageTopTracks extends HookConsumerWidget { ); } - final topTracks = topTracksQuery.data!; + final topTracks = + topTracksQuery.data ?? List.generate(10, (index) => FakeData.track); void playPlaylist(List tracks, {Track? currentTrack}) async { currentTrack ??= tracks.first; @@ -92,9 +91,11 @@ class ArtistPageTopTracks extends HookConsumerWidget { ), const SizedBox(width: 5), IconButton( - icon: Icon( - isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, - color: Colors.white, + icon: Skeleton.keep( + child: Icon( + isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, + color: Colors.white, + ), ), style: IconButton.styleFrom( backgroundColor: theme.colorScheme.primary, diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index 54fb6786..88eaef70 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -3,11 +3,12 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/genre/category_card.dart'; import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; -import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; +import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -77,7 +78,23 @@ class GenrePage extends HookConsumerWidget { ), if (!categoriesQuery.hasPageData && !categoriesQuery.isLoadingNextPage) - const ShimmerCategories() + Expanded( + child: Skeletonizer( + enabled: true, + child: ListView.builder( + itemCount: 5, + itemBuilder: (context, index) { + return HorizontalPlaybuttonCardView( + title: const Text("Loading"), + items: const [], + hasNextPage: true, + isLoadingNextPage: false, + onFetchMore: () {}, + ); + }, + ), + ), + ) else Expanded( child: InfiniteList( @@ -86,7 +103,16 @@ class GenrePage extends HookConsumerWidget { onFetchData: categoriesQuery.fetchNext, isLoading: categoriesQuery.isLoadingNextPage, hasReachedMax: !categoriesQuery.hasNextPage, - loadingBuilder: (context) => const ShimmerCategories(), + loadingBuilder: (context) => Skeletonizer( + enabled: true, + child: HorizontalPlaybuttonCardView( + title: const Text("Loading"), + items: const [], + hasNextPage: true, + isLoadingNextPage: false, + onFetchMore: () {}, + ), + ), itemBuilder: (context, index) { return CategoryCard(categories[index]); }, diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index 7fbd27ae..22224c39 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -4,11 +4,11 @@ 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/shimmers/shimmer_categories.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:skeletonizer/skeletonizer.dart'; class PersonalizedPage extends HookConsumerWidget { const PersonalizedPage({Key? key}) : super(key: key); @@ -46,39 +46,35 @@ class PersonalizedPage extends HookConsumerWidget { [newReleases.pages], ); + final hasNewReleases = newReleases.hasPageData && + userArtistsQuery.hasData && + !newReleases.isLoadingNextPage; + + final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData && + !featuredPlaylistsQuery.isLoadingNextPage; + return CustomScrollView( controller: controller, slivers: [ SliverList.list( children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: !featuredPlaylistsQuery.hasPageData && - !featuredPlaylistsQuery.isLoadingNextPage - ? const ShimmerCategories() - : HorizontalPlaybuttonCardView( - items: playlists.toList(), - title: Text(context.l10n.featured), - isLoadingNextPage: - featuredPlaylistsQuery.isLoadingNextPage, - hasNextPage: featuredPlaylistsQuery.hasNextPage, - onFetchMore: featuredPlaylistsQuery.fetchNext, - ), + Skeletonizer( + enabled: isLoadingFeaturedPlaylists, + child: HorizontalPlaybuttonCardView( + items: playlists.toList(), + title: Text(context.l10n.featured), + isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage, + hasNextPage: featuredPlaylistsQuery.hasNextPage, + onFetchMore: featuredPlaylistsQuery.fetchNext, + ), ), - if (auth != null) - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: newReleases.hasPageData && - userArtistsQuery.hasData && - !newReleases.isLoadingNextPage - ? HorizontalPlaybuttonCardView( - items: albums, - title: Text(context.l10n.new_releases), - isLoadingNextPage: newReleases.isLoadingNextPage, - hasNextPage: newReleases.hasNextPage, - onFetchMore: newReleases.fetchNext, - ) - : const ShimmerCategories(), + if (auth != null || hasNewReleases) + HorizontalPlaybuttonCardView( + items: albums, + title: Text(context.l10n.new_releases), + isLoadingNextPage: newReleases.isLoadingNextPage, + hasNextPage: newReleases.hasNextPage, + onFetchMore: newReleases.fetchNext, ), ], ), diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 36a9f316..9af71d94 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -77,7 +77,7 @@ class SyncedLyrics extends HookConsumerWidget { : textTheme.headlineMedium?.copyWith(fontSize: 25)) ?.copyWith(color: palette.titleTextColor); - var bodyTextTheme = textTheme.bodyLarge?.copyWith( + final bodyTextTheme = textTheme.bodyLarge?.copyWith( color: palette.bodyTextColor, ); return Stack( @@ -184,7 +184,9 @@ class SyncedLyrics extends HookConsumerWidget { ), if (playlist.activeTrack != null && (timedLyricsQuery.isLoading || timedLyricsQuery.isRefreshing)) - const Expanded(child: ShimmerLyrics()) + const Expanded( + child: ShimmerLyrics(), + ) else if (playlist.activeTrack != null && (timedLyricsQuery.hasError)) Text( diff --git a/pubspec.lock b/pubspec.lock index 06ca8202..8921f8a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1832,6 +1832,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + skeletonizer: + dependency: "direct main" + description: + name: skeletonizer + sha256: ff4c36e826efd5288d7a84e7619a6e9be8185d3064cecf101a9133762f3b401b + url: "https://pub.dev" + source: hosted + version: "0.8.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index ba758cbf..77a26911 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -118,6 +118,7 @@ dependencies: url: https://github.com/Tommypop2/dart_discord_rpc.git html_unescape: ^2.0.0 wikipedia_api: ^0.1.0 + skeletonizer: ^0.8.0 dev_dependencies: build_runner: ^2.3.2 From b59bea16f071aede5bf8a46ae730bddfdfaa151d Mon Sep 17 00:00:00 2001 From: Ashirbad Sahu <130544212+ashirbadsahu@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:05:05 +0530 Subject: [PATCH 079/131] website: dynamic copyright year in footer (#923) * Update copyright year in footer to 2023 * Update copyright year to be dynamic --- website/components/Footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/components/Footer.tsx b/website/components/Footer.tsx index b940815d..a51cf0a2 100644 --- a/website/components/Footer.tsx +++ b/website/components/Footer.tsx @@ -49,7 +49,7 @@ const Footer = () => { color: "white", }} > - © 2022, Spotube. All rights reserved + © {new Date().getFullYear()}, Spotube. All rights reserved From 66d492b4b3d0f9ee8e8798bc325e452f723ef2b1 Mon Sep 17 00:00:00 2001 From: Alex Stan <90788596+Ultra980@users.noreply.github.com> Date: Fri, 8 Dec 2023 07:20:22 +0200 Subject: [PATCH 080/131] Fix a typo in lib/pages/lyrics/synced_lyrics.dart (#933) "Synced lyrics is not ..." -> "Synced lyrics are not ..." --- lib/pages/lyrics/synced_lyrics.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 36a9f316..28239539 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -201,7 +201,7 @@ class SyncedLyrics extends HookConsumerWidget { children: [ const TextSpan( text: - "Synced lyrics is not available for this song. Please use the", + "Synced lyrics are not available for this song. Please use the", ), TextSpan( text: " Plain Lyrics ", From e4eb0e2596ade2bb5195e183f03af42742fc8486 Mon Sep 17 00:00:00 2001 From: Henrik Sozzi Date: Fri, 8 Dec 2023 06:23:53 +0100 Subject: [PATCH 081/131] feat: add Italian language translations (#818) * Italian language added Created and added the Italian language * Corrections and new textes Corrected the wrong TAB in i10n.dart and added translations of new text items * Fix it const name was lowercase * Merged suggestions from PR #676, added credits Added suggestions as in comments of PR #676 and added credits to @ncvescera and @OpenCode --- lib/collections/language_codes.dart | 8 +- lib/l10n/app_it.arb | 283 ++++++++++++++++++++++++++++ lib/l10n/l10n.dart | 2 + 3 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 lib/l10n/app_it.arb diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index 0518363e..d89e1a2a 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -288,10 +288,10 @@ abstract class LanguageLocals { // name: "Icelandic", // nativeName: "Íslenska", // ), - // "it": const ISOLanguageName( - // name: "Italian", - // nativeName: "Italiano", - // ), + "it": const ISOLanguageName( + name: "Italian", + nativeName: "Italiano", + ), // "iu": const ISOLanguageName( // name: "Inuktitut", // nativeName: "ᐃᓄᒃᑎᑐᑦ", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb new file mode 100644 index 00000000..d8f0ae88 --- /dev/null +++ b/lib/l10n/app_it.arb @@ -0,0 +1,283 @@ +{ + "guest": "Ospite", + "browse": "Sfoglia", + "search": "Cerca", + "library": "Libreria", + "lyrics": "Testi", + "settings": "Impostazioni", + "genre_categories_filter": "Filtra categorie e generi...", + "genre": "Genere", + "personalized": "Personalizzato", + "featured": "In evidenza", + "new_releases": "Novità", + "songs": "Canzoni", + "playing_track": "Riproduzione {track}", + "queue_clear_alert": "Questo cancellerà la coda corrente. {track_length} tracce saranno rimosse\nVuoi continuare?", + "load_more": "Carica altro", + "playlists": "Playlist", + "artists": "Artisti", + "albums": "Album", + "tracks": "Tracce", + "downloads": "Downloads", + "filter_playlists": "Filtra le tue playlist...", + "liked_tracks": "Tracce piaciute", + "liked_tracks_description": "Tutte le tracce piaciute", + "create_playlist": "Crea Playlist", + "create_a_playlist": "Crea una playlist", + "update_playlist": "Aggiorna playlist", + "create": "Crea", + "cancel": "Annulla", + "update": "Aggiorna", + "playlist_name": "Nome Playlist", + "name_of_playlist": "Nome della playlist", + "description": "Descrizione", + "public": "Pubblico", + "collaborative": "Collaborativo", + "search_local_tracks": "Cerca tracce locali...", + "play": "Riproduci", + "delete": "Cancella", + "none": "Nessuno", + "sort_a_z": "Ordina dalla A-Z", + "sort_z_a": "Ordina dalla Z-A", + "sort_artist": "Ordina per Artista", + "sort_album": "Ordina per Album", + "sort_tracks": "Ordina tracce", + "currently_downloading": "Attualmente in Download ({tracks_length})", + "cancel_all": "Annulla Tutto", + "filter_artist": "Filtra artisti...", + "followers": "{followers} Seguaci", + "add_artist_to_blacklist": "Aggiungi artista alla lista nera", + "top_tracks": "Tracce Top", + "fans_also_like": "Ai fan piace anche", + "loading": "Caricamento...", + "artist": "Artista", + "blacklisted": "In lista nera", + "following": "Seguendo", + "follow": "Segui", + "artist_url_copied": "URL artista copiato negli appunti", + "added_to_queue": "Aggiunto {tracks} tracce alla coda", + "filter_albums": "Filtra album...", + "synced": "Sincronizzato", + "plain": "Semplice", + "shuffle": "Casuale", + "search_tracks": "Cerca tracce...", + "released": "Rilasciato", + "error": "Errore {error}", + "title": "Titolo", + "time": "Durata", + "more_actions": "Più azioni", + "download_count": "Scaricato ({count})", + "add_count_to_playlist": "Aggiungi ({count}) alla playlist", + "add_count_to_queue": "Aggiungi ({count}) alla Coda", + "play_count_next": "Riproduci ({count}) prossime", + "album": "Album", + "copied_to_clipboard": "Copiato {data} negli appunti", + "add_to_following_playlists": "Aggiungi {track} nelle seguenti Playlist", + "add": "Aggiungi", + "added_track_to_queue": "Aggiunto {track} alla coda", + "add_to_queue": "Aggiungi alla coda", + "track_will_play_next": "in seguito sarà riprodotta {track}", + "play_next": "Riproduci prossimo", + "removed_track_from_queue": "Rimosso {track} dalla coda", + "remove_from_queue": "Rimuovi dalla coda", + "remove_from_favorites": "Rimuovi dai preferiti", + "save_as_favorite": "Salva come preferito", + "add_to_playlist": "Aggiungi alla playlist", + "remove_from_playlist": "Rimuovi dalla playlist", + "add_to_blacklist": "Aggiungi alla blacklist", + "remove_from_blacklist": "Rimuovi dalla blacklist", + "share": "Condividi", + "mini_player": "Mini Riproduttore", + "slide_to_seek": "Scorri per cercare avanti o indietro", + "shuffle_playlist": "Playlist casuale", + "unshuffle_playlist": "Ordina playlist", + "previous_track": "Traccia precedente", + "next_track": "Traccia successiva", + "pause_playback": "Pausa Playback", + "resume_playback": "Riprendi Playback", + "loop_track": "Cicla traccia", + "repeat_playlist": "Ripeti playlist", + "queue": "Coda", + "alternative_track_sources": "Sorgenti traccia alternative", + "download_track": "Scarica traccia", + "tracks_in_queue": "{tracks} tracce in coda", + "clear_all": "Cancella tutto", + "show_hide_ui_on_hover": "Mostra/Nascondi UI al passaggio", + "always_on_top": "Sempre in cima", + "exit_mini_player": "Esci da Mini player", + "download_location": "Cartella di scarico", + "account": "Account", + "login_with_spotify": "Login con il tuo account Spotify", + "connect_with_spotify": "Connetti con Spotify", + "logout": "Esci", + "logout_of_this_account": "Esci da questo account", + "language_region": "Lingua & Regione", + "language": "Lingua", + "system_default": "Default sistema", + "market_place_region": "Regione del mercato", + "recommendation_country": "Paese Raccomandato", + "appearance": "Aspetto", + "layout_mode": "Modalità Layout", + "override_layout_settings": "Sovrascrivi le impostazioni del layout responsivo", + "adaptive": "Adattiva", + "compact": "Compatta", + "extended": "Estesa", + "theme": "Tema", + "dark": "Scuro", + "light": "Chiaro", + "system": "Sistema", + "accent_color": "Colore accento", + "sync_album_color": "Syncronizza colore album", + "sync_album_color_description": "Usa il colore dominante della copertina dell'album come colore accento", + "playback": "Riproduzione", + "audio_quality": "Qualità Audio", + "high": "Alta", + "low": "Bassa", + "pre_download_play": "Pre-scarica e riproduci", + "pre_download_play_description": "Anzi che effettuare lo stream dell'audio, scarica invece i byte e li riproduce (raccomandato per gli utenti con banda più alta)", + "skip_non_music": "Salta i segmenti non di musica (SponsorBlock)", + "blacklist_description": "Tracce e artisti in blacklist", + "wait_for_download_to_finish": "Prego attendere che lo scaricamento corrente finisca", + "desktop": "Desktop", + "close_behavior": "Comportamento Chiusura", + "close": "Chiudi", + "minimize_to_tray": "Minimizza in tray", + "show_tray_icon": "Mostra icona in tray di sistema", + "about": "A proposito di", + "u_love_spotube": "Sappiamo che ami Spotube", + "check_for_updates": "Controlla aggiornamenti", + "about_spotube": "A proposito di Spotube", + "blacklist": "Blacklist", + "please_sponsor": "Per favore sponsorizza/dona", + "spotube_description": "Spotube, un client spotify gratis per tutti, multipiattaforma e leggero", + "version": "Versione", + "build_number": "Numero Build", + "founder": "Fondatore", + "repository": "Repository", + "bug_issues": "Bug+Problemi", + "made_with": "Fatto con ❤️ in Bangladesh🇧🇩", + "kingkor_roy_tirtho": "Kingkor Roy Tirtho", + "copyright": "© 2021-{current_year} Kingkor Roy Tirtho", + "license": "Licenza", + "add_spotify_credentials": "Aggiungi le tue credenziali spotify per iniziare", + "credentials_will_not_be_shared_disclaimer": "Non ti preoccupare, le tue credenziali non saranno inviate o condivise con nessuno", + "know_how_to_login": "Non sai come farlo?", + "follow_step_by_step_guide": "Segui la guida passo-passo", + "spotify_cookie": "Cookie Spotify {name}", + "cookie_name_cookie": "Cookie {name}", + "fill_in_all_fields": "Inserire tutti i campi", + "submit": "Invia", + "exit": "Esci", + "previous": "Precedente", + "next": "Prossimo", + "done": "Finito", + "step_1": "Passo 1", + "first_go_to": "Prim, vai a", + "login_if_not_logged_in": "ed effettua il login o iscrizione se non sei già acceduto", + "step_2": "Passo 2", + "step_2_steps": "1. Quando sei acceduto premi F12 o premi il tasto destro del Mouse > Ispeziona per aprire gli strumenti di sviluppo del browser.\n2. Vai quindi nel tab \"Applicazione\" (Chrome, Edge, Brave etc..) o tab \"Archiviazione\" (Firefox, Palemoon etc..)\n3. Vai nella sezione \"Cookies\" quindi nella sezione \"https://accounts.spotify.com\"", + "step_3": "Passo 3", + "step_3_steps": "Copia il valore dei cookie \"sp_dc\" e \"sp_key\" (o sp_gaid)", + "success_emoji": "Successo🥳", + "success_message": "Ora hai correttamente effettuato il login al tuo account Spotify. Bel lavoro, amico!", + "step_4": "Passo 4", + "step_4_steps": "Incolla i valori copiati di \"sp_dc\" e \"sp_key\" (o sp_gaid) nei campi rispettivi", + "something_went_wrong": "Qualcosa è andato storto", + "piped_instance": "Istanza Server Piped", + "piped_description": "L'istanza server Piped da usare per il match della tracccia", + "piped_warning": "Alcune di queste non funzioneranno benen. Usa quindi a tuo rischio", + "generate_playlist": "Genera Playlist", + "track_exists": "La traccia {track} esiste già", + "replace_downloaded_tracks": "Sostituisci tutte le tracce scaricate", + "skip_download_tracks": "Salta lo scaricamento di tutte le tracce scaricate", + "do_you_want_to_replace": "Vuoi sovrascrivere la traccia esistente??", + "replace": "Sovrascrivi", + "skip": "Salta", + "select_up_to_count_type": "Seleziona fino a {count} {type}", + "select_genres": "Seleziona Generi", + "add_genres": "Aggiungi Generi", + "country": "Paese", + "number_of_tracks_generate": "Nnumero di tracce da generare", + "acousticness": "Acustica", + "danceability": "Ballabilità", + "energy": "Energia", + "instrumentalness": "Strumentalità", + "liveness": "Vitalità", + "loudness": "Sonorità", + "speechiness": "Loquacità", + "valence": "Valenza", + "popularity": "Popolarità", + "key": "Chiave", + "duration": "Durata (s)", + "tempo": "Tempo (BPM)", + "mode": "Modo", + "time_signature": "Indicazione di tempo", + "short": "Corta", + "medium": "Media", + "long": "Lunga", + "min": "Min", + "max": "Max", + "target": "Obiettivo", + "moderate": "Moderato", + "deselect_all": "Deseleziona Tutto", + "select_all": "Seleziona Tutto", + "are_you_sure": "Sei certo?", + "generating_playlist": "Generazione delle tue playlist custom...", + "selected_count_tracks": "{count} tracce selezionate", + "download_warning": "Se scarichi tutte le Tracce in massa stai chiaramente piratando Musica e causando un danno alla società creativa della Musica. Spero che tu sia cosciente di questo. Cerca di rispettare e supportare sempre il duro lavoro degli Artisti", + "download_ip_ban_warning": "A proposito, il tuo IP può essere bloccato da YouTube per il numero di richieste di download eccessive rispetto la norma. Il blocco IP significa che non puoi usare YoutTube (anche hai effettuato l'accesso) per almeno 2-3 mesi dal dispositivo con questo IP. Spotube non ha responsabilità se questo dovesse accadere", + "by_clicking_accept_terms": "Cliccando su 'accetta' concordi con i seguenti termini:", + "download_agreement_1": "So che sto piratando Musica. Sono cattivo", + "download_agreement_2": "Supporterò l'Artista come potrò e sto facendo questo solo perchè non ho denaro per acquistare il suo prodotto dell'ingegno", + "download_agreement_3": "Sono completamente cosciente che il mio IP può essere bloccato da YouTube & non riterrò responsabili Spotube o i suoi autori/contributori per ogni inconveniente causato dalla mia azione corrente", + "decline": "Declino", + "accept": "Accetto", + "details": "Dettagli", + "youtube": "YouTube", + "channel": "Canale", + "likes": "Mi Piace", + "dislikes": "Non Mi Piace", + "views": "Viste", + "streamUrl": "URL dello streaming", + "stop": "Stop", + "sort_newest": "Ordina per nuovi aggiunti", + "sort_oldest": "Ordina per aggiunta più vecchia", + "sleep_timer": "Timer Dormire", + "mins": "{minutes} Minuti", + "hours": "{hours} Ore", + "hour": "{hours} Ora", + "custom_hours": "Orari Personalizzati", + "logs": "Log", + "developers": "Sviluppatori", + "not_logged_in": "Non hai effettuato l'accesso", + "search_mode": "Modalità Ricerca", + "youtube_api_type": "Tipo API", + "ok": "Ok", + "failed_to_encrypt": "Criptazione fallita", + "encryption_failed_warning": "Spotube usa la criptazione per memorizzare in modo sicuro i dati. Ma ha fallito a farlo. Passerà quindi in ripiego alla memorizzazione non siscura\nSe stai usando Linux assicurati di avere un servizio di segretezza installato (gnome-keyring, kde-wallet, keepassxc etc)", + "querying_info": "Richiesta informazioni...", + "piped_api_down": "Le Piped API non funzionano", + "piped_down_error_instructions": "L'istanza di Piped {pipedInstance} è correntemente offline\n\nCambia istanza o cambia 'Tipo API' alle API ufficiali YouTube\n\nAssicurati di riavviare l'app dopo il cambio", + "you_are_offline": "Sei correntemente offline", + "connection_restored": "Connessione ad internet ripristinata", + "use_system_title_bar": "Usa la barra del titolo di sistema", + "crunching_results": "Elaborazione risultati...", + "search_to_get_results": "Cerca per ottenere risultati" + "use_amoled_mode": "Usa modalità AMOLED", + "pitch_dark_theme": "Tema nero profondo", + "normalize_audio": "Normalizza audio", + "change_cover": "Cambia copertina", + "add_cover": "Aggiungi copertina", + "restore_defaults": "Ripristina default", + "download_music_codec": "Codec musicale scaricamento", + "streaming_music_codec": "Codec musicale streaming", + "login_with_lastfm": "Accesso a Last.fm", + "connect": "Connetti", + "disconnect_lastfm": "Disconnetti Last.fm", + "disconnect": "Disconnetti", + "username": "Nome utente", + "password": "Password", + "login": "Accesso", + "login_with_your_lastfm": "Accedi con il tuo account Last.fm", + "scrobble_to_lastfm": "Invia a Last.fm" +} \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 61a6d097..d6cf3e37 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -6,6 +6,7 @@ /// iceyear@github => Simplified Chinese /// TexturedPolak@github => Polish /// yuri-val@github => Ukrainian +/// energywave@github, ncvescera@github, OpenCode@github => Italian /// mdksec@github => Turkish import 'package:flutter/material.dart'; @@ -20,6 +21,7 @@ class L10n { const Locale("fa", "IR"), const Locale('fr', 'FR'), const Locale('hi', 'IN'), + const Locale('it', 'IT'), const Locale('ja', 'JP'), const Locale('pl', 'PL'), const Locale('pt', 'PT'), From 24a2294512bb0c4aff77bc8dcad9b4de3e8b45c6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 8 Dec 2023 13:27:31 +0600 Subject: [PATCH 082/131] feat: toggle for discord rpc --- lib/collections/spotube_icons.dart | 1 + lib/l10n/app_en.arb | 3 +- lib/pages/settings/sections/desktop.dart | 8 +++ lib/provider/discord_provider.dart | 70 +++++++++++++++++++ .../proxy_playlist_provider.dart | 3 +- .../user_preferences_provider.dart | 4 ++ .../user_preferences_state.dart | 6 ++ .../user_preferences_state.g.dart | 2 + lib/services/discord/discord.dart | 44 ------------ untranslated_messages.json | 45 ++++++++---- 10 files changed, 125 insertions(+), 61 deletions(-) create mode 100644 lib/provider/discord_provider.dart delete mode 100644 lib/services/discord/discord.dart diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index d00775c7..00010aae 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -108,4 +108,5 @@ abstract class SpotubeIcons { static const noEye = FeatherIcons.eyeOff; static const normalize = FeatherIcons.barChart2; static const wikipedia = SimpleIcons.wikipedia; + static const discord = SimpleIcons.discord; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5aded7d5..85900012 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -280,5 +280,6 @@ "login": "Login", "login_with_your_lastfm": "Login with your Last.fm account", "scrobble_to_lastfm": "Scrobble to Last.fm", - "go_to_album": "Go to Album" + "go_to_album": "Go to Album", + "discord_rich_presence": "Discord Rich Presence" } \ No newline at end of file diff --git a/lib/pages/settings/sections/desktop.dart b/lib/pages/settings/sections/desktop.dart index 41d6d61e..ae721fc4 100644 --- a/lib/pages/settings/sections/desktop.dart +++ b/lib/pages/settings/sections/desktop.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.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'; @@ -50,6 +51,13 @@ class SettingsDesktopSection extends HookConsumerWidget { value: preferences.systemTitleBar, onChanged: preferencesNotifier.setSystemTitleBar, ), + if (!DesktopTools.platform.isMacOS) + SwitchListTile( + secondary: const Icon(SpotubeIcons.discord), + title: Text(context.l10n.discord_rich_presence), + value: preferences.discordPresence, + onChanged: preferencesNotifier.setDiscordPresence, + ), ], ); } diff --git a/lib/provider/discord_provider.dart b/lib/provider/discord_provider.dart new file mode 100644 index 00000000..3aa547a9 --- /dev/null +++ b/lib/provider/discord_provider.dart @@ -0,0 +1,70 @@ +import 'package:dart_discord_rpc/dart_discord_rpc.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/env.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +class Discord extends ChangeNotifier { + final DiscordRPC? discordRPC; + final bool isEnabled; + + Discord(this.isEnabled) + : discordRPC = (DesktopTools.platform.isWindows || + DesktopTools.platform.isLinux) && + isEnabled + ? DiscordRPC(applicationId: Env.discordAppId) + : null { + discordRPC?.start(autoRegister: true); + } + + void updatePresence(Track track) { + clear(); + final artistNames = + TypeConversionUtils.artists_X_String(track.artists ?? []); + discordRPC?.updatePresence( + DiscordPresence( + details: "Song: ${track.name} by $artistNames", + state: "Vibing in Music", + startTimeStamp: DateTime.now().millisecondsSinceEpoch, + largeImageKey: "spotube-logo-foreground", + largeImageText: "Spotube", + smallImageKey: "spotube-logo-foreground", + smallImageText: "Spotube", + ), + ); + } + + void clear() { + discordRPC?.clearPresence(); + } + + void shutdown() { + discordRPC?.shutDown(); + } + + @override + void dispose() { + clear(); + shutdown(); + super.dispose(); + } +} + +final discordProvider = ChangeNotifierProvider( + (ref) { + final isEnabled = + ref.watch(userPreferencesProvider.select((s) => s.discordPresence)); + final playback = ref.read(ProxyPlaylistNotifier.provider); + final discord = Discord(isEnabled); + + if (playback.activeTrack != null) { + discord.updatePresence(playback.activeTrack!); + } + + return discord; + }, +); diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 84943993..ca0fb308 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -24,7 +24,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/audio_services.dart'; -import 'package:spotube/services/discord/discord.dart'; +import 'package:spotube/provider/discord_provider.dart'; import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; @@ -64,6 +64,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier ProxyPlaylist get playlist => state; BlackListNotifier get blacklist => ref.read(BlackListNotifier.provider.notifier); + Discord get discord => ref.read(discordProvider); static final provider = StateNotifierProvider( diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 88a0df2e..46569269 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -110,6 +110,10 @@ class UserPreferencesNotifier extends PersistedStateNotifier { } } + void setDiscordPresence(bool discordPresence) { + state = state.copyWith(discordPresence: discordPresence); + } + void setAmoledDarkTheme(bool isAmoled) { state = state.copyWith(amoledDarkTheme: isAmoled); } diff --git a/lib/provider/user_preferences/user_preferences_state.dart b/lib/provider/user_preferences/user_preferences_state.dart index b3d7fe8a..4244ca67 100644 --- a/lib/provider/user_preferences/user_preferences_state.dart +++ b/lib/provider/user_preferences/user_preferences_state.dart @@ -198,6 +198,9 @@ final class UserPreferences { ) final SourceCodecs downloadMusicCodec; + @JsonKey(defaultValue: true) + final bool discordPresence; + UserPreferences({ required this.audioQuality, required this.albumColorSync, @@ -219,6 +222,7 @@ final class UserPreferences { required this.audioSource, required this.streamMusicCodec, required this.downloadMusicCodec, + required this.discordPresence, }); factory UserPreferences.withDefaults() { @@ -255,6 +259,7 @@ final class UserPreferences { SourceCodecs? downloadMusicCodec, SourceCodecs? streamMusicCodec, bool? systemTitleBar, + bool? discordPresence, }) { return UserPreferences( themeMode: themeMode ?? this.themeMode, @@ -277,6 +282,7 @@ final class UserPreferences { normalizeAudio: normalizeAudio ?? this.normalizeAudio, streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, systemTitleBar: systemTitleBar ?? this.systemTitleBar, + discordPresence: discordPresence ?? this.discordPresence, ); } } diff --git a/lib/provider/user_preferences/user_preferences_state.g.dart b/lib/provider/user_preferences/user_preferences_state.g.dart index 54cd3aa2..59043601 100644 --- a/lib/provider/user_preferences/user_preferences_state.g.dart +++ b/lib/provider/user_preferences/user_preferences_state.g.dart @@ -63,6 +63,7 @@ UserPreferences _$UserPreferencesFromJson(Map json) => _$SourceCodecsEnumMap, json['downloadMusicCodec'], unknownValue: SourceCodecs.m4a) ?? SourceCodecs.m4a, + discordPresence: json['discordPresence'] as bool? ?? true, ); Map _$UserPreferencesToJson(UserPreferences instance) => @@ -88,6 +89,7 @@ Map _$UserPreferencesToJson(UserPreferences instance) => 'audioSource': _$AudioSourceEnumMap[instance.audioSource]!, 'streamMusicCodec': _$SourceCodecsEnumMap[instance.streamMusicCodec]!, 'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!, + 'discordPresence': instance.discordPresence, }; const _$SourceQualitiesEnumMap = { diff --git a/lib/services/discord/discord.dart b/lib/services/discord/discord.dart deleted file mode 100644 index 2a40e388..00000000 --- a/lib/services/discord/discord.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:dart_discord_rpc/dart_discord_rpc.dart'; -import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/collections/env.dart'; -import 'package:spotube/utils/type_conversion_utils.dart'; - -class Discord { - final DiscordRPC? discordRPC; - - Discord() - : discordRPC = - DesktopTools.platform.isWindows || DesktopTools.platform.isLinux - ? DiscordRPC(applicationId: Env.discordAppId) - : null { - discordRPC?.start(autoRegister: true); - } - - void updatePresence(Track track) { - clear(); - final artistNames = - TypeConversionUtils.artists_X_String(track.artists ?? []); - discordRPC?.updatePresence( - DiscordPresence( - details: "Song: ${track.name} by $artistNames", - state: "Vibing in Music", - startTimeStamp: DateTime.now().millisecondsSinceEpoch, - largeImageKey: "spotube-logo-foreground", - largeImageText: "Spotube", - smallImageKey: "spotube-logo-foreground", - smallImageText: "Spotube", - ), - ); - } - - void clear() { - discordRPC?.clearPresence(); - } - - void shutdown() { - discordRPC?.shutDown(); - } -} - -final discord = Discord(); diff --git a/untranslated_messages.json b/untranslated_messages.json index 0130c162..e3bdc047 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,61 +1,76 @@ { "ar": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "bn": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "ca": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "de": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "es": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "fa": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "fr": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "hi": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "ja": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "pl": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "pt": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "ru": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "tr": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "uk": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ], "zh": [ - "go_to_album" + "go_to_album", + "discord_rich_presence" ] } From 581b241f995712805baafa67c4f56f0e9e29b7cd Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 8 Dec 2023 13:58:12 +0600 Subject: [PATCH 083/131] chore: add not found for empty lists --- lib/components/library/user_albums.dart | 56 +++++++++++-------- lib/components/library/user_artists.dart | 19 +++++-- lib/components/library/user_local_tracks.dart | 10 ++++ .../sections/body/track_view_body.dart | 12 ++-- lib/themes/theme.dart | 1 + 5 files changed, 62 insertions(+), 36 deletions(-) diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 49243870..04ab5b63 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -9,6 +9,7 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/album/album_card.dart'; +import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/waypoint.dart'; @@ -84,30 +85,37 @@ class UserAlbums extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), controller: controller, child: Skeletonizer( - enabled: albums.isEmpty, - child: Wrap( - runSpacing: 20, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - if (albums.isEmpty) - ...List.generate( - 10, - (index) => AlbumCard(FakeData.album), - ), - for (final album in albums) - AlbumCard( - TypeConversionUtils.simpleAlbum_X_Album(album), - ), - if (albums.isNotEmpty && albumsQuery.hasNextPage) - Waypoint( - controller: controller, - isGrid: true, - onTouchEdge: albumsQuery.fetchNext, - child: AlbumCard(FakeData.album), - ) - ], + enabled: albums.isEmpty && albumsQuery.isLoadingNextPage, + child: Center( + child: Wrap( + runSpacing: 20, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (albums.isEmpty && albumsQuery.isLoadingNextPage) + ...List.generate( + 10, + (index) => AlbumCard(FakeData.album), + ) + else if (albums.isEmpty) + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [NotFound()], + ), + for (final album in albums) + AlbumCard( + TypeConversionUtils.simpleAlbum_X_Album(album), + ), + if (albums.isNotEmpty && albumsQuery.hasNextPage) + Waypoint( + controller: controller, + isGrid: true, + onTouchEdge: albumsQuery.fetchNext, + child: AlbumCard(FakeData.album), + ) + ], + ), ), ), ), diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index 7269d7eb..36b8528e 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -9,6 +9,7 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/artist/artist_card.dart'; +import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/authentication_provider.dart'; @@ -97,10 +98,20 @@ class UserArtists extends HookConsumerWidget { children: artistQuery.isLoading ? List.generate( 10, (index) => ArtistCard(FakeData.artist)) - : filteredArtists - .mapIndexed( - (index, artist) => ArtistCard(artist)) - .toList(), + : filteredArtists.isEmpty + ? [ + const Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + NotFound(), + ], + ) + ] + : filteredArtists + .mapIndexed((index, artist) => + ArtistCard(artist)) + .toList(), ), ), ), diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index fcaada9e..f4e782d9 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -17,6 +17,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; +import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/sort_tracks_dropdown.dart'; import 'package:spotube/components/shared/track_tile/track_tile.dart'; @@ -255,6 +256,15 @@ class UserLocalTracks extends HookConsumerWidget { .toList(); }, [searchController.text, sortedTracks]); + if (!trackSnapshot.isLoading && filteredTracks.isEmpty) { + return const Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [NotFound()], + ), + ); + } + return Expanded( child: RefreshIndicator( onRefresh: () async { diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/shared/tracks_view/sections/body/track_view_body.dart index b7149cc2..2ad1dc9b 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_body.dart @@ -8,6 +8,7 @@ import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; +import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body_headers.dart'; import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart'; @@ -92,14 +93,9 @@ class TrackViewBodySection extends HookConsumerWidget { index: 0, ), ), - emptyBuilder: (context) => Skeletonizer( - enabled: true, - child: Column( - children: List.generate( - 10, - (index) => TrackTile(track: FakeData.track, index: index), - ), - ), + emptyBuilder: (context) => const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [NotFound()], ), itemBuilder: (context, index) { final track = tracks[index]; diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index 9a5e473f..51e98269 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -52,6 +52,7 @@ ThemeData theme(Color seed, Brightness brightness, bool isAmoled) { ), sliderTheme: SliderThemeData(overlayShape: SliderComponentShape.noOverlay), searchBarTheme: SearchBarThemeData( + textStyle: const MaterialStatePropertyAll(TextStyle(fontSize: 15)), constraints: const BoxConstraints(maxWidth: double.infinity), padding: const MaterialStatePropertyAll(EdgeInsets.all(8)), backgroundColor: MaterialStatePropertyAll( From b04d8849e7169824ec5b980236b5d61b2629f56e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 8 Dec 2023 14:46:27 +0600 Subject: [PATCH 084/131] fix: track view header title overflow and player view drag glitch --- lib/components/player/player.dart | 103 ++++++++++-------- lib/components/player/player_overlay.dart | 9 +- lib/components/shared/playbutton_card.dart | 4 +- .../shared/track_tile/track_tile.dart | 43 ++++---- .../sections/body/track_view_body.dart | 11 +- .../sections/header/flexible_header.dart | 102 ++++++++++------- 6 files changed, 156 insertions(+), 116 deletions(-) diff --git a/lib/components/player/player.dart b/lib/components/player/player.dart index 889b7c5c..3957cc65 100644 --- a/lib/components/player/player.dart +++ b/lib/components/player/player.dart @@ -28,9 +28,11 @@ import 'package:spotube/utils/type_conversion_utils.dart'; class PlayerView extends HookConsumerWidget { final PanelController panelController; + final ScrollController scrollController; const PlayerView({ Key? key, required this.panelController, + required this.scrollController, }) : super(key: key); @override @@ -119,40 +121,43 @@ class PlayerView extends HookConsumerWidget { preferredSize: Size.fromHeight( kToolbarHeight + topPadding, ), - child: Padding( - padding: EdgeInsets.only(top: topPadding), - child: PageWindowTitleBar( - backgroundColor: Colors.transparent, - foregroundColor: titleTextColor, - toolbarOpacity: 1, - leading: IconButton( - icon: const Icon(SpotubeIcons.angleDown, size: 18), - onPressed: panelController.close, + child: ForceDraggableWidget( + child: Padding( + padding: EdgeInsets.only(top: topPadding), + child: PageWindowTitleBar( + backgroundColor: Colors.transparent, + foregroundColor: titleTextColor, + toolbarOpacity: 1, + leading: IconButton( + icon: const Icon(SpotubeIcons.angleDown, size: 18), + onPressed: panelController.close, + ), + actions: [ + IconButton( + icon: const Icon(SpotubeIcons.info, size: 18), + tooltip: context.l10n.details, + style: IconButton.styleFrom( + foregroundColor: bodyTextColor), + onPressed: currentTrack == null + ? null + : () { + showDialog( + context: context, + builder: (context) { + return TrackDetailsDialog( + track: currentTrack, + ); + }); + }, + ) + ], ), - actions: [ - IconButton( - icon: const Icon(SpotubeIcons.info, size: 18), - tooltip: context.l10n.details, - style: - IconButton.styleFrom(foregroundColor: bodyTextColor), - onPressed: currentTrack == null - ? null - : () { - showDialog( - context: context, - builder: (context) { - return TrackDetailsDialog( - track: currentTrack, - ); - }); - }, - ) - ], ), ), ), extendBodyBehindAppBar: true, body: SingleChildScrollView( + controller: scrollController, child: Container( alignment: Alignment.center, width: double.infinity, @@ -163,27 +168,29 @@ class PlayerView extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: Column( children: [ - Container( - margin: const EdgeInsets.all(8), - constraints: const BoxConstraints( - maxHeight: 300, maxWidth: 300), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - spreadRadius: 2, - blurRadius: 10, - offset: Offset(0, 0), + ForceDraggableWidget( + child: Container( + margin: const EdgeInsets.all(8), + constraints: const BoxConstraints( + maxHeight: 300, maxWidth: 300), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + spreadRadius: 2, + blurRadius: 10, + offset: Offset(0, 0), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: UniversalImage( + path: albumArt, + placeholder: Assets.albumPlaceholder.path, + fit: BoxFit.cover, ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: UniversalImage( - path: albumArt, - placeholder: Assets.albumPlaceholder.path, - fit: BoxFit.cover, ), ), ), diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart index 4869a0fa..2d63811e 100644 --- a/lib/components/player/player_overlay.dart +++ b/lib/components/player/player_overlay.dart @@ -43,6 +43,7 @@ class PlayerOverlay extends HookConsumerWidget { final mediaQuery = MediaQuery.of(context); final panelController = useMemoized(() => PanelController(), []); + final scrollController = useScrollController(); useEffect(() { return () { @@ -174,6 +175,7 @@ class PlayerOverlay extends HookConsumerWidget { ), ), ), + scrollController: scrollController, panelBuilder: (position) { // this is the reason we're getting an update final navigationHeight = ref.watch(navigationPanelHeight); @@ -188,8 +190,11 @@ class PlayerOverlay extends HookConsumerWidget { decoration: navigationHeight == 0 ? const BoxDecoration(borderRadius: BorderRadius.zero) : const BoxDecoration(borderRadius: radius), - child: HorizontalScrollableWidget( - child: PlayerView(panelController: panelController), + child: IgnoreDraggableWidget( + child: PlayerView( + panelController: panelController, + scrollController: scrollController, + ), ), ), ); diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index 60db648b..d6226716 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -90,17 +90,19 @@ class PlaybuttonCard extends HookWidget { Stack( clipBehavior: Clip.none, children: [ - Padding( + Container( padding: const EdgeInsets.only( left: 8, right: 8, top: 8, ), + constraints: BoxConstraints(maxHeight: size), child: ClipRRect( borderRadius: radius, child: UniversalImage( path: imageUrl, placeholder: Assets.albumPlaceholder.path, + fit: BoxFit.cover, ), ), ), diff --git a/lib/components/shared/track_tile/track_tile.dart b/lib/components/shared/track_tile/track_tile.dart index 6d4e236a..961f29c9 100644 --- a/lib/components/shared/track_tile/track_tile.dart +++ b/lib/components/shared/track_tile/track_tile.dart @@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/hover_builder.dart'; @@ -158,26 +159,28 @@ class TrackTile extends HookConsumerWidget { child: IconTheme( data: theme.iconTheme .copyWith(size: 26, color: Colors.white), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: (isPlaying && playlist.isFetching) || - isLoading.value - ? const SizedBox( - width: 26, - height: 26, - child: CircularProgressIndicator( - strokeWidth: 1.5, - color: Colors.white, - ), - ) - : isPlaying - ? Icon( - SpotubeIcons.pause, - color: theme.colorScheme.primary, - ) - : !isHovering - ? const SizedBox.shrink() - : const Icon(SpotubeIcons.play), + child: Skeleton.ignore( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: (isPlaying && playlist.isFetching) || + isLoading.value + ? const SizedBox( + width: 26, + height: 26, + child: CircularProgressIndicator( + strokeWidth: 1.5, + color: Colors.white, + ), + ) + : isPlaying + ? Icon( + SpotubeIcons.pause, + color: theme.colorScheme.primary, + ) + : !isHovering + ? const SizedBox.shrink() + : const Icon(SpotubeIcons.play), + ), ), ), ), diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/shared/tracks_view/sections/body/track_view_body.dart index 2ad1dc9b..20caf4f1 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_body.dart @@ -93,9 +93,14 @@ class TrackViewBodySection extends HookConsumerWidget { index: 0, ), ), - emptyBuilder: (context) => const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [NotFound()], + emptyBuilder: (context) => Skeletonizer( + enabled: true, + child: Column( + children: List.generate( + 10, + (index) => TrackTile(track: FakeData.track, index: index), + ), + ), ), itemBuilder: (context, index) { final track = tracks[index]; diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/shared/tracks_view/sections/header/flexible_header.dart index 7c469654..e16ccbff 100644 --- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart +++ b/lib/components/shared/tracks_view/sections/header/flexible_header.dart @@ -88,50 +88,68 @@ class TrackViewFlexHeader extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Flex( - direction: mediaQuery.mdAndDown - ? Axis.vertical - : Axis.horizontal, - mainAxisSize: MainAxisSize.min, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - path: props.image, - width: 200, - height: 200, - placeholder: Assets.albumPlaceholder.path, + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: mediaQuery.mdAndDown + ? mediaQuery.size.width + : 800, + ), + child: Flex( + direction: mediaQuery.mdAndDown + ? Axis.vertical + : Axis.horizontal, + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: props.image, + width: 200, + height: 200, + placeholder: Assets.albumPlaceholder.path, + ), ), - ), - const Gap(20), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: mediaQuery.mdAndDown - ? CrossAxisAlignment.center - : CrossAxisAlignment.start, - children: [ - Text(props.title, style: headingStyle), - const SizedBox(height: 10), - if (description != null && - description.isNotEmpty) - Text( - description, - style: defaultTextStyle.style.copyWith( - color: palette.bodyTextColor, + const Gap(20), + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: mediaQuery.mdAndDown + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + Text( + props.title, + style: headingStyle, + textAlign: mediaQuery.mdAndDown + ? TextAlign.center + : TextAlign.start, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - textAlign: mediaQuery.mdAndDown - ? TextAlign.center - : TextAlign.start, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const Gap(10), - const TrackViewHeaderActions(), - const Gap(10), - TrackViewHeaderButtons(color: palette), - ], - ), - ], + const SizedBox(height: 10), + if (description != null && + description.isNotEmpty) + Text( + description, + style: + defaultTextStyle.style.copyWith( + color: palette.bodyTextColor, + ), + textAlign: mediaQuery.mdAndDown + ? TextAlign.center + : TextAlign.start, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Gap(10), + const TrackViewHeaderActions(), + const Gap(10), + TrackViewHeaderButtons(color: palette), + ], + ), + ), + ], + ), ), ], ), From c592cff1eea9660f906aa95a6854152bfbbb41f9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 8 Dec 2023 18:41:57 +0600 Subject: [PATCH 085/131] chore: fix padding issues --- lib/components/library/user_albums.dart | 4 ++-- lib/components/shared/playbutton_card.dart | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 04ab5b63..200d1c59 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -85,7 +85,7 @@ class UserAlbums extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), controller: controller, child: Skeletonizer( - enabled: albums.isEmpty && albumsQuery.isLoadingNextPage, + enabled: albumsQuery.pages.isEmpty, child: Center( child: Wrap( runSpacing: 20, @@ -93,7 +93,7 @@ class UserAlbums extends HookConsumerWidget { runAlignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ - if (albums.isEmpty && albumsQuery.isLoadingNextPage) + if (albumsQuery.pages.isEmpty) ...List.generate( 10, (index) => AlbumCard(FakeData.album), diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index d6226716..94751f8e 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -1,6 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotube/collections/assets.gen.dart'; @@ -59,8 +60,8 @@ class PlaybuttonCard extends HookWidget { ); final end = useBreakpointValue( - xs: 10, - sm: 10, + xs: 7, + sm: 7, others: 15, ); @@ -160,7 +161,7 @@ class PlaybuttonCard extends HookWidget { onPressed: isLoading ? null : onAddToQueuePressed, ), ), - const SizedBox(height: 5), + const Gap(5), IconButton( style: IconButton.styleFrom( backgroundColor: theme.colorScheme.primaryContainer, From 82ed5e90576b57ef32e61a65015e04862ab15461 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 8 Dec 2023 22:18:18 +0600 Subject: [PATCH 086/131] feat: compact genre view in home page --- lib/collections/fake.dart | 6 + lib/collections/gradients.dart | 232 ++++++++++++++++++ lib/collections/routes.dart | 19 +- lib/components/genre/category_card.dart | 51 ---- lib/components/home/sections/featured.dart | 36 +++ lib/components/home/sections/genres.dart | 154 ++++++++++++ .../home/sections/made_for_user.dart | 35 +++ .../home/sections/new_releases.dart | 51 ++++ lib/components/shared/playbutton_card.dart | 19 +- lib/pages/home/genres.dart | 151 ------------ lib/pages/home/genres/genre_playlists.dart | 165 +++++++++++++ lib/pages/home/genres/genres.dart | 89 +++++++ lib/pages/home/home.dart | 40 ++- lib/pages/home/personalized.dart | 106 -------- lib/services/queries/category.dart | 23 ++ 15 files changed, 840 insertions(+), 337 deletions(-) create mode 100644 lib/collections/gradients.dart delete mode 100644 lib/components/genre/category_card.dart create mode 100644 lib/components/home/sections/featured.dart create mode 100644 lib/components/home/sections/genres.dart create mode 100644 lib/components/home/sections/made_for_user.dart create mode 100644 lib/components/home/sections/new_releases.dart delete mode 100644 lib/pages/home/genres.dart create mode 100644 lib/pages/home/genres/genre_playlists.dart create mode 100644 lib/pages/home/genres/genres.dart delete mode 100644 lib/pages/home/personalized.dart diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart index a02e8587..10cf2819 100644 --- a/lib/collections/fake.dart +++ b/lib/collections/fake.dart @@ -158,4 +158,10 @@ abstract class FakeData { ..type = "type" ..description = "A very good playlist description" ..uri = "uri"; + + static final Category category = Category() + ..href = "text" + ..icons = [image] + ..id = "1" + ..name = "category"; } diff --git a/lib/collections/gradients.dart b/lib/collections/gradients.dart new file mode 100644 index 00000000..e861dde7 --- /dev/null +++ b/lib/collections/gradients.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; + +const gradients = [ + LinearGradient(colors: [ + Color.fromRGBO(123, 102, 255, 1), + Color.fromRGBO(95, 189, 255, 1), + Color.fromRGBO(150, 239, 255, 1), + Color.fromRGBO(197, 255, 248, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(245, 204, 160, 1), + Color.fromRGBO(228, 143, 69, 1), + Color.fromRGBO(153, 77, 28, 1), + Color.fromRGBO(107, 36, 12, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(243, 243, 243, 1), + Color.fromRGBO(197, 232, 152, 1), + Color.fromRGBO(41, 173, 178, 1), + Color.fromRGBO(7, 102, 173, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(240, 89, 65, 1), + Color.fromRGBO(190, 49, 68, 1), + Color.fromRGBO(135, 35, 65, 1), + Color.fromRGBO(34, 9, 44, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(119, 107, 93, 1), + Color.fromRGBO(176, 166, 149, 1), + Color.fromRGBO(235, 227, 213, 1), + Color.fromRGBO(243, 238, 234, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(208, 162, 247, 1), + Color.fromRGBO(220, 191, 255, 1), + Color.fromRGBO(229, 212, 255, 1), + Color.fromRGBO(241, 234, 255, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(221, 242, 253, 1), + Color.fromRGBO(155, 190, 200, 1), + Color.fromRGBO(66, 125, 157, 1), + Color.fromRGBO(22, 72, 99, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(119, 67, 219, 1), + Color.fromRGBO(195, 172, 208, 1), + Color.fromRGBO(247, 239, 229, 1), + Color.fromRGBO(255, 251, 245, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(194, 217, 255, 1), + Color.fromRGBO(142, 143, 250, 1), + Color.fromRGBO(119, 82, 254, 1), + Color.fromRGBO(25, 4, 130, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(104, 126, 255, 1), + Color.fromRGBO(128, 179, 255, 1), + Color.fromRGBO(152, 228, 255, 1), + Color.fromRGBO(182, 255, 250, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(176, 87, 141, 1), + Color.fromRGBO(217, 136, 185, 1), + Color.fromRGBO(250, 203, 234, 1), + Color.fromRGBO(255, 228, 214, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(190, 255, 247, 1), + Color.fromRGBO(166, 246, 255, 1), + Color.fromRGBO(158, 221, 255, 1), + Color.fromRGBO(100, 153, 233, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(245, 252, 205, 1), + Color.fromRGBO(120, 214, 198, 1), + Color.fromRGBO(65, 145, 151, 1), + Color.fromRGBO(18, 72, 107, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(229, 207, 247, 1), + Color.fromRGBO(157, 118, 193, 1), + Color.fromRGBO(113, 58, 190, 1), + Color.fromRGBO(91, 8, 136, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(249, 222, 201, 1), + Color.fromRGBO(247, 140, 162, 1), + Color.fromRGBO(216, 0, 50, 1), + Color.fromRGBO(61, 12, 17, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(242, 247, 161, 1), + Color.fromRGBO(53, 162, 159, 1), + Color.fromRGBO(8, 131, 149, 1), + Color.fromRGBO(7, 25, 82, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(243, 159, 90, 1), + Color.fromRGBO(174, 68, 90, 1), + Color.fromRGBO(102, 37, 73, 1), + Color.fromRGBO(69, 25, 82, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(255, 200, 200, 1), + Color.fromRGBO(255, 155, 130, 1), + Color.fromRGBO(255, 63, 164, 1), + Color.fromRGBO(87, 55, 93, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(238, 238, 238, 1), + Color.fromRGBO(100, 204, 197, 1), + Color.fromRGBO(23, 107, 135, 1), + Color.fromRGBO(5, 59, 80, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(198, 61, 47, 1), + Color.fromRGBO(226, 94, 62, 1), + Color.fromRGBO(255, 155, 80, 1), + Color.fromRGBO(255, 187, 92, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(236, 83, 176, 1), + Color.fromRGBO(157, 68, 192, 1), + Color.fromRGBO(77, 45, 183, 1), + Color.fromRGBO(14, 33, 160, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(242, 236, 190, 1), + Color.fromRGBO(226, 199, 153, 1), + Color.fromRGBO(192, 130, 97, 1), + Color.fromRGBO(154, 59, 59, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(255, 253, 140, 1), + Color.fromRGBO(151, 255, 244, 1), + Color.fromRGBO(112, 145, 245, 1), + Color.fromRGBO(121, 63, 223, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(67, 83, 52, 1), + Color.fromRGBO(158, 179, 132, 1), + Color.fromRGBO(206, 222, 189, 1), + Color.fromRGBO(250, 241, 228, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(250, 240, 230, 1), + Color.fromRGBO(185, 180, 199, 1), + Color.fromRGBO(92, 84, 112, 1), + Color.fromRGBO(53, 47, 68, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(255, 186, 134, 1), + Color.fromRGBO(246, 99, 92, 1), + Color.fromRGBO(194, 51, 115, 1), + Color.fromRGBO(121, 21, 91, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(213, 255, 208, 1), + Color.fromRGBO(64, 248, 255, 1), + Color.fromRGBO(39, 158, 255, 1), + Color.fromRGBO(12, 53, 106, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(131, 96, 150, 1), + Color.fromRGBO(237, 123, 123, 1), + Color.fromRGBO(240, 184, 110, 1), + Color.fromRGBO(235, 231, 108, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(63, 29, 56, 1), + Color.fromRGBO(77, 60, 119, 1), + Color.fromRGBO(162, 103, 138, 1), + Color.fromRGBO(225, 152, 152, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(254, 123, 229, 1), + Color.fromRGBO(151, 78, 195, 1), + Color.fromRGBO(80, 64, 153, 1), + Color.fromRGBO(49, 56, 102, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(248, 222, 34, 1), + Color.fromRGBO(249, 76, 16, 1), + Color.fromRGBO(199, 0, 57, 1), + Color.fromRGBO(144, 12, 63, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(101, 69, 31, 1), + Color.fromRGBO(118, 88, 39, 1), + Color.fromRGBO(200, 174, 125, 1), + Color.fromRGBO(234, 198, 150, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(255, 246, 224, 1), + Color.fromRGBO(216, 217, 218, 1), + Color.fromRGBO(97, 103, 122, 1), + Color.fromRGBO(39, 40, 41, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(145, 109, 179, 1), + Color.fromRGBO(228, 133, 134, 1), + Color.fromRGBO(252, 186, 173, 1), + Color.fromRGBO(253, 229, 236, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(124, 115, 192, 1), + Color.fromRGBO(148, 173, 215, 1), + Color.fromRGBO(172, 250, 223, 1), + Color.fromRGBO(232, 255, 206, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(174, 216, 204, 1), + Color.fromRGBO(205, 102, 136, 1), + Color.fromRGBO(122, 49, 111, 1), + Color.fromRGBO(70, 25, 89, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(237, 228, 255, 1), + Color.fromRGBO(215, 187, 245, 1), + Color.fromRGBO(160, 118, 249, 1), + Color.fromRGBO(101, 40, 247, 1) + ]), + LinearGradient(colors: [ + Color.fromRGBO(255, 236, 175, 1), + Color.fromRGBO(255, 176, 127, 1), + Color.fromRGBO(255, 82, 162, 1), + Color.fromRGBO(243, 21, 89, 1) + ]), +]; diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 82597ddb..7816f204 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -1,9 +1,11 @@ import 'package:catcher_2/catcher_2.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' hide Category; import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:spotify/spotify.dart' hide Search; import 'package:spotube/pages/album/album.dart'; +import 'package:spotube/pages/home/genres/genre_playlists.dart'; +import 'package:spotube/pages/home/genres/genres.dart'; import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/lastfm_login/lastfm_login.dart'; import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart'; @@ -38,6 +40,21 @@ final router = GoRouter( GoRoute( path: "/", pageBuilder: (context, state) => const SpotubePage(child: HomePage()), + routes: [ + GoRoute( + path: "genres", + pageBuilder: (context, state) => + const SpotubePage(child: GenrePage()), + ), + GoRoute( + path: "genre/:categoryId", + pageBuilder: (context, state) => SpotubePage( + child: GenrePlaylistsPage( + category: state.extra as Category, + ), + ), + ), + ], ), GoRoute( path: "/search", diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart deleted file mode 100644 index 7f580157..00000000 --- a/lib/components/genre/category_card.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:collection/collection.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/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; -import 'package:spotube/models/logger.dart'; -import 'package:spotube/services/queries/queries.dart'; - -class CategoryCard extends HookConsumerWidget { - final Category category; - CategoryCard( - this.category, { - Key? key, - }) : super(key: key); - - final logger = getLogger(CategoryCard); - - @override - Widget build(BuildContext context, ref) { - final playlistQuery = useQueries.category.playlistsOf( - ref, - category.id!, - ); - - final playlists = useMemoized( - () => playlistQuery.pages.expand( - (page) { - return page.items?.whereNotNull() ?? - const Iterable.empty(); - }, - ).toList(), - [playlistQuery.pages], - ); - - if (playlistQuery.hasErrors && - !playlistQuery.hasPageData && - !playlistQuery.isLoadingNextPage) { - return const SizedBox.shrink(); - } - - return HorizontalPlaybuttonCardView( - title: Text(category.name!), - isLoadingNextPage: playlistQuery.isLoadingNextPage, - hasNextPage: playlistQuery.hasNextPage, - items: playlists, - onFetchMore: playlistQuery.fetchNext, - ); - } -} diff --git a/lib/components/home/sections/featured.dart b/lib/components/home/sections/featured.dart new file mode 100644 index 00000000..8a7c2c95 --- /dev/null +++ b/lib/components/home/sections/featured.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart' hide Page; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/services/queries/queries.dart'; + +class HomeFeaturedSection extends HookConsumerWidget { + const HomeFeaturedSection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final featuredPlaylistsQuery = useQueries.playlist.featured(ref); + final playlists = useMemoized( + () => featuredPlaylistsQuery.pages + .whereType>() + .expand((page) => page.items ?? const []), + [featuredPlaylistsQuery.pages], + ); + final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData && + !featuredPlaylistsQuery.isLoadingNextPage; + + return Skeletonizer( + enabled: isLoadingFeaturedPlaylists, + child: HorizontalPlaybuttonCardView( + items: playlists.toList(), + title: Text(context.l10n.featured), + isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage, + hasNextPage: featuredPlaylistsQuery.hasNextPage, + onFetchMore: featuredPlaylistsQuery.fetchNext, + ), + ); + } +} diff --git a/lib/components/home/sections/genres.dart b/lib/components/home/sections/genres.dart new file mode 100644 index 00000000..52467b28 --- /dev/null +++ b/lib/components/home/sections/genres.dart @@ -0,0 +1,154 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/collections/gradients.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/queries/queries.dart'; + +class HomeGenresSection extends HookConsumerWidget { + const HomeGenresSection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final ThemeData(:textTheme, :colorScheme) = Theme.of(context); + final mediaQuery = MediaQuery.of(context); + + final recommendationMarket = ref.watch( + userPreferencesProvider.select((s) => s.recommendationMarket), + ); + final categoriesQuery = + useQueries.category.listAll(ref, recommendationMarket); + + final categories = categoriesQuery.data + ?.where((c) => (c.icons?.length ?? 0) > 0) + .take(mediaQuery.mdAndDown ? 6 : 10) + .toList() ?? + []; + + return SliverMainAxisGroup( + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.l10n.genre, + style: textTheme.headlineSmall, + ), + Directionality( + textDirection: TextDirection.rtl, + child: TextButton.icon( + onPressed: () { + context.push('/genres'); + }, + icon: const Icon(SpotubeIcons.angleRight), + label: Text( + "Browse All", + style: textTheme.bodyMedium?.copyWith( + color: colorScheme.secondary, + ), + ), + ), + ), + ], + ), + ), + ), + const SliverGap(8), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: Skeletonizer.sliver( + enabled: categoriesQuery.isLoading, + child: SliverGrid.builder( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: mediaQuery.mdAndDown ? 200 : 250, + mainAxisExtent: 50, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: categoriesQuery.isLoading + ? mediaQuery.mdAndDown + ? 6 + : 10 + : categories.length, + itemBuilder: (context, index) { + final category = + categories.elementAtOrNull(index) ?? FakeData.category; + + return HookBuilder(builder: (context) { + final (:gradient, :textColor) = useMemoized( + () { + final gradient = + gradients[Random().nextInt(gradients.length)]; + final text = gradient.colors + .take(2) + .any((c) => c.computeLuminance() > 0.5) + ? Colors.grey[900] + : Colors.white; + return ( + gradient: LinearGradient( + colors: gradient.colors + .map((c) => c.withOpacity(0.8)) + .toList(), + ), + textColor: text + ); + }, + [], + ); + + return InkWell( + onTap: () { + context.push('/genre/${category.id}', extra: category); + }, + borderRadius: BorderRadius.circular(8), + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + image: DecorationImage( + image: UniversalImage.imageProvider( + category.icons!.first.url!, + ), + fit: BoxFit.cover, + ), + ), + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: colorScheme.surfaceVariant, + gradient: categoriesQuery.isLoading ? null : gradient, + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + category.name!, + style: textTheme.titleMedium + ?.copyWith(color: textColor), + ), + ), + ), + ), + ); + }); + }, + ), + ), + ), + ], + ); + } +} diff --git a/lib/components/home/sections/made_for_user.dart b/lib/components/home/sections/made_for_user.dart new file mode 100644 index 00000000..a3f96899 --- /dev/null +++ b/lib/components/home/sections/made_for_user.dart @@ -0,0 +1,35 @@ +import 'package:flutter/widgets.dart'; +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/services/queries/queries.dart'; + +class HomeMadeForUserSection extends HookConsumerWidget { + const HomeMadeForUserSection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final madeForUser = useQueries.views.get(ref, "made-for-x-hub"); + + return SliverList.builder( + itemCount: madeForUser.data?["content"]?["items"]?.length ?? 0, + itemBuilder: (context, index) { + final item = madeForUser.data?["content"]?["items"]?[index]; + 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, + isLoadingNextPage: false, + onFetchMore: () {}, + ); + }, + ); + } +} diff --git a/lib/components/home/sections/new_releases.dart b/lib/components/home/sections/new_releases.dart new file mode 100644 index 00000000..77481de1 --- /dev/null +++ b/lib/components/home/sections/new_releases.dart @@ -0,0 +1,51 @@ +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/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +class HomeNewReleasesSection extends HookConsumerWidget { + const HomeNewReleasesSection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final auth = ref.watch(AuthenticationNotifier.provider); + + final newReleases = useQueries.album.newReleases(ref); + final userArtistsQuery = useQueries.artist.followedByMeAll(ref); + final userArtists = + userArtistsQuery.data?.map((s) => s.id!).toList() ?? const []; + + final albums = useMemoized( + () => newReleases.pages + .whereType>() + .expand((page) => page.items ?? const []) + .where((album) { + return album.artists + ?.any((artist) => userArtists.contains(artist.id!)) == + true; + }) + .map((album) => TypeConversionUtils.simpleAlbum_X_Album(album)) + .toList(), + [newReleases.pages], + ); + + final hasNewReleases = newReleases.hasPageData && + userArtistsQuery.hasData && + !newReleases.isLoadingNextPage; + + if (auth == null || !hasNewReleases) return const SizedBox.shrink(); + + return HorizontalPlaybuttonCardView( + items: albums, + title: Text(context.l10n.new_releases), + isLoadingNextPage: newReleases.isLoadingNextPage, + hasNextPage: newReleases.hasNextPage, + onFetchMore: newReleases.fetchNext, + ); + } +} diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index 94751f8e..a8a75d30 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -4,10 +4,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:skeletonizer/skeletonizer.dart'; -import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/hover_builder.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; @@ -50,6 +50,7 @@ class PlaybuttonCard extends HookWidget { Widget build(BuildContext context) { final textsKey = useMemoized(() => GlobalKey(), []); final theme = Theme.of(context); + final mediaQuery = MediaQuery.of(context); final radius = BorderRadius.circular(15); final double size = useBreakpointValue( @@ -86,23 +87,27 @@ class PlaybuttonCard extends HookWidget { splashFactory: theme.splashFactory, child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Stack( clipBehavior: Clip.none, children: [ Container( + margin: const EdgeInsets.fromLTRB(8, 8, 8, 0), padding: const EdgeInsets.only( left: 8, right: 8, top: 8, ), - constraints: BoxConstraints(maxHeight: size), - child: ClipRRect( + height: mediaQuery.smAndDown + ? 120 + : mediaQuery.mdAndDown + ? 130 + : 150, + decoration: BoxDecoration( borderRadius: radius, - child: UniversalImage( - path: imageUrl, - placeholder: Assets.albumPlaceholder.path, + image: DecorationImage( + image: UniversalImage.imageProvider(imageUrl), fit: BoxFit.cover, ), ), diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart deleted file mode 100644 index 88eaef70..00000000 --- a/lib/pages/home/genres.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:collection/collection.dart'; -import 'package:fuzzywuzzy/fuzzywuzzy.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:skeletonizer/skeletonizer.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/genre/category_card.dart'; -import 'package:spotube/components/shared/expandable_search/expandable_search.dart'; -import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; -import 'package:spotube/components/shared/waypoint.dart'; - -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/queries/queries.dart'; -import 'package:very_good_infinite_list/very_good_infinite_list.dart'; - -class GenrePage extends HookConsumerWidget { - const GenrePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, ref) { - final scrollController = useScrollController(); - final recommendationMarket = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), - ); - final categoriesQuery = useQueries.category.list(ref, recommendationMarket); - final isFiltering = useState(false); - - final isMounted = useIsMounted(); - - final searchController = useTextEditingController(); - final searchFocus = useFocusNode(); - - useValueListenable(searchController); - - final categories = useMemoized( - () { - final categories = categoriesQuery.pages - .expand( - (page) => page.items ?? const Iterable.empty(), - ) - .toList(); - if (searchController.text.isEmpty) { - return categories; - } - return categories - .map((e) => ( - weightedRatio(e.name!, searchController.text), - e, - )) - .sorted((a, b) => b.$1.compareTo(a.$1)) - .where((e) => e.$1 > 50) - .map((e) => e.$2) - .toList(); - }, - [categoriesQuery.pages, searchController.text], - ); - - final list = RefreshIndicator( - onRefresh: () async { - await categoriesQuery.refreshAll(); - }, - child: Waypoint( - onTouchEdge: () async { - if (categoriesQuery.hasNextPage && isMounted()) { - await categoriesQuery.fetchNext(); - } - }, - controller: scrollController, - child: Column( - children: [ - ExpandableSearchField( - isFiltering: isFiltering.value, - onChangeFiltering: (value) => isFiltering.value = value, - searchController: searchController, - searchFocus: searchFocus, - ), - if (!categoriesQuery.hasPageData && - !categoriesQuery.isLoadingNextPage) - Expanded( - child: Skeletonizer( - enabled: true, - child: ListView.builder( - itemCount: 5, - itemBuilder: (context, index) { - return HorizontalPlaybuttonCardView( - title: const Text("Loading"), - items: const [], - hasNextPage: true, - isLoadingNextPage: false, - onFetchMore: () {}, - ); - }, - ), - ), - ) - else - Expanded( - child: InfiniteList( - scrollController: scrollController, - itemCount: categories.length, - onFetchData: categoriesQuery.fetchNext, - isLoading: categoriesQuery.isLoadingNextPage, - hasReachedMax: !categoriesQuery.hasNextPage, - loadingBuilder: (context) => Skeletonizer( - enabled: true, - child: HorizontalPlaybuttonCardView( - title: const Text("Loading"), - items: const [], - hasNextPage: true, - isLoadingNextPage: false, - onFetchMore: () {}, - ), - ), - itemBuilder: (context, index) { - return CategoryCard(categories[index]); - }, - ), - ), - ], - ), - ), - ); - - return Stack( - children: [ - Positioned.fill(child: list), - Positioned( - top: 0, - right: 10, - child: ExpandableSearchButton( - isFiltering: isFiltering.value, - searchFocus: searchFocus, - icon: const Icon(SpotubeIcons.search), - onPressed: (value) { - isFiltering.value = value; - if (isFiltering.value) { - scrollController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - }, - ), - ), - ], - ); - } -} diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart new file mode 100644 index 00000000..600880e0 --- /dev/null +++ b/lib/pages/home/genres/genre_playlists.dart @@ -0,0 +1,165 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotify/spotify.dart' hide Offset; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/components/playlist/playlist_card.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/shared/waypoint.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/services/queries/queries.dart'; +import 'package:collection/collection.dart'; + +class GenrePlaylistsPage extends HookConsumerWidget { + final Category category; + const GenrePlaylistsPage({Key? key, required this.category}) + : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final playlistsQuery = useQueries.category.playlistsOf( + ref, + category.id!, + ); + + final playlists = useMemoized( + () => playlistsQuery.pages.expand( + (page) { + return page.items?.whereNotNull() ?? + const Iterable.empty(); + }, + ).toList(), + [playlistsQuery.pages], + ); + + final mediaQuery = MediaQuery.of(context); + + final scrollController = useScrollController(); + + return Scaffold( + appBar: const PageWindowTitleBar( + automaticallyImplyLeading: true, + backgroundColor: Colors.transparent, + ), + extendBodyBehindAppBar: true, + body: CustomScrollView( + controller: scrollController, + slivers: [ + SliverAppBar( + automaticallyImplyLeading: false, + expandedHeight: mediaQuery.mdAndDown ? 200 : 250, + flexibleSpace: FlexibleSpaceBar( + stretchModes: const [ + StretchMode.zoomBackground, + StretchMode.blurBackground, + ], + background: DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + image: UniversalImage.imageProvider( + category.icons!.first.url!, + ), + fit: BoxFit.cover, + ), + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: const ColoredBox(color: Colors.transparent), + ), + ), + centerTitle: true, + title: Text( + category.name!, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + letterSpacing: 3, + shadows: [ + const Shadow( + offset: Offset(-1.5, -1.5), + color: Colors.black54, + ), + const Shadow( + offset: Offset(1.5, -1.5), + color: Colors.black54, + ), + const Shadow( + offset: Offset(1.5, 1.5), + color: Colors.black54, + ), + const Shadow( + offset: Offset(-1.5, 1.5), + color: Colors.black54, + ), + ], + ), + ), + collapseMode: CollapseMode.parallax, + ), + ), + const SliverGap(20), + SliverSafeArea( + top: false, + sliver: SliverPadding( + padding: EdgeInsets.symmetric( + horizontal: mediaQuery.mdAndDown ? 12 : 24, + ), + sliver: playlists.isEmpty + ? Skeletonizer.sliver( + child: SliverToBoxAdapter( + child: Wrap( + spacing: 12, + runSpacing: 12, + children: List.generate( + 6, + (index) => PlaylistCard(FakeData.playlist), + ), + ), + ), + ) + : SliverGrid.builder( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 190, + mainAxisExtent: mediaQuery.mdAndDown ? 225 : 250, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: playlists.length + 1, + itemBuilder: (context, index) { + final playlist = playlists.elementAtOrNull(index); + + if (playlist == null) { + if (!playlistsQuery.hasNextPage) { + return const SizedBox.shrink(); + } + return Skeletonizer( + enabled: true, + child: Waypoint( + controller: scrollController, + isGrid: true, + onTouchEdge: () async { + if (playlistsQuery.hasNextPage) { + await playlistsQuery.fetchNext(); + } + }, + child: PlaylistCard(FakeData.playlist), + ), + ); + } + + return Skeleton.keep( + child: PlaylistCard(playlist), + ); + }, + ), + ), + ), + const SliverGap(20), + ], + ), + ); + } +} diff --git a/lib/pages/home/genres/genres.dart b/lib/pages/home/genres/genres.dart new file mode 100644 index 00000000..0ab43e83 --- /dev/null +++ b/lib/pages/home/genres/genres.dart @@ -0,0 +1,89 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotify/spotify.dart' hide Offset; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/extensions/constrains.dart'; + +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/queries/queries.dart'; + +class GenrePage extends HookConsumerWidget { + const GenrePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final ThemeData(:textTheme) = Theme.of(context); + final scrollController = useScrollController(); + final recommendationMarket = ref.watch( + userPreferencesProvider.select((s) => s.recommendationMarket), + ); + final categoriesQuery = + useQueries.category.listAll(ref, recommendationMarket); + + final categories = categoriesQuery.data ?? []; + + final mediaQuery = MediaQuery.of(context); + + return Scaffold( + appBar: const PageWindowTitleBar(automaticallyImplyLeading: true), + body: SafeArea( + top: false, + child: GridView.builder( + padding: const EdgeInsets.all(12), + controller: scrollController, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + childAspectRatio: 9 / 18, + maxCrossAxisExtent: mediaQuery.smAndDown ? 200 : 300, + mainAxisExtent: 200, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + return InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + context.push("/genre/${category.id}", extra: category); + }, + child: Ink( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: NetworkImage(category.icons!.first.url!), + fit: BoxFit.cover, + ), + ), + child: Align( + alignment: Alignment.bottomCenter, + child: AutoSizeText( + category.name!, + style: textTheme.titleLarge?.copyWith( + shadows: [ + // stroke shadow + const Shadow( + color: Colors.black, + offset: Offset(1, 1), + blurRadius: 2, + ), + ], + ), + maxLines: 1, + textAlign: TextAlign.center, + maxFontSize: textTheme.titleLarge!.fontSize!, + minFontSize: textTheme.titleMedium!.fontSize!, + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 34f136b6..0a8a0aac 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -1,35 +1,33 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/home/sections/featured.dart'; +import 'package:spotube/components/home/sections/genres.dart'; +import 'package:spotube/components/home/sections/made_for_user.dart'; +import 'package:spotube/components/home/sections/new_releases.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/shared/themed_button_tab_bar.dart'; -import 'package:spotube/extensions/context.dart'; -import 'package:spotube/pages/home/genres.dart'; -import 'package:spotube/pages/home/personalized.dart'; class HomePage extends HookConsumerWidget { const HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { - return DefaultTabController( - length: 2, - child: Scaffold( - appBar: PageWindowTitleBar( - centerTitle: true, - leadingWidth: double.infinity, - leading: ThemedButtonsTabBar( - tabs: [ - Tab(text: " ${context.l10n.personalized} "), - Tab(text: " ${context.l10n.genre} "), + final controller = useScrollController(); + + return Scaffold( + appBar: const PageWindowTitleBar(), + body: CustomScrollView( + controller: controller, + slivers: [ + const HomeGenresSection(), + SliverList.list( + children: const [ + HomeFeaturedSection(), + HomeNewReleasesSection(), ], ), - ), - body: const TabBarView( - children: [ - PersonalizedPage(), - GenrePage(), - ], - ), + const SliverSafeArea(sliver: HomeMadeForUserSection()), + ], ), ); } diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart deleted file mode 100644 index 22224c39..00000000 --- a/lib/pages/home/personalized.dart +++ /dev/null @@ -1,106 +0,0 @@ -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/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; -import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/services/queries/queries.dart'; -import 'package:spotube/utils/type_conversion_utils.dart'; -import 'package:skeletonizer/skeletonizer.dart'; - -class PersonalizedPage extends HookConsumerWidget { - const PersonalizedPage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, ref) { - final controller = useScrollController(); - final auth = ref.watch(AuthenticationNotifier.provider); - final featuredPlaylistsQuery = useQueries.playlist.featured(ref); - final playlists = useMemoized( - () => featuredPlaylistsQuery.pages - .whereType>() - .expand((page) => page.items ?? const []), - [featuredPlaylistsQuery.pages], - ); - - final madeForUser = useQueries.views.get(ref, "made-for-x-hub"); - - final newReleases = useQueries.album.newReleases(ref); - final userArtistsQuery = useQueries.artist.followedByMeAll(ref); - final userArtists = - userArtistsQuery.data?.map((s) => s.id!).toList() ?? const []; - - final albums = useMemoized( - () => newReleases.pages - .whereType>() - .expand((page) => page.items ?? const []) - .where((album) { - return album.artists - ?.any((artist) => userArtists.contains(artist.id!)) == - true; - }) - .map((album) => TypeConversionUtils.simpleAlbum_X_Album(album)) - .toList(), - [newReleases.pages], - ); - - final hasNewReleases = newReleases.hasPageData && - userArtistsQuery.hasData && - !newReleases.isLoadingNextPage; - - final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData && - !featuredPlaylistsQuery.isLoadingNextPage; - - return CustomScrollView( - controller: controller, - slivers: [ - SliverList.list( - children: [ - Skeletonizer( - enabled: isLoadingFeaturedPlaylists, - child: HorizontalPlaybuttonCardView( - items: playlists.toList(), - title: Text(context.l10n.featured), - isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage, - hasNextPage: featuredPlaylistsQuery.hasNextPage, - onFetchMore: featuredPlaylistsQuery.fetchNext, - ), - ), - if (auth != null || hasNewReleases) - HorizontalPlaybuttonCardView( - items: albums, - title: Text(context.l10n.new_releases), - isLoadingNextPage: newReleases.isLoadingNextPage, - hasNextPage: newReleases.hasNextPage, - onFetchMore: newReleases.fetchNext, - ), - ], - ), - SliverSafeArea( - sliver: SliverList.builder( - itemCount: madeForUser.data?["content"]?["items"]?.length ?? 0, - itemBuilder: (context, index) { - final item = madeForUser.data?["content"]?["items"]?[index]; - 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, - isLoadingNextPage: false, - onFetchMore: () {}, - ); - }, - ), - ), - ], - ); - } -} diff --git a/lib/services/queries/category.dart b/lib/services/queries/category.dart index 960b5702..6a4b196e 100644 --- a/lib/services/queries/category.dart +++ b/lib/services/queries/category.dart @@ -5,12 +5,35 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; +import 'package:spotube/hooks/spotify/use_spotify_query.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; class CategoryQueries { const CategoryQueries(); + Query, dynamic> listAll( + WidgetRef ref, Market recommendationMarket) { + ref.watch(userPreferencesProvider.select((s) => s.locale)); + final locale = useContext().l10n.localeName; + final query = useSpotifyQuery, dynamic>( + "category-playlists", + (spotify) async { + final categories = await spotify.categories + .list( + country: recommendationMarket, + locale: locale, + ) + .all(); + + return categories.toList(); + }, + ref: ref, + ); + + return query; + } + InfiniteQuery, dynamic, int> list( WidgetRef ref, Market recommendationMarket, From 956f4b198b103aab65844a067cff9a3906266a31 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 8 Dec 2023 22:37:56 +0600 Subject: [PATCH 087/131] chore: fix translations issues --- lib/components/home/sections/genres.dart | 2 +- lib/l10n/app_en.arb | 4 +- lib/pages/home/home.dart | 4 +- untranslated_messages.json | 60 ++++++++++++++++++------ 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/lib/components/home/sections/genres.dart b/lib/components/home/sections/genres.dart index 52467b28..190f24f7 100644 --- a/lib/components/home/sections/genres.dart +++ b/lib/components/home/sections/genres.dart @@ -45,7 +45,7 @@ class HomeGenresSection extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - context.l10n.genre, + context.l10n.genres, style: textTheme.headlineSmall, ), Directionality( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 85900012..ccb95160 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -281,5 +281,7 @@ "login_with_your_lastfm": "Login with your Last.fm account", "scrobble_to_lastfm": "Scrobble to Last.fm", "go_to_album": "Go to Album", - "discord_rich_presence": "Discord Rich Presence" + "discord_rich_presence": "Discord Rich Presence", + "browse_all": "Browse All", + "genres": "Genres" } \ No newline at end of file diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 0a8a0aac..2970cb62 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -1,4 +1,5 @@ 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/home/sections/featured.dart'; @@ -15,7 +16,8 @@ class HomePage extends HookConsumerWidget { final controller = useScrollController(); return Scaffold( - appBar: const PageWindowTitleBar(), + appBar: + DesktopTools.platform.isMobile ? null : const PageWindowTitleBar(), body: CustomScrollView( controller: controller, slivers: [ diff --git a/untranslated_messages.json b/untranslated_messages.json index e3bdc047..7ec3886a 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,76 +1,106 @@ { "ar": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "bn": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "ca": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "de": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "es": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "fa": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "fr": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "hi": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "ja": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "pl": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "pt": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "ru": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "tr": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "uk": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ], "zh": [ "go_to_album", - "discord_rich_presence" + "discord_rich_presence", + "browse_all", + "genres" ] } From 9ee60677f6d50df7468e12dc6653ecedefa2494f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 8 Dec 2023 23:31:20 +0600 Subject: [PATCH 088/131] fix: add safe area in home --- lib/pages/home/home.dart | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 2970cb62..9b33a66c 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -15,22 +15,25 @@ class HomePage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final controller = useScrollController(); - return Scaffold( - appBar: - DesktopTools.platform.isMobile ? null : const PageWindowTitleBar(), - body: CustomScrollView( - controller: controller, - slivers: [ - const HomeGenresSection(), - SliverList.list( - children: const [ - HomeFeaturedSection(), - HomeNewReleasesSection(), + return SafeArea( + bottom: false, + child: Scaffold( + appBar: DesktopTools.platform.isMobile + ? null + : const PageWindowTitleBar(), + body: CustomScrollView( + controller: controller, + slivers: [ + const HomeGenresSection(), + SliverList.list( + children: const [ + HomeFeaturedSection(), + HomeNewReleasesSection(), + ], + ), + const SliverSafeArea(sliver: HomeMadeForUserSection()), ], ), - const SliverSafeArea(sliver: HomeMadeForUserSection()), - ], - ), - ); + )); } } From 781dfdd7c9fa9a85e96be3182bf899921393d0b9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 10 Dec 2023 16:51:38 +0600 Subject: [PATCH 089/131] cd: upgrade flutter version to 3.16.0 --- .github/workflows/spotube-release-binary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index f5cc65ee..d4f15664 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -26,7 +26,7 @@ on: default: true env: - FLUTTER_VERSION: '3.16.0' + FLUTTER_VERSION: '3.16.3' jobs: windows: From 0def714af29f05bbfde348e0494eea019ad6032b Mon Sep 17 00:00:00 2001 From: Gorom <1423386+Go-rom@users.noreply.github.com> Date: Mon, 11 Dec 2023 15:33:21 +0100 Subject: [PATCH 090/131] fix(android): wrong app name for the french version #830 (#944) --- android/app/build.gradle | 6 +++--- android/app/src/main/AndroidManifest.xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index cd6bc457..df13c9f4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -78,19 +78,19 @@ android { productFlavors { nightly { dimension "default" - resValue "string", "app_name", "Spotube Nightly" + resValue "string", "app_name_en", "Spotube Nightly" applicationIdSuffix ".nightly" versionNameSuffix "-nightly" } dev { dimension "default" - resValue "string", "app_name", "Spotube Dev" + resValue "string", "app_name_en", "Spotube Dev" applicationIdSuffix ".dev" versionNameSuffix "-dev" } stable { dimension "default" - resValue "string", "app_name", "Spotube" + resValue "string", "app_name_en", "Spotube" } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bfb51226..a3f1390a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ Date: Mon, 11 Dec 2023 20:35:48 +0600 Subject: [PATCH 091/131] chore: fix italian translation format error --- lib/l10n/app_it.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index d8f0ae88..ada9f1f6 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -262,7 +262,7 @@ "connection_restored": "Connessione ad internet ripristinata", "use_system_title_bar": "Usa la barra del titolo di sistema", "crunching_results": "Elaborazione risultati...", - "search_to_get_results": "Cerca per ottenere risultati" + "search_to_get_results": "Cerca per ottenere risultati", "use_amoled_mode": "Usa modalità AMOLED", "pitch_dark_theme": "Tema nero profondo", "normalize_audio": "Normalizza audio", @@ -280,4 +280,4 @@ "login": "Accesso", "login_with_your_lastfm": "Accedi con il tuo account Last.fm", "scrobble_to_lastfm": "Invia a Last.fm" -} \ No newline at end of file +} From e74d880bac39204bb523a3e5981d1cd8f7521785 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 11 Dec 2023 23:34:28 +0600 Subject: [PATCH 092/131] cd: add pr-lint workflow --- .github/workflows/pr-lint.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/pr-lint.yml diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml new file mode 100644 index 00000000..72495b98 --- /dev/null +++ b/.github/workflows/pr-lint.yml @@ -0,0 +1,26 @@ +name: Lint + +on: + pull_request: + +env: + FLUTTER_VERSION: '3.16.0' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Lint Dart files + run: | + flutter pub get + flutter analyze . + + - name: Lint translations & config files + run: | + jsonlint -q -D --enforce-double-quotes ./lib/l10n/*.arb + jsonlint -q -D --enforce-double-quotes -T .vscode/*.json \ No newline at end of file From 11949b39ffbaa1f8c9c61ea980463a530edb63de Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 11 Dec 2023 23:39:05 +0600 Subject: [PATCH 093/131] cd: lint ignore warnings --- .github/workflows/pr-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 72495b98..d4130c0f 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -18,7 +18,7 @@ jobs: - name: Lint Dart files run: | flutter pub get - flutter analyze . + flutter analyze --no-fatal-infos --no-fatal-warnings - name: Lint translations & config files run: | From d4a2e5c3278d06c17cac08271ef68a7e0a69bb55 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 11 Dec 2023 23:48:13 +0600 Subject: [PATCH 094/131] cd: fix secrets not being generated for lint --- .github/workflows/pr-lint.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index d4130c0f..490cb82a 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -10,14 +10,19 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} - - name: Lint Dart files + - name: Configure repo run: | flutter pub get + echo '${{ secrets.DOTENV_NIGHTLY }}' > .env + dart run build_runner build --delete-conflicting-outputs + + - name: Lint Dart files + run: | flutter analyze --no-fatal-infos --no-fatal-warnings - name: Lint translations & config files From 69396e64657ea29d88209bad4078647357924e94 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 11 Dec 2023 23:59:25 +0600 Subject: [PATCH 095/131] cd: no fatal lint warnings --- .github/workflows/pr-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 490cb82a..b6087c95 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -23,7 +23,7 @@ jobs: - name: Lint Dart files run: | - flutter analyze --no-fatal-infos --no-fatal-warnings + dart analyze --no-fatal-warnings - name: Lint translations & config files run: | From 2b0d17e9cad742a6e651bc48752ff7cfd2782c32 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 12 Dec 2023 00:01:27 +0600 Subject: [PATCH 096/131] cd: fix json lint --- .github/workflows/pr-lint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index b6087c95..e4fb55c5 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -27,5 +27,6 @@ jobs: - name: Lint translations & config files run: | + npm install -g @prantlf/jsonlint jsonlint -q -D --enforce-double-quotes ./lib/l10n/*.arb jsonlint -q -D --enforce-double-quotes -T .vscode/*.json \ No newline at end of file From 2fb16e64e9cdfca54d633cdf287b0544ecdda3b6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 12 Dec 2023 21:27:14 +0600 Subject: [PATCH 097/131] fix: genre border issues --- lib/components/home/sections/genres.dart | 4 +- lib/l10n/app_en.arb | 3 +- lib/pages/home/genres/genre_playlists.dart | 10 +++- lib/pages/home/genres/genres.dart | 13 +++++- lib/services/queries/category.dart | 6 ++- untranslated_messages.json | 54 ++++++++++++++++------ 6 files changed, 66 insertions(+), 24 deletions(-) diff --git a/lib/components/home/sections/genres.dart b/lib/components/home/sections/genres.dart index 190f24f7..41ba235c 100644 --- a/lib/components/home/sections/genres.dart +++ b/lib/components/home/sections/genres.dart @@ -117,7 +117,7 @@ class HomeGenresSection extends HookConsumerWidget { borderRadius: BorderRadius.circular(8), child: Ink( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), + borderRadius: BorderRadius.circular(8), image: DecorationImage( image: UniversalImage.imageProvider( category.icons!.first.url!, @@ -127,7 +127,7 @@ class HomeGenresSection extends HookConsumerWidget { ), child: Ink( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(5), color: colorScheme.surfaceVariant, gradient: categoriesQuery.isLoading ? null : gradient, ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ccb95160..bebfafac 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -283,5 +283,6 @@ "go_to_album": "Go to Album", "discord_rich_presence": "Discord Rich Presence", "browse_all": "Browse All", - "genres": "Genres" + "genres": "Genres", + "explore_genres": "Explore Genres" } \ No newline at end of file diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart index 600880e0..19f98c7f 100644 --- a/lib/pages/home/genres/genre_playlists.dart +++ b/lib/pages/home/genres/genre_playlists.dart @@ -43,8 +43,9 @@ class GenrePlaylistsPage extends HookConsumerWidget { return Scaffold( appBar: const PageWindowTitleBar( - automaticallyImplyLeading: true, + leading: BackButton(color: Colors.white), backgroundColor: Colors.transparent, + foregroundColor: Colors.white, ), extendBodyBehindAppBar: true, body: CustomScrollView( @@ -52,7 +53,11 @@ class GenrePlaylistsPage extends HookConsumerWidget { slivers: [ SliverAppBar( automaticallyImplyLeading: false, - expandedHeight: mediaQuery.mdAndDown ? 200 : 250, + expandedHeight: mediaQuery.mdAndDown ? 200 : 150, + pinned: true, + floating: false, + title: const Text(""), + backgroundColor: Colors.brown.withOpacity(0.7), flexibleSpace: FlexibleSpaceBar( stretchModes: const [ StretchMode.zoomBackground, @@ -76,6 +81,7 @@ class GenrePlaylistsPage extends HookConsumerWidget { title: Text( category.name!, style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Colors.white, letterSpacing: 3, shadows: [ const Shadow( diff --git a/lib/pages/home/genres/genres.dart b/lib/pages/home/genres/genres.dart index 0ab43e83..dc165fe4 100644 --- a/lib/pages/home/genres/genres.dart +++ b/lib/pages/home/genres/genres.dart @@ -1,12 +1,15 @@ +import 'dart:math'; + import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart' hide Offset; +import 'package:spotube/collections/gradients.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -29,7 +32,10 @@ class GenrePage extends HookConsumerWidget { final mediaQuery = MediaQuery.of(context); return Scaffold( - appBar: const PageWindowTitleBar(automaticallyImplyLeading: true), + appBar: PageWindowTitleBar( + title: Text(context.l10n.explore_genres), + automaticallyImplyLeading: true, + ), body: SafeArea( top: false, child: GridView.builder( @@ -45,6 +51,7 @@ class GenrePage extends HookConsumerWidget { itemCount: categories.length, itemBuilder: (context, index) { final category = categories[index]; + final gradient = gradients[Random().nextInt(gradients.length)]; return InkWell( borderRadius: BorderRadius.circular(8), onTap: () { @@ -58,12 +65,14 @@ class GenrePage extends HookConsumerWidget { image: NetworkImage(category.icons!.first.url!), fit: BoxFit.cover, ), + gradient: gradient, ), child: Align( alignment: Alignment.bottomCenter, child: AutoSizeText( category.name!, style: textTheme.titleLarge?.copyWith( + color: Colors.white, shadows: [ // stroke shadow const Shadow( diff --git a/lib/services/queries/category.dart b/lib/services/queries/category.dart index 6a4b196e..d520b909 100644 --- a/lib/services/queries/category.dart +++ b/lib/services/queries/category.dart @@ -13,7 +13,9 @@ class CategoryQueries { const CategoryQueries(); Query, dynamic> listAll( - WidgetRef ref, Market recommendationMarket) { + WidgetRef ref, + Market recommendationMarket, + ) { ref.watch(userPreferencesProvider.select((s) => s.locale)); final locale = useContext().l10n.localeName; final query = useSpotifyQuery, dynamic>( @@ -26,7 +28,7 @@ class CategoryQueries { ) .all(); - return categories.toList(); + return categories.toList()..shuffle(); }, ref: ref, ); diff --git a/untranslated_messages.json b/untranslated_messages.json index 7ec3886a..66d8c7b2 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -3,104 +3,128 @@ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "bn": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "ca": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "de": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "es": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "fa": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "fr": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "hi": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" + ], + + "it": [ + "audio_source", + "go_to_album", + "discord_rich_presence", + "browse_all", + "genres", + "explore_genres" ], "ja": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "pl": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "pt": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "ru": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "tr": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "uk": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ], "zh": [ "go_to_album", "discord_rich_presence", "browse_all", - "genres" + "genres", + "explore_genres" ] } From 4050f556400aaec5515231578512cf1a6b990110 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 18 Dec 2023 22:12:16 +0600 Subject: [PATCH 098/131] feat: Deep link support (#950) * feat: add deep link support * feat(android): add intent share support * chore: untranslated msg for it locale --- android/app/src/main/AndroidManifest.xml | 26 +++++- lib/collections/initializers.dart | 25 +++++ lib/hooks/configurators/use_deep_linking.dart | 93 +++++++++++++++++++ lib/main.dart | 10 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + linux/my_application.cc | 13 ++- linux/packaging/appimage/make_config.yaml | 3 + linux/packaging/deb/make_config.yaml | 3 + linux/packaging/rpm/make_config.yaml | 3 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Runner/Info.plist | 13 +++ pubspec.lock | 30 +++++- pubspec.yaml | 3 + untranslated_messages.json | 6 ++ .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + windows/runner/win32_window.cpp | 40 ++++++++ windows/runner/win32_window.h | 4 + 19 files changed, 273 insertions(+), 10 deletions(-) create mode 100644 lib/collections/initializers.dart create mode 100644 lib/hooks/configurators/use_deep_linking.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a3f1390a..5ab7a0b5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/collections/initializers.dart b/lib/collections/initializers.dart new file mode 100644 index 00000000..9627de1c --- /dev/null +++ b/lib/collections/initializers.dart @@ -0,0 +1,25 @@ +import 'dart:io'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +import 'package:win32_registry/win32_registry.dart'; + +Future registerWindowsScheme(String scheme) async { + if (!DesktopTools.platform.isWindows) return; + String appPath = Platform.resolvedExecutable; + + String protocolRegKey = 'Software\\Classes\\$scheme'; + RegistryValue protocolRegValue = const RegistryValue( + 'URL Protocol', + RegistryValueType.string, + '', + ); + String protocolCmdRegKey = 'shell\\open\\command'; + RegistryValue protocolCmdRegValue = RegistryValue( + '', + RegistryValueType.string, + '"$appPath" "%1"', + ); + + final regKey = Registry.currentUser.createKey(protocolRegKey); + regKey.createValue(protocolRegValue); + regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue); +} diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart new file mode 100644 index 00000000..546ab2e8 --- /dev/null +++ b/lib/hooks/configurators/use_deep_linking.dart @@ -0,0 +1,93 @@ +import 'package:app_links/app_links.dart'; +import 'package:fl_query_hooks/fl_query_hooks.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/routes.dart'; +import 'package:spotube/provider/spotify_provider.dart'; +import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; +import 'package:flutter_sharing_intent/model/sharing_file.dart'; + +void useDeepLinking(WidgetRef ref) { + // single instance no worries + final appLinks = AppLinks(); + final spotify = ref.watch(spotifyProvider); + final queryClient = useQueryClient(); + + useEffect(() { + void uriListener(List files) async { + for (final file in files) { + if (file.type != SharedMediaType.URL) continue; + final url = Uri.parse(file.value!); + if (url.pathSegments.length != 2) continue; + + switch (url.pathSegments.first) { + case "album": + router.push( + "/album/${url.pathSegments.last}", + extra: await queryClient.fetchQuery( + "album/${url.pathSegments.last}", + () => spotify.albums.get(url.pathSegments.last), + ), + ); + break; + case "artist": + router.push("/artist/${url.pathSegments.last}"); + break; + case "playlist": + router.push( + "/playlist/${url.pathSegments.last}", + extra: await queryClient.fetchQuery( + "playlist/${url.pathSegments.last}", + () => spotify.playlists.get(url.pathSegments.last), + ), + ); + break; + default: + break; + } + } + } + + FlutterSharingIntent.instance.getInitialSharing().then(uriListener); + + final mediaStream = + FlutterSharingIntent.instance.getMediaStream().listen(uriListener); + + final subscription = appLinks.allStringLinkStream.listen((uri) async { + final startSegment = uri.split(":").take(2).join(":"); + final endSegment = uri.split(":").last; + + switch (startSegment) { + case "spotify:album": + await router.push( + "/album/$endSegment", + extra: await queryClient.fetchQuery( + "album/$endSegment", + () => spotify.albums.get(endSegment), + ), + ); + break; + case "spotify:artist": + await router.push("/artist/$endSegment"); + break; + case "spotify:playlist": + await router.push( + "/playlist/$endSegment", + extra: await queryClient.fetchQuery( + "playlist/$endSegment", + () => spotify.playlists.get(endSegment), + ), + ); + break; + default: + break; + } + }); + + return () { + mediaStream.cancel(); + subscription.cancel(); + }; + }, [spotify, queryClient]); +} diff --git a/lib/main.dart b/lib/main.dart index 91ec789d..052e6809 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,9 +13,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:media_kit/media_kit.dart'; import 'package:metadata_god/metadata_god.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotube/collections/initializers.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/hooks/configurators/use_close_behavior.dart'; +import 'package:spotube/hooks/configurators/use_deep_linking.dart'; import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart'; import 'package:spotube/hooks/configurators/use_get_storage_perms.dart'; import 'package:spotube/l10n/l10n.dart'; @@ -41,6 +43,8 @@ Future main(List rawArgs) async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + await registerWindowsScheme("spotify"); + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); MediaKit.ensureInitialized(); @@ -181,8 +185,11 @@ class SpotubeState extends ConsumerState { final paletteColor = ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); + useDisableBatteryOptimizations(); useInitSysTray(ref); + useDeepLinking(ref); useCloseBehavior(ref); + useGetStoragePermissions(ref); useEffect(() { FlutterNativeSplash.remove(); @@ -193,9 +200,6 @@ class SpotubeState extends ConsumerState { }; }, []); - useDisableBatteryOptimizations(); - useGetStoragePermissions(ref); - final lightTheme = useMemoized( () => theme(paletteColor ?? accentMaterialColor, Brightness.light, false), [paletteColor, accentMaterialColor], diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a07f7f9b..c69c17c0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) local_notifier_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin"); local_notifier_plugin_register_with_registrar(local_notifier_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 97d541b3..a4487f4d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dart_discord_rpc file_selector_linux flutter_secure_storage_linux + gtk local_notifier media_kit_libs_linux screen_retriever diff --git a/linux/my_application.cc b/linux/my_application.cc index 759285af..d1ac5d12 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -17,6 +17,13 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); + + GList* windows = gtk_application_get_windows(GTK_APPLICATION(application)); + if (windows) { + gtk_window_present(GTK_WINDOW(windows->data)); + return; + } + GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); @@ -78,7 +85,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch g_application_activate(application); *exit_status = 0; - return TRUE; + return FALSE; } // Implements GObject::dispose. @@ -98,7 +105,7 @@ static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, + "com.github.KRTirtho.Spotube", APPLICATION_ID, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, nullptr)); } diff --git a/linux/packaging/appimage/make_config.yaml b/linux/packaging/appimage/make_config.yaml index 68e36df7..c7332ea2 100644 --- a/linux/packaging/appimage/make_config.yaml +++ b/linux/packaging/appimage/make_config.yaml @@ -11,3 +11,6 @@ keywords: generic_name: Music Streaming Application categories: - Music + +supported_mime_type: + - x-scheme-handler/spotify diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml index 46493122..f4c279b4 100644 --- a/linux/packaging/deb/make_config.yaml +++ b/linux/packaging/deb/make_config.yaml @@ -32,3 +32,6 @@ keywords: generic_name: Music Streaming Application categories: - Music + +supported_mime_type: + - x-scheme-handler/spotify diff --git a/linux/packaging/rpm/make_config.yaml b/linux/packaging/rpm/make_config.yaml index 00f4c20e..1f952d0e 100644 --- a/linux/packaging/rpm/make_config.yaml +++ b/linux/packaging/rpm/make_config.yaml @@ -28,3 +28,6 @@ categories: - Music startup_notify: true + +supported_mime_type: + - x-scheme-handler/spotify diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 270e6261..a7965e14 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import app_links import audio_service import audio_session import device_info_plus @@ -24,6 +25,7 @@ import window_manager import window_size func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 19f1c02a..1a8bb655 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -2,6 +2,19 @@ + CFBundleURLTypes + + + CFBundleURLName + + Spotify + CFBundleURLSchemes + + + spotify + + + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/pubspec.lock b/pubspec.lock index 8921f8a7..526898d5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb" + url: "https://pub.dev" + source: hosted + version: "3.5.0" app_package_maker: dependency: transitive description: @@ -907,6 +915,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + flutter_sharing_intent: + dependency: "direct main" + description: + name: flutter_sharing_intent + sha256: "6eb896e6523b735e8230eeb206fd3b9f220f11ce879c2400a90b443147036ff9" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter_svg: dependency: "direct main" description: @@ -1018,6 +1034,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.8" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" hive: dependency: "direct main" description: @@ -2246,13 +2270,13 @@ packages: source: hosted version: "5.0.7" win32_registry: - dependency: transitive + dependency: "direct main" description: name: win32_registry - sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 77a26911..267ab17f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -119,6 +119,9 @@ dependencies: html_unescape: ^2.0.0 wikipedia_api: ^0.1.0 skeletonizer: ^0.8.0 + app_links: ^3.5.0 + win32_registry: ^1.1.2 + flutter_sharing_intent: ^1.1.0 dev_dependencies: build_runner: ^2.3.2 diff --git a/untranslated_messages.json b/untranslated_messages.json index e3bdc047..438ad6b3 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -39,6 +39,12 @@ "discord_rich_presence" ], + "it": [ + "audio_source", + "go_to_album", + "discord_rich_presence" + ], + "ja": [ "go_to_album", "discord_rich_presence" diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b9c6a481..fcf9927e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -20,6 +21,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); DartDiscordRpcPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DartDiscordRpcPlugin")); FileSelectorWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 5cd55ff3..0fe6e076 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links dart_discord_rpc file_selector_windows flutter_secure_storage_windows diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index d5c04f23..9823151c 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -3,6 +3,7 @@ #include #include "resource.h" +#include "app_links/app_links_plugin_c_api.h" namespace { @@ -105,6 +106,9 @@ Win32Window::~Win32Window() { bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, const Size& size) { + if (SendAppLinkToInstance(title)) { + return false; + } Destroy(); const wchar_t* window_class = @@ -244,3 +248,39 @@ bool Win32Window::OnCreate() { void Win32Window::OnDestroy() { // No-op; provided for subclasses. } + +// app_links +bool Win32Window::SendAppLinkToInstance(const std::wstring& title) { + // Find our exact window + HWND hwnd = ::FindWindow(kWindowClassName, title.c_str()); + + if (hwnd) { + // Dispatch new link to current window + SendAppLink(hwnd); + + // (Optional) Restore our window to front in same state + WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) }; + GetWindowPlacement(hwnd, &place); + + switch(place.showCmd) { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hwnd); + // END Restore + + // Window has been found, don't create another one. + return true; + } + + return false; +} \ No newline at end of file diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h index 17ba4311..1d817bd2 100644 --- a/windows/runner/win32_window.h +++ b/windows/runner/win32_window.h @@ -93,6 +93,10 @@ class Win32Window { // window handle for hosted content. HWND child_content_ = nullptr; + // Dispatches link if any. + // This method enables our app to be with a single instance too. + // This is mandatory if you want to catch further links in same app. + bool SendAppLinkToInstance(const std::wstring& title); }; #endif // RUNNER_WIN32_WINDOW_H_ From dcbe7294b742d43fbff4e89ab4c4825e94421dd9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 27 Dec 2023 21:57:18 +0600 Subject: [PATCH 099/131] fix: wrong artist name sent while scrobbling #958 --- lib/provider/scrobbler_provider.dart | 2 +- untranslated_messages.json | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/provider/scrobbler_provider.dart b/lib/provider/scrobbler_provider.dart index 5ac3c5a1..4ff4cce7 100644 --- a/lib/provider/scrobbler_provider.dart +++ b/lib/provider/scrobbler_provider.dart @@ -43,7 +43,7 @@ class ScrobblerNotifier extends PersistedStateNotifier { _scrobbleController.stream.listen((track) async { try { await state?.scrobblenaut.track.scrobble( - artist: TypeConversionUtils.artists_X_String(track.artists!), + artist: track.artists.first.name!, track: track.name!, album: track.album!.name!, chosenByUser: true, diff --git a/untranslated_messages.json b/untranslated_messages.json index 84d6b95f..66d8c7b2 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -72,12 +72,6 @@ "explore_genres" ], - "it": [ - "audio_source", - "go_to_album", - "discord_rich_presence" - ], - "ja": [ "go_to_album", "discord_rich_presence", From 3ad7ba66b56e93e69d2181d47029b7549ed225fc Mon Sep 17 00:00:00 2001 From: SecularSteve <33793273+SecularSteve@users.noreply.github.com> Date: Fri, 29 Dec 2023 13:12:39 +0100 Subject: [PATCH 100/131] feat(translations): add Dutch Language (#969) * Update language_codes.dart Added Dutch * Added Dutch language * Added Dutch my contribution credentials included * Added Dutch Language * fixed a mistake "Vlaams" is a very specific term to describe the Belgian Dutch dialect. "Nederland" is the correct way. * fixed a mistake "Nederlands" actually, got mixed up a bit --- lib/collections/language_codes.dart | 8 +- lib/l10n/app_nl.arb | 284 ++++++++++++++++++++++++++++ lib/l10n/l10n.dart | 2 + 3 files changed, 290 insertions(+), 4 deletions(-) create mode 100644 lib/l10n/app_nl.arb diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index d89e1a2a..de6f6d1c 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -164,10 +164,10 @@ abstract class LanguageLocals { // name: "Maldivian;", // nativeName: "ދިވެހި", // ), - // "nl": const ISOLanguageName( - // name: "Dutch", - // nativeName: "Vlaams", - // ), + "nl": const ISOLanguageName( + name: "Dutch", + nativeName: "Nederlands", + ), "en": const ISOLanguageName( name: "English", nativeName: "English", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb new file mode 100644 index 00000000..306d63c4 --- /dev/null +++ b/lib/l10n/app_nl.arb @@ -0,0 +1,284 @@ +{ + "guest": "Gast", + "browse": "Bladeren", + "search": "Zoek op", + "library": "Bibliotheek", + "lyrics": "Liedteksten", + "settings": "Instellingen", + "genre_categories_filter": "Categorieën of genres filteren...", + "genre": "Genre", + "personalized": "Gepersonaliseerd", + "featured": "Aanbevolen", + "new_releases": "Nieuwe uitgaves", + "songs": "Liedjes", + "playing_track": "{track} afspelen", + "queue_clear_alert": "Dit zal de huidige wachtrij wissen. {track_length} tracks worden verwijderd\nWilt u doorgaan?", + "load_more": "Meer laden", + "playlists": "Afspeellijsten", + "artists": "Kunstenaars", + "albums": "Albums", + "tracks": "Nummers", + "downloads": "Downloads", + "filter_playlists": "Filter uw afspeellijsten...", + "liked_tracks": "Geliefde tracks", + "liked_tracks_description": "Al je favoriete nummers", + "create_playlist": "Afspeellijst maken", + "create_a_playlist": "Een afspeellijst maken", + "update_playlist": "Afspeellijst bijwerken", + "create": "Maak", + "cancel": "Annuleren", + "update": "Bijwerken", + "playlist_name": "Afspeellijstnaam", + "name_of_playlist": "Naam van de afspeellijst", + "description": "Beschrijving", + "public": "Openbaar", + "collaborative": "Samenwerkend", + "search_local_tracks": "Lokale nummers zoeken...", + "play": "Speel", + "delete": "Wissen", + "none": "Geen", + "sort_a_z": "Sorteren op A-Z", + "sort_z_a": "Sorteren op Z-A", + "sort_artist": "Sorteren op kunstenaar", + "sort_album": "Sorteren op album", + "sort_tracks": "Nummers sorteren", + "currently_downloading": "Momenteel aan het downloaden ({tracks_length})", + "cancel_all": "Alle annuleren", + "filter_artist": "Kunstenaars filteren...", + "followers": "{followers} volgers", + "add_artist_to_blacklist": "Kunstenaar toevoegen aan zwarte lijst", + "top_tracks": "Topsporen", + "fans_also_like": "Liefhebbers willen ook", + "loading": "Aan het laden...", + "artist": "Kunstenaar", + "blacklisted": "Op de zwarte lijst", + "following": "Op volg", + "follow": "Volgen", + "artist_url_copied": "URL artiest gekopieerd naar klembord", + "added_to_queue": "{tracks} tracks toegevoegd aan wachtrij", + "filter_albums": "Albums filteren...", + "synced": "Gesynchroniseerd", + "plain": "Eenvoudig", + "shuffle": "Schuifelen", + "search_tracks": "Zoek nummers...", + "released": "Vrijgegeven", + "error": "Fout {error}", + "title": "Titel", + "time": "Tijd", + "more_actions": "Meer acties", + "download_count": "({count}) downloads", + "add_count_to_playlist": "Voeg ({count}) toe aan afspeellijst", + "add_count_to_queue": "Voeg ({count}) toe aan wachtrij", + "play_count_next": "Speel ({count}) volgende", + "album": "Album", + "copied_to_clipboard": "{data} naar klembord gekopieerd", + "add_to_following_playlists": "Voeg {track} toe aan volgende afspeellijsten", + "add": "Toevoegen", + "added_track_to_queue": "{track} toegevoegd aan wachtrij", + "add_to_queue": "Toevoegen aan wachtrij", + "track_will_play_next": "{track} zal hierna spelen", + "play_next": "Volgende afspelen", + "removed_track_from_queue": "{track} uit wachtrij verwijderd", + "remove_from_queue": "Verwijderen uit wachtrij", + "remove_from_favorites": "Verwijderen uit favorieten", + "save_as_favorite": "Opslaan als favoriet", + "add_to_playlist": "Toevoegen aan afspeellijst", + "remove_from_playlist": "Verwijderen uit afspeellijst", + "add_to_blacklist": "Toevoegen aan zwarte lijst", + "remove_from_blacklist": "Verwijderen uit zwarte lijst", + "share": "Delen", + "mini_player": "Minispeler", + "slide_to_seek": "Schuif om vooruit of achteruit te zoeken", + "shuffle_playlist": "Afspeellijst schuifelen", + "unshuffle_playlist": "Afspeellijst onschuifelen", + "previous_track": "Vorige nummer", + "next_track": "Volgende nummer", + "pause_playback": "Weergave pauzeren", + "resume_playback": "Weergave hervatten", + "loop_track": "Nummer loopen", + "repeat_playlist": "Afspeellijst herhalen", + "queue": "Wachtrij", + "alternative_track_sources": "Alternatieve nummerbronnen", + "download_track": "Nummer downloaden", + "tracks_in_queue": "{tracks} tracks in wachtrij", + "clear_all": "Wis alles", + "show_hide_ui_on_hover": "UI tonen/verbergen bij zweven", + "always_on_top": "Altijd bovenaan", + "exit_mini_player": "Minispeler afsluiten", + "download_location": "Downloadlocatie", + "account": "Account", + "login_with_spotify": "Inloggen met je Spotify-account", + "connect_with_spotify": "Verbinden met Spotify", + "logout": "Afmelden", + "logout_of_this_account": "Afmelden van dit account", + "language_region": "Taal & Regio", + "language": "Taal", + "system_default": "Systeemstandaard", + "market_place_region": "Marktplaats-regio", + "recommendation_country": "Aanbeveling Land", + "appearance": "Uiterlijk", + "layout_mode": "Opmaakmodus", + "override_layout_settings": "Instellingen voor responsieve opmaakmodus opheffen", + "adaptive": "Aanpassingsgericht", + "compact": "Compact", + "extended": "Uitgebreide", + "theme": "Thema", + "dark": "Donker", + "light": "Licht", + "system": "Systeem", + "accent_color": "Accentkleur", + "sync_album_color": "Albumkleur synchroniseren", + "sync_album_color_description": "Gebruikt de overheersende kleur van het albumartikel als accentkleur", + "playback": "Weergave", + "audio_quality": "Audiokwaliteit", + "high": "Hoog", + "low": "Laag", + "pre_download_play": "Vooraf downloaden en spelen", + "pre_download_play_description": "In plaats van audio te streamen, kun je bytes downloaden en afspelen (aanbevolen voor gebruikers met een hogere bandbreedte)", + "skip_non_music": "Niet-muzieksegmenten overslaan (SponsorBlock)", + "blacklist_description": "Nummers en artiesten op de zwarte lijst", + "wait_for_download_to_finish": "Wacht tot de huidige download is voltooid", + "desktop": "Bureaublad", + "close_behavior": "Sluitgedrag", + "close": "Sluit af", + "minimize_to_tray": "Minimaliseren naar lade", + "show_tray_icon": "Systeemvakpictogram tonen", + "about": "Over", + "u_love_spotube": "We weten dat jullie van Spotube houden", + "check_for_updates": "Controleren op updates", + "about_spotube": "Over Spotube", + "blacklist": "Zwarte lijst", + "please_sponsor": "Sponsor/Doneer a.u.b.", + "spotube_description": "Spotube, een lichtgewicht, cross-platform, vrij-voor-alles Spotify-client", + "version": "Versie", + "build_number": "Beeldnummer", + "founder": "Stichter", + "repository": "Opslagplaats", + "bug_issues": "Bug+problemen", + "made_with": "Gemaakt met ❤️ in Bangladesh🇧🇩", + "kingkor_roy_tirtho": "Kingkor Roy Tirtho", + "copyright": "© 2021-{current_year} Kingkor Roy Tirtho", + "license": "Licentie", + "add_spotify_credentials": "Voeg je spotify-referenties toe om te beginnen", + "credentials_will_not_be_shared_disclaimer": "Maakt u geen zorgen, uw gegevens worden niet verzameld of gedeeld met anderen.", + "know_how_to_login": "Weet u niet hoe u dit moet doen?", + "follow_step_by_step_guide": "Volg de stap voor stap gids", + "spotify_cookie": "Spotify {name} Cookie", + "cookie_name_cookie": "{name} Cookie", + "fill_in_all_fields": "Vul alle velden in a.u.b.", + "submit": "Verzenden", + "exit": "Ga weg", + "previous": "Vorige", + "next": "Volgende", + "done": "Klaar", + "step_1": "Stap 1", + "first_go_to": "Ga eerst naar", + "login_if_not_logged_in": "en Inloggen/Aanmelden als u niet bent ingelogd", + "step_2": "Stap 2", + "step_2_steps": "1. Zodra je bent aangemeld, druk je op F12 of klik je met de rechtermuisknop > Inspect om de Browser devtools te openen.\n2. Ga vervolgens naar het tabblad \"Toepassing\" (Chrome, Edge, Brave enz..) of naar het tabblad \"Opslag\" (Firefox, Palemoon enz..).\n3. Ga naar de sectie \"Cookies\" en vervolgens naar de subsectie \"https://accounts.spotify.com\".", + "step_3": "Stap 3", + "step_3_steps": "Kopieer de waarden van \"sp_dc\" en \"sp_key\" (of sp_gaid) Cookies", + "success_emoji": "Succes🥳", + "success_message": "Je bent nu succesvol ingelogd met je Spotify account. Goed gedaan, maat!", + "step_4": "Stap 4", + "step_4_steps": "Plak de gekopieerde \"sp_dc\" en \"sp_key\" (of sp_gaid) waarden in de respectievelijke velden", + "something_went_wrong": "Er ging iets mis", + "piped_instance": "Piped-serverinstantie", + "piped_description": "De Piped-serverinstantie die moet worden gebruikt voor het matchen van sporen", + "piped_warning": "Sommige werken misschien niet goed. Dus gebruik ze op eigen risico", + "generate_playlist": "Afspeellijst genereren", + "track_exists": "Nummer {track} bestaat al", + "replace_downloaded_tracks": "Alle gedownloade nummers vervangen", + "skip_download_tracks": "Downloaden van alle gedownloade nummers overslaan", + "do_you_want_to_replace": "Wil je de bestaande nummer vervangen?", + "replace": "Vervangen", + "skip": "Overslaan", + "select_up_to_count_type": "Selecteer tot {count} {type}", + "select_genres": "Genres selecteren", + "add_genres": "Genres toevoegen", + "country": "Land", + "number_of_tracks_generate": "Aantal nummers om te genereren", + "acousticness": "Akoesticiteit", + "danceability": "Dansbaarheid", + "energy": "Energie", + "instrumentalness": "Instrumentaliteit", + "liveness": "Levendigheid", + "loudness": "Luidheid", + "speechiness": "Sprakeligheid", + "valence": "Valentie", + "popularity": "Populariteit", + "key": "Sleutel", + "duration": "Tijdsduur (s)", + "tempo": "Tempo (SPM)", + "mode": "Modus", + "time_signature": "Tijdsnotatie", + "short": "Kort", + "medium": "Middel", + "long": "Lang", + "min": "Min", + "max": "Max", + "target": "Doel", + "moderate": "Matig", + "deselect_all": "Alles deselecteren", + "select_all": "Alles selecteren", + "are_you_sure": "Weet je het zeker?", + "generating_playlist": "Je aangepaste afspeellijst genereren...", + "selected_count_tracks": "{count} nummers geselecteerd", + "download_warning": "Als je alle Tracks in bulk downloadt, ben je duidelijk bezig met muziekpiraterij en breng je schade toe aan de creatieve muziekmaatschappij. Ik hoop dat je je hiervan bewust bent. Probeer altijd het harde werk van artiesten te respecteren en te steunen.", + "download_ip_ban_warning": "BTW, je IP-adres kan worden geblokkeerd op YouTube als gevolg van buitensporige downloadverzoeken dan normaal. IP blokkering betekent dat je YouTube niet kunt gebruiken (zelfs als je ingelogd bent) voor tenminste 2-3 maanden vanaf dat IP apparaat. Spotube is niet verantwoordelijk als dit ooit gebeurt.", + "by_clicking_accept_terms": "Door op 'accepteren' te klikken ga je akkoord met de volgende voorwaarden:", + "download_agreement_1": "Ik weet dat ik muziek illegaal verveel. Ik ben en crimineel.", + "download_agreement_2": "Ik steun de kunstenaar waar ik kan en ik doe dit alleen omdat ik geen geld heb om hun kunst te kopen.", + "download_agreement_3": "Ik ben me er volledig van bewust dat mijn IP geblokkeerd kan worden op YouTube & ik houd Spotube of zijn eigenaars/contributeurs niet verantwoordelijk voor ongelukken die veroorzaakt worden door mijn huidige actie.", + "decline": "Weigeren", + "accept": "Accepteren", + "details": "Bijzonderheden", + "youtube": "YouTube", + "channel": "Kanaal", + "likes": "Liefs", + "dislikes": "Hekels", + "views": "Weergaven", + "streamUrl": "Stream-URL", + "stop": "Stoppen", + "sort_newest": "Sorteren op nieuwste toegevoegd", + "sort_oldest": "Sorteren op oudste toegevoegd", + "sleep_timer": "Slaaptimer", + "mins": "{minutes} minuten", + "hours": "{hours} uren", + "hour": "{hours} uur", + "custom_hours": "Aangepaste uren", + "logs": "Logboeken", + "developers": "Ontwikkelaars", + "not_logged_in": "U bent niet aangemeld", + "search_mode": "Zoekmodus", + "youtube_api_type": "API-type", + "ok": "Oké", + "failed_to_encrypt": "Versleuteling mislukt", + "encryption_failed_warning": "Spotube gebruikt encryptie om je gegevens veilig op te slaan. Maar dat is niet gelukt. Dus zal het terugvallen op onveilige opslag.\nAls je linux gebruikt, zorg er dan voor dat je een geheim-dienst (gnome-keyring, kde-wallet, keepassxc etc) hebt geïnstalleerd.", + "querying_info": "Info opvragen...", + "piped_api_down": "Piped API is uit", + "piped_down_error_instructions": "De Piped-instantie {pipedInstance} is momenteel uitgevallen\n\nVerander de instantie of verander het 'API-type' naar de officiële YouTube API.\n\nZorg ervoor dat u de app herstart na de wijziging", + "you_are_offline": "U bent momenteel offline", + "connection_restored": "Uw internetverbinding is hersteld", + "use_system_title_bar": "Systeemtitelbalk gebruiken", + "crunching_results": "Resultaten kraken...", + "search_to_get_results": "Zoek om resultaten te krijgen", + "use_amoled_mode": "Pikzwart donkerthema", + "pitch_dark_theme": "AMOLED-modus", + "normalize_audio": "Audio normaliseren", + "change_cover": "Dekking wijzigen", + "add_cover": "Dekking toevoegen", + "restore_defaults": "Standaardwaarden herstellen", + "download_music_codec": "Muziek-codec downloaden", + "streaming_music_codec": "Muziek-codec streamen", + "login_with_lastfm": "Aanmelden met Last.fm", + "connect": "Verbinden", + "disconnect_lastfm": "Last.fm verbreken", + "disconnect": "Ontkoppelen", + "username": "Gebruikersnaam", + "password": "Wachtwoord", + "login": "Inloggen", + "login_with_your_lastfm": "Inloggen met uw Last.fm account", + "scrobble_to_lastfm": "Scrobbel naar Last.fm", + "@@locale": "nl" +} diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index d6cf3e37..3434cf66 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -8,6 +8,7 @@ /// yuri-val@github => Ukrainian /// energywave@github, ncvescera@github, OpenCode@github => Italian /// mdksec@github => Turkish +/// SecularSteve@github => Dutch import 'package:flutter/material.dart'; class L10n { @@ -23,6 +24,7 @@ class L10n { const Locale('hi', 'IN'), const Locale('it', 'IT'), const Locale('ja', 'JP'), + const Locale('nl'. 'NL'), const Locale('pl', 'PL'), const Locale('pt', 'PT'), const Locale('ru', 'RU'), From b4999993bf51dbd8d07387620ce1844c44d3d527 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 29 Dec 2023 18:28:25 +0600 Subject: [PATCH 101/131] fix: wrong artist name sent while scrobbling #958 --- lib/components/player/player.dart | 15 ++++++++++----- lib/provider/scrobbler_provider.dart | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/components/player/player.dart b/lib/components/player/player.dart index 3957cc65..cd89d3e4 100644 --- a/lib/components/player/player.dart +++ b/lib/components/player/player.dart @@ -74,10 +74,14 @@ class PlayerView extends HookConsumerWidget { useMemoized(() => GlobalKey(), []); useEffect(() { - WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = false; + for (final renderView in WidgetsBinding.instance.renderViews) { + renderView.automaticSystemUiAdjustment = false; + } return () { - WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = true; + for (final renderView in WidgetsBinding.instance.renderViews) { + renderView.automaticSystemUiAdjustment = true; + } }; }, [panelController.isPanelOpen]); @@ -90,10 +94,11 @@ class PlayerView extends HookConsumerWidget { final topPadding = MediaQueryData.fromView(View.of(context)).padding.top; - return WillPopScope( - onWillPop: () async { + return PopScope( + canPop: panelController.isPanelOpen, + onPopInvoked: (canPop) async { + if (!canPop) return; panelController.close(); - return false; }, child: IconTheme( data: theme.iconTheme.copyWith(color: bodyTextColor), diff --git a/lib/provider/scrobbler_provider.dart b/lib/provider/scrobbler_provider.dart index 4ff4cce7..bf234e62 100644 --- a/lib/provider/scrobbler_provider.dart +++ b/lib/provider/scrobbler_provider.dart @@ -43,7 +43,7 @@ class ScrobblerNotifier extends PersistedStateNotifier { _scrobbleController.stream.listen((track) async { try { await state?.scrobblenaut.track.scrobble( - artist: track.artists.first.name!, + artist: track.artists!.first.name!, track: track.name!, album: track.album!.name!, chosenByUser: true, From 9877c6a3b0f403e84f5b224060123406fe188f04 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 30 Dec 2023 19:21:56 +0600 Subject: [PATCH 102/131] chore: fix l10n error --- lib/l10n/l10n.dart | 2 +- untranslated_messages.json | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 3434cf66..47e5eb99 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -24,7 +24,7 @@ class L10n { const Locale('hi', 'IN'), const Locale('it', 'IT'), const Locale('ja', 'JP'), - const Locale('nl'. 'NL'), + const Locale('nl', 'NL'), const Locale('pl', 'PL'), const Locale('pt', 'PT'), const Locale('ru', 'RU'), diff --git a/untranslated_messages.json b/untranslated_messages.json index 66d8c7b2..5d165f85 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -80,6 +80,15 @@ "explore_genres" ], + "nl": [ + "audio_source", + "go_to_album", + "discord_rich_presence", + "browse_all", + "genres", + "explore_genres" + ], + "pl": [ "go_to_album", "discord_rich_presence", From d645f607ac673bfb7d1764b973fa87ca933e77f2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 30 Dec 2023 20:18:35 +0600 Subject: [PATCH 103/131] chore: platform check deep link for android --- lib/hooks/configurators/use_deep_linking.dart | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart index 546ab2e8..9431f04d 100644 --- a/lib/hooks/configurators/use_deep_linking.dart +++ b/lib/hooks/configurators/use_deep_linking.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:app_links/app_links.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -7,6 +9,7 @@ import 'package:spotube/collections/routes.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; void useDeepLinking(WidgetRef ref) { // single instance no worries @@ -49,10 +52,14 @@ void useDeepLinking(WidgetRef ref) { } } - FlutterSharingIntent.instance.getInitialSharing().then(uriListener); + StreamSubscription? mediaStream; - final mediaStream = - FlutterSharingIntent.instance.getMediaStream().listen(uriListener); + if (DesktopTools.platform.isMobile) { + FlutterSharingIntent.instance.getInitialSharing().then(uriListener); + + mediaStream = + FlutterSharingIntent.instance.getMediaStream().listen(uriListener); + } final subscription = appLinks.allStringLinkStream.listen((uri) async { final startSegment = uri.split(":").take(2).join(":"); @@ -86,7 +93,7 @@ void useDeepLinking(WidgetRef ref) { }); return () { - mediaStream.cancel(); + mediaStream?.cancel(); subscription.cancel(); }; }, [spotify, queryClient]); From 0f6d0a44eac8accfbef6c8940a83ea0c19c92d21 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 30 Dec 2023 23:13:27 +0600 Subject: [PATCH 104/131] chore: fix not closing player --- lib/components/player/player.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/components/player/player.dart b/lib/components/player/player.dart index cd89d3e4..33283c3e 100644 --- a/lib/components/player/player.dart +++ b/lib/components/player/player.dart @@ -95,9 +95,8 @@ class PlayerView extends HookConsumerWidget { final topPadding = MediaQueryData.fromView(View.of(context)).padding.top; return PopScope( - canPop: panelController.isPanelOpen, - onPopInvoked: (canPop) async { - if (!canPop) return; + canPop: false, + onPopInvoked: (didPop) async { panelController.close(); }, child: IconTheme( From ec9d9d7d7e83a41aad2daab039734ef55f54bff0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 30 Dec 2023 23:25:29 +0600 Subject: [PATCH 105/131] chore: fix genre playlist title bar --- lib/pages/home/genres/genre_playlists.dart | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart index 19f98c7f..78f32245 100644 --- a/lib/pages/home/genres/genre_playlists.dart +++ b/lib/pages/home/genres/genre_playlists.dart @@ -14,6 +14,7 @@ import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:collection/collection.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; class GenrePlaylistsPage extends HookConsumerWidget { final Category category; @@ -42,17 +43,19 @@ class GenrePlaylistsPage extends HookConsumerWidget { final scrollController = useScrollController(); return Scaffold( - appBar: const PageWindowTitleBar( - leading: BackButton(color: Colors.white), - backgroundColor: Colors.transparent, - foregroundColor: Colors.white, - ), + appBar: DesktopTools.platform.isDesktop + ? const PageWindowTitleBar( + leading: BackButton(color: Colors.white), + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + ) + : null, extendBodyBehindAppBar: true, body: CustomScrollView( controller: scrollController, slivers: [ SliverAppBar( - automaticallyImplyLeading: false, + automaticallyImplyLeading: DesktopTools.platform.isMobile, expandedHeight: mediaQuery.mdAndDown ? 200 : 150, pinned: true, floating: false, @@ -77,7 +80,7 @@ class GenrePlaylistsPage extends HookConsumerWidget { child: const ColoredBox(color: Colors.transparent), ), ), - centerTitle: true, + centerTitle: DesktopTools.platform.isDesktop, title: Text( category.name!, style: Theme.of(context).textTheme.headlineMedium?.copyWith( From ec7c952270caca55131ed6aba9467c975eab772a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 30 Dec 2023 23:41:43 +0600 Subject: [PATCH 106/131] chore: generate changelogs and bump version to 3.4.4 --- .github/workflows/spotube-release-binary.yml | 2 +- CHANGELOG.md | 26 ++++ README.md | 19 ++- bin/untranslated_messages.dart | 5 + lib/l10n/app_ar.arb | 7 +- lib/l10n/app_bn.arb | 7 +- lib/l10n/app_ca.arb | 7 +- lib/l10n/app_de.arb | 7 +- lib/l10n/app_es.arb | 7 +- lib/l10n/app_fa.arb | 7 +- lib/l10n/app_fr.arb | 7 +- lib/l10n/app_hi.arb | 7 +- lib/l10n/app_it.arb | 10 +- lib/l10n/app_ja.arb | 7 +- lib/l10n/app_nl.arb | 9 +- lib/l10n/app_pl.arb | 7 +- lib/l10n/app_pt.arb | 7 +- lib/l10n/app_ru.arb | 7 +- lib/l10n/app_tr.arb | 7 +- lib/l10n/app_uk.arb | 7 +- lib/l10n/app_zh.arb | 7 +- pubspec.yaml | 2 +- untranslated_messages.json | 140 +------------------ 23 files changed, 151 insertions(+), 167 deletions(-) diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index d4f15664..caeb7238 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -4,7 +4,7 @@ on: inputs: version: description: Version to release (x.x.x) - default: 3.3.0 + default: 3.4.0 required: true channel: type: choice diff --git a/CHANGELOG.md b/CHANGELOG.md index 1544f055..ea429caa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [3.4.0](https://github.com/KRTirtho/spotube/compare/v3.3.0...v3.4.0) (2023-12-30) + + +### Features + +* Add Go to Album option in track option [#917](https://github.com/KRTirtho/spotube/issues/917) ([b0beeca](https://github.com/KRTirtho/spotube/commit/b0beeca0cbaf810fae27832cff98cfda95715050)) +* **translations:** add Italian language translations ([#818](https://github.com/KRTirtho/spotube/issues/818)) ([e4eb0e2](https://github.com/KRTirtho/spotube/commit/e4eb0e2596ade2bb5195e183f03af42742fc8486)), closes [#676](https://github.com/KRTirtho/spotube/issues/676) [#676](https://github.com/KRTirtho/spotube/issues/676) +* compact genre view in home page ([82ed5e9](https://github.com/KRTirtho/spotube/commit/82ed5e90576b57ef32e61a65015e04862ab15461)) +* Deep link support ([#950](https://github.com/KRTirtho/spotube/issues/950)) ([4050f55](https://github.com/KRTirtho/spotube/commit/4050f556400aaec5515231578512cf1a6b990110)) +* improve loading animations ([b92583d](https://github.com/KRTirtho/spotube/commit/b92583d0df7b8dee0d121cd2bb666b14c77d8c86)) +* toggle for discord rpc ([24a2294](https://github.com/KRTirtho/spotube/commit/24a2294512bb0c4aff77bc8dcad9b4de3e8b45c6)) +* **translations:** add Dutch Language ([#969](https://github.com/KRTirtho/spotube/issues/969)) ([3ad7ba6](https://github.com/KRTirtho/spotube/commit/3ad7ba66b56e93e69d2181d47029b7549ed225fc)) + + +### Bug Fixes + +* add safe area in home ([9ee6067](https://github.com/KRTirtho/spotube/commit/9ee60677f6d50df7468e12dc6653ecedefa2494f)) +* amoled mode and color scheme can't be changed ([840e014](https://github.com/KRTirtho/spotube/commit/840e014f2b18f193d040baef0e0cd595088a4a84)) +* doesn't minimize to tray when system title bar close button is used [#866](https://github.com/KRTirtho/spotube/issues/866) ([bb8f250](https://github.com/KRTirtho/spotube/commit/bb8f250f5f351c1a353791b77b25b9de7586191f)) +* genre border issues ([2fb16e6](https://github.com/KRTirtho/spotube/commit/2fb16e64e9cdfca54d633cdf287b0544ecdda3b6)) +* Incorrect "Artist" label/heading on Search Results Page [#920](https://github.com/KRTirtho/spotube/issues/920) ([f86d544](https://github.com/KRTirtho/spotube/commit/f86d5449168068e338f769d7f504d2146b86dc79)) +* metadata not getting added for YouTube tracks [#916](https://github.com/KRTirtho/spotube/issues/916) and Wrong duration of downloaded tracks [#912](https://github.com/KRTirtho/spotube/issues/912) ([a7b9398](https://github.com/KRTirtho/spotube/commit/a7b9398708ede865dc2c25fb791c8e98eeff7a38)) +* Playlist refresh not working [#915](https://github.com/KRTirtho/spotube/issues/915) ([5f1df5a](https://github.com/KRTirtho/spotube/commit/5f1df5a87d8fb7980b52cf57b7b6bedea57a1269)) +* track view header title overflow and player view drag glitch ([b04d884](https://github.com/KRTirtho/spotube/commit/b04d8849e7169824ec5b980236b5d61b2629f56e)) +* wrong artist name sent while scrobbling [#958](https://github.com/KRTirtho/spotube/issues/958) ([dcbe729](https://github.com/KRTirtho/spotube/commit/dcbe7294b742d43fbff4e89ab4c4825e94421dd9)) + ## [3.3.0](https://github.com/KRTirtho/spotube/compare/v3.2.0...v3.3.0) (2023-11-27) diff --git a/README.md b/README.md index 498c45de..2736d1f1 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [SponsorBlock](https://sponsor.ajay.app) - SponsorBlock is an open-source crowdsourced browser extension and open API for skipping sponsor segments in YouTube videos. 1. [Inno Setup](https://jrsoftware.org/isinfo.php) - Inno Setup is a free installer for Windows programs by Jordan Russell and Martijn Laan 1. [F-Droid](https://f-droid.org) - F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device +1. [LastFM](https://last.fm) - Last.fm is a music streaming and discovery platform that helps users discover and share new music. It tracks users' music listening habits across many devices and platforms. ### Dependencies 1. [args](https://pub.dev/packages/args) - Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options. @@ -208,7 +209,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [auto_size_text](https://github.com/leisim/auto_size_text) - Flutter widget that automatically resizes text to fit perfectly within its bounds. 1. [buttons_tabbar](https://afonsoraposo.com) - A Flutter package that implements a TabBar where each label is a toggle button. 1. [cached_network_image](https://github.com/Baseflow/flutter_cached_network_image) - Flutter library to load and cache network images. Can also be used with placeholder and error widgets. -1. [catcher_2](https://github.com/ThexXTURBOXx/catcher_2) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer. +1. [catcher_2](https://github.com/ThexXTURBOXx/catcher_2) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer. 1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections. 1. [cupertino_icons](https://pub.dev/packages/cupertino_icons) - Default icons asset for Cupertino widgets based on Apple styled icons 1. [curved_navigation_bar](https://github.com/rafalbednarczuk/curved_navigation_bar) - Stunning Animating Curved Shape Navigation Bar. Adjustable color, background color, animation curve, animation duration. @@ -221,9 +222,9 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times. 1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI. 1. [fl_query](https://fl-query.krtirtho.dev) - Asynchronous data caching, refetching & invalidation library for Flutter -1. [fl_query_hooks](https://fl-query.krtirtho.dev) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter +1. [fl_query_hooks](https://fl-query.krtirtho.dev) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter 1. [fl_query_devtools](https://fl-query.krtirtho.dev) - Devtools support for Fl-Query -1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft. +1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft. 1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite. 1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices. 1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability. @@ -234,7 +235,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android. 1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files. 1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets -1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs! +1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs! 1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling. 1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more 1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256. @@ -251,10 +252,10 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [media_kit_libs_audio](https://github.com/media-kit/media-kit.git) - package:media_kit audio (only) playback native libraries for all platforms. 1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files 1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents. -1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. +1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. 1. [palette_generator](https://pub.dev/packages/palette_generator) - Flutter package for generating palette colors from a source image. 1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web. -1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. +1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. 1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. 1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video 1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area. @@ -273,7 +274,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback. 1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window. 1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key. -1. [simple_icons](https://jlnrrg.github.io/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands. +1. [simple_icons](https://teavelopment.com/) - The Simple Icon pack available as Flutter Icons. Provides over 1500 Free SVG icons for popular brands. 1. [audio_service_mpris](https://github.com/bdrazhzhov/audio-service-mpris) - audio_service platform interface supporting Media Player Remote Interfacing Specification. 1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support. 1. [jiosaavn](https://github.com/KRTirtho/jiosaavn) - Unofficial API client for jiosaavn.com @@ -282,6 +283,10 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [sliver_tools](https://github.com/Kavantix) - A set of useful sliver tools that are missing from the flutter framework 1. [html_unescape](https://github.com/filiph/html_unescape) - A small library for un-escaping HTML. Supports all Named Character References, Decimal Character References and Hexadecimal Character References. 1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter +1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort. +1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included). +1. [win32_registry](https://win32.pub) - A package that provides a friendly Dart API for accessing the Windows Registry. +1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app. 1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation. 1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied. 1. [flutter_distributor](https://distributor.leanflutter.org) - A complete tool for packaging and publishing your Flutter apps. diff --git a/bin/untranslated_messages.dart b/bin/untranslated_messages.dart index 172f218f..e19f9a07 100644 --- a/bin/untranslated_messages.dart +++ b/bin/untranslated_messages.dart @@ -35,6 +35,11 @@ void main(List args) { ); } + print( + "Prompt:\n" + "Translate following to their appropriate locale for flutter arb translations files." + " Put the respective new translations in a map of their corresponding locale.", + ); // ignore: avoid_print print( const JsonEncoder.withIndent(' ').convert( diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index b74564a4..2bdde72a 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -279,5 +279,10 @@ "password": "كلمة المرور", "login": "تسجيل الدخول", "login_with_your_lastfm": "تسجيل الدخول باستخدام حساب Last.fm الخاص بك", - "scrobble_to_lastfm": "تسجيل الاستماع على Last.fm" + "scrobble_to_lastfm": "تسجيل الاستماع على Last.fm", + "go_to_album": "الانتقال إلى الألبوم", + "discord_rich_presence": "وجود ديسكورد الغني", + "browse_all": "تصفح الكل", + "genres": "الأنواع الموسيقية", + "explore_genres": "استكشاف الأنواع" } \ No newline at end of file diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 59985b24..39f8a1ee 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -279,5 +279,10 @@ "password": "পাসওয়ার্ড", "login": "লগইন", "login_with_your_lastfm": "আপনার Last.fm অ্যাকাউন্ট দিয়ে লগইন করুন", - "scrobble_to_lastfm": "Last.fm এ স্ক্রবল করুন" + "scrobble_to_lastfm": "Last.fm এ স্ক্রবল করুন", + "go_to_album": "الانتقال إلى الألبوم", + "discord_rich_presence": "وجود ديسكورد الغني", + "browse_all": "تصفح الكل", + "genres": "الأنواع الموسيقية", + "explore_genres": "استكشاف الأنواع" } \ No newline at end of file diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index 1d1bb5e4..15ca9e31 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -279,5 +279,10 @@ "password": "Contrasenya", "login": "Inicia la sessió", "login_with_your_lastfm": "Inicia la sessió amb el teu compte de Last.fm", - "scrobble_to_lastfm": "Scrobble a Last.fm" + "scrobble_to_lastfm": "Scrobble a Last.fm", + "go_to_album": "Anar a l'àlbum", + "discord_rich_presence": "Presència rica de Discord", + "browse_all": "Navega per tot", + "genres": "Gèneres", + "explore_genres": "Explora els gèneres" } \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8b7b2593..1a13e4a1 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -279,5 +279,10 @@ "password": "Passwort", "login": "Anmelden", "login_with_your_lastfm": "Mit Ihrem Last.fm-Konto anmelden", - "scrobble_to_lastfm": "Auf Last.fm scrobbeln" + "scrobble_to_lastfm": "Auf Last.fm scrobbeln", + "go_to_album": "Zum Album gehen", + "discord_rich_presence": "Discord Rich Presence", + "browse_all": "Alles durchsuchen", + "genres": "Genres", + "explore_genres": "Genres erkunden" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index d90ec972..2fecd8f1 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -279,5 +279,10 @@ "password": "Contraseña", "login": "Iniciar sesión", "login_with_your_lastfm": "Iniciar sesión con tu cuenta de Last.fm", - "scrobble_to_lastfm": "Scrobble a Last.fm" + "scrobble_to_lastfm": "Scrobble a Last.fm", + "go_to_album": "Ir al álbum", + "discord_rich_presence": "Presencia rica en Discord", + "browse_all": "Explorar todo", + "genres": "Géneros", + "explore_genres": "Explorar géneros" } \ No newline at end of file diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index a8bb9f8c..84b9b448 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -279,5 +279,10 @@ "password": "رمز عبور", "login": "ورود", "login_with_your_lastfm": "ورود با حساب کاربری Last.fm خود", - "scrobble_to_lastfm": "Scrobble به Last.fm" + "scrobble_to_lastfm": "Scrobble به Last.fm", + "go_to_album": "رفتن به آلبوم", + "discord_rich_presence": "حضور غنی دیسکورد", + "browse_all": "مرور همه", + "genres": "ژانرها", + "explore_genres": "استکشاف ژانرها" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 06015964..82997bad 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -279,5 +279,10 @@ "password": "Mot de passe", "login": "Se connecter", "login_with_your_lastfm": "Se connecter avec votre compte Last.fm", - "scrobble_to_lastfm": "Scrobble à Last.fm" + "scrobble_to_lastfm": "Scrobble à Last.fm", + "go_to_album": "Aller à l'album", + "discord_rich_presence": "Présence riche de Discord", + "browse_all": "Parcourir tout", + "genres": "Genres", + "explore_genres": "Explorer les genres" } \ No newline at end of file diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index adbc6c06..4bfff3da 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -279,5 +279,10 @@ "password": "पासवर्ड", "login": "लॉग इन करें", "login_with_your_lastfm": "अपने Last.fm अकाउंट से लॉगिन करें", - "scrobble_to_lastfm": "Last.fm पर स्क्रॉबल करें" + "scrobble_to_lastfm": "Last.fm पर स्क्रॉबल करें", + "go_to_album": "एल्बम पर जाएं", + "discord_rich_presence": "डिस्कॉर्ड रिच प्रेजेंस", + "browse_all": "सभी को ब्राउज़ करें", + "genres": "शैलियाँ", + "explore_genres": "शैलियों का अन्वेषण करें" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index ada9f1f6..033bb516 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -279,5 +279,11 @@ "password": "Password", "login": "Accesso", "login_with_your_lastfm": "Accedi con il tuo account Last.fm", - "scrobble_to_lastfm": "Invia a Last.fm" -} + "scrobble_to_lastfm": "Invia a Last.fm", + "audio_source": "Fonte audio", + "go_to_album": "Vai all'album", + "discord_rich_presence": "Presenza ricca di Discord", + "browse_all": "Esplora tutto", + "genres": "Generi", + "explore_genres": "Esplora generi" +} \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index a288cf0e..ac23728b 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -279,5 +279,10 @@ "password": "パスワード", "login": "ログインする", "login_with_your_lastfm": "あなたのLast.fmアカウントでログインする", - "scrobble_to_lastfm": "Last.fmにスクロブルする" + "scrobble_to_lastfm": "Last.fmにスクロブルする", + "go_to_album": "アルバムに移動", + "discord_rich_presence": "ディスコードリッチプレゼンス", + "browse_all": "すべてを閲覧", + "genres": "ジャンル", + "explore_genres": "ジャンルを探索" } \ No newline at end of file diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 306d63c4..6e50c461 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -280,5 +280,10 @@ "login": "Inloggen", "login_with_your_lastfm": "Inloggen met uw Last.fm account", "scrobble_to_lastfm": "Scrobbel naar Last.fm", - "@@locale": "nl" -} + "audio_source": "Audiobron", + "go_to_album": "Ga naar album", + "discord_rich_presence": "Discord Rich Presence", + "browse_all": "Alles bekijken", + "genres": "Genres", + "explore_genres": "Verken genres" +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 8c6147ad..dd173a37 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -279,5 +279,10 @@ "password": "Hasło", "login": "Zaloguj", "login_with_your_lastfm": "Zaloguj się na swoje konto Last.fm", - "scrobble_to_lastfm": "Scrobbluj do Last.fm" + "scrobble_to_lastfm": "Scrobbluj do Last.fm", + "go_to_album": "Przejdź do albumu", + "discord_rich_presence": "Obecność na Discordzie", + "browse_all": "Przeglądaj wszystko", + "genres": "Gatunki muzyczne", + "explore_genres": "Eksploruj gatunki" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 46c0a88e..705217c1 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -279,5 +279,10 @@ "password": "Palavra-passe", "login": "Iniciar sessão", "login_with_your_lastfm": "Inicie sessão na sua conta Last.fm", - "scrobble_to_lastfm": "Scrobble para o Last.fm" + "scrobble_to_lastfm": "Scrobble para o Last.fm", + "go_to_album": "Ir para o álbum", + "discord_rich_presence": "Presença rica no Discord", + "browse_all": "Navegar por tudo", + "genres": "Gêneros", + "explore_genres": "Explorar gêneros" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b1964f5f..32415863 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -279,5 +279,10 @@ "password": "Пароль", "login": "Войти", "login_with_your_lastfm": "Войти в свою учетную запись Last.fm", - "scrobble_to_lastfm": "Скробблинг на Last.fm" + "scrobble_to_lastfm": "Скробблинг на Last.fm", + "go_to_album": "Перейти к альбому", + "discord_rich_presence": "Богатое присутствие в Discord", + "browse_all": "Просмотреть все", + "genres": "Жанры", + "explore_genres": "Исследовать жанры" } \ No newline at end of file diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index fa0ea587..63646af6 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -279,5 +279,10 @@ "password": "Şifre", "login": "Giriş Yap", "login_with_your_lastfm": "Last.fm hesabınız ile giriş yapın", - "scrobble_to_lastfm": "Last.fm için Scrobble" + "scrobble_to_lastfm": "Last.fm için Scrobble", + "go_to_album": "Albüme Git", + "discord_rich_presence": "Discord Zengin Varlık", + "browse_all": "Tümünü Gözat", + "genres": "Müzik Türleri", + "explore_genres": "Türleri Keşfet" } \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 22fc341e..2ae29237 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -279,5 +279,10 @@ "password": "Пароль", "login": "Увійти", "login_with_your_lastfm": "Увійти в свій обліковий запис Last.fm", - "scrobble_to_lastfm": "Скробблінг на Last.fm" + "scrobble_to_lastfm": "Скробблінг на Last.fm", + "go_to_album": "Перейти до альбому", + "discord_rich_presence": "Багата присутність у Discord", + "browse_all": "Переглянути все", + "genres": "Жанри", + "explore_genres": "Досліджувати жанри" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 325216fa..85b57724 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -279,5 +279,10 @@ "password": "密码", "login": "登录", "login_with_your_lastfm": "使用您的 Last.fm 帐户登录", - "scrobble_to_lastfm": "在 Last.fm 上记录播放" + "scrobble_to_lastfm": "在 Last.fm 上记录播放", + "go_to_album": "前往专辑", + "discord_rich_presence": "Discord 丰富展现", + "browse_all": "浏览全部", + "genres": "音乐类型", + "explore_genres": "探索音乐类型" } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 267ab17f..16cdc2b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El publish_to: "none" -version: 3.3.0+26 +version: 3.4.0+27 homepage: https://spotube.krtirtho.dev repository: https://github.com/KRTirtho/spotube diff --git a/untranslated_messages.json b/untranslated_messages.json index 5d165f85..9e26dfee 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,139 +1 @@ -{ - "ar": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "bn": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "ca": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "de": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "es": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "fa": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "fr": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "hi": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "it": [ - "audio_source", - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "ja": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "nl": [ - "audio_source", - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "pl": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "pt": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "ru": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "tr": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "uk": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ], - - "zh": [ - "go_to_album", - "discord_rich_presence", - "browse_all", - "genres", - "explore_genres" - ] -} +{} \ No newline at end of file From 0dfd40153714b7a4b83ac30f0c56830bc0c05ffd Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 1 Jan 2024 21:01:21 +0600 Subject: [PATCH 107/131] fix(linux): crash after login --- lib/hooks/configurators/use_deep_linking.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart index 9431f04d..be6facf9 100644 --- a/lib/hooks/configurators/use_deep_linking.dart +++ b/lib/hooks/configurators/use_deep_linking.dart @@ -11,9 +11,11 @@ import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; +final appLinks = AppLinks(); +final linkStream = appLinks.allStringLinkStream.asBroadcastStream(); + void useDeepLinking(WidgetRef ref) { // single instance no worries - final appLinks = AppLinks(); final spotify = ref.watch(spotifyProvider); final queryClient = useQueryClient(); @@ -61,7 +63,7 @@ void useDeepLinking(WidgetRef ref) { FlutterSharingIntent.instance.getMediaStream().listen(uriListener); } - final subscription = appLinks.allStringLinkStream.listen((uri) async { + final subscription = linkStream.listen((uri) async { final startSegment = uri.split(":").take(2).join(":"); final endSegment = uri.split(":").last; From 7e1005dc62c99a6f0bd569fc8f293b334ba721f4 Mon Sep 17 00:00:00 2001 From: franchioping <43936644+franchioping@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:08:51 +0000 Subject: [PATCH 108/131] docs: Update dev branch in CONTRIBUTION.md (#977) change development branch from main to dev --- CONTRIBUTION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index b2823e62..13996cea 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -145,7 +145,7 @@ Do the following: flutter run -d )> ``` -Do debugging/testing/build etc then submit to us with PR against the development branch (master) & we'll review your code +Do debugging/testing/build etc then submit to us with PR against the development branch (dev) & we'll review your code ### Submit Translations @@ -163,4 +163,4 @@ Make sure you're familiar with [Flutter localization](https://docs.flutter.dev/u - Now restart (hot restart if running already) the app in debug mode & go to "Settings" > "Language" & see if your locale shows up - If it does, select it & see if the app is translated properly - Now git commit the changes & push -- Finally, submit a PR against the development branch (dev) & we'll review your code \ No newline at end of file +- Finally, submit a PR against the development branch (dev) & we'll review your code From 02e44fc6b849a873adad382f5d46ed8caf32359f Mon Sep 17 00:00:00 2001 From: Piotr Rogowski Date: Tue, 2 Jan 2024 17:21:12 +0100 Subject: [PATCH 109/131] fix: Black window flash when starting the app (#1003) --- lib/main.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 052e6809..f96920a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -58,15 +58,6 @@ Future main(List rawArgs) async { await DesktopTools.window.setPreventClose(true); } - await DesktopTools.ensureInitialized( - DesktopWindowOptions( - hideTitleBar: true, - title: "Spotube", - backgroundColor: Colors.transparent, - minimumSize: const Size(300, 700), - ), - ); - await SystemTheme.accentColor.load(); if (!kIsWeb) { @@ -107,6 +98,15 @@ Future main(List rawArgs) async { path: hiveCacheDir, ); + await DesktopTools.ensureInitialized( + DesktopWindowOptions( + hideTitleBar: true, + title: "Spotube", + backgroundColor: Colors.transparent, + minimumSize: const Size(300, 700), + ), + ); + Catcher2( enableLogger: arguments["verbose"], debugConfig: Catcher2Options( From ba4e11a40ab18308437a05333a46eace6f8eeb5a Mon Sep 17 00:00:00 2001 From: franchioping <43936644+franchioping@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:37:51 +0000 Subject: [PATCH 110/131] fix: songs doesn't play when sources with preferred audio codec is empty (#976) * Fix song not playing when m4a or weba is not available (one is available but not the other) for that song * Update lib/services/sourced_track/sources/youtube.dart --------- Co-authored-by: Kingkor Roy Tirtho --- lib/services/sourced_track/sources/youtube.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index 096de2d4..2bcd6e3e 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -79,14 +79,17 @@ class YoutubeSourcedTrack extends SourcedTrack { } static SourceMap toSourceMap(StreamManifest manifest) { - final m4a = manifest.audioOnly + var m4a = manifest.audioOnly .where((audio) => audio.codec.mimeType == "audio/mp4") .sortByBitrate(); - final weba = manifest.audioOnly + var weba = manifest.audioOnly .where((audio) => audio.codec.mimeType == "audio/webm") .sortByBitrate(); + m4a = m4a.isEmpty ? weba.toList() : m4a; + weba = weba.isEmpty ? m4a.toList() : weba; + return SourceMap( m4a: SourceQualityMap( high: m4a.first.url.toString(), From 69559ba24285636e42b2f2231f956c31388c5cf3 Mon Sep 17 00:00:00 2001 From: Piotr Rogowski Date: Tue, 2 Jan 2024 17:41:03 +0100 Subject: [PATCH 111/131] fix(macos): Respect Minimize to tray option (#1001) --- macos/Runner/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index d53ef643..218f93e0 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -4,6 +4,6 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true + return false } } From b2ba6d9261319240877cbde3aa27d2459d9ee17e Mon Sep 17 00:00:00 2001 From: powen Date: Wed, 3 Jan 2024 02:40:50 +0800 Subject: [PATCH 112/131] cd: build iPA support for iOS (#971) * Create build-iPA.yml * Update build-iPA.yml * update-iOS - Set AppIcon(same as Android) - Set to correct app name(Sptube -> Spotube) - * update iOS flavors * Update build-iPA.yml * Update spotube-release-binary.yml * rename release-bin iPA filename * update appicon * Update dev-Info.plist --- .github/workflows/build-iPA.yml | 42 + .github/workflows/spotube-release-binary.yml | 44 +- flutter_launcher_icons-nightly.yaml | 1 + ios/Podfile | 21 + ios/Podfile.lock | 153 +- ios/Runner.xcodeproj/project.pbxproj | 2038 ++++++++++++++++- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../xcshareddata/xcschemes/dev.xcscheme | 77 + .../xcshareddata/xcschemes/nightly.xcscheme | 77 + .../xcshareddata/xcschemes/stable.xcscheme | 77 + .../AppIcon-nightly-1024x1024@1x.png | Bin 0 -> 296365 bytes .../AppIcon-nightly-20x20@1x.png | Bin 0 -> 1067 bytes .../AppIcon-nightly-20x20@2x.png | Bin 0 -> 3063 bytes .../AppIcon-nightly-20x20@3x.png | Bin 0 -> 5544 bytes .../AppIcon-nightly-29x29@1x.png | Bin 0 -> 1908 bytes .../AppIcon-nightly-29x29@2x.png | Bin 0 -> 5273 bytes .../AppIcon-nightly-29x29@3x.png | Bin 0 -> 9629 bytes .../AppIcon-nightly-40x40@1x.png | Bin 0 -> 3063 bytes .../AppIcon-nightly-40x40@2x.png | Bin 0 -> 8549 bytes .../AppIcon-nightly-40x40@3x.png | Bin 0 -> 15152 bytes .../AppIcon-nightly-50x50@1x.png | Bin 0 -> 4221 bytes .../AppIcon-nightly-50x50@2x.png | Bin 0 -> 11770 bytes .../AppIcon-nightly-57x57@1x.png | Bin 0 -> 5119 bytes .../AppIcon-nightly-57x57@2x.png | Bin 0 -> 14102 bytes .../AppIcon-nightly-60x60@2x.png | Bin 0 -> 15152 bytes .../AppIcon-nightly-60x60@3x.png | Bin 0 -> 27017 bytes .../AppIcon-nightly-72x72@1x.png | Bin 0 -> 7219 bytes .../AppIcon-nightly-72x72@2x.png | Bin 0 -> 19833 bytes .../AppIcon-nightly-76x76@1x.png | Bin 0 -> 7952 bytes .../AppIcon-nightly-76x76@2x.png | Bin 0 -> 21551 bytes .../AppIcon-nightly-83.5x83.5@2x.png | Bin 0 -> 24166 bytes .../AppIcon-nightly.appiconset/Contents.json | 1 + .../AppIcon-1024x1024@1x.png | Bin 0 -> 279220 bytes .../AppIcon.appiconset/AppIcon-20x20@1x.png | Bin 0 -> 1169 bytes .../AppIcon.appiconset/AppIcon-20x20@2x.png | Bin 0 -> 2967 bytes .../AppIcon.appiconset/AppIcon-20x20@3x.png | Bin 0 -> 5238 bytes .../AppIcon.appiconset/AppIcon-29x29@1x.png | Bin 0 -> 1903 bytes .../AppIcon.appiconset/AppIcon-29x29@2x.png | Bin 0 -> 4947 bytes .../AppIcon.appiconset/AppIcon-29x29@3x.png | Bin 0 -> 8997 bytes .../AppIcon.appiconset/AppIcon-40x40@1x.png | Bin 0 -> 2967 bytes .../AppIcon.appiconset/AppIcon-40x40@2x.png | Bin 0 -> 7960 bytes .../AppIcon.appiconset/AppIcon-40x40@3x.png | Bin 0 -> 14374 bytes .../AppIcon.appiconset/AppIcon-50x50@1x.png | Bin 0 -> 3996 bytes .../AppIcon.appiconset/AppIcon-50x50@2x.png | Bin 0 -> 10909 bytes .../AppIcon.appiconset/AppIcon-57x57@1x.png | Bin 0 -> 4900 bytes .../AppIcon.appiconset/AppIcon-57x57@2x.png | Bin 0 -> 13357 bytes .../AppIcon.appiconset/AppIcon-60x60@2x.png | Bin 0 -> 14374 bytes .../AppIcon.appiconset/AppIcon-60x60@3x.png | Bin 0 -> 25638 bytes .../AppIcon.appiconset/AppIcon-72x72@1x.png | Bin 0 -> 6770 bytes .../AppIcon.appiconset/AppIcon-72x72@2x.png | Bin 0 -> 18578 bytes .../AppIcon.appiconset/AppIcon-76x76@1x.png | Bin 0 -> 7415 bytes .../AppIcon.appiconset/AppIcon-76x76@2x.png | Bin 0 -> 20128 bytes .../AppIcon-83.5x83.5@2x.png | Bin 0 -> 22732 bytes .../AppIcon.appiconset/Contents.json | 123 +- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 0 bytes ios/Runner/Assets.xcassets/Contents.json | 6 + ios/Runner/Info.plist | 6 +- ios/build/.last_build_id | 1 + ios/dev-Info.plist | 66 + ios/nightly-Info.plist | 66 + ios/stable-Info.plist | 66 + pubspec.yaml | 1 + 76 files changed, 2696 insertions(+), 172 deletions(-) create mode 100644 .github/workflows/build-iPA.yml create mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/dev.xcscheme create mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/nightly.xcscheme create mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/stable.xcscheme create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-1024x1024@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-20x20@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-20x20@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-20x20@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-29x29@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-29x29@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-29x29@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-40x40@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-40x40@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-40x40@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-50x50@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-50x50@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-57x57@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-57x57@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-60x60@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-60x60@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-72x72@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-72x72@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-76x76@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-76x76@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-83.5x83.5@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-1024x1024@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-50x50@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-50x50@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-57x57@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-57x57@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-72x72@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-72x72@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 ios/Runner/Assets.xcassets/Contents.json create mode 100644 ios/build/.last_build_id create mode 100644 ios/dev-Info.plist create mode 100644 ios/nightly-Info.plist create mode 100644 ios/stable-Info.plist diff --git a/.github/workflows/build-iPA.yml b/.github/workflows/build-iPA.yml new file mode 100644 index 00000000..72e68774 --- /dev/null +++ b/.github/workflows/build-iPA.yml @@ -0,0 +1,42 @@ +name: Build iPA +on: workflow_dispatch + +jobs: + build: + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@master + - uses: actions/checkout@v4 + - name: submodules-init + uses: snickerbockers/submodules-init@v4 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Build + run: | + cp .env.example .env + flutter pub get && dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns + flutter build ios --release --no-codesign --flavor dev + flutter build ios --release --no-codesign --flavor stable + flutter build ios --release --no-codesign --flavor nightly + ln -sf ./build/ios/iphoneos Payload + zip -r9 spotube-dev.ipa Payload/dev.app + zip -r9 spotube-stable.ipa Payload/stable.app + zip -r9 spotube-nightly.ipa Payload/nightly.app + - name: Upload spotube-dev.ipa + uses: actions/upload-artifact@v4 + with: + name: "spotube-dev.ipa" + path: "spotube-dev.ipa" + - name: Upload spotube-stable.ipa + uses: actions/upload-artifact@v4 + with: + name: "spotube-stable.ipa" + path: "spotube-stable.ipa" + - name: Upload spotube-nightly.ipa + uses: actions/upload-artifact@v4 + with: + name: "spotube-nightly.ipa" + path: "spotube-nightly.ipa" diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index caeb7238..b561bca8 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -321,13 +321,54 @@ jobs: mkdir -p build/${{ env.BUILD_VERSION }} appdmg appdmg.json build/Spotube-macos-universal.dmg + iOS: + + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2.10.0 + with: + cache: true + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Replace pubspec version and BUILD_VERSION Env (nightly) + if: ${{ inputs.channel == 'nightly' }} + run: | + brew install yq + yq -i '.version |= sub("\+\d+", "+${{ inputs.channel }}.")' pubspec.yaml + yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml + echo "BUILD_VERSION=${{ inputs.version }}+${{ inputs.channel }}.${{ github.run_number }}" >> $GITHUB_ENV + + - name: BUILD_VERSION Env (stable) + if: ${{ inputs.channel == 'stable' }} + run: | + echo "BUILD_VERSION=${{ inputs.version }}" >> $GITHUB_ENV + + - name: Create Stable .env + if: ${{ inputs.channel == 'stable' }} + run: echo '${{ secrets.DOTENV_RELEASE }}' > .env + + - name: Create Nightly .env + if: ${{ inputs.channel == 'nightly' }} + run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env + + - name: Generate Secrets + run: | + flutter pub get + dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns + + - name: Build iOS iPA + run: | + flutter build ios --release --no-codesign --flavor ${{ inputs.channel }} + ln -sf ./build/ios/iphoneos Payload + zip -r9 Spotube-iOS.ipa Payload/${{ inputs.channel }}.app - uses: actions/upload-artifact@v3 with: if-no-files-found: error name: Spotube-Release-Binaries path: | - build/Spotube-macos-universal.dmg + Spotube-iOS.ipa - name: Debug With SSH When fails if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }} @@ -343,6 +384,7 @@ jobs: - linux - android - macos + - iOS steps: - uses: actions/download-artifact@v3 with: diff --git a/flutter_launcher_icons-nightly.yaml b/flutter_launcher_icons-nightly.yaml index e531efd4..c6892d4b 100644 --- a/flutter_launcher_icons-nightly.yaml +++ b/flutter_launcher_icons-nightly.yaml @@ -1,5 +1,6 @@ flutter_launcher_icons: android: true + ios: true image_path: "assets/spotube-nightly-logo.png" adaptive_icon_foreground: "assets/spotube-nightly-logo-foreground.jpg" adaptive_icon_background: "#242832" diff --git a/ios/Podfile b/ios/Podfile index cfd01b62..5b0d5a2c 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -34,6 +34,27 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end +target 'dev' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +target 'stable' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +target 'nightly' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 68fcdacc..35f3dc18 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,13 +1,12 @@ PODS: + - app_links (0.0.1): + - Flutter - audio_service (0.0.1): - Flutter - audio_session (0.0.1): - Flutter - - audioplayers_darwin (0.0.1): + - device_info_plus (0.0.1): - Flutter - - connectivity_plus (0.0.1): - - Flutter - - ReachabilitySwift - DKImagePickerController/Core (4.3.4): - DKImagePickerController/ImageDataManager - DKImagePickerController/Resource @@ -42,6 +41,8 @@ PODS: - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter + - file_selector_ios (0.0.1): + - Flutter - Flutter (1.0.0) - flutter_inappwebview (0.0.1): - Flutter @@ -50,44 +51,77 @@ PODS: - flutter_inappwebview/Core (0.0.1): - Flutter - OrderedSet (~> 5.0) + - flutter_keyboard_visibility (0.0.1): + - Flutter + - flutter_mailer (0.0.1): + - Flutter + - flutter_native_splash (0.0.1): + - Flutter + - flutter_secure_storage (6.0.0): + - Flutter + - flutter_sharing_intent (0.0.1): + - Flutter + - fluttertoast (0.0.2): + - Flutter + - Toast - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) - - metadata_god (0.0.1): + - image_picker_ios (0.0.1): - Flutter + - integration_test (0.0.1): + - Flutter + - media_kit_libs_ios_audio (1.0.4): + - Flutter + - media_kit_native_event_loop (1.0.0): + - Flutter + - metadata_god (0.0.1) - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter - - path_provider_ios (0.0.1): + - path_provider_foundation (0.0.1): - Flutter - - permission_handler_apple (9.0.4): + - FlutterMacOS + - permission_handler_apple (9.1.1): - Flutter - - ReachabilitySwift (5.0.0) - - SDWebImage (5.13.4): - - SDWebImage/Core (= 5.13.4) - - SDWebImage/Core (5.13.4) - - shared_preferences_ios (0.0.1): + - SDWebImage (5.18.8): + - SDWebImage/Core (= 5.18.8) + - SDWebImage/Core (5.18.8) + - shared_preferences_foundation (0.0.1): - Flutter - - sqflite (0.0.2): + - FlutterMacOS + - sqflite (0.0.3): - Flutter - FMDB (>= 2.7.5) - - SwiftyGif (5.4.3) + - SwiftyGif (5.4.4) + - Toast (4.0.0) - url_launcher_ios (0.0.1): - Flutter DEPENDENCIES: + - app_links (from `.symlinks/plugins/app_links/ios`) - audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) + - file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`) - Flutter (from `Flutter`) - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) + - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) + - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - media_kit_libs_ios_audio (from `.symlinks/plugins/media_kit_libs_ios_audio/ios`) + - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - metadata_god (from `.symlinks/plugins/metadata_god/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -97,63 +131,96 @@ SPEC REPOS: - DKPhotoGallery - FMDB - OrderedSet - - ReachabilitySwift - SDWebImage - SwiftyGif + - Toast EXTERNAL SOURCES: + app_links: + :path: ".symlinks/plugins/app_links/ios" audio_service: :path: ".symlinks/plugins/audio_service/ios" audio_session: :path: ".symlinks/plugins/audio_session/ios" - audioplayers_darwin: - :path: ".symlinks/plugins/audioplayers_darwin/ios" - connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" + file_selector_ios: + :path: ".symlinks/plugins/file_selector_ios/ios" Flutter: :path: Flutter flutter_inappwebview: :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" + flutter_mailer: + :path: ".symlinks/plugins/flutter_mailer/ios" + flutter_native_splash: + :path: ".symlinks/plugins/flutter_native_splash/ios" + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" + flutter_sharing_intent: + :path: ".symlinks/plugins/flutter_sharing_intent/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + media_kit_libs_ios_audio: + :path: ".symlinks/plugins/media_kit_libs_ios_audio/ios" + media_kit_native_event_loop: + :path: ".symlinks/plugins/media_kit_native_event_loop/ios" metadata_god: :path: ".symlinks/plugins/metadata_god/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - shared_preferences_ios: - :path: ".symlinks/plugins/shared_preferences_ios/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: + app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875 audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_session: 4f3e461722055d21515cf3261b64c973c062f345 - audioplayers_darwin: 387322cb364026a1782298c982693b1b6aa9fa1b - connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e + device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95 + file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de + file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 + flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62 + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 + flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef + flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be + flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98 + fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - metadata_god: cebcc48708aca3e9d1ef60c74b23404ff3730d5e + image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 + integration_test: 13825b8a9334a850581300559b8839134b124670 + media_kit_libs_ios_audio: 8f39d96a9c630685dfb844c289bd1d114c486fb3 + media_kit_native_event_loop: 99111eded5acbdc9c2738021ea6550dd36ca8837 + metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - SDWebImage: e5cc87bf736e60f49592f307bdf9e157189298a3 - shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad - sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 - SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + SDWebImage: a81bbb3ba4ea5f810f4069c68727cb118467a04a + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 -PODFILE CHECKSUM: e9ba2289804955e1370e293b204c6e8651354f4a +PODFILE CHECKSUM: e36c7ad9836dfd8d22934c7680185432a658e28f -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d9cf2138..8b7c2b1e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,17 +3,39 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 051977801F58E8DBB6712352 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7E9EBDD27997A73A4D38EE1 /* Pods_Runner.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 17438EB903776D8D0E926C9B /* Pods_nightly.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BAC36FC304DBD4E8A8C00694 /* Pods_nightly.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 46249B26D47C5DB81A4F972E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7E9EBDD27997A73A4D38EE1 /* Pods_Runner.framework */; }; + 4E86E0C42011EDB42C34AF9A /* Pods_stable.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5F91A319C771EEC978B238A /* Pods_stable.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B536BD902B405DB1009B3CE4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + B536BD912B405DB1009B3CE4 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + B536BD952B405DB1009B3CE4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B536BD962B405DB1009B3CE4 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + B536BD972B405DB1009B3CE4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + B536BD982B405DB1009B3CE4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + B536BDAE2B405FDE009B3CE4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + B536BDAF2B405FDE009B3CE4 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + B536BDB22B405FDE009B3CE4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B536BDB32B405FDE009B3CE4 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + B536BDB42B405FDE009B3CE4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + B536BDB52B405FDE009B3CE4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + B536BDD02B4060B3009B3CE4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + B536BDD12B4060B3009B3CE4 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + B536BDD42B4060B3009B3CE4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B536BDD52B4060B3009B3CE4 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + B536BDD62B4060B3009B3CE4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + B536BDD72B4060B3009B3CE4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + C36A05AD330BBFAED75A62D5 /* Pods_dev.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4238A4985255EC9F93067739 /* Pods_dev.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -27,17 +49,78 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + B536BD992B405DB1009B3CE4 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + B536BDB62B405FDE009B3CE4 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + B536BDD82B4060B3009B3CE4 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 04C104D3779B4D1635D939BF /* Pods-Runner.profile-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-nightly.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-nightly.xcconfig"; sourceTree = ""; }; + 0F8FB58820FF492BD3CF9315 /* Pods-nightly.debug-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.debug-stable.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.debug-stable.xcconfig"; sourceTree = ""; }; + 126B91CED32FAD3C40A67A23 /* Pods-dev.debug-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.debug-stable.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.debug-stable.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 171073CFF94F5751BC2B78DD /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1C9810F8B3FD927ED8C94791 /* Pods-dev.profile-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.profile-nightly.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.profile-nightly.xcconfig"; sourceTree = ""; }; + 21C0B1DEE0F0BFD3F3651F79 /* Pods-stable.debug-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.debug-nightly.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.debug-nightly.xcconfig"; sourceTree = ""; }; + 261A31AC0DBA2D93BD1910D9 /* Pods-nightly.profile-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.profile-nightly.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.profile-nightly.xcconfig"; sourceTree = ""; }; + 285DE2278D380EE2A6647CA9 /* Pods-nightly.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.debug.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.debug.xcconfig"; sourceTree = ""; }; + 29304D1832AA30DE0C33E05C /* Pods-dev.profile-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.profile-stable.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.profile-stable.xcconfig"; sourceTree = ""; }; + 2DA87118BE2AF25875B7C376 /* Pods-stable.release-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.release-stable.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.release-stable.xcconfig"; sourceTree = ""; }; + 2F9AD76AF35FFC693C051CE1 /* Pods-dev.release-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.release-nightly.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.release-nightly.xcconfig"; sourceTree = ""; }; + 39E15EE1745C9266FDB59558 /* Pods-stable.debug-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.debug-dev.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.debug-dev.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3E262038FF3BDA3B8A7BDAC3 /* Pods-Runner.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-dev.xcconfig"; sourceTree = ""; }; + 3F754C793C1BC0E8B8FFB5B7 /* Pods-stable.profile-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.profile-stable.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.profile-stable.xcconfig"; sourceTree = ""; }; + 4238A4985255EC9F93067739 /* Pods_dev.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_dev.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 46E04A5AA989356A32CD8E66 /* Pods-dev.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.profile.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.profile.xcconfig"; sourceTree = ""; }; + 48E7E801EAE1B520AA5F35DD /* Pods-dev.profile-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.profile-dev.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.profile-dev.xcconfig"; sourceTree = ""; }; + 4BDAF8FFADB62CA017755094 /* Pods-stable.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.profile.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.profile.xcconfig"; sourceTree = ""; }; + 5014E8BD9F7181E528538444 /* Pods-stable.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.release.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.release.xcconfig"; sourceTree = ""; }; + 53AD516AAEB9A1331C99CBAE /* Pods-stable.profile-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.profile-dev.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.profile-dev.xcconfig"; sourceTree = ""; }; + 5A8B64E98ADDA28FB63AA32C /* Pods-Runner.release-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-nightly.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-nightly.xcconfig"; sourceTree = ""; }; + 636F4A85470D9E3B4CC8AFB8 /* Pods-nightly.profile-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.profile-dev.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.profile-dev.xcconfig"; sourceTree = ""; }; 66F649AFA6E49EA44F469DA3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 68BE49B58C0EBB578948D773 /* Pods-nightly.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.release-dev.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.release-dev.xcconfig"; sourceTree = ""; }; + 6AE8151F4499707FA23C8223 /* Pods-dev.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.debug.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 77EFEBB27B276DD5F6B01B4B /* Pods-Runner.debug-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-nightly.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-nightly.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 864AC9150518DFBA85A46A15 /* Pods-stable.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.release-dev.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.release-dev.xcconfig"; sourceTree = ""; }; + 869E7B97AE866F2BCA2E5A6A /* Pods-Runner.release-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-stable.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-stable.xcconfig"; sourceTree = ""; }; + 89CD409D60E1362C529707A4 /* Pods-nightly.release-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.release-nightly.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.release-nightly.xcconfig"; sourceTree = ""; }; + 8AD587044EF2C6A6FA3059DC /* Pods-stable.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.debug.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.debug.xcconfig"; sourceTree = ""; }; + 8B9DFB8E20C11066C3AB696A /* Pods-dev.debug-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.debug-nightly.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.debug-nightly.xcconfig"; sourceTree = ""; }; + 8CF39CF9464623571B63D15B /* Pods-nightly.release-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.release-stable.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.release-stable.xcconfig"; sourceTree = ""; }; + 9232DBE472C8CEA1101843D9 /* Pods-nightly.profile-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.profile-stable.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.profile-stable.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -45,7 +128,30 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9878519B106548FD75CA15C0 /* Pods-nightly.debug-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.debug-dev.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.debug-dev.xcconfig"; sourceTree = ""; }; + A59B7A01EEC476AF3141B518 /* Pods-Runner.debug-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-dev.xcconfig"; sourceTree = ""; }; + B38E6C7315D66215AFD8B218 /* Pods-stable.profile-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.profile-nightly.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.profile-nightly.xcconfig"; sourceTree = ""; }; + B536BDA02B405DB1009B3CE4 /* stable.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = stable.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B536BDA12B405DB1009B3CE4 /* stable-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "stable-Info.plist"; path = "/Users/xiaobowen/Documents/GitHub/spotube/ios/stable-Info.plist"; sourceTree = ""; }; + B536BDBF2B405FDE009B3CE4 /* dev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = dev.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B536BDC02B405FDE009B3CE4 /* dev-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "dev-Info.plist"; path = "/Users/xiaobowen/Documents/GitHub/spotube/ios/dev-Info.plist"; sourceTree = ""; }; + B536BDE42B4060B3009B3CE4 /* nightly.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = nightly.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B536BDE52B4060B3009B3CE4 /* nightly-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "nightly-Info.plist"; path = "/Users/xiaobowen/Documents/GitHub/spotube/ios/nightly-Info.plist"; sourceTree = ""; }; + B5F91A319C771EEC978B238A /* Pods_stable.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_stable.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B95530D9046F7F9BA07D2ADD /* Pods-Runner.profile-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-stable.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-stable.xcconfig"; sourceTree = ""; }; + BAC36FC304DBD4E8A8C00694 /* Pods_nightly.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_nightly.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BDE1B62C8A5219CAA5D19583 /* Pods-nightly.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.release.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.release.xcconfig"; sourceTree = ""; }; + C3F494F4E243EAE21CEC5765 /* Pods-Runner.profile-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-dev.xcconfig"; sourceTree = ""; }; + C63F01302EF00EAECE6BEA7C /* Pods-dev.release-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.release-dev.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.release-dev.xcconfig"; sourceTree = ""; }; + CA0F4EAB0789E68A7C771A07 /* Pods-nightly.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.profile.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.profile.xcconfig"; sourceTree = ""; }; CE8646F5A4BCC46B0416DC84 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + D32BAE0F55672DD7669755B8 /* Pods-Runner.debug-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-stable.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-stable.xcconfig"; sourceTree = ""; }; + D9A69004587D01A7C68666CF /* Pods-dev.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.release.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.release.xcconfig"; sourceTree = ""; }; + E0EAB4380EE7C7EA7A350B6F /* Pods-stable.release-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.release-nightly.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.release-nightly.xcconfig"; sourceTree = ""; }; + E81F11471FD7D807286E33D6 /* Pods-dev.debug-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.debug-dev.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.debug-dev.xcconfig"; sourceTree = ""; }; + EB7783C1029CEC13F4B05D36 /* Pods-nightly.debug-nightly.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-nightly.debug-nightly.xcconfig"; path = "Target Support Files/Pods-nightly/Pods-nightly.debug-nightly.xcconfig"; sourceTree = ""; }; + EBBED0A8DE0D0E230CD03613 /* Pods-dev.release-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-dev.release-stable.xcconfig"; path = "Target Support Files/Pods-dev/Pods-dev.release-stable.xcconfig"; sourceTree = ""; }; + F6F397A82E788E50B186ADC7 /* Pods-stable.debug-stable.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-stable.debug-stable.xcconfig"; path = "Target Support Files/Pods-stable/Pods-stable.debug-stable.xcconfig"; sourceTree = ""; }; F7E9EBDD27997A73A4D38EE1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -55,6 +161,31 @@ buildActionMask = 2147483647; files = ( 46249B26D47C5DB81A4F972E /* Pods_Runner.framework in Frameworks */, + 051977801F58E8DBB6712352 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B536BD922B405DB1009B3CE4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4E86E0C42011EDB42C34AF9A /* Pods_stable.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B536BDB02B405FDE009B3CE4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C36A05AD330BBFAED75A62D5 /* Pods_dev.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B536BDD22B4060B3009B3CE4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 17438EB903776D8D0E926C9B /* Pods_nightly.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -65,6 +196,9 @@ isa = PBXGroup; children = ( F7E9EBDD27997A73A4D38EE1 /* Pods_Runner.framework */, + 4238A4985255EC9F93067739 /* Pods_dev.framework */, + BAC36FC304DBD4E8A8C00694 /* Pods_nightly.framework */, + B5F91A319C771EEC978B238A /* Pods_stable.framework */, ); name = Frameworks; sourceTree = ""; @@ -75,8 +209,52 @@ 66F649AFA6E49EA44F469DA3 /* Pods-Runner.debug.xcconfig */, CE8646F5A4BCC46B0416DC84 /* Pods-Runner.release.xcconfig */, 171073CFF94F5751BC2B78DD /* Pods-Runner.profile.xcconfig */, + D32BAE0F55672DD7669755B8 /* Pods-Runner.debug-stable.xcconfig */, + 869E7B97AE866F2BCA2E5A6A /* Pods-Runner.release-stable.xcconfig */, + B95530D9046F7F9BA07D2ADD /* Pods-Runner.profile-stable.xcconfig */, + A59B7A01EEC476AF3141B518 /* Pods-Runner.debug-dev.xcconfig */, + 3E262038FF3BDA3B8A7BDAC3 /* Pods-Runner.release-dev.xcconfig */, + C3F494F4E243EAE21CEC5765 /* Pods-Runner.profile-dev.xcconfig */, + 77EFEBB27B276DD5F6B01B4B /* Pods-Runner.debug-nightly.xcconfig */, + 5A8B64E98ADDA28FB63AA32C /* Pods-Runner.release-nightly.xcconfig */, + 04C104D3779B4D1635D939BF /* Pods-Runner.profile-nightly.xcconfig */, + 6AE8151F4499707FA23C8223 /* Pods-dev.debug.xcconfig */, + 8B9DFB8E20C11066C3AB696A /* Pods-dev.debug-nightly.xcconfig */, + E81F11471FD7D807286E33D6 /* Pods-dev.debug-dev.xcconfig */, + 126B91CED32FAD3C40A67A23 /* Pods-dev.debug-stable.xcconfig */, + D9A69004587D01A7C68666CF /* Pods-dev.release.xcconfig */, + 2F9AD76AF35FFC693C051CE1 /* Pods-dev.release-nightly.xcconfig */, + C63F01302EF00EAECE6BEA7C /* Pods-dev.release-dev.xcconfig */, + EBBED0A8DE0D0E230CD03613 /* Pods-dev.release-stable.xcconfig */, + 46E04A5AA989356A32CD8E66 /* Pods-dev.profile.xcconfig */, + 1C9810F8B3FD927ED8C94791 /* Pods-dev.profile-nightly.xcconfig */, + 48E7E801EAE1B520AA5F35DD /* Pods-dev.profile-dev.xcconfig */, + 29304D1832AA30DE0C33E05C /* Pods-dev.profile-stable.xcconfig */, + 285DE2278D380EE2A6647CA9 /* Pods-nightly.debug.xcconfig */, + EB7783C1029CEC13F4B05D36 /* Pods-nightly.debug-nightly.xcconfig */, + 9878519B106548FD75CA15C0 /* Pods-nightly.debug-dev.xcconfig */, + 0F8FB58820FF492BD3CF9315 /* Pods-nightly.debug-stable.xcconfig */, + BDE1B62C8A5219CAA5D19583 /* Pods-nightly.release.xcconfig */, + 89CD409D60E1362C529707A4 /* Pods-nightly.release-nightly.xcconfig */, + 68BE49B58C0EBB578948D773 /* Pods-nightly.release-dev.xcconfig */, + 8CF39CF9464623571B63D15B /* Pods-nightly.release-stable.xcconfig */, + CA0F4EAB0789E68A7C771A07 /* Pods-nightly.profile.xcconfig */, + 261A31AC0DBA2D93BD1910D9 /* Pods-nightly.profile-nightly.xcconfig */, + 636F4A85470D9E3B4CC8AFB8 /* Pods-nightly.profile-dev.xcconfig */, + 9232DBE472C8CEA1101843D9 /* Pods-nightly.profile-stable.xcconfig */, + 8AD587044EF2C6A6FA3059DC /* Pods-stable.debug.xcconfig */, + 21C0B1DEE0F0BFD3F3651F79 /* Pods-stable.debug-nightly.xcconfig */, + 39E15EE1745C9266FDB59558 /* Pods-stable.debug-dev.xcconfig */, + F6F397A82E788E50B186ADC7 /* Pods-stable.debug-stable.xcconfig */, + 5014E8BD9F7181E528538444 /* Pods-stable.release.xcconfig */, + E0EAB4380EE7C7EA7A350B6F /* Pods-stable.release-nightly.xcconfig */, + 864AC9150518DFBA85A46A15 /* Pods-stable.release-dev.xcconfig */, + 2DA87118BE2AF25875B7C376 /* Pods-stable.release-stable.xcconfig */, + 4BDAF8FFADB62CA017755094 /* Pods-stable.profile.xcconfig */, + B38E6C7315D66215AFD8B218 /* Pods-stable.profile-nightly.xcconfig */, + 53AD516AAEB9A1331C99CBAE /* Pods-stable.profile-dev.xcconfig */, + 3F754C793C1BC0E8B8FFB5B7 /* Pods-stable.profile-stable.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -99,6 +277,9 @@ 97C146EF1CF9000F007C117D /* Products */, 67CBFE209DF24C94A9837AD5 /* Pods */, 0E0B839C4E103F896209E822 /* Frameworks */, + B536BDA12B405DB1009B3CE4 /* stable-Info.plist */, + B536BDC02B405FDE009B3CE4 /* dev-Info.plist */, + B536BDE52B4060B3009B3CE4 /* nightly-Info.plist */, ); sourceTree = ""; }; @@ -106,6 +287,9 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + B536BDA02B405DB1009B3CE4 /* stable.app */, + B536BDBF2B405FDE009B3CE4 /* dev.app */, + B536BDE42B4060B3009B3CE4 /* nightly.app */, ); name = Products; sourceTree = ""; @@ -150,13 +334,79 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + B536BD8C2B405DB1009B3CE4 /* stable */ = { + isa = PBXNativeTarget; + buildConfigurationList = B536BD9C2B405DB1009B3CE4 /* Build configuration list for PBXNativeTarget "stable" */; + buildPhases = ( + F0C8BA10A27CA77E18F842E7 /* [CP] Check Pods Manifest.lock */, + B536BD8E2B405DB1009B3CE4 /* Run Script */, + B536BD8F2B405DB1009B3CE4 /* Sources */, + B536BD922B405DB1009B3CE4 /* Frameworks */, + B536BD942B405DB1009B3CE4 /* Resources */, + B536BD992B405DB1009B3CE4 /* Embed Frameworks */, + B536BD9A2B405DB1009B3CE4 /* Thin Binary */, + A6D446F111DE4C4A202BE7F7 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = stable; + productName = Runner; + productReference = B536BDA02B405DB1009B3CE4 /* stable.app */; + productType = "com.apple.product-type.application"; + }; + B536BDAB2B405FDE009B3CE4 /* dev */ = { + isa = PBXNativeTarget; + buildConfigurationList = B536BDB82B405FDE009B3CE4 /* Build configuration list for PBXNativeTarget "dev" */; + buildPhases = ( + 6228176255365EAC646F2745 /* [CP] Check Pods Manifest.lock */, + B536BDAC2B405FDE009B3CE4 /* Run Script */, + B536BDAD2B405FDE009B3CE4 /* Sources */, + B536BDB02B405FDE009B3CE4 /* Frameworks */, + B536BDB12B405FDE009B3CE4 /* Resources */, + B536BDB62B405FDE009B3CE4 /* Embed Frameworks */, + B536BDB72B405FDE009B3CE4 /* Thin Binary */, + 244D41CE80E4BC0FFD63F8C6 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = dev; + productName = Runner; + productReference = B536BDBF2B405FDE009B3CE4 /* dev.app */; + productType = "com.apple.product-type.application"; + }; + B536BDCD2B4060B3009B3CE4 /* nightly */ = { + isa = PBXNativeTarget; + buildConfigurationList = B536BDDA2B4060B3009B3CE4 /* Build configuration list for PBXNativeTarget "nightly" */; + buildPhases = ( + 5CD4405E93760FBD048E36E2 /* [CP] Check Pods Manifest.lock */, + B536BDCE2B4060B3009B3CE4 /* Run Script */, + B536BDCF2B4060B3009B3CE4 /* Sources */, + B536BDD22B4060B3009B3CE4 /* Frameworks */, + B536BDD32B4060B3009B3CE4 /* Resources */, + B536BDD82B4060B3009B3CE4 /* Embed Frameworks */, + B536BDD92B4060B3009B3CE4 /* Thin Binary */, + D566C841A84D807A607F6DE5 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = nightly; + productName = Runner; + productReference = B536BDE42B4060B3009B3CE4 /* nightly.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -179,6 +429,9 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + B536BD8C2B405DB1009B3CE4 /* stable */, + B536BDAB2B405FDE009B3CE4 /* dev */, + B536BDCD2B4060B3009B3CE4 /* nightly */, ); }; /* End PBXProject section */ @@ -195,9 +448,59 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + B536BD942B405DB1009B3CE4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B536BD952B405DB1009B3CE4 /* LaunchScreen.storyboard in Resources */, + B536BD962B405DB1009B3CE4 /* AppFrameworkInfo.plist in Resources */, + B536BD972B405DB1009B3CE4 /* Assets.xcassets in Resources */, + B536BD982B405DB1009B3CE4 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B536BDB12B405FDE009B3CE4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B536BDB22B405FDE009B3CE4 /* LaunchScreen.storyboard in Resources */, + B536BDB32B405FDE009B3CE4 /* AppFrameworkInfo.plist in Resources */, + B536BDB42B405FDE009B3CE4 /* Assets.xcassets in Resources */, + B536BDB52B405FDE009B3CE4 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B536BDD32B4060B3009B3CE4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B536BDD42B4060B3009B3CE4 /* LaunchScreen.storyboard in Resources */, + B536BDD52B4060B3009B3CE4 /* AppFrameworkInfo.plist in Resources */, + B536BDD62B4060B3009B3CE4 /* Assets.xcassets in Resources */, + B536BDD72B4060B3009B3CE4 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 244D41CE80E4BC0FFD63F8C6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-dev/Pods-dev-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 2AF6C7D149EE8481703D5255 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -222,10 +525,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -234,6 +539,50 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 5CD4405E93760FBD048E36E2 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-nightly-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 6228176255365EAC646F2745 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-dev-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 6E9FEF583EA597C8B76255B2 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -253,6 +602,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -265,6 +615,155 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + A6D446F111DE4C4A202BE7F7 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-stable/Pods-stable-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B536BD8E2B405DB1009B3CE4 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + B536BD9A2B405DB1009B3CE4 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + B536BDAC2B405FDE009B3CE4 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + B536BDB72B405FDE009B3CE4 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + B536BDCE2B4060B3009B3CE4 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + B536BDD92B4060B3009B3CE4 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + D566C841A84D807A607F6DE5 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-nightly/Pods-nightly-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + F0C8BA10A27CA77E18F842E7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-stable-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -277,6 +776,33 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + B536BD8F2B405DB1009B3CE4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B536BD902B405DB1009B3CE4 /* AppDelegate.swift in Sources */, + B536BD912B405DB1009B3CE4 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B536BDAD2B405FDE009B3CE4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B536BDAE2B405FDE009B3CE4 /* AppDelegate.swift in Sources */, + B536BDAF2B405FDE009B3CE4 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B536BDCF2B4060B3009B3CE4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B536BDD02B4060B3009B3CE4 /* AppDelegate.swift in Sources */, + B536BDD12B4060B3009B3CE4 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ @@ -520,6 +1046,1437 @@ }; name = Release; }; + B536BD9D2B405DB1009B3CE4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + B536BD9E2B405DB1009B3CE4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + B536BD9F2B405DB1009B3CE4 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + B536BDA22B405E06009B3CE4 /* Debug-stable */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug-stable"; + }; + B536BDA32B405E06009B3CE4 /* Debug-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-stable"; + }; + B536BDA42B405E06009B3CE4 /* Debug-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-stable"; + }; + B536BDA52B405E19009B3CE4 /* Release-stable */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release-stable"; + }; + B536BDA62B405E19009B3CE4 /* Release-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-stable"; + }; + B536BDA72B405E19009B3CE4 /* Release-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-stable"; + }; + B536BDA82B405E1F009B3CE4 /* Profile-stable */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Profile-stable"; + }; + B536BDA92B405E1F009B3CE4 /* Profile-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-stable"; + }; + B536BDAA2B405E1F009B3CE4 /* Profile-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-stable"; + }; + B536BDB92B405FDE009B3CE4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + B536BDBA2B405FDE009B3CE4 /* Debug-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-stable"; + }; + B536BDBB2B405FDE009B3CE4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + B536BDBC2B405FDE009B3CE4 /* Release-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-stable"; + }; + B536BDBD2B405FDE009B3CE4 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + B536BDBE2B405FDE009B3CE4 /* Profile-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-stable"; + }; + B536BDC12B406014009B3CE4 /* Debug-dev */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug-dev"; + }; + B536BDC22B406014009B3CE4 /* Debug-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-dev"; + }; + B536BDC32B406014009B3CE4 /* Debug-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-dev"; + }; + B536BDC42B406014009B3CE4 /* Debug-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-dev"; + }; + B536BDC52B40601C009B3CE4 /* Release-dev */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release-dev"; + }; + B536BDC62B40601C009B3CE4 /* Release-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-dev"; + }; + B536BDC72B40601C009B3CE4 /* Release-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-dev"; + }; + B536BDC82B40601C009B3CE4 /* Release-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-dev"; + }; + B536BDC92B406021009B3CE4 /* Profile-dev */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Profile-dev"; + }; + B536BDCA2B406021009B3CE4 /* Profile-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-dev"; + }; + B536BDCB2B406021009B3CE4 /* Profile-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-dev"; + }; + B536BDCC2B406021009B3CE4 /* Profile-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-dev"; + }; + B536BDDB2B4060B3009B3CE4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + B536BDDC2B4060B3009B3CE4 /* Debug-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-dev"; + }; + B536BDDD2B4060B3009B3CE4 /* Debug-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-stable"; + }; + B536BDDE2B4060B3009B3CE4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + B536BDDF2B4060B3009B3CE4 /* Release-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-dev"; + }; + B536BDE02B4060B3009B3CE4 /* Release-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-stable"; + }; + B536BDE12B4060B3009B3CE4 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + B536BDE22B4060B3009B3CE4 /* Profile-dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-dev"; + }; + B536BDE32B4060B3009B3CE4 /* Profile-stable */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-stable"; + }; + B536BDE62B4060FE009B3CE4 /* Debug-nightly */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug-nightly"; + }; + B536BDE72B4060FE009B3CE4 /* Debug-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-nightly"; + }; + B536BDE82B4060FE009B3CE4 /* Debug-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-nightly"; + }; + B536BDE92B4060FE009B3CE4 /* Debug-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-nightly"; + }; + B536BDEA2B4060FE009B3CE4 /* Debug-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Debug-nightly"; + }; + B536BDEB2B406105009B3CE4 /* Release-nightly */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release-nightly"; + }; + B536BDEC2B406105009B3CE4 /* Release-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-nightly"; + }; + B536BDED2B406105009B3CE4 /* Release-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-nightly"; + }; + B536BDEE2B406105009B3CE4 /* Release-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-nightly"; + }; + B536BDEF2B406105009B3CE4 /* Release-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Release-nightly"; + }; + B536BDF02B40610B009B3CE4 /* Profile-nightly */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Profile-nightly"; + }; + B536BDF12B40610B009B3CE4 /* Profile-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-nightly"; + }; + B536BDF22B40610B009B3CE4 /* Profile-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "stable-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.stable; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-nightly"; + }; + B536BDF32B40610B009B3CE4 /* Profile-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "dev-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-nightly"; + }; + B536BDF42B40610B009B3CE4 /* Profile-nightly */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly"; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = "nightly-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = oss.krtirtho.spotube.nightly; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = "Profile-nightly"; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -527,8 +2484,17 @@ isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, + B536BDE62B4060FE009B3CE4 /* Debug-nightly */, + B536BDC12B406014009B3CE4 /* Debug-dev */, + B536BDA22B405E06009B3CE4 /* Debug-stable */, 97C147041CF9000F007C117D /* Release */, + B536BDEB2B406105009B3CE4 /* Release-nightly */, + B536BDC52B40601C009B3CE4 /* Release-dev */, + B536BDA52B405E19009B3CE4 /* Release-stable */, 249021D3217E4FDB00AE95B9 /* Profile */, + B536BDF02B40610B009B3CE4 /* Profile-nightly */, + B536BDC92B406021009B3CE4 /* Profile-dev */, + B536BDA82B405E1F009B3CE4 /* Profile-stable */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -537,8 +2503,74 @@ isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, + B536BDE72B4060FE009B3CE4 /* Debug-nightly */, + B536BDC22B406014009B3CE4 /* Debug-dev */, + B536BDA32B405E06009B3CE4 /* Debug-stable */, 97C147071CF9000F007C117D /* Release */, + B536BDEC2B406105009B3CE4 /* Release-nightly */, + B536BDC62B40601C009B3CE4 /* Release-dev */, + B536BDA62B405E19009B3CE4 /* Release-stable */, 249021D4217E4FDB00AE95B9 /* Profile */, + B536BDF12B40610B009B3CE4 /* Profile-nightly */, + B536BDCA2B406021009B3CE4 /* Profile-dev */, + B536BDA92B405E1F009B3CE4 /* Profile-stable */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B536BD9C2B405DB1009B3CE4 /* Build configuration list for PBXNativeTarget "stable" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B536BD9D2B405DB1009B3CE4 /* Debug */, + B536BDE82B4060FE009B3CE4 /* Debug-nightly */, + B536BDC32B406014009B3CE4 /* Debug-dev */, + B536BDA42B405E06009B3CE4 /* Debug-stable */, + B536BD9E2B405DB1009B3CE4 /* Release */, + B536BDED2B406105009B3CE4 /* Release-nightly */, + B536BDC72B40601C009B3CE4 /* Release-dev */, + B536BDA72B405E19009B3CE4 /* Release-stable */, + B536BD9F2B405DB1009B3CE4 /* Profile */, + B536BDF22B40610B009B3CE4 /* Profile-nightly */, + B536BDCB2B406021009B3CE4 /* Profile-dev */, + B536BDAA2B405E1F009B3CE4 /* Profile-stable */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B536BDB82B405FDE009B3CE4 /* Build configuration list for PBXNativeTarget "dev" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B536BDB92B405FDE009B3CE4 /* Debug */, + B536BDE92B4060FE009B3CE4 /* Debug-nightly */, + B536BDC42B406014009B3CE4 /* Debug-dev */, + B536BDBA2B405FDE009B3CE4 /* Debug-stable */, + B536BDBB2B405FDE009B3CE4 /* Release */, + B536BDEE2B406105009B3CE4 /* Release-nightly */, + B536BDC82B40601C009B3CE4 /* Release-dev */, + B536BDBC2B405FDE009B3CE4 /* Release-stable */, + B536BDBD2B405FDE009B3CE4 /* Profile */, + B536BDF32B40610B009B3CE4 /* Profile-nightly */, + B536BDCC2B406021009B3CE4 /* Profile-dev */, + B536BDBE2B405FDE009B3CE4 /* Profile-stable */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B536BDDA2B4060B3009B3CE4 /* Build configuration list for PBXNativeTarget "nightly" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B536BDDB2B4060B3009B3CE4 /* Debug */, + B536BDEA2B4060FE009B3CE4 /* Debug-nightly */, + B536BDDC2B4060B3009B3CE4 /* Debug-dev */, + B536BDDD2B4060B3009B3CE4 /* Debug-stable */, + B536BDDE2B4060B3009B3CE4 /* Release */, + B536BDEF2B406105009B3CE4 /* Release-nightly */, + B536BDDF2B4060B3009B3CE4 /* Release-dev */, + B536BDE02B4060B3009B3CE4 /* Release-stable */, + B536BDE12B4060B3009B3CE4 /* Profile */, + B536BDF42B40610B009B3CE4 /* Profile-nightly */, + B536BDE22B4060B3009B3CE4 /* Profile-dev */, + B536BDE32B4060B3009B3CE4 /* Profile-stable */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a3..a6b826db 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/nightly.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/nightly.xcscheme new file mode 100644 index 00000000..7ec18a73 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/nightly.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/stable.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/stable.xcscheme new file mode 100644 index 00000000..ddc19e2e --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/stable.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dbc4596b3890c55cd35881c4e0de4effc2b6e3b9 GIT binary patch literal 296365 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 z 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%0A?;vo`h%2>QL~BKA8NNzf0< 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 z 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%0DW&|lr0dK)`^G1b#G)c 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..90954ce985ea06213d56c4472c9bca6ac248ac0a GIT binary patch literal 3063 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 u{PnV$|MydDibt8Cj3E6@sA@<_tY`g5nV_x`{A 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6cdda1b66e9ff45973ef9a1efa1ad2978890494b GIT binary patch literal 9629 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 TzJ)qT00000NkvXXu0mjfqu|gU literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-40x40@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..90954ce985ea06213d56c4472c9bca6ac248ac0a GIT binary patch literal 3063 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..57e21a753f1a622a69b0cd821b8588640db986af GIT binary patch literal 15152 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-57x57@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..22da495020b15bcfaf10635973eb9bd670b18096 GIT binary patch literal 14102 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⋙xU(Kpwy`vLT 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 U{YXANifTM$o-~r+)tEkuu)qv3*mG9-t%s9UI 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 sQ2^J=qdrG%8|U|$etX<~zepHq)$ literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-72x72@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..a826bb738dc448be4cdb81dd9d379e2fc917a64e GIT binary patch literal 7219 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 BuCD+9 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon-nightly.appiconset/AppIcon-nightly-72x72@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3a8a783224aac626902e9f63ffbe5be96a1877ba GIT binary patch literal 19833 zcmV(_K-9m9P)n z3AiRzdFFRc)xX}Y_on-@L$k={f;5UO8BK7EOHhG`jyf5Q8V4UGae3UpL`_DcPKcUu zjEWdzhKV}CB_=xJg3%`mC*X2GvIU3x_&9`vt0fxFF#>Zc(5;E36l?q^763k|F&OBp$M48WPm3q zpq^*|>IVqxeGXdoKYbwSF^2&r@jlDTNok)a9H5>E02&0A!29gCOquR~CFV7lmt&rA zx$d@wZoe3Fgt?1(2J>#rdob_7yw$DqK@*p=H4W~(syCrg!xX)x4V1~<`(4Oi(oLY5_p_4)8h?5{aD5ai{=XE z6EL5N`3%fY!93|5V_)@;rm`tojd>O3r+V86Jil`JX3RHX-U-5D5%dRDKT~%|r^f?; zv_gHRQD&)lnV971n4gRJKY&!I{?<2J1%Mc1z}Nt+vG|G0^hxWCkEayURO&NL5?oG9 zg5PO5N#gSLYUR^4ZFJZ4YPaqen7@PhpD{lS0w&EqpnO(3q=9rO1E>rfWf~te4b10Y z{w>VsVLr_LE`Wl0GY|v_63W))KtKZyDDqJxAOBlVEfRpTMFJA$I3_?D{51G!9k_h4 z{WK6Zd@~4`z7GPZdqF_dfg+d=c_1CC0P?ks51gvYq-S1+`Q;!YSoc@a%S;khDiDTM z%oRq6vWlVIJ-}o@F@vpF&lY7Q{%av{XJ=SBa1nr_Zr4On$HYMz}O5;KvBy#n)3 zFkgfDMiA4dK%(5wyd5&>Py>)^5ntO>y-ecw)gWeyJSE@TGC>d`Xaq18m)R8YQRT4t zGUO&o_sLG5FBTZ6mx8?Yx}3^y9Ds=7=$dM29mwP%h=9NpMcok1AKjZW%rh~c4Z=j% zV*Waam)C}(i96&N=}-ZX?@^V3Q^ULg^B-eAzqF4brm5EIP^r{V!}$8eWM-+XX$t1b zcr0YHJqBK=FpMrk1{nFs>orYfUy}+W@l%1Ovr?_`egvjYr)8sVN12mY=6K9kVg3`$ ze}nl8nCC%&q^wkCq=T9c1pt+SL#C?c@@XK9^E>Xlif2Qni7JE4lnUQ6Sp0S1u~7z@ zGGO}Xpx%r1&;t%oyx^8fU{Z#n@61s2X;QEVcw$P@PTfu$y4`kwCXeooz(KL7w_$z- z2#Edx^Az+4(m`dTg9#v?b;`gYjYC57QYe};LrhbxH~48bP6TJ5VQ>_gN7u>!T>BKv zS$0Kx5eh13AkTa`hRU^&fXJk&2Py@VVb&x;Pxlmc+t8Yy3s98P{Sw1a=z)OrpD|yF z`FkK5sSKop#zqGVKtAiJI40nXVZH?OYd}0I4>(mcRS2B0;*?5BX!y>^5UpW2q0@Qe*#2gUk&2H zQMsr_QuqIWv(bSDkS~&zbw~`8JN_IH);T`kZcG);6aj~xQUQwX1r3A43Rrw_DS&!F z;{nURva(m!Cn@ikQZ;1&Dw!>}QEE2LS;+@dp7G2nGExm03BlBjI?!p&2XP#!X(J6t zg8OTj{~9Fv?R#x1v=11F4kUnl@Tfp0@Qi{8$BRH%i9ltEY3hv;_F{U0Q#4&=HsX`@ z(L(M^(d#OBTc+^Q_a@Tff;wTwq$P0qKoX|P0VJ^_lM1L(tn)Mx0P$ATMMz^%t<*UT zL5$RH&7x^b)tpmV5tzul{SZWGYmn6U)|NaSC;*jB7BLQi=NTZj{W!NRL~xjTqbXjM zg2ooX)5|_(0C}EPNi+`>(B-QwEv1QO|%^Eaq6<5ATg1Gw>EBiOd z&DoNBX-f$p1&?}J1fFpa@prB}cSsTA=ExYgPcb-b&NM^K7xXb%3LbzWNJ`6j8q7Eg zCZjc??51v+$pVM@U#D%#pc4N+*Fp@Wgn^bgH=o4)3`B=d zWIF>2e@}r_Dy!Dq3^m?X+=cul=3_8_5{e*N5)W-D08|DK>78RSe-0$J!+NLD9K!;r zI*`~Rh^*~loF4Jq4>%>wQwB^wK~dU^W@m_;Si@1sAMQYc+Y2}oI+8fNR--mJ$) zAoxr8xCw7+jEti>LlAX43L?^&1eW)fE_A_165Xy84t}3=(I%!aIqcbiaUTe*uW@yll3f3=*fHo`F>a34JdX)P`vX8_@Q`4p&gx{gn0d%eZz9D~R9Qe1c;GRPefl#G3;>V<$7Fx>QWj2h zv8(aCPpyUUlu7`{R|8lV26UlfgY{ax-=xknqD;Wq8O3maH-cYwVz?ReJyDXmJHv|# ziZYB3F2cLWNV!>qw8%z@phhr-+9*LZ5ug&<7w@ZHZ*n=dTeDif6uBmM{=HBPF8Tq- zeM*lNKx(!$NJ!uz58xWi>u4QThG`VQdX#m@8BITnIR{l>oC5f%4RP1vVgP zCMzLb9EH9iaM@;z*L%cFl{gTsz`H#LKhH(`5#i@91k~M~F5HG-`boPBcSR}m^p-AP zRRKu3c`JiU*C`uWOQy@Rh)!(IM!Y+@{rFj{)%h3g)|}SyOF@PqeGH216T?9UkTy?f zyB4|XM}P=s(`fc#o(w#Gw7Iu^t1s*9C2&r!Sa{h;4bE&-p^oQa(0IVH9zYl3F~8JP z?Y2sQ!q4$a3X`=eJQD%+O3a%OO#e93hX31&xS?w3RT}lT(=5)@myP6%kkb#as7`1> zTU4AL5)d_beY?#dq90SVdK2a^LvQcKeFD*A0g&IeMc|a^CYKoR-=ikQAaJ0ZOVWd1{5t(J)^3wVj$a#0^x50_sMQ3feFt)iXP`{ z5J)c@i{M5C(U*{w?ue2BHPw%ea{kFq%>=~s!xD)!5d+2nhp#;mtWl!&MM};)qs{*f z^P`w=fgC{lH_ zMBr4o;uJX6F^nz+=+tHOm}B0JoQ6?+?2#xC}@FyrPQjz{pdw0X?9!aKJ9*F~% zEizEzeopvu!kX3-h|(n1U?X*z{U4Zr5A%cY7&`0kD}el#bqW+-<&G6n=dX$%BvhSK z@U(`2Co@@)1r3LR=SZx`*N@fVoMsJ1LSdcC5a1ACZb7j82*L7GbjN9VbFz4k_S;c@ zJ=A^j^uWT?Lknl&b)1KKYb?8NF>wKbi5SoUn!t3~L;%l3G5@Kl7W|+c4QC?;Oxfk< z$f65T3Qtly88x<2d9zA#7LN8}a2W#8D9q06L1!)@5Uq0cb~y-mBG?B#w6_3K_VMFA z?{&)|nXDSQkk$(lDsZeRX`Ui@vO>??^M%24HhNX>K-M`7O@y*e&<8lg2s^MyzJMOm z=P}=#sBua?W$cw%I-vY~2E*lP{2(CyD32$y30%WJPCzI$i-*I7d?cf1Efo5XLf7o>)9aV z>!>Ba3%#?6ESUBNKt6cXWq&8gWCa{3B-53dDGdsGrmNUhqsTnHdS@_j=+0j=T7^r- zn=DrA9_yGAaPB~M`51aaU+u=w_PUsIEZDW0TUS;=q0Gb#QOdgZxwZ%{UG6<-TxrYo z&f&+k?F6nx59}n!kHN7Ss>`;hpS#6kuDiF zXPLZ3UCmf;!>B2PX92xKdgRIpG+8K=M^Nw7J=P(BYI+ob^X~aBT!&szEIrz>&=Uqr z0Ky1R0t^~nSb@hn0?pDWevUZ}GlRdUyd8i409G=wg+yC&$DgcVj5QmB0PEjz#mk}? zPDUVI-3%E}dau?AZR+PurfnnUPlN%yeb*fPx;rR()DJ}JyR)GJ=@;&GB4g-K>k=ap3V;hbsDWs4K37Hd~yAwXo&TNAR}Vc zlr74P?G1o@_Mt{P8fIJ}(?z`r%S{o3%occA0kat9p%Y)Vyavx0A+bz)$1o_=RV6`r zH?qTJGaa}KE0>n8E8ayAeW{he^{Bu8qFI4g)BKHWY&3FU!_F^0gqQwA6!K3U$ zT~hxLJkwR^(^J+t1I$+59I-I-j9_WMdwCVk9<4z$q@W-HH08qL&U(Z;tr*@r(}j7g ztQVUN$^25v_)ni04X2(LBltIG*yH&G&eV0#uLwjL8H6_ z&Sd2psDGm4y=i#`&Ka*Fcr2ja6jNlS^LZB+FQ1O!y7`EKQ}cjBpX>4WpIp&|v#NTl zsc?*=e%Bw+MU$cMy<89`yXL&YtoNAH(#bUgA4HAu=A;I1Mosg*cI=*h%jS((>3<^| z-H1TD9`Eaz(73=Lf(R?N;Xw4B)pdB&);ZWRu*pBC9^*h%yjtM(5)cAU52dbNQZZ%d z+RseyQj+*DnEwyR3~T;i#eqYF7Q;Zk+fMy^Ud1QFn&%M|{Tf2On9m^HlY?i-iE;bu zmRb0ni4X;|g(`u^#EyB;>%k}Yjk{y`CbCbBMP!<_T%&UXe=rupYsRYhq>IS(M}sOq z#~_WPB(&p18bFgI2bNz5zV{^UXj~A^IgoA@3Z&KO#$Ssd_!a{DBYQeO0XQyEr`fbXE#1F~1dx-Czw2FlEC4RAwIn&@0_?Nd1|03Kc)H4E{y-(>aLJAse%TR`EUc5TIXDvU>YB9;IH4jMC!uoj znkNV}F|`ljzFj;{2S4Xf7;mN+Q746mvBLJ?j#E*}Tt1E^FoxonKyeJ-{n0fG6Uc@n z;>ny=w7G;#E4Vn6TYlbX0KbJE<2$yu;VZL|Fogr)76j1?wx{sFRtK=LW?h{l6XAst z6CG(nxO`;<@7kI$$cKSQ+T|SU_%-OfLiK_Zez?iam$o~DpA`H>02zme=YezryaRGm zHc$&K#6Z61MKh`GS%XpQTy57ylw4Vg`9@e^Z4}up1k8Q&G5lyYg=knV!6ltwjwtZQ#o7U1U!2 zj;JOhyzbW{h`z8kgeU2ApKQ*gL)*rQ3cR}0hHG}2MSuwMZnL26XL-|5s`7&RR~yeO z<7sE=)K;csLGiuU!Xj8`AqMi<=Pe+S=YYm<@mO_7-@cgwTWpr-+c|8AXr}$JPT$al1Fz<|zV;ypy-?SotD^>~9e*a^Bu0XpW!MKrd`_2X-aS z2ghxXqKkd;)(E~fDGbNufY(a|+QegAy4u1W$c{grHN_qQ<=Uc@4QvF+#($(*M_R^d z&PGPYr&0eQ1d!G_#rS9jHJZ$!1G%8?NrM1XHeWRK_(D+jsnv7VF@8rZA#me5!2704 z{jCKYE>6F{B7vitSP>PP+c0tVn@b1&e7k|~&gG0lpX6n|Z*2gtSQbF)?o&Hfb*U>V z0Vj&!#yv57d8dIp6gYc_n}KO(Y~b^|0Ir`(;dv7sc=1XDE70sV3_jTb#i|k~whf%p z0QkEjD)9O(U3ieR4OI=At!V_&8@5{b%HbrW<+$|VRIzfKSf;OD6T>Z=0PkfGi1H?n zJU)~Qd})JK`Cw&8k>??k93iXH5f&oWxfmomU4fj1dOWgW0P@-ARqlI0R3}rc4CqDZ zyX`*LWLO%`KBqJixM;0`CTfB(aHBUZca(sCnFjb<>dC^aBeAX#Ec{O#9>Dpd)`>~c z3&cU9ZIE%MBDiK7KEcT~)hvZNwftjwUVc~?{_Fr2 z0i=30_sBThP;b&iR|bzw^-j|WQj0x#WCKUI_5)CveW;^YrhTZFTRFBsyY9VYEi!0cX1SX-Qx^J_GOE8Wg}o`Komm&YKXIUco~qtDVHiIx&2AX96F6 zD8xr-iBQugO(7sYy)A%Eoe2JDGJ<6&hU-?_>%^o@ASnLkC<`yVr;7|`vyO)UIA!6? z5!7KVbVsoh$}#WT#zNoIjo{jcQ~2ukDr=U5k^Xge2%Fk5y#1&Snq+6v zthoNvy>}gQ`?beexa^(;K?fO#u6)G8Q%7Pru0|9@ah{kfgIcd&vNnR7?hS^VwPecj zZ@dlC95lI*cAeAWlt;UEaxL^9F@FT|If!~ZvOxeUyY8$7b;^}~0=Wn($X)gfJHy2f zamWj?aB9dtZdTq*IA6arfS*wBt~3YFd1DBm$sjA5B#9V2Edf(MK(O{3sViOlcb7 zn7TZ`=@S-C8$r`ID{6ZX3yEi+El6F65Xxrz(AW?-^BPll0ksfGlsM{YpD)tBp9R56 zPp`!?fPC{s+i3nCo@!3#T`tCQ1_TH*+%KMeetTsbj=;hs_A!BUuOnue!^Nw&Riq!V z1JF2{wkwaKa+L>y`uw8&VAq_O=Ti>^@U0!yCDS^4n;vXic;`J;c;AK=96cIB12s{_ z$YcrRbXM%+|GFuZEA18=)4M65Y>=>d^@ zaJ5N>%9wSLbL>m?$KZh+P$Z`3WnOFAk+jg?K|Tj}p;U(fNIM+yx(dgAyr{DEp^~#u zQTyoE{s_`zRk1KHo9sfZT0!))?8&^Y&u*pO}3NJa#%~rvpIe`um(Hx5Af7_YDm$!~Ae8Pj2?wSwb>ic5&;}bggM22jlLl+=j zhNAip(Ug7o!NiSuPvN1qfp6?ap}i_{A&VGIfw0iCRtbR2Ko(NjkZU2?BXw~hXUgwx;a{}tqux@TrAES#^%KKh26kmo71!d|}4DQX}ncwOpJ4$?lE>BIC4 zG+)&!#Rw8@bYk;$9Wu!*%K$N56y9+;2eWA~1u4m!lBr<8uU zs|KfT>%t3=acvlIQITUpw0qg%0H4`Xf$gyix1b*S{I(FDzosn@DbEo^ozYU*h@h(8 zj}K2OR$)Oc1m1KrMkl(0Oao;Zq|Riv*p^~~y6@0*F+FCi3?LQIwEp?-yOo(Va-b9u zt2(C-Tn1<#1rDW=Acm)5Veo_tfkb8#Cyf8;(J(VzwD!1q0#9F&xKJVKo)imEVm)7Y zpa%2Fp$#5Q|FF3MXRVroNi;_mDJ4Yjk;;U68eFtChHD-Qp=+e&?pMP;vp`CFq9reZ5-$75i(82lKeO0*Pls1AP;7(rFG5SjnPvG;L1J*k_ zeaq(`uEIx`&B%|bTdpB@JAvm;&LM!vxi>>uNPeeA%*~c&9Eh}*3iT@EkqpK}Ef8CZGy@9f2tM3CD#(8*yxCa>T#|$jCSB zs9;%$SN5IlA#B4cTve5ROrlW?9D#apO+A8bZqVr<7Saj!RQndRQD(*&N zbtQFsy#~CO+rR(@3f<0PzQU@9N}ujLgH>4P4mAKlzV;bK);VE03Pa>>7SC43eDCa_ z3%i{(yhP8bKlphl8ducw3A?#f9l!i^hlK~`4t~66?~{RVZmGlZD_hhr*s+jQ^QA|1 z@%}^U00t6S|NgEDym(!kf5-iJQtDA?;lzn<2B2Z*G7d045vZ6h)T!s0@oTaKCsZ*s zWIcqE4)PgD%@^fUbUo{zY?oX>7XpSo7ps4Vr{l*vSO{dkx_nlNQ&_lZI?ULIqCwPW z<&vX2F788~R|H)W&nPOt`|#L;=Q&ibdFwue^Zq0R&Y%j5pCx3h;lpCIs^W*7SvOaOlK}C9h2{o!0 z%-A3kc7|Ivf%meuve@eHeU&cSzYmXnD4o1~9%&N22=@-2)b)`6@y-gUr$`sEwvIx3 zb?j~$#WNOM2fsWrpTN&|HWsX?Lp8Nx3*UGUL3DCQ<|eX9R)puS?ZS1N0`6c&_x+Qd zRiIa}Na}?(M(zR%^Xs|SXVq# z6LHzB83pbxqUA7{g^ZifoWDTcWbZMM7hIIRRR3lcirxYHDadsYg!vIN6Lz{grIlAI z75uG&t<;r8b7ee8mG7ar?y#{AQ!!6(!(8)6axGNxxhDu^;R48K|s(%;LdA z;8;CMk!K_3%gXjmGaYGl%l*puL2~7H>_iN|*cm!z00vMRXbU|V zaRJbg9N&>9S<3RtRo%| zk&x3}_DkQ0EmHrC*CTY_ql^$!E4pX%oCSU2WR9H_?R?j@eMRhnQ4u~m|M)jwnzqeW zfbNRgQ4<~xF0uAwmUR(8wXEVdBRkV%1z^^E;da^GDfYG-q3ElwawB*MKyy+5UU^mA>6w)PPN@cb$lE#*{GgL?XQ>07 zmg6bU$LlyUv@H66+>UrAw6s)8b5Y0-keDz4omXb0ro9k^gSx1PT3Eq#3c98;%7RMa z#I+rG-*Yy>M}9a74-M0Wo(&VM$O^GyMH(>f5BIz11yIHwWfQhWzZ)B5d-$Y@dWa z@LR^}a7-;9|NcU(xEHiK@R8{@JT!2C?*303D;BO8uffVNJcje-gd#g@R^@Z!mW=?3~w77 zG%_gj#AHV$BpX=&*F-XXO&H|~UABHu5Q?xc#Xs4~Vb6FyjNpp%?uHF(ThOf0OG1Eq zLEtXWaNn7yGz@$Y z&-KP#^Dwhmk@gtU;dp&-pQ!VEjiJr}Lf3)-I=g9kUYxg0<#Up?x&b~E9hoSCdv=I} zFxKoH7*85;<%q7Q?y+qMk=5(CxNZf&<>%fDAG%=!+`WCQFNl2n6tL9uB~R8A07MuW z7>UlLJokN8A(eSr;JITyc!pOBZGHKf55jTlTF|J|tPJ4#7uB=qGu`L6?wx?GbJ_+b zDI~gf)+!r-$^a^Ar580qSnehdrvZdF%yk%q7c?q7On7~@0vDp#{Bmpf^xyjGxUl5!~3-jwyyi7fF!1kcJ$mS#yG!|4JEu@z<;1 z#8nSMEp)Y=QV6nxo>B*DGz1%e``UBvhxh->iLhtBtcesQrf}5UNV+uzkEp;5v@`}D z8cNOVpbbmLpY5w~u!T~eT~1xU2hQ2B3mOgT^Xxa$V%rq9qFFT7EO|H_3l~t>byLqcXT6o^F)(@bsFxo)Bqx5ddU9dPm+&m5!|I^lAOHUGgRGU82q0;1fr8Hq(o-=_lQMvM7^t5A zc(^#uNN52}Rl@{ce)fYA`f&A3p59}%Uq9PhrFQ=@Zf;G#!P_0qVeA>On>L0ZrTyevRa3?p0O6RGdV4Y1i zlZBcv9hi{@y$_5u$)n5q$h-$0B%WyrN9Nl&H7v1uePwgCrNx?(Kcq?4w?y zNOg=Q7a8pkn_qm40-onj4iAhQM(2sKB;&YfF6qT6am7viP`VanTKRPMYeNS?cejrA z#krS6uKn9@I0?S=;+tWNJDiaN5krVrI`zn5?HfbfvC`I3S^Ki5Z>RcxKicQCVR8n7 zDtKKsTJX?R1HO3M;gAbD8JDDO@S5a(>R8}`yskuE{g@ZDpOQ+1l|zBH2>eNwh?Qtt zg1RdS7aOsC42b|;6DAc>1!6u+`EjQAl*!Xmg**QAqjVAZ{z#agfRJQuTZ=G@4( zt+REw{+^X^*=hGnky;+m3d_c0IBL}_Y}&pbMyi-OcyICLE{Ate|G7Bz>?{^~r;nHDe{`f$`HX zBJ+jd;A<&?geGq$FpF7MWv1I8iv#KXa%8#?Wswa<0P#uT=i8RU%k2H`hrl&U<-TI9 zvrho(ss6nEHS#x?O<33eMbY6KcB=k-MNJmJID`4tr2ChoEee zHnG&uAUxOOE^YCsRTK1{k{TIlPKm^7#VGBk44{S3YZ^)I-t zK&xd2%Fb2@Kn;MS|E(W{Lagkd67r2Oh^!P?H`RjkR8|Hc5EV&GLR}B=hbEN$EqQ8* zG$59}#JG~klTRkk+m<>;hy&{wXo-{4TuB%@2J!(!*C-E2Qo0rzBW7uE0Y7MRzb4Q= z$u?u0oPCs$vO~UgbaMBQZM7!v**qN}RJ|gy@CIi2mtOCeui|l8<+ySz3N~xSuVg6o6 zyZ3sv+l+SypzV(N%50|@Z|7`OhJt81-@jDY3$RdOwReAXQ`8-RV~2s0$^3+AtU;fo zn0?!BBJ(QeeXJ{3213m?sd5iS8^kVBo+r^jZdj0^^Jr(dM7*%T6vv<}RbmlM9wCCI zG`ngpqIof)y`be1D0f9vW0CtSEVZ_pmO3FSSjW-{H7NqA_oo6B25Dn z8Aub|(UfjX-E_G+IwC)oy!h3BV~p%$pdS;Npy4JH$Xa#TEagv*g2zWMSBRyEfa&o52 z-D?-Dci(xL(Wksl)GH^dBP^_9i$YQV6gVf%m;L?uZ*ij!pwu^Ely~W#Ns{zA)bP|# z3)!H~RBXe%V1O8i?kh2#n06h>kcFaz=2DjVpy?^Jp_(v!25EeF5S3^gSd=EH0TL4x zTzocRq>b=62TB7mV8fu-gfRojgrP}TEKz1^dc$L2LAw$Yn83A3#(l-J_Yj&=IS~Ve zG+1%*_na2~AIvLPp;`!x2d13Y6_SX&^3`;v2T8Vv&lO-|=Q~?AQ)Th20=tUk_>AT?nml9usGeUD;~qVj$)a;5|9{&X#2Wq%Nrgx;r3`S$-!SD7VPrWpM6#Pd5P(}I>r3XR- z8OQ^PrItkoP(?&^nGL0!-0c6^gGQ}*^TIw}6OYe66tb8Hz{mZP>&Y#~Or%4|E2g%SXPA+pQ?BpN7{xu^;NqRQ-WBV}?R zOc-4!osWW=CBTvii*XwRsI*N?7?oA%F;E9f$(gQ#dT}u1xmG+vjO^{;+$|9Op-lpI2hbHgA>v?1ap_M|`v}?*BGHk*OJz1f=`kNCy z6mcW}rw4xW@~V=x~+vOqiaJ<3MFs zD;1z~AhCEYf{CPG%Cpkp2f5@ATkF{MCa611h_TqWj}Min_P40HfY7+*_eo&i(_ ztaxGba!o-ITx7mT1RK_iYQn(v`>=UdFkzXs*bQ+H6NzYz12F@|@;VAX4pb1(-fB;l zl%j_a=lX{>JP4wZ`!bLM=uVIc6)SNZ+l*(@)XnDn@S9^2kHr*hUW64Zz{atQu$CgxPIK^872J3bHt-sAmoynUk1$Yi6ZiMg z$x+JJ!T_R&t(uop*7)n#M_*8fpB?9|Le}-zdXZ>lAS-Q1ywbmrG}Al%1hfyRILh1C zG2a5Ew;A?hAW~hw!u$*#hZRTMsX9ARK}fs$cGQ$=AEGoXOXr0(T{PMijG2RsCJ-Rh zxr}Dxt2-$qf2@#@#R7Ft7Qd=7mZ)+$5*<4L{lE6E1lp3KJpbK&m-pVhnPHhFvZLl4 z9YBq1;!cc&9D~tl0vbI?7Lyn~CvI6dZsbTLW;_@YBOXvOkQ_ZIL_oj=+z22`;<%y6 z$c#ASFgP&Fd++Yusqe4)tNyOr_boH;&6d=entN~eecgSlzyAL3|Eub+p@okVw>=xa zp-WM4`a#{+Dga@-fLfQ(PL0S53jmSVzNsDG`)arIdKa}|!Yl*9wAD!&tP;a`6E;#n z7cwM8@bOhDFU&g>2a=sIW5~o%^!q(d@sc>el>5ZW-k3zBWu+L=K*5z41`_u~Tv6+B zbrY7~I-aUfC~@fY8b z^Yr`Oq*;u6Ly;v%1SI5djGMxR<-O9}ol!)5v%PW?CW zv+i+1TDGf)P`6XUNvMeIV61Q+&q7`!SLq*juZ^T|0viAQ*i;O@mTdK0;^7FPSlpZIl^^(u$$ii51Rmr=@GWsET7C=f+zGs@X9?(X5fI zaTI0Ej5D5=6Ft<-_BE7Ys6>5<2b1A+=G<9hHD62Gh&oJ7J7kTuJRdG@f;4S9k5w~J z+k*w*nW4maWilWt{7%mAtlG(XtHccCm+y=~B1?e!&s3BE)$4VL0xs^Qu#W1;t_(3@ zzaYy($Q6xs69~D2LOiG(Ok}er9jXE?_crw+x~@72N6c4pW~pV~VtNXaaO1n;p5`o~ zYc2&!6v?e0VQ!AiZffnmG;G~$KhgjQH*J3m=cw89HB!pzWkH6ADQh&?B4zdRXhmje ze$bk#FRbOk)0zc5wxp$Y!U8QUkvszx{w5b@(l4a%5-ro&MUXtU-D$3S?Z%6({yUd(l6&7(~P^Wj|WunPccjynb_UkgSb zrfllR6a$Sp%`+Exh?y#8AQs_CnCqhBc%B4=pZxu_pV4jP^p5}r`bW%Hr3oVno*2IP z1ylbL-|6>yHF#u&n<|^A6PL`GqaDBaE90T*1Y*NKS!tCCv#mhR_2N&^M1!QJ+<4P4 z{mcr)2tdq0&87oTn|zd-8OWHoIsgP<{bIzq7pHJQ4-8}gWI?hm>_f1yd6Zq8w~lbi zj&S|r#&Z=0Y8ta8f;rMWL+4FP?F0>E$4$9NjR@vK3+ULZ4%Lu`92%Io!_-G;0Swf4 zzkGwK&obbtAszrxs0CNaj5#V|1>{6^r-x;3o4z|k+*7R2Im;~|4I<Mi*do?I;;ph~%>1_Spv)dcKX%p1FsL40 ztZ_Dqq(_hJm*e<4ZXb&{0Fc6I6a;~RFwU1pPZK6l;YXKf7Ctk#Y9!xRcT@SKNp#9r7D z#&oPOgx@Q(6HK7FpX&L}U=~-ZsGG-tEs9oA;#j`Mw0?-`5k@DOp zpJdyKm4wxZIW_3fozszcE))rgCCN*G>UR0s#2S6gR6Geo9vI4O0Ky-j=k*(8^8?r5 z*5Su#+DnuqwQFviuy&kY|Lf17D{sAb&W3>vPf|Bb0RZLo z>3c(cVW|O7%{<+G2-6Y+i3x)yY0Y`QeJvOU$BbvZt!yX`1TXApXTqkUedDf9gaKvg z{PI8^xon)ye&I7iJ3%{|Td;F?mwvqQXA%3515rN2L_!L^F1zmn6lh^Qc|)reqExv< zS_i=pe!|ojVw*W|)-Ya#KN+SPr$NOtBOxB4SZKrdHqtB3vQGo0E=XoT^pQ88O}F2- znKu4>4o$+t6r6j?!&$*V-6005u^WFR)r!c3K?9i^TC0P}cFl1s7NKB~5*E=-o{_6d z%3he&Kw15<@}Ff*7f2j0iRifs@;F`bj@QxSRKhHT;d>97*Fo8JS1!Bu z!UR;%K5jwnVLQoMMtWVDKL535+r7sG*V40jeA??x!7P|AU@6fjG zJ85$H5e7oWBTDVsW0T9q=-T(aiO%`^%jgH2e?Izt@K8-R%>{8F#B(^n7Db{VqTxO# zWd=eBx@NZ9c~?7VsS5}6SObll>xl!2W9C|pvYEhY+e`aW_EwwD8;M`wI_r`* zokM3m_nG{e0WrWDYPoS>a{BVEcUEd2dxwJA5>VxAzDmKvkCTlEUYo{VdpJk!kI`gTcMLvb?Wyesb>FF6-|KZDN{YB^Vnyb8wkXt(Qw3Cmg zZ-4Z?^x1FTOV{7Afwt^;j6FA9o_Nd}FPQzYBk-W)$qIABR#US!powKj+VBW(pO~1W z#>84aU!S(jdP5eY^dUd$hVjNGY5B@B1udIAlCK3!+RbB6G_j+=xozz4n;ajb=RE1; z(zh`5Wu-A$&p-8v^oBE@N5>q^qmL~B%yE0B{bpJ%`o?W{(;eUXpNL_c-pQ5b;qHo@ zws3ST6FRgR?yg3jm~}jYv_Xa zujhW8m!I`AYEG;&Pgai}-L=F)L{oO|+)eFXgI3m8(yFyb&?|rEWpv#QH`B!UIGuUs znN%M?n%eCSZQVAvIbC<+F?8o8=g;~b8Te(v)1#IF&rcuPY989lhE0_g%ee)QcIVse z89tP&*P05^*i+jU1Rzua5LEajroImc!djXzV=0?4AUd)6i?v54hj0UfKJu|E>7jsetY*shH<4e^b5y7H(7%kvNzlk#7;-%Y>rw5J-e+@Ke~=mm7@DeIV$*Q{Q} zjoa>Y>9*VNn)CSkTCfzujW^#yuRZ70hW#e!9dCQHdH*V!oSdW;E0*)MOi#Dy<}ZD9 z_;;0JYopsjPdn!!H*mXq1Dcs>(W8$(Mj!mp#~8@v9)n@AEUYDQpFBjKF<$r@3hYy9 zzEEZxA){kLFk@GndakLD9}N0UP#roa!^OD8JT!pZS`2uMFS;~BJWH7` zfJ6&jY3jRi&`zh7VVqE^SjWbN$S!l5CTiVs8+*VL^6m>RrnkK5Tsq?=FECH#GHL=u zh!UsH_K#|}!h)gQ>2PB`y#0~VZqWC8F1UzZ_uAj1GhTW+EuWm^?YsBvp=++afj)c9 z^|RiP#VpVv7hJfW{``$^pl3hp>BihQ85rBl4Zile8|lWIzB2ST4{Y2-t5zMw%$(=- zIT=VECTq6Z^n?3;O#gWCWpw`o56%%Z%!fWBXz2RAaG!x}+~*F>!9FPSn0e125&$8j z{YR!AXBa5*$dE-o;rU2*{e`li7E?B5MD<1MFQxyy?#uKi=bcSYIqgX_Zp32E4S6js zqYyP3p0uf{X}bOkx6qcY+lRgbZE^W0uA)y}`58L#gyVSI&$evk?fbjHlw5ex#cZ~I zY3&-m(VKtz&=7MJveTF6WFM6I0!XwF4h#mm41e2hO=p=~-IwPelB8ct zc;LJ@WkncAEm!ix`!~`%{`Ld3cFoZSL{Fw;jJE~WXf`Ax#qh=8CVcmMKca8me$T8I zjqg3M@xeKM+WslKckkgkSJ||A3%&FF57Ox`{#BY>Ho@^7l+v983meB^YHEfaeQXzf z|GxWai0UrKKO5wsh_2JvJc99MhV9(guv3Rrk-IgAnUeU zG&X?@SV{28yeUg~ewVdicEFg%1q_@fUh=vE6BRD+ft~Nwn&JLBVVd`tx|1wfR7RfTvJe1?7D8a@ zpG|#{sadz%uGNeg!z#i^@n!6N6(XR)NG?G`aP4K{U`{d)I*dh|KZy7nhY^tW z7Ccq49nm^@#6GzL5j0P`#htk#oUb4|X>iu4b5&W$OsP07wAIwNoBByp(_Xh-#}eW7 zdctnJP0y11Nv4HNak8}JteEXD(`$GBUjxznOcp7ZoIlJWnseKsDoo_qCs+GAt!eBR z)AbXwWB^i_m*=I+LZ{m4J~Eu}Tc-Y-sppY3Wm&pxB@dt%Hy&PV$`dRMpsaoKqB?Q8 z$f`iZ%rpy#Y?~h1K$*nhKtM|)RF50Txr^BO(n}FSMcf`c3 zztNhRO2#Lavv6jQEDxqka#q<_0S?(ix&-4|ShLXf9{%h=l0Kw^6-e_34u8vm#hH5l zIzsn|#u43t`icv}T|x{2 zBy3ZZz!P*&!J`9=HWkr~d$oxyctA(8|t`^GnKtrAuNkmx*=2Cx` zV$nWC|?e;WQK4m5XP^_?aA)!1`7A$BY zo`2?sci3o=iy2V>l#D_06B_pdO#2euUN~U1w4gdkMK#I=rXFePOYs3rgl{&--0aDu-~??*U(2ZT z|M5=p>Fy|Tv+WglC;-MjrHYaK6)X%8dk=;Moh1T}Y*TC_W~`)pqA|<@rd(+t_ZgbU znyrDvdT?JLDej~Io_*3ix-3@CF1Zj9)hM`w@XY?s)ZfL1PG^Sa*f++^foNy0oQGF9MAD0VfvB6tRvrSw-|7aUSQY3wR)sVYa|LEh@X~ zi#Er5v9ktNfe70$2n{m`|CH>Y{CbfOlCg2bi87UR-GT|dXer|$VKyp68+kA}z0RWA zFAzlmA`U*zYvXS)TiMI;#&8Hr3jtSA@iqV^ybOcJZ!H0b)cf+VQ*p_4SA`bLh%mQp z|CVS`2xSOn(LCB+U(^HvOObX`gQ!py!v%gy)}*07ranErE18&B!PAa&$2h^H6+Eny z*@!fI2zV5b{Ao1+>}S8%hX52i$&L3|(yU$uregSA6^;~W{HIc2P=h7*wuZ+n=P`(K z8IEfEwp{mw47UMz&U3G^=2`UY5=)i0R6vB%r2rxfmqZ8NKbX4GG(>%RYG>LUTgGla z@*{FOy|i#+f-M_aQzqLIUs>}ALy4d70Wu6GqKKW;?jizAQUX&r%BWA8da9VlzK_7; z_JvN>!h@qq`{aI~jq!9^=oszd4`mLYM3@oh5m#N$>ss`(l!{3&Y zHnon~u1IJ!j;@b1bNa_7Qb>8J@6-zG#J<-c(rm=g@XQp0sjQ$RLwbOXy3Ax4P)f)Q z{Yd+!81cFA;41uU;a~0B+?lYlw-pQL!U3#j+NwP>C3;8L1$|o*VU{4FP6)LmWvMeK z%Y}zrJcRbk(!BdC6Rr%vhl|7Xe76S8bxoQRXsuA_f`^co?c1NhquXM*lF` zX~^i?lH7TV66cw#?4R|IY|*LFP%#1yUIK>2{K%2a8f z03_008d=ko__$Coafz6ugxokI^6??RaImPJWFBoEF_Z?2ed~UrV+DX?}YQ9K2pf)?|Uf0UYh3FR9+CD*GEiC|V0?CPD+;V(bj~2GQ6b=XknFrI%&HU*EB3PXD|k>21b|RRnKNOe^`?H_)IT%z zWn`}tjFj~ZkZN_qNQRX%ET@zt-ZMifv5_}x6#COVGx4|G-^gVtP|@(+DB@2Zbjs7> zmpH;ahj`AzG#i(nN7@4wbJbz4FpUa1VMHXyT4%q52jxHkNXjTSLgQe;7xd%6-G@GU zfvJ7d;B};z0gwP7uo7m2l!Y`79kY_%CVgifNEDl~;gDkaf`gZ(1^7^;xk5OXHIUUi zNN8z(i!hlm4%z|Q22;qdB0HsA14k$PJ|Mu6a-ac(5;KuzBQzK$F=EU$1ok{#Ykx6`^kDHcxnNrSDX4uvfU$ooHopysk+ydS5M4%Db38L*~4qJfLrW=#RA#_ z#SR_i>N2Of6uG4SOq(Y;48j&LQ^+)h=%6r-Hd6;RaHJf}078+*!~N9yNX%NzRiq*Q zAldlHDW)Q0=d;M_rezem@p&4oVF<2R6@en!8S>z$tZTfBUVWWaP65g$va?oDF`M^c z3SbeY(V5^6df-So*a0L(8d4fKq0s06Mdm7g4{i|p+MtzS@=hZ=bN1<`t|JR5qB3v< zNJdX_jv6^(+@-JIR|#494A&d$t-(BDK-a?*z#`oc11d93KH``MbvaZ3q)Ys?<0cD` zG?=79eBb5X15lVTa+0aXy81X%*P6PVEVxiHAwbwIrEt?xA#kT%#jM<|svNkl9XgaV-x#HWpXcx-RP4 zW$lG(K~{PZ5=bJXx0_r3ddj}vlylDi-y6scNp_O&{_UJOGw*r7GT+Rc0RMkQ6~ycN zTrBAnk`#VBs3kav*K)v!`1aaC;k~>LP-aj#B}#>|l5#xd@%H^9%6v+-aGomygn+Y+vXSxt(s=7Y_^K3l=)EvrtD6gYbg0SS3D>Vcele|prG(ZkCs0^sdEK4nB_BFL? znkLz+ur(}IkW?bC-EMlFPV341;fXx@M#@E$dtiVQI~PBe&OTPaME%4@iFbP?<^7Ze z1j?^eYRV5v1|W*3%&P%wGQ;eDOpYpYcEQ;+D*|33z$GcKlOSRJ!am+4)owLPop$pS zp7c}7mnoN1wo_Vo3~bw1z!Y*;EvI~#avBX(P^k{6(5fB{x&h0op#jY@3k8Rzn=W9|elhHWl$TOI!Hy4BYlBK`hy%)V zAwm%_3l>yv;B#gv_cG~X4lDhlQYejLngeISOB1O-$$-<>lkWt$vM26YH7CZ(^svG* ziTf_5?1E9zy#{SMfH|;c!Wi~i$*=H)&~hvXn@K96Oepk+^sk4 z1*#V$xdTUsK2E3vII0w2MHpZf8S2~PXvZk>^;yHn+3?|26hmbMLIA(YTU%LcvG>UO!qm5rR+qkh6 zX;)_g-@*k1P}V6RdRh45el!QtgNpt0Tg~xj@Z%+vf3#qA@CYHtqX8yymV^#Zqg+N? zMMpTOGO)DW3Ua$O1+$8(_l$CYbB1a-x>`n6K+^G9j~~>nZCYo|v%Zv$AV;&jWAckrhETev!SsA-*j8$M(kyVGV!5^4e!zlw*-Ql3%DgR8p`(-!54N-zS z;uM?G-kUp{haJruax&#Pp@$QF0;r4}$!q@$!Fx%wg-dr%;Qq*Jle<|4Ep6AltV@`Zluw^f$u-sQaFrEKCA5GjkI~Q zdA$MFcVfMUFO))je8(7m+;rqny(Uwxox$a2?<6d#)&|u0*hq76CcMcC=P&)=-D?9%Bu@3&&?ed}oio=;R%OT!M!+&DnBe&Ll z_f@%U5wMydf04%iDgxd~6h704@k8E#uk%_*24oxC^y=Al46#=q+|jh|PTE!&Gw5)G zZBGr;^zh10z%WT1fzKH2WiTnCU%`wyd%WgUfH^}iI=!e`tt&m;$%0Y=$)D^A3t*jE z_i^sr3hJRWF2zj@3*Io&!OkQ`NT8lx@^Hb7DxMJf8l5PO7(%3ott1vPiQq&8a0xY5 z5R{M5X1y&AaQRpp-)%GSAqsRJ2CM zqMq2Znhc{ts-}zrL^VY%dsx;Fn2WiNq`XP84myWb81kqo!iS=nc1CCstRX`2u{kBYpeB|oAxmOPf-$PxtrHP$ZzkB-O&NQoItCk{fuBc^j;?q( zfj40`?O~a1S;QN0IaTqjwuiTGYhwc=`FHJ3FhLD??SO|$nBwetWjw%oe>R$AT~jVH zP7KTRM?DS|GKgyGOozu%<8l1yH8A63H*YW{V2)jq{HxM~C>Oc+s$szs7Qo`j1OL3l z!|VaMkuh#+C3y4p1TCu4k|4ns7lt^z#CR(0piO2xNY%P(dyH$xBtV^NXk;ge2u4YD zaZ+93tT`Tz9Sn4caom8yk7^;#-AGmEwJzL6^`d&edImc|(EeeukJS$*c%YT@L0Oh* zuw-E1`Xz&SPcj@8P%Zr=iDlR%gLRj+odlR;*Fwtk8K8>!njDb2;g0I{Lh|`d^AoJ3 z0r3dVCSKy4&AaNI@bzUr&R!qkrUs2Jqp6j2SI1XU?M2_aIK?|3^bjSfYiJIg9cyZa*^Gzu zSMq5b1;_yXd6aLUkP0dSCS;YkON?H%T&dGy8-@#Dj2FM}9IGmv}kt{6YxX$*XZnI3=IoA0@_km+YwDucnSuuPJo-$!9^ zUIfgA60eXPyqlnyG~B`Dz*Ih;wWxzS8xOMRr@Ixd85O=q_~6n6hYyljWZDig>w+~t zZX18>nen?C9?o0M;m46}EFJVQL?g<^nz4Fvh9lX-hKhgl?^odmp;Il?3j)Q?6@HYweu9d zwngDfTLQdcL4sN-!P1#2jvwmck0aq8SeA=ulVm$enq?ExSkX8c0f;4xUF+S9j|{NA zuvE$?EmTnzt=;MyAV1@f4k}eLmfq_RqdxvTA=D+CR(6qu2@QOV>vp90$<~4W29_&# zj)nNohq`$4stygkk9Sc0|7W|xP6oH%*{N{$0;(n*&?)m7{O&gKoB_?UEOjKS#8K|xUOzfAQvZw7*xfk?$CM;C{_>5(CDZ8NwVk z8(2_@@DOd>b-MyA8=?lN7{||dgcFeeRs|_;)}cazRWl+CR7D*-*xXTAJz741P?<6w`M6`3!ZT-d5RhfZ4|nnK z#t=8tJD$CY8sK$t*zn}Mptb#)XuT{mDoW6(mffRZ0MiU}GuNw>EDGtLWGzfu6yb8L zTQZ9wO4y;l^>E)Kal!q9-ZoF~eQkLu?!u zHIE1w85Rm)Z8R-lPY*r3eP$i^GTIkSc;iqF>lw`crZc^@A;OJIX4Oz)oOE-$i)Ym; zsMCy`H_}8y01MhWSgO*7y1xA4;Tm4Fs!dh(Fehx{yyvXNM}Bb_?%Cv=Q6U%CH*q5* za@|J3=nn$9sug1{H3b7+d(vO9aBc)YEOQ`T!NtE_ir=jpl+2b^6OJ(ST}~36NzvuF za-eIvV8=Md*tTEJpMl3Q6qznyi+#lbw~s4#H}Op}^kW3?1lAqc2@kn$D)%kO^N=6A zX)%7V_7Gh1%yn2apfKb&@Rnz;$NRqXc-q-w2B9YqKmij+D&^v^QsMAPrTFcffi_N9 zIR?L?5b^YP{o--Bb#slOL{}&KM8#5i%!GDB0CPEEpPsAhN7>rSHsAZ+^cT=bN+xC? z+OQdy2Ezpz;OSJPGNE`Z*O&a3z3I}X+qMnj%|Aa1-+1xum{IYu@Q^N!S~QB=|1y+$ zN~HbT)iY++!-Syc_q`}9=1-tRf05GA-@KuMTepZUh;$Y~PoT==so0UQtqpQ&0gJ;3 z-AbATicy8C)82=rS_C<&wh59sH!jf|S7XxyUMmSj#A~$XM(KUzB*XC1Q} zKArc<1>+j9y(}6d2bkRS6q6@ObSbW)8K}i%7u!~<;NiwBos5%TV@hCCnyFu==j1_Y zD`S|pX*9&Cow13^ZLL1jT@NEUtGLqSO)h?>q*nzKj?XvPy2OUYvr4U`7JFRl=h#hiyxHXm*!B|GQSRYYh~<}+JUSW)cAV> zKc%PgD;BVrtP&516QAlPoqvoTOx!tLtIJ>X(Ko44~y^=&3ye zngh(^d472|w+{nyUQ6mp0+XuIHcYEJEW-v2 zU?nk?3A!>(r#x#q*gi7eFU{6tOBLj+obr{9`66TiE=ghsz!n%MtUt)No02#_)GV@c z=jFh;WksH5*i79h@|q2j2@21|55^sufS5QSHlUAT zYl!BFW+`{RhSagsIyY^T{PM826N-&(s|n(knu$?@CMuJLyvyYo1~A_&m)EwY(#Slw zyidaE56X>7@?75popGs6@7u7Ud8pORFPHeXMSz-sWmwABmdvLHqLS=`sAM3jJeDp0 zL^^q;c5DFKOc{Y?SYiRYR}SSs)2@ThmqY4WltJGBHe26C704ZIf%h9#c0?t{TX|x` z1XOMGWl_wiWkNG^w-H$(AWj=Fwj*LaSyiRIwF*m-_$VqkrMP0gn>JvcsVA}KFRND>8|H;GDHV!u;&O3D1k2en zWxZsg#OQBB!P49Q+V%f3nH^{Zt@SLKZUOl=1|Z8TNZ zC`s*8nBK&Uf+G%G_N+8XFRHd`n%~Coo{D7Dm3?6m8<-0S5ag(tg2Hy(yoZ# zwJgi@P*1m?UQ$Im{6Q8Cm_8;JEWs&?bUH{Xmt_N82N*11vJCs$DC)jgwxh@ivT4Aq zgEvdNb&|~!x{FFOI1P5YZdlqI^u5rjWIKz){PH}M1{Y{AC#5vFimEgHp^?;A8a$lW zF!co0h5QUd|ICKf*hYFt0n76XIR+S~CC7LK%&%oY_3ti`*Ql%2ag=fs`li|3QpKO} z$QQ`o9a((hknYiIX_UT;6$D|e*cA<0hl9OD+c04m0rQOe_bYP{4$S0H56X4a3}%W3c1HYWiCI916Z3eEGX59U7KJ98M9pRB=DBDDH6u7_UtUl!AkUE zVlqqs`wa}4w9#~c$q!xo8nB0_4;bsy=Y}oa55F>x_0FaJT4&?1S@6nr0@|1amXw8a zhIQK%**`kb%dR9bK*?*jD@!UgNHX)CEfy}0ON4TwV?XS6JLe=xn1oPV4mkx@(`-dO8$Jk{u~Y*#_yyZ_fHEj zsV}5DdnMUxx`8S=pY^VQ-ZtqOzyj*17miN?t4c@pnrEM^O-kGz$C|ZUb5AP{p zg`dXyEjd#>iFHIZ;wHhfDoKWw239~R0#(b5q9n#!5?nE+eGte&Maf>8LC8GVbu3v#*mS#rwEuJ zD0WO-=pQ@n=EgN{ z>6$+2C3w6kk6*F~KfL(u`nwY2$|oJZ48t?(dZ;r#-oR(B{2E3_$1-*q&|>|2yQOmx zLec+5-`f5DR6%SJy@JS}C|l@cU8~vXluvNpdG~$z;6<0=!xy~=!^5*tp^N{k(AS_@4zp9eZ#Z?+qh{9Zus3TIPKKu;*8Tzqpj)U zGgo{*GulxcYo^Q^tl|YH9c$ZS0@6;WL$Jp2*(<+_A71^-Oa-;r^XTwIq|x7`h>^|I zaVo$Pi>U0MzLqR|Zn<3bvW-qMV72M%-(2^5y#6ik!<){2Ii7a%lc=8cEHj}i;>EXb z--$c!x-SFh+;_Ye&pzep`uyu}_&v64-8OB&qa{8sEn0v|*#@FE*}Y-oR*dW( z%_`oxa~Hnzy{q>Lq+*F^gRZ&u#!26gjZfg5zq=62mo2v0U-NyJHfi(bZP+!kI}aa{ z$?T$`r$$85|4+m0Kk8xEo&a+!6KebiLyUKJ!?rJ5=;EYzsixj6%8O4lnpnMN9rF1C zQ}P|R2d^}nE!=(2>Ynd~Pytx>GEu?2-N?<W&(3IKOlSvMmDF+)gca2`KY-*}F+(~jzC?GM`)J{pf zBEtyj)#qRsw#Ti2kCafC6NHw(qkNbbxTw_}4`sih11j&n-OHm)q;E-BoT54)z)cO5 zX;1%fC$V6~v168bPm?2n30M-KitQ53+T(QAqs>6bi9*vaP!`c^{;lWvdS^)9G%3a- zDYrExvA|)^Y;$E_z7ThXZ~cH1~dm#RhSRmGjRIXJ%Cp{eoyCSdAmYNsu;m%YG?tI z)Xl{Bs2seBXUkIuB&)h#Hkdll#1Km8{9m$@RW@fDrsW&f?fkQuc(aRG+(I=6R=+3R zD=;3RX>RTzXI6qKy`c^rjLnjPbz@u`c^S{W1!l~s7yUlk758>hFtHU%WR_VX}9}Y!b*fZ}kAD?D8UK1N#>`ek1cFAx@B6u5=%J}dzFy63d)760l49HX3 zI^hU2N$^40oGJSa%hj4!l8miY&BUr@@5ZjaHMCNcn$nu(PQ?YIW_Bm(V^G9?L!!m)4dGuY=k4`&~BpqE=)4NZCSHF4vT7bka{=(pzyH zV3Qw-CFRk=ik@k|!t72lQ!2?nu~jxjBTV`f<*P7W-g(05&%W5QKQ{ivg@(wsUt!y8 zs19#s__4U%nvnhCN;A$c?&R$QO=?nmF#1IUUjQp(St6lAN2ir-rc~5Vw(W@ux<|!` zMm**x)$OmHQZ7~#11jLjPFC5R`PaPjui_nl3MJqm!3)EZ3iPvj!k)e2IlELJV!cs) z<_&3|B={tgQnJd;t*(XHuPwQ%Sw6ftlTrSNH5g_cXfw0+cHU>M5IU zpR_bu7$*~>y|P0ei0N zfqsswW1Is{Y>UJ}zXh@<#K1^@s67{VYS002|=Nkln z3A`Oundf(Ex!YTkysRW41TbMA0TC5NpmkV88%1sHRzXC^aTjeH7i_idR$99qai;Z$ z+fTb)N5^*DmuYldu)9SCL12(Y0$IuS-phM;tEw~Ka?Yt!b(i-Nl913RIj`=my7$(p zbN==J{@-_&ir_zPQUkWbUiV9Sj@N^Z|A$D?BVB-ln0z3WUk}<>{$4qiL8SZS^8S9` zd(@^!Gr;-*Cg}4z7_fg3_$ZZG$fZ4|O0cF|90xCA!QOr-m{AA4MVP0PPep!F@o=gRxf1H2+h(PuW%s<3@HweJ2`L%3k8LyuiJ>=8j02{+^ZyKimSqUGjJ7FcX{ypR zQ3$}G^i`|D&M^74m~Y4YJc|8>6Z6Y4AL)HQGOrbxw%Cb5pv#`IU|BEYCvNQ$7N*zDHKa!JxrNzzqGw`0q# z&6Z626ERf&SxwOtc4)JdND{C zpzsLM8%)bEv8D*c*4hggQNCT|QTE502Q@9@UUxUPzlsOjC z%SyGz;9`#tf+C;{Xe_9d$6!T3d7p75@&HxPV}lce$j2yw#h%XTFwK^e-+9226U&tm=z=1*YWWsVWZtTXV4v)LmFFdtYp7+N)3s=POV zc!f5u6(C3yX%+$(d4Z~A0+nGf{_zsneBITn!)$SUe3dm=sq|ix`5KJ>>YT|0FyOt` z?l=R}fKFSm`RrzL5RPP<8tfvpp&xBr6q`ItY3`SX~YAg1$>V6#UGV7|vk znLtI1MIi^$S#QQX0=YRSQHNe)&ODeGgWRb&3pUC-`xYc0ZH4#&JfBD zaVO0|0g#wMZwtOjgxq8*3pxRo>nY$&Ub14-iEPFi4dezT5K$M{7eGMvzrcCcoD);x zg9@^P39x=1qGhe~F<%YRKgh=sOsaxj6`MnqYb*|q5-0)-rl1I}9#HuL-@-|Gcv2-S z^+8i#vsrGV9F|(pB=4<9X+dX!q~vqLRKCf~Su7X-s=#L2fQ}fQyykYh1>H`|IzE)& zi}@)aVU4@YzOptuC?Gpn0Q0>>pS7rC`28TU9(yOMIIcpiJ|g{qo+)&%gTOIW4}10V z8u?{86w!~x{1O-U^uo@ltXyDuN0irk+U?O)1`{W9pi;)Gbj&5+aeB;|%d7-6#ql$R zOjf6O8Un33yC=Y7md!52{6`>i;|CxnvjsVuS(AA{)4>3k1(#(m0v53r#kJ@TI}37i zspzgG=Hh-q@W508T2a^+wU}e7exT|Bl+R{h(Yvl_2awGU zG{AgtSuc^mMf6QEoV8%I7HTY2YZSGX53ay5RnQA8Uw2vdDuHSsWAy-O9s++r((bo( z>6dijFL?Vc*jQ^R10ZEW8uU7_o~OrNN?r%lu^!mKtz1=OWDGi;7POkA&2-h&|NB5j zlu(^-0bw#q*2@GqKp;Di0JD0_g3IcyOE6yxxk(JjL#&~YFS1q{WMvjBvz97?sQ{{e zfGPp0An^O{7@TaEkuUUZptazYYR7a8FUNWoemRqg`OQtGjg29`%vKw4DQ7Y>;Dc-d z!Z1dYIju}@00D_~E)A%pzT55Qq-`iV0Q((e2MS`icB~;;sTpHK_b0IMGkZL|=vJ^nf$jFWSJY zHK!)iZE2W|e@_s{rjqK7!A2$&wHcet<_tOjZEG@V#Ku=KUxWE)kb{iU0Rh>80GJOh z%UsmmLm`NZAO{yTM#poL7+5nH4Afoy%_8KX2>>qyp`gH0eF3#EhHyTYUwo;GXmciy z5(|Hi*@|DbKvTvuEru!=lYTows-23HwmCIk`qppt$@5&1>NDg()(%aK)Z6jUBVlksKr-HeZC1h$OtTL&`gVxwOWkg@n*iurSx z-vYv9KFIc4oNRvq%x5mTLx{DgtA{$OpKZ<)QS_O-oV>fo4pS})`@j?S!dhj34PY!! z;O{M{2>nMz0h}0zaBLjFiZJB&;|Qvl!0-K{yVbE_XR!r0B^hkQydg>9&MszrJ)`H_ zLcV^q0$*9Ym98Q;waWdzVKc{I+P%y)+W_7Y(#V;jdT!24;b}Cr6Lm3N4>D5aUm-8D zAN7A&X@3LE*Ik!^fQ!u{gNv9;g+(#%7|Um{KC!N{IqVmP*b89Mr-G6638)!=6r!WSDqv+rMU%G;`VgR#AMwxg%-ZPy`8|X+g-Q`w|A%7*_}B{ z+pG!w?iL2lVElP_e}Y#Qof%xT{30EgR%Z}M@N*)v5bw&s$&dg=q-1jV_U1;e~A zX@3B$+^^Wcd;#V+nct7_q7-Q^1{bkG!7L6F_9_RN16U3u0h{|XuY#^aR(djm>GVnj z$5&&h5vcI{JPyl%irLD7*?hPB>$5pcii zq-bJwpI@{o*J=fzDV-}z|LZDnUZrA@A8-hRsV5LNW9)e*(^`_*nd!iQjE00c-oJ(tmkMM%kvib)n1rGD#YU}A-rs?3dbR^sx&Ow z16B`HSyMO+P(8Jfn@>0Pi*%5=A*f|yd{5`C;g{Y|a5S3129}{>fdw9@ zh5$;uL(Ls_ZEZ&J8IQ51gsQ7b*wKuJEI1w(aJDOh;$0EGr*e>4t|Q1^g$?|tV+nkH zPYb>@(}ih0PoP;xzz1F-bdlxnjRqtCl1(JYVIG%>z^=qq^eHoFw`c9FCXzN&n7;(Y zNhJFOvV8?uKi%~nkluUlTdbRYMHk;9Qy8d{Sq$9V#J~fTZyxv7Bp!($6uXVk0E zz(({L%V(-GsK}JjDkeWoG~9$<;9Xexcc3``gVi$w(qjCb6G8>+@Y+tr?YVgVRRPm? z58%i{3Eu1nPL|b9MA$I`01Sj zC;T%d0Hiq$X_nAL6FZxU+LD$zu%2akWuSSdeF2zlCm&$vfb=o)4NGIVc*>Xwq87S+ zRM9Mg2c|N6<=~p1NxT?!*6S8FU>qB)Zp>lmhZsu0B8{{GtL%SvQ}|&wgPYUL^qZ9H za8R8p*nx+BmPo&g-FGy$;vXTSh9no{ejMh0+ijzV~Br3D|HY`OlM zLFc8Pl<{P?{}OVo=#XYiC7VcNF6kzxtqBLQF2lpr7d(^fR%W;lou1-jFM{HNEql#k zdke6Bx{CsKZ!`O2@=&U^5ohu&gZTkfU(^Tt0bDDvhOR)?x^SchqX@2w@eD0%`M@Gj z-Hy!kLu9G%Ah3R!W^6w4YAe0=Hi5U-eZ()Dd3_%?$*0m3KGjKJ4W9QR1loCVz@V#n zWwCcEH8gp#RPJZs_Azk;pPXvK&pYXm)>H;hK9iMVXY|6YS|AEv<)r3BKbuedM^*<# zKYfcpb{fdUUy?oLasBfK*}ekIG8ah^8Zfw43Z+!NKH@w@-UbF_-jEv~WCOvatTr0X zT4!Lxy=&13EUv^bLg4ZQd>L5xpqY4gD}irzjdqn_mhl9&8DFfqtHoZZq`?eke@4O- z%Cv$J)_Cp>xVgTa!Sx*!r&zXYYB5}lWiGH9byVYPV%rdP*@?LQ>q-P)n4E=Aptm^; zWVtTO!Q}`nx0ztsOzOjQwHbn=v1PQIJWCpnXZkIvI;aPRrpJ8^`mgviXWAQp^)rpn z0+|rXZBwf^crlS&N0mmT`Xf=nf4y(uqNw{jCmOJfUmWJ93BrLu&zOG?JEeZ z#EoqiAcY1{L7~1hN5JVKkHi~fF>1z9{9VelZx4Q#GKup0kO60bY)CP-#ULZ609!pZ z4f=ch{N*zlYPJwQ(um+pc7RM-wa&*c;eNV~7cFS;_Tw{c{GB1qVgs<+24Ks*0QOa2 zSTFM&DBDg_ev&sdsU%Ihgbe}}StsgB@+bDro5l7PVE#1zwV2-lCJsh@U6fsnEAV4o zK`B(>4Z$f3dh039;Uzv7-TW)Z>&#qrV-g)h8g|%?E&NYtrasb6U@HPau~(NlP+1R* zwoC9kC!q#916lAi+^)v*t_}rQ!%`Q2b2Bo~W<1wzNrDbS%G-Ns?Fp_hwo}|)BVbc- zhtA;Q=@gz@3E?&9<(?AeWwu^!U|9Ii7AWqz`didx4-E`wSSHgslNG`l;!$!a!>N1; zRahpY`Vc*)*;LLBy` zS{2Sg(-icaO_#!%eJT)7c_ada@|kF2US{^+gBt8}?KXU+)q(BotSQHeWJe#;ySLLXaWXG>hDhf}NLFIbVR;2^)4y{m~$k3OEEr!$DM`Q`%#z z5|9d@>L=*uFpE#Ah4A7U z4*UU+ePA?(aZjOBXn{g3b*%a0g9`kmPT?IpJJ22&FZ0c%=UsxWA29Ew-eoMB(PON` z0@o!)9pjyLi`F@_R_m=G{mXWC(!3^(%m-j)_M(^<(Io*bq*#|VjbOdSo*~ZxfESh! zpyj4;zTi5m9>P18)Lef7qvE_@dOMy9alyg<3xh<0B(f-Eyq zMe}D3Waf?Y^9S&mJ#G?4Z>3mpc^C&j5KNm<{PW7O3S2%~fv-0^@R8{@{DB0OxzS?Q zfeM)}gP)^DynaCpPpX)D!0QS*I14a>^>`+F%2(n05AN#Bp~>W0teJFz-!C`>rf)LK zb(m)=l{J-nCapB0h5cyZ8S1n20XCGy<^q@nmoMz;BxjiKDjrL3huIWNW6o9uljy@r zK@P4V*rMY{KHh)USim`UG*X6M{Ron|kBe)dJ`)AQ~ ze_(eTX3VGPJH%)YA!`0~7n;tA7@kUj!Q;lbc`UMQ$nuU_>6(YJ;NucHtF@^3$St_d{=2Qr|Qm2J-<%eT^>%13X%M zgqEN5ObJ>mzev9CW?Dg_k zb%~t+)kGCuI&Kz!1KDp`it0bjq*FSIoN@}KZ$uNrTq%V@sh_8 z?j0xDQ>~vZr z_z5dy1ZW5rt%KoD;#DvJWWxaFZc@nE>$xD4WqHCzrPhFQl)gOt#@hSbhUB`-?Hd9v z8bx}|as`h;6H`as6-m#Mn>evC(eP*6Qn)wCB-G%-5E-0_jd;yO4Nj|uj(ud>mtiWb znr;SwRJ%f)fiQwW!4h|X>QNiOQ zDuh=rk72{cF8r=N9As7q6t$yY_vUkkxdJoa(uPiGOCW2v9E*|Qr;)J!nq&i6Y(6H^ zYA*s9i9G2JLJ^p+aatOI+K3^(1BW)ZC$ zu$n3VWEOvXR|;$5E?m4o!37HfbhE9eORQ#r#x2P%_}iC7a02qeRa?6<^3*Uh-7Z({ zNZ<$*V5im0$tcW%76pyxu;UZf4(06w-I;LE6|E*NjaB@+QGC04`IRmk%V%upc{3w-v%04CxH zuHKTsCImdksgOyU_^Fp{K_*+F;OvH}7o@R}hyS3birV2-%QCoboAkE!2brhxdrYNr zbq%I{EU2U>#t44gQiQ+NoUwxc3Xtf&UBSm+zO#@90nAd11sGl4tHGGeY9(Kvt2lnX z2&#ff^g|MJ=q~y`-KAHp%wQbNTLs}A8WTcZ9bL)mHv{~F#?UZpi8Djc`_Re=E?FR< zhm3)jL5ioL2tJv@*))OQO(*aPY?RwuVc+xbue1|S{KZ`gzBP&BeQ^h#wHO&qp>S5E zp+oro?6Cm8QIFx0`?@^7pT{_(BQu3ydnI1$_tyffW*I3nx+(To$M2oLSi#M+Dg1a^ z+G7yNL{VtyUBUpob}_Lr=xxuKSUpSL)HBS+BwBYTBm=5!oY%{L|kxPfja10d@uUpArOj~5w} zNPB4Ma7D2uyw$Q41$zqL+@8TFwnoV7`;W2aO4I0_ef*&awOtBtUfqF(6=dZgWT!wt zCO7F~ypgV56~N{9n;|=9T+IyLI@cm2eq@yyRH$TlG=mIT>53yV_;-{@vqJ~|EA|Y9 z$UHS>-|I4i39I{UOEbn>%{_K-%B7gE17R_{>SB+X><2Jk+}ptFE9`z*T~Gq3e)mto z`xqeXo!2-AxYpJ(c;0fXJOo!1X^C?w2~2KAt@GY(P+%^~7mjQA3uLd%E$W8Mh15=*CIPO(3QG35Yc?`^>Rfqmu z*cUyQL3My`_>wM+kZFvmzW%qE)l zm?Hd5J7x44APe1*nY3PJfyIU~m<8B5W?x8y2n*x4JWx5n`RwKCu(`ZO`&%P;SDw9C zi)oCs3@K3(zxp}e#lM(=f>|8H&wuC`VmcA>pb1Fyv~np ztiXb}1y7hDK9$kF67ZB7dhRk{?e?>2Gk}X?b9k+v+8MyRR+(vw9O=)10{@Ha3Z6F6 zh41dD&T($D)kSM&+s%5E&a(tF9Ke8%hV+m}2?8hzxKn3%8}fO|1+iZSm<5-=w!{Ty zpQ3f>`+=+HeIM;L1Y6EDQhLD=ZCFqvuMzi$;woz;-8moH5({OU@mn-D;f<>Rj;Luf zXfR{-m6!>!_oG?7_MR%-cYwwGzN9ub;I)6K!E3S>Jbf8?$268r%%xp5`<>{sUwXSt z;^l*{&Tk65gVFQ0!z7k<7wf~i!lcWxF_Io?d9tjDADLKp-(9tIzV zU>=1-Xr{Q>KS2I)h@lKt_8uP#GG~rI#}7Fe6zB2;p*4kNzzvH_&}Uav61Zr2f~FCf zGRmCSNag*(WCXW(>_y;OkHC82@_fl~f)`oO7J_SAJArHNs=}7eBfV1l;6xz%Z}-<= zG@gaCmUhWT@EskRQ4MR+cYpnA1s{AMb5pH5$jtBC7{jO6wOn`I^lGs?nm>q>>iz9l0HzSA=5{t})p@H!WRU10VpdghO%Oy>n! zK-q6P>!>^^DHw*W+%W)nDr&D<{{K}>f!4>yE=^>P;X+OKM3MVg`B@z=I7UuQ%qLmu{_x1qpXv(@11cska1@P;p zg40K>;Ll{u2E6`r@do|k=Heu_-c%I%xh~d4uM&(UEm?#9!s1S_ZNE_&YUprJWp~>S$EB%sK03!z_+)J3_ahWlW-&i5 z=GHD#P!RjBZ9#^>li2Te+kVL6Ch(tbHwS}JS?NKZ)bALQS!C1>(O*7I`3#mbUY~39 z*z1%9U0B>8v5xy9Fy>KY#`T*jZpaNeDl`n|HAg25Kv_IDl~@>4j1{kv#>a-`I zB_MzePdTt$T+EqDqrd!+h3^0;887Amj8;#44+yUnVq#?mv*wZVlkGnH)H18HoO$#W zym?=QmYR#WTzc%HHr^FjsZlDPBMc4So|P|iPa5@80xU;tt!Z>%!|YrvW-(^WqU-?|Co}kSF)_%)7OchO@ivyl=%KK` zh5X?UoGidB3T*s~(6|)L{3bGwxk-c~s(p12C4oOg-1})^oWL410UY;=s2k2YA^g1^-V?x`XeN&vH5SRtOo}QB9=8Mmwm|?kn8j?H>Cy_>%8cjFO`4_27v%{URbaaRcI$IX0nElb~%^*VW?3IDudZ0-{}IbPn8>uUQ~D}SHjc$i64 zOu~!>mzzxAkHP3eC)qFZJV(!SEW50Zvy>0G!Qkqb9=Eg!VH}9K4=jVB@I~)2bCY6u z>@qxSO;@Zw-E}lIOpAH^^^O2`9tL5F`K0e{tix%`_nH}?W*732t~w_NA~?}uoT$N$ z0*UI+f``sBz$}BEXuhOEGm$peU%naiJxFk$DHObXEhYA%ble2F=OM>`c)XVq0e{0b zqIdxq6{d=!_w=P56Ijh8u$lqSviZTr1^wFUuuQ+%J^~w?Q_!eunUcrz59HV>ixSwj zH8TB!D0+6q+&4Q>G5yRG-p~{jSFm=h3%Bl^>!!21%CeRgi=Ih033@cS=1)W zQp;MT!{|W|PKQ}n03fF}I(U@bFJtcW-z`S!i~DnUslMRqm#U~S7BvXTDW6%}p~IMN zo3!}PxY8aoo+2%K<}r{6_Z~#qZ9D7pJK^Dw@cMs+%(ND7yp#bZ0a!}w7k1%aP;08t z!a~DsQz4watSj#gz=&K{k981W^K*I(iODO*nq5%E_*lxj@xCO)HXd4(|2G9 zgV9ycGlY!5XB!EYsY=l;_p%?B=4&F?j3ax+QWDJolZxEk31DkeI_O2uYd8^UUz>Rl zJW4f_&aYc`kht%iHhVayo442CS?dL0A?s4<^gdxCHDX?YT^i-isTiKJ!aV1Sc7@!v z2}h213coqfOnN%3N0NCu%wPM$wk=t>uB-(_J5GY!3UY%)Pz0AXkNlp&Rn}7u!KDDJ zMgJW7t85;R98a(^v@wq)Bjgh9oQ~b47>5y7jbW9Bq77tuN{Q&*vb_e6wsZ$FPb=HW zOBZCuOj6yh9;Yj|>>~2vP8#DD+OQ4Pf5tcL+C`mVrzoZp$N+ky`uRA1+s>-2!%l#r z7g=Di!21v%42{6gC0M!BS~*3DGZ(Z^tvwlrfn7Bv+QYGnqK z^}LA5|1h0Ln0B;7$Q44!LZ1LO(l9f%y#^T;6D!2S%u3h}cz#UGckUF0D*_t6V^2c( zxf_Ns*b!i{6oh`!ocJ!4Yaf$_Q&&vE8AmpG^aMmXdr_L~+I2VbRIivPjDdMn3VwVx zQP4?;kN1j=hm>ePp!mooWxBkmREaY?y=kfw@-f#On!=CJ?0mDG9Mnl4HZ8_eURsafv`Pfa@UBOf1@7&p@a@?y+?d#@ zg?VZrMN^j<-&CZrOx=uBil&bkW)Grp%$FD3uU>(o z?f7a2r#IX1S5s|l;DZM;dT7)87uMidyx@viFOAI9N@V5JYZdtPWD~y9N(wh~Gffa+ zd-ystUY%Q7xlv7eet%atfzg^3^?3UPO`Zi$s}CHv0^sWNCgHlD9Rs&+Sva_mWftRo z%Em7o@Z84?lbOZ-Y{Btt{Y5_IG4EhWl$Uv_sKH!>zx1hM((r;)AA~haI#91tFwlaF zD6HzPt%YXqp2@2Dh9q97wrVHRn%+qQSReCvUaa8kdWDxSr#S>xZ^ULnyfd*uU(!k8 z)2;bE;N@W_JpZdkD{zciERX6nGi&MF6&WuZufbjEEZmahE!<2*TXD8#64QcOuXw+E zstT*^$O>KogJyrV7-TiWCzn0`0eJ5ZMqsKP4`0)wNABDG{ogB90N4T;Vj?HTY4pWny)?QZut8f)0{9Wo4pv?O*(q0hmn_{wK;Na*_R(|0Pxa z!G5KfW8dn9v+yTpY=k<3i{_4m%v^OWv|#*xWUT9MS_!}2I0{{sBUxUf=0~+5E4RK$ z08rMhE3tN$N3zB#t&sC~yPNQDT>VX^#o``AnnQ04D}+&QBc3&F!wnZa^@ ze{a04d3^NZ|FoHzi-wj3GQ-k=>-L8l@P-@K!Oocw3y>D7j((g6rzqt`@y+BS&$F_r zK{IoV=p=w`%5rn^kd|RRQ7A)^>jz1KjhaPI=&H6QX<-fg2hEe%_?bx{%QH>egDSs? zEXcq+R(vfx?whQ`TYq{iTsgTJUU}*!)X@fH!HmqJ#uk3+`W^7?TaSRr=DrQgoWp!p z{hd+i6Ru~pr|!#u8^&s-w0N`wr>&iW3Z9xro`lq)E#>&-<}rBF|2ZD@1~6;U3N!gy zs`9Jayz~LaCbbWP@p1YcMO`Vqj|!zx{8?b!BGvB>Jpf6G2T0?%WGt6gR_?M-&69tN zWzinMve1Shz)a;U2SNEn9P^_6pS*Q7Y-!fuFQ0O+@$zVlejCWTk`+1qs9o@*JC~Ne zo!9l;)#7Wg!p@3+ungh+pqjsjI9FP?Vw&%qumWa4A>FH8m_-r$_8%X|VwUxml4ZZc zLaCK`G#$8TwS6|V6~BIB;6>PEp)_c@p3(>*#>aBE|3D{=z)9nTO;(ze0EXpHu)Gvj z$Y>Oa4Utna024DwF+3Bk5AAgsOJo?#&D%HczjgO=IPd7~@YFRs+~?>9kMV=Yub%D! zSe|@@sd)gd&(efWcaz2CoY37oxlqbZ2o;50~f&C+I{lI|m;7p+?X zLq5RhmpkqZa|pmbDWRGc(*v5IN|kA>HG!2)3~lUu2$tT$evvEZA@ z@1-8t?EYVESzM?${s`91!m+M+<^>b5kM&W+O4ol=Hj#zy_tL#z`cePjaJbcL83T+b zlycZ!SajR0oxA8&l#&M)M0Pl(+0QH%nlWFRLZpc|D5O{{9d+4EYWG;XylfdVNET_C zof8}!fDuz>k!Zh+U1YD?2`ph3v+6BxVCsxBX*3fXRQWJ=GpGvu$Jh9v+c8_knrg`E zkQyPVm8H|Xucn85N?4f&4^uvaQQnz< z$APfq(aRFxZv0vQG87IdbRPp+`iadXb(IM1C`B$nuLs@4?d4N*|umxdF7L(!nh?&8Ha6Sa02eO*Ql#Fc2{7>zI5`*~%cNcy^ zga27@_P8Ia9143A4U|(%UuKM%HBE(H_#(9@ti#Lz$3RZ1j0RD>T689Bqo}gyWg0Uw z&w(rfjLlJ$^bK(wuw;N0m@c1Ud4X>TVmU3dT=PS`;&atJJw-_!@&Pmb%G7t#M|%}U zo(45Dzfz#cUp4bB^Q24LJ}S!vAd8n^7|iaSg#1%F^51~0w@Hg6@f00u#K09oyT7WZ zK+2xUESOP5p`ts8`UZ*SBY7+o?{}ji!84O|^|B^I!p{}5l}8$^Va}Mbq@}wsD7pQ_ z2q}sD*tq>YUL-PDTtO3wQbEl<^Lc+H4l6_*(O_lfV3SUpGAV@a*C6bc`w)v|9{j>Xg0*mka_{gD`XLe9bv64bIX>L>{2W6C5W!=+Z%vxA+X<#0`7cdK` z$a<0HaeTA@YfyUluwk$?G9tgUx;8rilnDmlB5V)7)*?*GAWbFUY1o!Fm6#WY9&^TiYCUAxPiD3w}4CBNED4g zxuB(1KubXu6=Y`!`^+$Jxm$P6_y6ZV=Re(j-@9*^_htqUoVoXQ-`lVI^w;12{r~yT zIn)j^cUO8O0nxnhZaSuGVg?&~E1TXJ_7njyWF*$RW(sd2sa)1E*_#Cs_{syUD(a~= zrI>U8tgHbn@34Mi-CE=!u`kPHzFl>OQuZ>RUHc)RBOtBhzLm=SV8gaxt1*ZAxemO_ z0Iu@W#YYTnH}9!#BJs^Tb!R!Q=SvC5WcZS*Jb9WDqgWUD`7A$0c@1EdYd{1|u&4lq z42nD+#Cw#G^gKy!p(4K$)Iv$Z1}V*_vd! zDtu5@R>)U6*K)Ll{(HhX!e-;qDE>siHBS*sRM%jj127mYSJxdRXLKgv1iNyVJ%h{~ zN(Vd1wmhE^p~*88n}8tt-e5M|MGxGz8C_30F_r$%Si;XUSmsS+&J0!;`M8M-a>JVA zDBQlja{xs8uOEW})C(S9wYeggapio+X*VVJc!)sn2_V!&7Tp%ps4SK<`1)!tgUHXk zW-dACD)B?p#?dOa_=~!>CQ(G4Y#)ykfZaf~HND$RBz|#^tH)yp>}%n^R(aM+&c?r0 zP1f`%J@7bSI2&B<7z11gjWVc+C;G@WlQo#iSRr`HUnBRcF~FKPOht2*2Jz*$9k0Ro zyX(ndJ`UDN17Xa#6V8_h+j6`0#6ePkWip*pY8olL9|T78(>|~T$^E?&=O}9nf!PgJ z`_5FSRaU2C1IVcXFiS(&34r~GWB?^26f+!V#^z?8A|;~ly^}06APcCI_TUB*Es>iB z&L}>0IQF9Z4c3Rubqpt)O8=BWJ9v>VP3s!T33&~^nu7{^b()$6CF~<=ruwW^$s}P= z>%{YIi2Op@je9~maS#qaRED`!he59*e#j5CGMv|7-ZZnoXA?=J0q@9AUT;@%Yh~_r z<@blhTwx-|EKw9tS5XSEB$BTThA)8XuntY-0G8?BCJNFnc#)|N^WV^hPy)+lyca1E z{m@XE4YcMA2*~vfY$Z!`9N##q!2oOyQFIyrV=BhYQVmus7GUe;MRNSBDsTr>w=_k1j2U8DT?v0Zd`G~#mY?+MSD*cBNVedbKy}VUJ zbHW@!zqbNRf2|<9Nk3yn2W2W1_!_PVk=W76OgF`Q28~X)kNkhRzsMgW~01N#k$oz7dwND7-W?ft8&pk z4(0@VRy0jy)-BkIn5o9%Zr)dU2W*`X<^*-#l?nE;Uj3R$_O%4oneD+oof+}KGwotC zA|K4316-=bOs<$O!eFwRc{WD;aOAs=av=l#Vqzl6+;0Oef74muiwCL11k8*mf@~s# z0b~#OA3Pb`heO9H00yZY_I5d|!nQ2rlBa9n@KclbycgNygu^KE?c;28mpjIi_jtOz zq?~Wd5C#}8+{ys#Q9f@^^?9}{bWi;v^=Z>A!@9C9p71LT1zLa8FSjP{E9 zOJ2vLpubSC5JR288rc+<#lA%zVB^uiS;pDeA8Cc4nolAM@qDFMWhyn1#R?2Y8V3{6 zzvSl}?d0MxL@yGiwBx%^>iJ+DcQZG;dytEu%~IDa?;b*VuV8PrzHhAxvruow0W6cW znPjJPe7c(*z%oAH6k&z$1)1gUwxGdaA}zRVn4(2kDnM~t_`2ELdV8D(1PoIJM^U#1 zUejo>SoFopS;C>_dG2beM#0CqXaGRh^Ust)n6D*a;_X}_eddlR*odXvmx9WPdk zy2x)dhuK#;&ysi&4|tcFaHtxLJ<_7aU<|O0j;S#A5l?9vCo6heT zifHvBOC<~zSd1cZxc`QOf&V4remLD)x05Z<4(vxK8X1i8rGF>lDkGC^eVjVe*Qr-b7~n_eXx(%~VGr#ii&T z?5ewQV~j<7fGohUFYon)!7}7mu06mq)?l02L>8Pf9s!02qUasWvux)aQ@WSp1FCT_ z)nGnum3NJ|xIDN}x?N$g*oT{Qn$u_$J@g>;jmA#9hvNN4c)|x*6-<|rG~}`X)4!!= z5jXy1Fx_-(^M zkQUeht*+)#0TxOdg;;>$ak|>o)5x1iUT)4(s9#H)c(k>M^Gu_!9>|Z(BmZiKL1w+7 zUpfA0`q-5>EZacZfXJ)GZ_Yfw8X?v?%Mt!dwmr~(;yAl zQ~hr~`2_ckE36~4OaYi;Uu1|zHv+ANm?N0M@u1;I9~#@*>qlBK5gGvb!T* zBp7$Jl!jHZg0ER}>U+kEw96YTU(PuB@uA&HzIO#LzjN329frL;h=|hIR}~pd!8RHN zYk;Q!gM@&OR`ZC`*1|SafggU^)xRTjV~t0>G3%Ocq|PdfAk(KnC-L`x>)PvYp}`0j z*7c^cVB>kxalb%kJ@0gS$0c8%sNnV4o#;q|JYZ_;A5Edx=RF7)2shV+KW;PUe ze;o?55Y7p+`t(wO;Q{Hu@#uPh8Q?IG3ZJam=|!h#%J9nLwr!yoKld5F-74;WLk8Z+ znc^Q@cS{R~X(g#VBF~+6x;LU=6OA1TZ}Set3^R&~sfa`%4RXvGL$U?31G0 z$G{|H5j!YW!Q<&7|f zd+HfPvkb6$tc!1EZzExb)b%ov8GDgiS;!aNG;9(d2nVq7V(MAv+7~GzJ)}%eao)!s zwuLVHmzUGFtun!h+^;dZW@z9p{i^R>Ut_OScadkUMBHO-s3C_ZWU_vuOG#3@T0KaV zNsmI^O#TPS^2(0HhF~%loRK~=A0=*Mv5epxT>Qx|&}(1w5}KW>sDp*CNWvAM{R_{2 z0nIs+_P$H6LFD0MbT>Swp!(J#W0SrF`La!rXvFEs2I2Ox_jh)0iiUAE2^6R-I z3X5CTSnP_ge3PE_%v0&dH&1(+|l{z^sVb|qPrj1LH$u`O7bcD z4HuW(^%l69n`ovO(-DUsL3iDKAMZPK>lWIy`AAv+aZSZnRL5A|A#~sCW z>QXhY=PcLe zE6P3q2#(oz$A4t^ZhGt6-%mH(c(XBwWld|9LDbvV{?a1()&?e!mV+3nHIJ)eFrrk4 z;Xp{oxWd(EyIMH9%&_8VZdgoDG%G6}=#W*CgQW9&?b@}Q-g54H>CLZyC2iYwm;>1y z_2BLCNTcbq|74(nObt-glD@F-qK|!=&U*QasMyq@V~#nR-h2MJqTAe44l{u#`O+_4 zPCIt)oceD*n)0I`-%Q{B&Q{D_18()djk09=jZ97kAJ#xEdWSq z^w2cLvJZ~Ew75jO`S_R6SHJ$9MA!$FIRYDW#iNwf==n)tE1AiF))1icqk}x7|KOS%=p|?U6P@$Av+0B<9Opo`!7*W% z*MH9aAO^998MRr(%2WCX7mdd2_Z#2&^5tKp6Hj9x9~)&j9MYm|?5}ZXEBD(%HUzzxnmc7;KTn(pZ@F@Q(?ahuoCv_2kiwMeB0IUQ8bSe!G*Gt z%C%x2XhjR4e-hnC9noS3M-!P@FC>aY75t+4GrHr>d+4lJznNZr_L+3rsi(N7dX^g< z+AQbUnDb@BJZL~@n5CsY-Fesc*6&>4ZrB44K19!X_8GK!vn;i|u&_v%e(^H8_!FP; z6OF82fr)yXz?)hmj-m(9e2_@&;N)6?={gZ*6vXnwNJqX?Pb^tx9}1YEZErB#IhQI;edeh z8(saFtA+C@yXZFPp^wt1n((|eOj=n0SjHg?9j+!|-?4KSz3!ZMy2d_4tlRo=jm$#nyz%}s2&g6XZQ=g;5w{7EX4?gq|gK}S$ zha8yR`nGp7Yj53p2s6`t_ubEjr85`Uq_c+@lD5&^D*Y7z@i#!LDrO|R0Fc5MT|lV7(hx+TkLC(8H}jOV(=(G zK+;E9xD&`b2m)e2sh^w|Y0tTT&j;{bNPL(q4;Hg>Psdg6$-4Tx*Rl>f;R(k(ZFVSa zIpkoPo%J2t_Dp;tBSIP5@41(*{@xFr>%4qtH3a?L+wYm;GS{yxEG%$csep~nd+$XI zvXh?l1ZFZFT4Qf1>lg>DojV_KUg9pg;YT;KpbutK!w`+&sc6_bYbnSE{Y6bGRGIXT zB;y_jw2bFy%W46pTI?gP{u#--Mm?O!Y#|FRN$|Dwx-7;~BC4tcHJS(uFY`v%FgN~K zSAuIVlbG$XTz-1X9b8r9FE92LF!DYDl`(^Hn`p4of{Y|%LOjQy!_nu66s{FLM_ZqTBsiL4T6^}Ba_lE*%~W=OuX00Ue#l%tQK5V z8@LiPSP22B2U&`KQ`0yc4z$lvfdp`2=0eq8Q-KR*wS6VF6I=!VX9&Dhmc9Ps0?lpM z6a+qliDj3X%b3!mjiT^~3yx=o$?(0OT_&5(B=R@f!9^zCRr3(b5KByht1yj5*yDa9 zY5-wh_Z;^+!1RF1Vu+$*1UQ5r0&lrPeWw_7omV+a5*9;WqXa~G7#`0r^%kX(j&Jy9h#=H;%dL_xATH7r& zUBs7G&tP>^2^K>>HWCtHF0-Yj`GS)Y3+=0vUs&Q27BfLuUld#!Pq!UyX7)1#vguii z0=TT8Zv&S+?UAX<;3=9QYbo79elioaQm-J##l7o9IH z?9MhgJ(ly(CW?BDKgjjUz)?L^9#ils1zFalv&koRfCFtcCQ%t{C2%F(YDrka#=S}g z7NP+ZyMsPU68D-q=G9gn4Zt)u0;FJGD8$7e26K0^-TvZyHaoXbhRv5UDhMlGa|s{8GrKt0NScpD}X7uNDV=sg3I(}hW^_!0a$5e zW|?bhud!YpZAzy`d2u4zN%qgK9zzlz%Mc&KO~o8zL64Q5v;4FZIR|2+@ULVHQR9lD zhv`$Z!|W!jW3zC)bp)F|hdlw7eN`q401H9XG67j-S*#8&_v*B=jd$Tbo1ALl&i|j-?#~bOS=JNRC+ie=#OauO6>-cxET+$t(sXC zk<9|7O>H*$+v^CnJp)zvh0$8(vm7iiR-(6(aKsAWa?EAGg^Gky#54gejo9yz?y_ZX zB&E0MF+_b|OOnfvugomW@5(l86p)29G3`BK2BX?WqA;ya2xpjlbu@68EZCdB^q-Ya zd@qBoe!SI#ihtE)Ejd_&wWt+Xbz$EG*Ra2&gJ%_7Uvc$))XrS{9AHGv&;{-grt>+| z)pK3l<}S8350Gs*NT%fUi51*MnPpMyXIY$|HB8a1^EXlBauc(;hd|>Cn7}sEfNjr! zlpGaLQ_<%W$8WITcf3nEA=v!COReMp(PJd;W16nT{gT(YVKk(fvUSCi3H zqYdl@GN`u8e92o@`nvu>8te;TbL{H9IeMF+l=P2Gbp`^BzXbgS}_HBwPkOQ z+fpV|kO3Y@@pv@}1>-9WGW1z?9g~GPSs*H5D%NErOWg6QV|9F(IGHv@)7vEP3{d)o zeOjvUvKn9sYk3bmF^8VXyvQ#bUZaE}S~M3Pet>H?MQ}YTkFl++1;9|$v(#;fk^Ln} zpQZCKt1i6Da%KbdX6O8RSmG7R5_VpfsmbKYC}buXnU+(SOlDH70j8>aVqz60z|%mj z$T&0mPx$Ftg02Rr@IKua&0`C&8iKx_LvN$>s<-4lf~bgd`A~#{>#HO&OJ%M->Svs$ zti{MvJ;9OrjQSl{|Jcx!l zDVtRq!tfcbA+uF=R}v3XkFyCmo#sXUN-HRsN5Wi#RhvvhkFoPYqd%9u^YkOHo%y;67|;bIsroOzahti4VA)g;y#71M z^1E;JpS%5~g#shS;Q-{`qtx{n0HTuTYVh01>#xcHjWVBw;d!c3FR}(Q!(J`EL+#Xc zWBr>AyO-`UwlVx0eo}?~+Vzu|#Rh8`(^v;vspb;nAP3G>;2UoUD*q;X=bZ3np`nguRki3nnY~f`r1M&&ICdx=V|@3up+iS-}HN{S4Qc zWXZ||!ya@bz=}*p;XQvxz0uIWnT45uWJVyO)Owa`rk-mW{4^ug6&vkWPZ1> z8Pa|fOl9b|%p_AYlR+fXcd6@D8eLXVV7g%Lg6SlR3wY^1s-2?W-g-IaUBfJr(P0)$ z`WhL`vREBh*sinOgz!Ttu`NGt5nmekwgss0u-;CQwdyghbpaR3dIJnaK~|af`ec2M zWJ<=Dx%#L6{4hm&f;YED|R3xU58YVGnFv&b!vgS~|z*|W|5J;KQ9D1FUf3F8n zr!D&eU?^3sswPmc2ktb$hG5{+NX(`K+PnkW0?o{B^!iNVW2I*#8@h-9i|@71GEA;Q z6jvkhxo5$I%1e62%Uw`Sr$sPX9hL@9qe-N|PLsra>BlP8rm)o9`A?8!6JuU>u$9f_ zT33@;FZ&8$TH4r58|503eoW{y%n^&Z1RT&R=(B7vSafVQ=YS@2mo|jHvX&R_5o{C# zvkEw(Buo)LujZE65QKjhKv&mY22=^4_)Db$oB#_m3-auOazK3u@c}aozK&!eP>sl| z)~d%K_et5;07IF?WI7kdhe>Gvd9MD6tC*p#z%qjlXnB7rXOLlj45R|ZsRgpydkWk5 zUFbNcU^@Lb;@z0K?{a0YF^c_EgC(A%GMW!Lj0Ucbk|kTTUC@ts2}N@_pfn=CPg%>B z{Q+QFESp(#iF%u>7r2T^y`E1Jq}@y$>MA=7Ci#F56HFyES-ZabR1d0 z`8O+hl7Y7@uJ}y~CfQ_wRtv65xM9RhH5~Nq;jQO~iD%wQaegiftj0=b`$O5E0EV)h zHdDQUtc?G5^@Sv?2Gfb2@B&_^^QHJF@)F_#u?t*2`o z#YBV2=LVpR22+m^fI{`Fml35>0Y5|U2Q=3IP#Vv&I&0lM#A(X@1~4raTy=oyfZsQe zG&*L##^l6UdJxycOOK6)1FirD1_C3NrmuNeiZ}vH0hBFg78RU)hHdvz!BhVn84N*D zbnqj*fCW80sOr~?icAMgS^6bc|CeIl;Q;|GE&D6LP_)77W>d8pDuRd~arNV_VwO3C zHvYiB_nnayCd$iE$y*+nQeJhV@WA6hZd2^H8ORB+O802U!DlqsTBAGr0O>Xap3x!w zHH!L5AFei2xL*QG%l-^7E!v1m9@c#5N>|Ei=oW!i!-%FoboC5Z=P2g)FuOU{K+R|v zrwj+^5Xi-A?ia3?YGzvu3^qB^&MT3XQft|;b+c~+&gd?~a6gFl+bOb?0&7gE$*c#f z_uFzH08EPon%*FNqIHBht_Q4kC5fj6efC6GPx1Bft{&-+XF-=S;7Y8)HcV~io}FW5 zK9{xaI9GrO^W9>hv1|Qjdo9)*w1C|28LM6nB!Fqr29_mTBhUa8+?>L{$16+>^a}={ zTT|ipX918f;1_)VmYu)y6L(N0v+BTR)qzq5WBJ&q$5fJWkGd7u~l0+ z5CLXOoz?KFz*F<7VAFc5t9MWYBHG|+SAWsfV@bwx0)z*VghDVyb=TJ(MIbBF0K`Z; zLT>xR{@AEL^>r6T(2OV4y1=iq-~n3>bbx8GH^VZVT@p55rUC>%HlMSWI?dm;tTRn* zGokjl2Yh*~0L+#)uvw4<-Bqj00jvIM0Z{*YZJ?=te#`=@ULK18v!#9m6(D0uK{M&S meO>8q==(N+PkMdKm;VQ#$}aecA}%xl0000tz;(;0oD%+G~+jE;=RjLWDX0*WkIfRK;`I!ULy?^etEzO&V-x^-{ghOF>Ta=Pl) zty^{K)W81U|NG8T0sem~CQ-6pwyn6{E2TX?r#<#5?*F7s!M-lQK2AlTi9YMSzAGNb zO+_H4PVV&?J+?jR(vuOKWuPl&ug7|?Ts(F^P@0_dSnrqJV?N>2lLee*fb^VP{9MeU zWLd_jZYpEQq-0r^%f5Tk0riOt&LSX-K=TXPD_PblucW+!@^YOQQ(mC+808_TdgL5J z0j5Ja(#u_xx9dDXc?;!DIzObBye|VVE?LIT=U!k1o`67oqJgs)AWe3`XaHqCGXS4T z`Ebflp?sL$<`s@zcCKOy^^yUzkn*aMeGHhIl-E;UOZh&^f24dH6sQ@Lk<30(E~0!Ux~GpMV&^465$UZ6BUG?|?gX2|m?KONnU zkOidY`S61fK@boqLt;jCc!fO!MvCXHoB3>mIvxYl0GIxAdH7*T)0*6y0E$kh zO@M8C#I~NkW&^hQlwVHyFQ6Q$Z&LmN+jJm+vrJENxhpy|FxgyR zNcnks8<06aC4lsmRBNWTT8ReOQdWzzjJ?a$zJ*|KQ54KR+w9Bx zNtR@gg<%z8wIBr$znAfD{^-8jy@tA(yFI zYGyS+trAe#^mkfK$*8(6jF-=GSO(}tl)pjw?~#Gj1*&WAy9>G>r2Q3~MYc46cON9S!&O@_vNOQvKk$v1}?)5ahT6PAJ0uwj>(*Gp3zi`gz$ zXmD4ZQ;JMOx|_Q+rUs;NwxUi~Q)di7*6II1`BP9V%HOf6H+r-fdfI1cUxU*%Klz&ZmDYDR!VCSY* zA9l>Q+iFu}Nn_6Qj*!)s)?{}xs4Zcsw(O-t^IyXWBN+%9$H#0I8ic6^ti+wjfC$Z| z3Xhsu_5NyG;;^UzsUV4?bT@0#R;$4W4s|_q2#O#0^C*9m@&;t#HsjTEsrIQyzc0aA zrY#Lya*Xm)sQ8`Hl|J9NYITUb?`i>%UKc~=S+Wd}uKv`;$(i+RTMn9YG3R!F{iD=n zP@zn@)uIA6*6Dy%@~BAjUcF^fW%5(?g=W!29Wqt8626eug`*a&)(o*}%Xs$;LT<+W z4ir#1KFR=fS(MX0GxPfjoGx3sP7kvs_YC@5sCIBqf9J88Vz$hOgoF*EcY(41OnK76 zdbc;;tvA)D+z0gd&?gn|t3aMMp`G^3K{8-fxO6;qk!GgffYvgr)}KbJ8aAcl_yp2= zeMEw^F*C*O`wUPv;mo3+rhEpJCVd21gM~5kagPhxqiJu0v&@VePkbCIJoGALfb|G` zqE;L3YR%#H?mSa^1z@7YA<{+Y5i(M1d2?P0zb*%%AckDJ>*%GOXmje_e$EE z;4E``7?j+h_avxF;pRufmV$+SjZBj238c-q%ak6k6+l@4W5$p!Ad8Ht{$&17iN+MA zFfCl3NojvjyUmw?HOrhf?W&g7 zn&qI>ENOs(hDGbOb$>aYeylSyySPczw7#()a5d%kQU0NGC2hUjqX6BL;4A|sv*iesu6Y46r$}(K zBZoXZ!-Q00o&jTz-sfE&c^4@Av1Az_MHi6vOcI4vtu(v9+7GCX<%bJWI1<4DYND8v z0n5NlxK6ch&CGm=;gL}^rgtHZBio}3${%k}Pax%seYeULP8sPmaq^G=a;WB48V`iW%*OqP^(`8AKN9G>5{N0r@6bg$% zIBaV7(%UoVKDG6bmS!nEJx#L)lwdRO$=H2L^<5vDhgM_Sb(9#0?6~she<~CtGn($EA672-lX*zT5gUKY^7zKW;Sqc)d_vg=H92hL25r0aN@8i zJ5AEGLpx29IcbL^Q5Q)|mRIa3alN-%Gp;B9aj4)JR z7c3c5wT+iJK3!oW4OH}Uevn)4E>QACjMI6KsrWcP^s$EWasuTTISwXzZ67G-cZ!gd zL&D~g?^uIw_!a``{hbJZ=p>Y5tfkM!E)HD1IkT=~nC`Wf{YGOabC{I<_H=-rXjC#M z>(C7UiM+N-tuE`Y)toktEAPQ3o&kEJQ~b2B{P+tL#+_Qt#RwMJ!K}c{YfTd- zDgx7(_Phz7o1?Rgc6-Ti70;u*An;N3GEn*&l+qlSa~3XIt@JsUabiRk60!nK16Bf+ zazy!rAiz(q6=E{Wl7Q8bMG%U9U@RtQUu*K6CQ5ntSTb-W(RMBegLH_>@}~sUQ|eXx zveCj-yINRB`gHa?T?A=?MN`w;iIZ;H6bTz9@bLKT3}paKaRCY5V#TRzor9 zh3Ia3e_Rauc)?kwGZ*N>ax0K*lH>~tLemG+8wi-iu5ZPVk`tp~5KtQ(-I^5VG_ZzkbDeJ9X|`xB^PG}mA^ z&VcuL!07^$k7N2{UJUg#$sxL7r7F#V0c76!=?%?ROq*|?Snp?QuYwrpC;tR$jPQ zDL;E&H$a7#DFPj4;{EEwBbc7rX_`8D7arfmla6gb`7C&IPP+q~ZrF$i_A|Xpz;fpe z?u#kGhhiYzn;n>r&U~z9d_L{+=8-C%PbPerZg5TCY~y)b$EpBG1`5xX^{Xhw&&Z_y zjFz{NcinnSWRM;EP&)T%1k`8I!@_wb)nMR06r6PjVxh~(K&=oc*z8}jz{ffD7QQgq z#BH6~)Tk1j!imnO4nXZ?xT_YR`t}lQdgQq$nnl@P5~d}Id+28D_4NoPYo*(*n z8RZq8cD@{tS|-&j&rEs@<O_{ZtaEa9h&K~+cP44_tjb^(+-hRDb#6hNWd z(39drHg$m^%C>Etj>B#06O`{Ioq8`Z;LBC8+5Ib`)<=3X(^wwg4viJGvMIJu2V;Vmr_V0i96@z9b zUU#)Ky4L}y6qn|?IspF?c;YBKegP=1PMfF*2MIr`y|bYaQz@r454+10=`q(d8N zzx!VVXu(Nx_OnZ=JB9M(9U~?*v8tIyIy|wMueQjRqg$Zn4|VmY3DnP;<_gYYD-gGt zzff=U<&17YC1Xm5HPO{+xI4h!&6tCk@^h;M%tfp(Id;jdJc^k-VJciT1^$+9^hHx` ztdpQjNslBBJEr;~QyfOWzo1sdiwWq5`rSr4En!r%)?HGyC7{I`I_??*(x2=188Ey4 zYrBK%T5a4!%XVtyhmjqEK%L;KMuNX@b?~8jhVXDL;V6VBQA>XBlg9 zu>D! z{Wk#3hJ09UYOh8CRO$5+_LRdt)J!amnO*!91C%2;j1g|ep7ZF>P>n6vXzVxiqlLstvpcKw698_bTNDT3T#Z`=j98@P@`a)H-P&B~*TJ-V1Oh&kt zz{!BD6+p@PF#7FZlU6;q7FsXdwECDorY&589J|k_`gR zGS)lfO{a=a_7R0>{rAvEMPN#?wBI8UH4HM3|;KF*<3k`FHlv9LnsB;fcT2d!iM zAgEU2Df;uK?GgTsoP;W}!CwZH+`jq_Vxf!aM%Lu%vesog8G#04qQ7NJsj`X!rn>i# z;a7VTbvZpL*OZ;`g^6Qkp{K0dpSs?I>5I=Vs^L{s=F7G>@q6~bg`y|^O_lELR&w=U zvOUGu7X&!1ngNuX7ui{=aDbeCbJl1Dlk|{&VIo$;F#6nLqd|>bHx}O_Kr^a&;P^Tc zWcQb4MG~uq1Gb>i)J`CqdHKCiZv}B@SDtlv@u1)=vgRdvsaaE-bBHco>6&wM+cMAG zVNX*d12{k2apQwad@Lc8E`jMQeaXj?Q?$_aPEbX??$HFdwqwaUaDhpHI+|GL>LoR- z3^fzTYRVW=b_`Dy7-P~|+#w@MRPJ!k9NN~R^$7ycPO9c9?)^?xUZeAmP?<)k9>xic zL%!-Q&vGO1RD3emH&n}zqLevpNZ|T<4Ii0ksb`SvnF0?!t=%g+)F^eVOr!mos`|?Ovq%y(}FZ-$%Mn4p6)o3YO?WUaGodc)k8eE zIQ*WcGohL{UGv_z{a`k5ma!&}XJ#izo|tLe%=TtXkQ>X)g^K!l=2naK2PWUpcaD=| zFisDrcT6Ih1=4ATR~APOl5*7U=XNOV^h z1$Zhk>039qup@Enl4GL@-u4KY_+>s$tvRKm^7|fl!vn_XzPxXlhxcqu=)vs=P-D^^ z51u!hA)Yz&F6=##vx&&TamIQH>3>M}x zh(kyG5~;EtL_!Ozj4e@W&NH29PT8X{w9`?w6mM8UrjkTeg-XPguMVbw#=`%dTT}eJ z+0B;Bpzj?IaLGa?gY3jtriL^_wwNW!bG~aj!gbpPemSGH)ox1DbR&N<4Vjk*9lUgm zSZ~zBC_QOa0%`@GH2=(^(%kF!B0XbR;M$cHyy2lX9^wm4bvwWw#*HyaKc3ebDndE2Gp7STQ+fTTbf{AC6vGnd>z)1 zUAh!&UjC@S52qbqCJK67*v~8raQ#$8GcWt zO_7H$l3BiXM~c@jYU8Xi(yBqAnAGMx6F8b%azxku6ehAV5ArvK*|Rx5gB`W%DlIl zGlt~&b&34aGB)pgTg)W2_7raF1y1t=*QA?Q+{x{7k}b=co-S-YwrLA*L75#m`(}2X z$?1h-362;c6IkK5NbuoOZO+v-{dh{?D?7S@$#DGiK|Wr(Ak&vDD%siLnvv$4@{Ue~ zYqzEN?sgxod?b30r)?w`KD))ozmj~ocxf9)4Ev~)R;*GEJf$B6t#g^^Qt)*G=*u^B zu%TnFRh4%m9p|rz8LwQC!h}I(_co#WnbxT`E?ySlo$EcKuEF$YTIRjyc2GdHAtlFp zGJA5zkXDm>yIS_V5Nc4IJ70ZiSZNUEG_3gwWF9){iF#Mq$atG>c%)yu`5?ID{l|h7 zf4M9{jVLn=Q`s{c^66A8_#hdHOCRsn?J?HBl_u6OdC5k~Qts;P4d5Di!t&P!0b@O7P zbm$c%OHLa}aY9{Rw`|Z+@}@>K^Wwz}PIVj!qCO029CbTYsM?w>wRKcrL^peK7_BP9 zO1z_qdot18jXZmHr801qnRJd(E!CezMb63I&6>vXv8-AAXTdzH%yf>I;yH^VRJn75 zug1Elc%Q1NZ%lZ&MOuwKFi##z@Bxm&n3qsVU?$3x$K{f?occq&r zLriRl@R2objQauIc;zT@j8ncIk^UV&3|zKC-~$`^k;?!YQ5}7Fvxn;ri=|eViD&Wf z<>aSWL(b6&!x3(u4rh&hmN`c%IqaEc_dhku)O_}2vnI`J!b`6&^(M^pUCW>Vr%67y(lVbdMBE^-Ps(ak&c|7UPnE;=EFvX>7Ivi0Z z^X19Aq8c1j>;KEh6ZqvcYh}|xLq#X$NTTo$J=6RhXJN({GC|vQ74xU?r-*(Ix>!8Fj>cyNcI1hr-2@>@z zaC>P=h~hT%X3ajyWz7qgwNVXNMx@&BQf8X3;4=?bFd1dekMvGydD$vpdDXO!r*eM@ zOzvk%8uOlAF|OQL->-pb(tqp>v1)USSFWJ@Cj!}rAmA{OPz?{9E?W)!W-7(Tj+SnE zikZ)C4)FZ34wi)aV4iZ6co^~tU##fnm>ln|o-2^yYm? zOn7GQd1Kmhy`_ubEShxw{&~&;=n1Ih9U${$xga%J6fb6W*36CzoJy>D*gQ|Nrtpn- zt}5kTavr`nVa5v>V8(dX{1ne$l=%p0swO4iXO=@n4>UXY#JW1Bli9*4`%n7D<|^g| z4V-sS459i0st(cG`5dN8c|Sdn?_SGyT{9>FZHnZ}m0Lr6Xr-D;lPB;JR38C1sgj62 ze;I*uZKdzIRhpeWbLQNB23!-*!^3q@o-3c5G@1C;Bmc_rzRYRH2N}KjT>T@D#{_Zs zC}SKqd}KCdrrmO<_0Z==R_82klM}=iEs-}R)pCJL|0Qx75^Ks8b8@@(2M$SO$<@|a z?!yt6Cb7v*a)7QNU~cQ|>vx0AF;S&m^-v9~My7Gn0s<)1m?(I$Htz!Gj00yp;^C&9 zX5^$|&u`JTH!V+zJ@q{&a2obJk&d;j65-+Y0Os9gPrEI(+mxMibZ4ptw+P+mL57tS zIQbWbDH&T|MfU3PoM|s(PM0-TP(BgHq=yxhnRI9%3JXePVC=fS!B}%q7~!c4V}zAJ zYE4BG%1GZg3fR0eSD-gH=t)(ToND*TW$`c!(Y(A`OuC+-d!c z@iwmAG+6&1JEy(%lZ+)aucSe~)kv5)srmsE8xvd-M}{RCoL@r^(;no`o@E2hbDYo8 z<8nRp%se~Jkum3hWzo6BHBCBwyoG9oKxaI3FKgYo=HaTIDmv4d!?e%qRz$YGm^EXO zN?>vu`R{fD-+j0~*va?Pw3Qg{s~bZ6*CShU{&uP51a!b%VAT-tg7F03+vbypVf1H; zYq$D%`^p6K^-S+PG=)9)kUl?qX$#-pRL`}S_hwI#wX+-RtIsV`$4aKE1C1pxr#+qt zt`$dy1bHYiM}f>+jOWChr<-lkq({`OzBA z-r2$_hoy|4O$O#FQBgE!>#82?XLt99FRZ%IM zb0}aok_gv6P{I9;L4^N-kOaPb-!Lwpzg>F3{Mtb7WbFy%3#^$3oVV1&4@i@Dgf{Q= zpnh#r1s5!DD3(oC-4Wj)qzCuxWiu9>vz=T0Epd!Q_L7BM9}CTz*0g)RH1A0qyQZDr zzbU6Lat|9gXDx!0yEmCPdzG8?9cv!}YDnEjuxJ;W`j zN@vQkfVBF8r4bg^kg=xV$yp)Cp>Jx$`2NO`0V@8$Nn2VWzEAb=>Ze5DIhGZBvU8+v z=W7q?;0K!n3EW86)%s}<|2NwJ*hjnS_68bPa?-NxR*Or>mkVf*wY#};oMFI zRWt34^4L)@?ZPqbRWj`}25+ZP9)p@GITw&G)0{?}J`KjCGul0`+oX4CO@~FhE^2U- zdOTI2A5aDQvG&qcdH*mOU~?mDOps3vA@&6;It1WES7Fl&qX1bsr9!4;(Qs!aeN#i7YsO<0B!?SgfW1Q?Djel4yvY_rvG6 zao5hl*wcO4ml9jT`6Q%=-fh~U!%C1AQBOo>n#kEg-+;VX?{bLTFw%);NBK52S2gMr zWneU{?U-=u7`aba$))DBCa|#5!E!R`9=F|lP;V#E-ssP_n+eT0Db`#$6yvxBk$pXM zO0eXBJBN=ZI}v{R@WKHq{J=}QA|L<0#m9MvM5v^~=^(G_iXYf}?hfSGeKXC%FCGoa zs_22c2blQ-_eH3ajv8xGWqDd_LV4->r@xq}z?gI;FH@Yc zv}5C%vSUZeJ&sgs(66?IXvF1{!#okve{31Tb64*K8+XN?S;vqwmPPp7U8x+Sq?#do zY;JnEzv*N3Fe`GQhNAnB=Kq+5O*MJppvl;6xF9u6mQMFX&1P;uo;Nb#MN!9qf{D;{ z0_kGTA~;Xd+a-#*7m_jIYHwF(!ojM@9V~w(CD&JAO-_f5wq#SKMiEtVaMUDJLrIYvwBF}Ain z84J~-haT_%tLEvKuL)Ie+$(R@s5@KXY{`M26Je1bQ&^L31VCo`UG}7tDsb|xS2zXE=RBI9Mz2!>dnK=c8PEY@xqGETpi_2w++0{370ahB<7Trsr@$P3;}DWj7is3Ppe0c zMI`3k*e0aj!}`7&wxqK66Y(^zMMS#J0G3H$qTyEJOru5NoqBy zWmn@!>*3?X-Uc;?qY#KMGwUqW4l>(iM-Kx-$Fv)ho!^!fF@FVvrKQ8<425m&2+5OM zXNaHptdj@iawSz}>UGp7Ht7UzzElS2hRM-^&wC)JsZL027h&Fzitl<}*5rNgd_6oz zOCVN7Gm(dhPJ)Hi7-xp1bneWp(NMRy8CAmQDG}a$82QWl`^NBS2#zK2XML28g@7Mjy}#%ugmHR%BL8scMw!` zsiaxY!a$Rq?t?8IiLHK0QWeeP8^j|jx6V8{%sQtnGroK%S#wI=>8a2WCw4F=cenrTvBuDVX%Bl*aL4w zs(Rg{)hmf()>8#g!CmBfd+A5gvEMLM#c|b84jAL{>yyN^*H5?cEz)k~b06)!#53Q$ zW+=e9Llq2bX5r~^ZfSOK^>hahmPB!$cHuWhxzd$1{ zf9g*mCi>u54WmIIdy=zR;R+w3{2CJHM^@VS_+)FJ0hB$_PmWgQi@eNhY53;yEO)2V z=cB3tKC-=mdkgO+Xml8uQI(e)pv>^lYNE-MndT3FY^Xrg;1I zCLCyCnj9W$(na2^kJelytNU@^&MNXDV0xI-h6HG6BHgrn&8ZJ!M=Qh~o5y+EI!1IbiaD z!f*U>y!#j;%~i0RuSV4>yeAMMvzwngvbgDtR`g4k?1BobHECfq8VCH$8I$KK?I9~uV)ZMGhTemw1O}kcamY${0_8z+(w|pm_ao7&j7?>4q zyNBwFDC+j|woV85!F`MHqjh80IB8;>`Ge=4e*W6T_(~+_2^Alqe)&Ly%;ftgrm(z z7Usbz8P@jT%7fFO#J!qHl1Ufc@D!ZBY7*xiK7mu0P3ilqq=1)bfYSN`(n_y7`2qaT zo1cQxR?GpCBTJoFr~XdlW*jlt%b9gJ8?9g~*BWJ}eDH!YlmErZ8!a$7z9wX_U7eQ8 zdHvc&xZ+nUG1C&{!s$S=y&@f1E8GOhoQ`QLrg36O)vFPBpc89gs`Q_&2rn7Y-D2pP zNB4o`WM=ereX|2}YrA8=`y)LlJBdjb5@J|}j29n1edp};cY(98GaaK%PSlUqFU0j5 z7UMaGY{y?e<9>2ZxvoR`a7%!)lSK7&;_6*E_Mn}(n4R|m*gC1(122DI(GRamM>}}B+c$ulvAFHtRLYuw;zNr-pcP-l2J=^n~uyx zxi>-14{JM%be~uHSi1eao#r(qPZ-O+nF9R=3txJXdslaw@~P;Anl{HxJn5KuJwi|< z4Uy?>p0dF8Gi1jZuNddh-|6Lz4=lol$st_z{JSvE0+ifBPfcf2rQ7iAqqg+~r^}4x z-}c^K@LNLn%d!D(VgBPRsdSg#zkAwY6Uvm^kWpwmcl`F@5q$Z#{JyQMnKCsOU@u<3 zFtorBvhS>6&63k|=p+u5a}c-l?k@i(_7lo_>g}Fle$OR7Z=%Z(BkB5{TYX|*H#B(N z-<$dEO8WuzQGvEl4Mm=*ND#^^KuQqg!-z2iNJ0n+uOuWn z_uli^d#~B~k1@xbW6rhqKIhz=d(XWI^WJ&(+UvE~n(JTxc+4@!ylp?d>=PHz%O3Z| zup+EV`SRAVQZy7-?A_3Q+Is0y8~0e7^s> zbLffp{~G<^`CpIc1qLvdYwXpq2j1%ry8H`g&A*53V6%5j*3OyLWx=VZ9e6Ewr`-F@ zHQK+@Q9TmYREIHs`lHvMLtj~4p-hxll)BQgo2Vq@xJj29nRAZLi3i%jPlBXgZ6P!( zdEgL<-K-0~(~}!P57y)t2>FESnTeFt?Q;DL&Sn8~)*>Y$xN8kG4whn2@gguYm5=D6 zEALB>z28^mOSlA_3IP2sZ#XRk=baZg3v(sm^w7i^!N+Tz%_}R*GZqtZ2wjYM_DcV) z2Pt*NCP2=tBSGiC3*aotz}5LVZRwf`gHQIGQ4T&AfFTnXxa#DKbz-qT<>^^4=DxhQ zF1$P})|>MBb2pN!)4R<6Pun1s73qbe#()3(Er&>xbf}TZJOS6z%1#4A78h>i9AumJ z%TOMYWG41rg@K2bzu=2ja?}yP*@O_S8a)}6Xc4!rmqRL&jF9nzs&`65a<4;kM%uet zWfHCmgCJfJ*rpXm>e~F_s=kM@sBShhU3(|5fUVsKtc|!#TwnfZoL(P?kr&{sykV=u01%|2Q; zDT2BSr-NOzRx zdCzV(e4k==q-GjcwQ_WWRO`br3oiZ{+Au@mpeCh42+kH2>H)H72fqN?c|6wVtQ zt`URC4N>n2F8qSD9Y?au3sNu)ME*z|)~+daX2b~`5Y{Z(RrDy{DT?&j9Fea3Rn3wN zKnsV(&bxXW?Xm!mLtYSq3=+!MT!6PSI$k-aPG&d0aftU22_2cUNuV@_Pt-fv`R+3ItQ!*=S4{(R zBC$YUaR=+Twhm&k*jRO9&3mP|n@#*rpZp;n%--#$_*w`2oG$ojhp)dCKsEh#<(Hlp z<@>Y{;iX9DxieY1$&1{DcE$IqR3~VACeXYinEcX8)Z6S1t=XXUYkEM}rSKjV?Hrxw zC}YiSs+66Jp`Ltv2Mqa4j4v>J!^K9=VE;t45BHgOe~9tb*%4t50}T?U~+@j z1tHC z2nDeobkK%1@30$OADOgJLXj5T>~aT*rzq1otw_hSxzU~zCveo}&nqygq|p<7Gs|Ni zFDAE`II_ORoS9=*+U*-&al?=dpID%KqJa@o#x|%G>&Q9>V~Cjp4>kuUi_GvUy0$q6kf-iUR1LBh;6Jy()rCp~X#3h~Nf-M4GqZ&xrUVw|RP#W|6_`0HW4h;`Y* zv&JE94WAcavN8BpRL?PpV&49+#i{eiaY6@k07x-SjY54VJT}}w;tZAamx^vhJ-^ju z&#$f@lBZ@z);X8Cqn1T`#+>+_88^@h*3XnZYf1)P#nK4keM+S(NoH`9Ml8fOj&4M3 z@;w#SINUa_Sr>NVlbx{eg1JQuqV)*Q^7W8yq9k=qG3+_&oC?m=e+f~He|=;Mu} z90ZtCs-pi>R4+ZZf^*8EU0jiT#l;PnYF6apK7C^j7HsLtYbgikAaml$e#zIr&y6yh zk-Ck9Db0_E5sw)roeL8qDV&VX$>3ZHVj~wnuG-*+KyMI4%ZQVj*8wOMrBI`eJ?%4y zn^LUv6nc0#_U#TwwG?&EzP0NFm;s!7kzy(KjW;A&u}g!f3HLB~{o+4n_T=3CMwhRT zZ6VHUeqQU8zRUB`=hE@C*i|#>z4Qr(IcvK9s)&`;>*xeq=`@Jdi@?MROCg=9UmyXe z^XSgpNkYrkVs{CUQT0rS%>Y_8u@Qwi>+}$yxI@qK$8NxDSydr_`H*-g2B>9Do^r37 z$-Iztf4qzH3=(y=f_d0lZ;nY|1z+m!1~_b|up!jSHFN{ubj^nNVcDR|H;T@=6xcIw z*DB09JXf6i$_Hi+~DE-+^s3?h3`@lW`=m8G6^Qbd2L<`@=$xfxO4TDQzO z3p?_G-E7*Z#U7cnkm+I|xqO2Hv=r;yh|7WD(&SoIwJtltmC|8QaP7r=XT*EKo~*bh zaCQn#=wo%?Psy$r1bnd0*6*8!6W%Fv?t!%~-Iucn&NA#zTwSx!LsWLZYR_8&XTqPx z@79!w^kBJ9gAtU(JO$|{ZS5GGGxnWkFa4m=LxpHJDyLBkIof|VEe{3e2F1GWq#HkL ze+DPw8xFfgZK5=a_|7!to}MxRlno-&h?%vXlpJns3{?>EUI#1&K^|Sl@aR$p`eecSVbWGRT0McF#9jX2j{CDl3A z|LiSwSHAQE`ef{vRMsSmOxe#R1~uTsgGFw;8)*xJi1kzwa89)ua3aj65p_&a9->8A z`a1QZt745<>q{&)Y85}Rfo+$Y^qsF`uEwg9F*DKfucK0y1$aPlXR%(f&$BEp=xn?^ zFhI=eoO|O+Qg_j?Sl8=gvEBo(w(F7veHd%Lip;5Eo7ZxxpLLQc06AJFiv7^H9uv5s zUBwGh8|KXPO`}I9l1w2TE{gcD-jRY+F^9aIvrv0Frxoj{82|8jl61&!Hl0*1_NAw{ zM$}6%XQ_9(boG@niCE`c3VpY2A=V9ZhJBGxU#xfEFlxe9;Yp%dtyu4ZFK!NVMuURp zGbf96*B7ZLqbM^n2xt9kaMmCyJxSaxtnE4)5bJ84lPvq~YgC))r2K?J9@0oqvBUvi zC9xjivps!N_jN`+>;#;uUtK{--(1*3qzL{DDL8pLxb&krdj`6$jNVz!QR5Qe@OIas ztF5*MQRY}ykyJBDu3`GXN*PnyvI{Pc^njpqP6KleBmltaRf*||McEdWoV^n3tz3r= zp~{N6{=T(ty&$77d2ZNNw?>~#dge@-Gz(SK(}~)Jg6CSJT^j{wU!>$FDo6LzF0P7F=`V4%>lpNEkO!rq6Af=#ls+3o~Q*YLac6KGT`=mCZt4$lE z8c?*jF-6R6RLFb`DHARe>Qff@5gM@-hji~uxQ0fit1fc})Z+uxj6)pjsJOmLf?%q4h0l|p#1_^UcX^zgRziGkwDPM1;kl1Uwxk;mT zZ2XJJ z2fn@~iK?p0>nAldm$~;%CYVs^6Le1B))V$;E(WGt3%Ww34bZMNdqFBz9fm}@d^3Oy z0CfSl$-gPVyNYyDz~Zy-nde3j zgJ?NegQL7BV~{ze=~eW=g2$Ae`|6JUpk)sci}i+R$dy!&&hYFbX>ICa^d*djW;7OX z;ym@q_jB?K3wDR$;rhWV^$5(~$jj9mJqlLSb{&gB6nlt91u(0Hz~p1Xd3ae3%|>2g z8(oyF=wxHqjzsn5Sd|clP!RJ9&i_YAq(|0785(_Q+4Cd5J{jwr>+0FmKm1W0*f9Yy zx-`va>KnGH?`nCo>!2IeMmO4OXhhf!sSbdiQAvP8+KxB8k+{P_a31mxe;k(KQ$1!j zwtm||zg~h$s%Di=cE%4jLPq;N@G1dkjcZ8$*m`u3UoLL*b|brfyz~t-=2Bozt8PJ5v`jr-BnF z05aN%*A*~tq0)4veeBLrU({P)r?y4Ik?{PM*ZPKs2ARv8daw)PNbDlqVy}m2CXqA4 z;5@@Kqs-n4y)1jUet&?o8Dly=4GlZiJI9GExR++^Um&kDH(mlzh2OW}#Pu)n%UQQa z^6N8xPBCUuyH5R!H_42j#lV!)NpF+^S1iG&{lXX_vCXulZiBfJP-PiJPae%J%FWd{q@w{3APyI+{CAiDiMG@XBd z73_r6m)$UP?vpu#_^Ex<9vwy)Ece=#{hT`hCd$gnf#^y?$fQKmceJLY=MoqbAPeh! zNTg38()oo`5MsW|L13Z`4VEbZiW5A6f@o)}y%wuot+U9Sq`GK0O_cFcjvhsD2XfIU zKvN^0ch+{MX0b-nkSl2tz&T*8gL!;qcozfQ;By6QH6#D&_Q)Wrg*gGv8uw915{UI? zw8G6;zX)!CFp zIzA_xnMeoqgE>oZE@aLUoR^X?D6-DB_yP15$>d+!shOm6o)nJW?18hMA<}6!*h2(xR^!9M9i!5qX#KiFtkWSFZ*Q!1 zFy~=r|9ZA*03}6q&I8=pP_=8t!L{)p%S)g6Ic2A9UB_b|^qr|)@!$}kQEmFgO}gXA zW@NjiK=>VfyC^Upa<*MDQXBDIpt7O{j_PVUW^C)N*> zSnC_vHtgT9HSAf(P4>pm*(h^*_U<+YnG+Q+yy%?vVB13!B$95*(*AE<{`n7H7~54a zHwzeT>5JEWE$x`Olw1MC^r>KlD>4*y7rmWCZ?VQuR_2X?b(Fw42`BoQ@O_LofPHDM z$_*ix15_E6`P8@1WICfyUvUkc^Y_m4S2Gro6Q(a5`)Bd;XFr9W@jDmwuX0Z5xsqlR zb^{GDCv-MEd=cw~u~Ugyhc(~q6?4|Ab9%jZwlT<@eEqC*ek(tgk}HXI*USsD{bA0J ze&7Z46OVpaRJE9NypaiOeKM=*(|>(+&YF=ub3@3vVx9looN$b-;tH{wn1ppg13g1p z<}s(T=QPx-Njm2-)wSE)Sg*$8WgelIy)l~G((0fEd;aC8uB6BR@MHN+bTkfq?9v)P z{X>tWkNoZR^x8{4wynnOiS@vogMQ8yeojfe-;7rCZO}PAtFo^X%((|cgJImsH4HN+ zzg&JhJK@_Ruaq)pwAd3k=lhTIa_*T2>7rkIwm-KK>0t!@)C2SL*W5@q-~6q#O~tu! z6DbbJl|MvBv#NC-4yOqXF|JL8BprpC%dFjVK zML+v5ev0<*KS*N-r+3S4B<-&IgXcVh&RE$;uX@J^c#4xnrLGG8g#P$V7t`~9{W&z^ z?z%>zYATso0pDNw3;&Fs_NWUzK;KW7UU4-Y-Pp{me!lRw$$O#VyxuezRpjaw5u0^Q zD+lr%ljEg*G+I8>pR=XS*}8uzPJZM^M_&JK4^r#fbX-k*+vNG4Rt|gzjg}62)yRuk zmv4(LQ^h~AZCc-+%cFsp)k~`f+~-_+))Rh!e*Q_1q5Z*?r{DrO@vPR@H|VohUQL&M z{IAlF8COy=e7ZsR5g2nSfgYAMLBYq(oO&6yD-%k?MZW$j@z|tUiw=ASs9{gF%s4Nc zUTEw-lE(YcrI*u>{pjQ9k^k_aao%kJL+3&SNdNJJ?#~t0>FV0L7cD#Ju^CWWZ~oQ> z-M=~LAMl9I|IQUU=bW?Y8{fE@f9tW2el#uZ3xM5d=#JI3{s~TSx%t*ra*b#A@d_=q zjvo1s9zj=KeJyW$=tCY%htE2X_0frE_FHc2kCB>Je&c7JOuzC^pO~$~J)f~}iAHTP z&`-s%Z)*kS?YFPeAHDuf^DS}p&KTD0%yPk+03$F|0&}ozr7c@5rSSJA^C{s_-XW6wg%^zxUyfIjxI%jtp(&Zi%K++&!rH@!ul zuKc?%(8gv*`&L?(D9`@6XVN{-J(q5|}c#H=oOnz?oa4%{k28mt@>nP_*n2Fj7(Y6xQ|fZ0}D zwXK*FMXf3L0+2`PjlTYSV)mRy_Out9JLQ!tDR@gY@H1{t>#@z0RYhr6u~_2Ye4#EmJQp*VfkP?eBaS zGsej-U;oB8>3#42F#VGse-hvOXf&dyKIKVqJML>^V}q`}_Dl59%P!B31t1_{I@cLC z+urZ${(L;F^2yZS|2nO%9-+6r^F8#gcYh#>@xYi=05bb-bj^*8=oL}J`n4oDvijz6 zGbd55d*Z}z^7Wa%LJl??>&HBMjz}jv%Gi^VZc>F_gBS$j_rL1uYv{SZ`eJ&~Z~iiU z_x&%Ref##&65KiH!b41Ar4ij#FG?phu3X}5)_KuC<){}lZdr~TIZ7}8-Ph1>J^y)h z_St9Acs!1CUjmpo=F*Q`=E3>r+h~##Q~t*r-bDAl*SYkt3m-zu%gaF@ub45x>Nnna z6TRYVqi93vh=Q<*$3gTj*`?xMZ>GC_eA5r+4Bx zgt$#*P58>8rk}@n1fjp7{7j(}4r~88`?qX?Wbq)+{RSiWxWT+S&$v@tW%g_q*zIpQr!$ zYcHT@J@e`Gh=)Ive+Lid=9_P!cmCNWbjf?(Z}az@^n&aE?HB(p{p8c0LQj6u6X@{a zLwx_oj;+z^CB_%7 zxsG1@-``B1`l~C9$7sHDeMqb`YpzGuRF!Z?tckK+=CryeWFFi~Pp4qHd-|?gFkWDy zXt@V)MxCe>@8Rq1x8FgpeD&+;&)@SQdd5#bl`g#SL3HrI0czp%Q-G2k`e7THBKxg* zJ?Yx(zUz*;Myy^?|wu|4(Al<5fEm2R^f|u>u>xL!3P3TvBv-6+DpOYB4U?RgrFfT9JB`|A zD<4d7#Or^T)1@E%1P#)qDXL$6yAWOW@lSI_Q48F;tuUYfljRJQ2AF+u-USJn7!wBB z#>Sc^=$gt2=?_Wb`>aUck~L9w0!|dor34y;gYpF=2GW_nj@Q?YRQsDl47N^E8pTgJ zgZe%@N<$oTBS`_OX%N*QntNfU@xKjHDai-9aIf%x4lpx4vkShL5mV+&Pl*GcIdN z0F}=nfL1|o{Wd_iS9Zdd+Xtp`5k)<-3*VEa4mXr1r*JaH$IZU}I!WB7f^*v<9c8DP z`G>^&|Mm5QzCIp5zPYj9!0*We&^l2ypbo4(vD%fbwy7V~}nin6v$aK9`uaw_x0Wstamtr0a9Vj zng^a^k|#BgNaq*gksmI-TB#E$7)cazMaB^39kZyM>huQb&e)RoOstrFPog|A=79L_ zTx7_kXUg}97v&+aLjm|klF_;f%q@dE7L_|YRaps8#Oc8?&(=Gf^J_;}z5aQK4Rogl zx(hZsi}x;^sOJ2d$O2*ks<|0lSk4$U>~nWu(Vc^m_g(uw4EnD7qfG7H7r*2pA)GU$i_%i1uFinXJI$hU+EWjt_8NDEE#*WDfN4a!1!h;Qo6YFhu{*HN0`rf2{Uph= zF9)q1ciD{@NK5?_Zw~rXpF`4nbe^x(WOJjzSfTOqz7R13fYt*5WwR+}l8duxs6L>1 z=1C1E8USdvF#o~_9{OGb_4t^Qk0{tu*A+$&mB1|gm@3N6veh@in5KIY&m5s}e?TJ6 zcics}sO;wSxC&5QAdrNJ=U?*mk9|GE*J@+q7%#KEMWdN<}Xe8xboO1f$OybfP z^JsIi?BlE^$06!%IaxqGewO4TRD=|4sq6ZznV8e)mNXxjow|&U-l6HT6_{X4Sm(AE z=uYXGw(J%-^+Gf!?`HVC($`-md9zT)8|%lAdzeN`%Lbs87)%6gR+MJBRn}DI3?(8n zh1`_7=uSm_ypaXq*e$S@zp-;b@=v^dwYo6KneiVIal!L{%_U;j(kK3-ox$}aeE1gHdx;MsSz7)5e)Fp78| z!=#ZiWg%lx%0)d99T!OXJjVr^6Mfzifb(=qir_3odKY!bc}9@yiEqC z<$L&y$(8wvEJ%aS8ourL=1ve;xB2Ut_zr7GLCH(z(^~5D`^(GjN$V~Oz)YfCpDE_3 z`~?!VP2EPjrf1r++u%e|fNHiZMbf)`MJkOKg?(7WqanDPlW8*$WmhHUBylluTJ5Sb z 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%0AFkYKT=W)XxRVzefvViwUW{7 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ν^G=x5 zU->PxO$@Fm5{IfhM~FL}zFl}lb zpvi|)BqFrGcZyZwRqwY?%g4feHYJcI2n7v2T)w41oWLTPEywBj9}p6kg`KGHw_3k% zcR?8baqq`fwVy1tmQblhH}|s`hih%MdV)Br$V?#CbXDhXxf)+OUQuR8dlKY^&l{zJ zcw>5vlS|_GB77OTehWuw2@ zT+0V(5sdO8hkjlxc|cki%*0^Gj?D8mdv%6Ms=w#E;YZB{kuEe;RX=fStTcR8ze9WQ zz-0sX*)q?|n^(P6c(UsXR%=6hvH0tmQ+5Ba@-SV7r>_1z;X?ZNO)dl+;2dz4)0-c; z0sSYe&A3xFc#_`NMs9fj?eNSorA8bs@+D4MviXreUc}Sv{OmEh2`9|*_8<~5TLvmU z-#MG+c949v03G)&#q^%I8c4O@wRr=u-wvX!?B}b1r3KTw2wU@Ot@^uI0xD~grkj!v zHLpo+^}$}9nM#GI?oSV~U~pXM2RARRCw+INu8p{FGF4w@+3H&bK@C++_v6IT zr|*;B73zG<1|G_y;~CPF(qqegC{bH}(`xgGS9HK0SkyO-;kvHXPYDA5biaN-n$4yZ zhAt+E%+>;Hvh362e)zyl-a!}|GgSTZS^(F`7o-;fxG-X%`Vui9fwRg%^&Z~@gd+qT zns3S186TG&uf7_%h{ZzURCLU532*6Mpx#R*64>l7QSpV${@A&W&rJ)d>q!sx5E`Bp zr^~2>yAL=2#Ub2|aZ4ij06KBXR*f=t!RL|Q>jM8cus$fn@nZ9>&pm_TO^>boyWfsE zLAGF2e#SO@kyt+8sw2{Me8&uaTm>a8hY(u4_pB28F*rvzcUxmRhLvwP|LhW%dv+<} zJFm@k)cP0E6OQ{S@!>r%RuWX1qxPEV(QYM|z+Kzn>GBxqL>mfim_YHEx@p;Bf6(iX zhZU|?1!+%PZ49wY2IgM&OPtTu7e1BDW+-+VTY8@lRf4}TH4u_a%*KL{_U^AiaXydV zfLx{-@^Jc=^mC1Wrg^zA3;RkGf;@2h5rH4#{JVYs>aT8p_DS*~KjTgW*)-vxpTnx8 zTOfffnr6gphe}pUI!|lFtC?vy5F6%M170g`&bCr(0!wqBXo4b zwqXWnvJ+m&39@?ml7d7P?s)Q+Ug`l9DvagF@_W1IxC=_{eFXOkf(rX>Q=aYM zItOugH3bpvuSdrN_JlY*3mEUk9TI!Xc({Y$32NG<&jVfCI~`y#*wQFm^bc$6u!erp zH!2ibk)ZA4 zkv*fXXwQa=eq0`VI>5LhAPOTqb&Wr(ReStzb7sZ*{7)#VKiPf~^Wqp`;8rnz z8c)^#mkH%LUnIe2(!qXK#SQ*0A>_>`A<)4Ci_M>#xvdfF($nNWsaA*WKo7lND}1EI?!I)xOw|`v!GL@HN@76Z zL#)vy8+Vs|ty>;D*yfw6t$>^eA)1B?J|{mrA$MA#AnxxwZSYzvdFL|RvG zSIfp9>5O1=l4Z>Rxx$Tqr<+t9itHTRyki%&c#NVs!^&A|Y zhxcYYXjsX*b?r_XFgqMQM=d_dldBbF4w=wv3Ipq%E-5)|4!=@^gVveS(^9& zaU?#WGb09BB5q%%kk-?_K6gJ2>t~y?47j=fT5iP0M*A~raqMg+CqTY3dqB=45|?yy znxmTb0P{jI2s(i(Bt?gfoQOi!BD##@A^0F6Y4Go34bb^RLunOosxq)A(xY~kxmaIj z)K2sAfs#8@^BzAlb?&d1K_!ElUE7W*!Otrwws zaog2AA|11Xdqy=3%10?2SJ63_(RY z)4s#v=rgW$WbOsFiH9pp(_>yacpv2VvH$353EO;!O8u(a?q@bY1^=X+W%kLI;!A`6 z>)K1koG&YJ88Xt&K~mNO+Z>$sLfKf|<#ZIH4JWI7T{iV0daUtvdn^e~YMPZkGhB7N z%p-4CR!e@YrNe(W{ z2&zB)>U4$)_&Ud`!Uhb;oReFbVs+KDl55E+iSJ(~Yjou>@z3y6A}D zJQbi(IaO>}@v8I@>XQe-PR2fAW$dXJEDu;2(uk;o!Bs=8Sjve3SSU5v42Go9jIr-+ z^y=u7IX{2gl#@&Tos^0DpY>h2)KPgAY*s)DXc&RsM6K_P#sZ%4jLh^;_LD9zNF&Op z!f@MVB2C|p$Da@{7YJ9&JZbT8ouAp&(2MzNhCQ&o3dtL(;o(3-+2#Soe@z8-*Q)%z z@BgACsm~%I@L~xN`f`=w94`Vo&QlZi4XU3syLL1)+1{wH|m9q9dCV zx;HcRkk5{FdsO-6ZN?uazG+_9zUEcOrRLL6mSBj6(3V+1ar)a1>n1;)8``gzEq;w) zindrffM>I#)Vqs-P}DW8(dN%psmHe^@`5uBF?DWL$$=^d69Qg7kGYZq@FBpy1~35nvlitKFQt#5)Etd{RLp{-c& zep_K}xmqI_{POzAv9+%x&f=ma2T^JJrxeHTc&N%!89BcWlR(^nhBxIEI0(8+ zG8d%uY!|3zJYdFB0kFD+q+|p$P!DKf!8bBq8?qjk6s9Dkis;aL9*JG+>{^E-R;Ciir4y@gltw zy$85ONue(B(m!%V815pmGSt$hLjLpze*Td8p~)LlQEBmA#Arky`N>~x)aRAI#>=$} zcLh7WUX>-CyH?zu>~_;Hn36I5w-A+_%aK;~q@m~%Dm?AAeYR|X*5Nvz0p|qJpMEm| zApOrbuSKp`0`|t4cQSLnb#Oe<2;_a+&YKZ6D_?ndvS6HQje;Lz$i6N;rXi{fT%=Iw z(en2y;>0BE>8y@8yNHhuXit7ykIcU;9yuP~u{Wt$dH*0` z#_lDKMV8{+d?-+!c=i3uH>){mHg0_f5AfDFQ$X{cfr)i%uBd&E4!E`NbStjq1!Bj( z>pFwsbxYOYA`Y`gpSO2A0&CO(8b)oA;uO*m<{smD1)fQex*QJm>wuXJ93KX8qfrJ5 zAp;}5R=e^^TcY1CJ0E`tzwv`EVZmd;eovVMki(^Sx(>>s#9guSX)7qca~`Tzlo)$Y zg&c%_e)l!pSQL)BCI{J^#$fZV)#(IAU|*em2q`2ZwbcFnfR7w)D6Vw^lcSO z&QLlzaW)AuaUa}Uax-cAXu0z5pJfPkr~eSiRz5q6>F6G~uFojV4Y>X+e|0)R7*5wP zxUf=cDDGntpiW&H;yz(-mqc?l@Ag|?QEr*&4d9}8PFy$XDnGQOnf0T(6UOPQ@bngT{y0j~7u*M5?iQPHZ0{A-z$#X_T3;IzL%82b!rt7= zv!2!5e=`RC!UNotV6jc{J-1gYIxdm{ou2%fqu;TI2Q2?yhIotX*Cr=rzN78=cRYAP zOCQHL;rNED8lU`iUmbVizoVJlx%PoUikV{JwJ0c^zf>zV9X=3RcJ#}ybUyIi#fc1O zuX5nsVn<|3Uj8taC;qpSWPs$!jUFU{3H?9sMZQ}#V>*W;H&*N$5-LA&FP+74{(CAu znhTdceG6_U0A>4Ta$%p|<831I5PcGc`1WZ@D|!5KAkl=o~=GU==1r^U^ZucXVro z94VYMlYSFdqyN(r?xdcZN{!CP_f3g5w?PQUh?*}&!V;b-jMvz`tq;sl>56zLH9=@v zAcVh_Vt~J2k=dilwlR5kWMs3xT&?@7WH&r3-y+6AxhHX(YVKdn^qv1`0Q-RNifPTf zB@&4f-q7eeFv-)uOQw|2gq?R0elzPf@~Tta+#hU*qtfgi}uzPoMN!3{$a z+=6qDuye(~qB2=rU4P(Lnaa+=xbYzU<+ut-$1O`Wpq;kJ5Ey0#i50a1v$cam#RZL@ z>ll@NkPKt*{Y$=8Lw#>mv{46bH%w|%Gk7vnj5l{Tkf4=Ac)^i%4~h`u@nLI1uLN~V zJqdp#&wvMENAICNk8Cm$F_VuwVj_7VcRdA->i9_iqGyNpkNC`ngr;LMv*cv4T_-re zv%`YtyhS@d1wQCKT0gjt?ti!+wST)tsd%9qq^)K|E9dkCPUTGIo#9F6sCmb%%xfM)+12uqNrI8;6ER z)fBaV{-{6hy!Oqxo#W1ma%WoU0?$8LG!+z*KwiqX)5^cmpZluyh!Kb0n>b0vkj#Ar z{vvbc*UpchKw5!Go5W%ctGDO#4h5G!TnMC(l;iEcnf6eF?`IT>O%1>)F3f@QDCkKt zox@+3ya%Po@AM;F|K4gnzxz_J_^}+VcmBSA$A-d1CvpSb5PBN zKRF$-sU3H!cY@uD?fN9sQ~I-5t0$*XQX6l z7n;=)DDc&t^Bb4jOfp(zQ74oi99kK{3i@GIbsT8_PyVK$lB)iFU?YngsA_JKnC3s( z;<7)#eB4RcKI=_sYUS@iU5k0p-480!qA}-UZ&}{?tVA zU7ZZwX->`LM$wG%f>VJmhuxI0v6p31b@EhT%ix~Sz z8_p39PQKzk(exSymh9(tj1Kr}LPE3Jz}A`r#fI@N;dT128|`vMVD^g}HB1Oic`&cp zsN{jFf)3V296ad~?ax`zKbAMeZNfR+5RR(-EUFCXcdw{j4F(2UR%PFqLXD1;_o)s; zWX7|K*l!TuslsitZ*CG+CW{~)E2y-0K=;Ma0XFiWiRyE#o3UkW$6*Q!Kvl(M5TIcFd=p zbta)Ji4WccSj|P%iAgnB+@(#dNAN~KY!{X@_SMfk-~^Kc1iNzo4XGxou_^c6r=BWC zF?FMEA5cGo9%X*!BJ;!U(FBcxHqC0Yva>=-nVv#KHa9GhW>WXno71usKtDqW8gj6p z&_~#rO;T|WYtVJm1#DG+3<$HyyOi5?Wf;J{;M=+Y>8)ts-=|yY+CG6`*w6300lK4u zY4{bCQGixePpk2gREe4M3iFQ#b&B%pKF?4WIwQ+h8T7E+J{8!&(-R!fC|O>LR@r@F zIl#AKTQ+$ST?+b5#&on4NUhSkXb6F`JDrU`BIeh@*!o*)@?IzRhVvr-R9*f-hD1OLFxLvFJCPj1 zHH^9WN`f*%SISW6)eCs3A~oW1CEd&uF)gS7CZ?UdTQ*L?!LFmJuKK*Wa;zsT5B%39 zlm4(;5zWtk1dYUrsvI0<8MMPS`^9+saIX0bea^+kmX(0|VeU`kn*04V9ieH7lL|v# zOxMonogC}t*8c|(+L)og=u_uNRE=0>yGg z;+uLGW+k~k3!|#XEIbcz;T4qhDR#jlV1Kwb50)(aJsTWIcJC30)VV@qLnkg!#Szyi z?Z8XXw;8Hzf77RBwAkbD)0A$U>j4&r%)0#v6z2VF#cq5@Ls^SKY@L}Asa9v98XKMz zLvPquFgP`3Yr`fP*SuHyDC%Q^Z6o`pF%!1`;)^=6M;3~rQ>xVH0Ww)BSpfP|xCiqa z7KbDA?Xi^58&0Z744ek97hboAsiI7G{V5P++VE!2%SjE@9&XT>xU3@uao(#Ev@jAg z3ksIc@CweNWGYD5)en5DU4aP@cL}g5S4Hq}0}me9(O<4R_dB2PF!MNBc$k647h1FL zzkRX@?{*wHkxANv2Hv2NA02tn@AuNQ0l0{zT4nI;iKs2UMAEA`_+;|UnMxvhK!<+ADWm=u4d=E}WF*1n4A1p@Yc8EHYkcf z=#U)x&j&t?LYa}13zCz&kJK$W@O}6ct04cC&61R9u7B#NmZ#>2HRwM*_ERW#KFUwg zDR_IWGJ&TFD~|tTd-+-)AG}frlZtH5O>h!wxc!O`V;$2AqW|%tk-}_W_6^*SFs%Oq zV)`1_s3_2Qn%>9n&iDDf`r}Zag7k2zzuGQ}qb@T-b07C#Wj5iLg?gqR%bvLTGmzz* z%x=`x5UQd6AnimvW6-Q?ILQ@)4?1<^u`%F26`fcyafFO~kNd-+Gcxa0kxMvzbGrFs zMt=T^40PHQ9zgL4JIOTl$HAF4oUvUkg=~m4sm$-Q_t)L#^0Io9%^H13Yo$hT+KGcg z*OBMrMM7rhDK)e6!09>S!5~UAw@>jv>-s;+N002TRK28)p2zpWoD37rRG7wtbDnR3 zTwu&WzSO3noVswZ&D#tL-_t@Fd}lH5_)-9^q$J%saIxH$PK0KzKG|Wx&5*`@=H;58 zJjPWQ0pQ$D{JM#OsTOL#t?N7&5Ua))Q$BI`D=|_I3iP8|ai+hUVMMW;N(WI@`7Na3 zpCJb&9}f?9%UJXqFAT6hcOU;3arYj2^8DcyW#(;(!xk(1@%h)_Zw zMboea=}JZxu$6pxN$qQMsxBjfy?%3;Vh6$butuSMPG6HfIKN1KH*)rGiWgTB3v$0#Fin^#!G818SdgZ zVCCa$Lr_HANKXMDas||Z}7=_0l zx?b!LSqpvR&VfpOlcn1iM|_(5^8Udw+OU~h;^*2PZjDvz+|YSV1Yj{ z=gjOoJnl5i)PpWQ3OuzF;Q|3{R(HTnvVOs(=U?rV7Czeb)YU222r6iXO~r6iZ=kk% zZO)D+64qmgP%{gQ#>-DOR(w93y+D=kt9EyUg%0{A&_bl%b@6*G6V+$JGTV0Wv!hmX1 z`KM&+rjr;L%W@8EvJe3Dv3&~DCLdm?OLtHmc?G<1u+4829J zK`RylOgtp~Mch>y&fz0RX3%$?IsGam|7h`3SxtM2WYV)qh*oI5Ex*&0b>Cah*D+tW zhf9*;GNremXnYpn;eb!elLu(!1v+JRW&OShW!i`o6yCv8`8oD+e@|q_QxCQSU;Y}P z-+n!RtM{jTCU}Mp7vJ-Qtid75XH0?x?exvLSL}Td65kKE0=2)pDf^pl-GORTv&Yf= zJFXF)HdT{Hk84=$gNh-62U49+6h8y6B2=MB3j;Q-0Hob3c5{=$*}8={8myL=&B|aG zk>u*lg*RXtUmjmqgI}=+7m=QasLSN$0HoD;6YmN#;1@k8<9ogiCpvs&EzIt{{n$w# zChi1VDa%;fv9TxXcL)&3N2Yx$>Tr>^KQ0)0MqGy4`7C4@(4A1kB0YZ<8zO>)BQMR? z7b-BI<3@|SW0f4Vh|vm+uFrQi9Vq{K3k+`j>qV?j9dGz(ZlJWp=u6?j+V5yqav*(I zLle|smV>qY3WDI=<*44}xL0$!{rlpzn z4b7koVj)x3kJzty+mVgxTt0q?_DZ)(>B@+Z)P+Os<7xZv!=wUKVJAH8>4ANtH`eOv zn_cRANsKIEIV96Yyr=r1tG*};`_Y41lbwSD=b-@T<3IPiOxgcbU?(W!X$Vd%m>){t1sDyM4$$DaZxnKNR~-glyF=5N@o;f}A)) z1R>dZ`ONw+a^XA*u&lOmij8(d8+vh5;@RIVBImlVXg^qnj4ZTb4${PDC(^Ba{T*3Q zkFa;moZ#{XjvSkxT%oJ{#!CN9g3OmR? znitBxcg6vvP-c;b2b8Fp3Z$WIbeUl>*jDrLo)~qH4mMYnTB>|Tqi%qQRSp6i(HAWh zRUrnWvA@ynMCRs|f{P$Qtlv_}!r6{Y5GA9N-{#g<=2HB1 zzS0j3nYVT|9XTXMK0jB(0Ew-k1O7m8o^Wk*jHw166EJU*ZtcIT^s$N0?K@v84{8}s zz2b~9(Zm>JNCl$sScX;(GRhbZi;+DpUxt{b-aR^$piTx(Q3)h|&!CPE4hit5eA8<+|oqJB;S9!!YMq9~Q_es|tY1 zA$g@$Y6nom=beVn%-4v8iX7)p4lj?cB2^%ycaGU-7!e7=6if&ZxhFZfclgw-3DrgY zfj-kluTlq&HB-{g(Ksw{-^Lb}?7-F_)t z>&MK;fSKB8I46qov(@T`B4Lgs!4D2>;&Sg7c=j|5J*AjpJEY=xTko4yUyO}Jovi4F z=pC@ngU;@&0x@$$q6%Lgp1;T>g zJVBk3hT~NY^mw}l-nk1*QY&_z5xke`U%l4iO~Ukh3;Wn$?diz|6GkgA@3fCHT+u=D zdy<8p8ZILij?~o&LW~RaQlCqGQYJn;Yx+$#Dw1>WtO`qOkm56X3>4MzqrL8LeY6i* z+iLQZ=r*F$8}fqTDFP*V3!W=591{8OCu5D&^xk$e;y?2=RfLbF9HHqF467Pn?tTSn zg1mloUmYrr@(PG7b3tV^-k5tQ41aO6GFx``H5t!Qk@{dwjYbygAtha#`%!m*CBx2(7@Xzl`> zAtG-=Bld?_$L9?T&^*rNMWBm<{^1s7C4KoX14TrB6One4+n1J$>`Vm2-@jesL9xYw znFf&k`fMOjmjKXV-d-fWiGI_4zz^*RT;UV#MWsQj20Js4{3y;kQ!X9f;tpY9`hL;2 zat1VC&UV9WG`35q9VJ_;Y@uF?owmq+N=^Gx;%6yqL;#C7x|; z4TpgFrocF5`{c3TV&@u})HMHRb@fHoOL3}EwYCUPsep}_iu-r&s7E`1Z+pr{wdLDq z{U#C(^knD3jt1dTJACjjH*lH%8h{ZMK%D?XMCxNNP<~``9#Mb&6r86(q7qXKY;J1) zn6pcNRu^|{aAMK->iK4C?(I;drq_Z@$LY%cV_Iji%=VQTKHi_JWu8X)%ovDh|9|H~ zZI#&Cj=QKGv$7dRf27^50S^fFvr-U;?&Z&5d0lOGN^EF?D}?}AmztnUq8S0C?w!v& ziLF%vEas=UE*-RBSzkd*x}HP=G|O`6e(DJd9c%a8>fgNu6S(W|lT@_W1pw?zI*AWlgyz?lxj#|qsTEeNu zK(RO`F8hAF1!P|RtS(HgzkFMOI`(t^CO|c7mx4&1MIm}F$2a$hA-_C8@FvN zxe3kBm2l;@Ww@!RuH9>ru-n7R9=w=kJ<93SqiU2O?4Svb+O@3pjxt8&p z=8Zi%CCVGm%Eg6LiurfiT^7c3e#e_NZf&|F=@R;DqduXa8fb@Q>)t`2$*brqA|A9USX2DvMaDqeQS zWMbu+&19Cl{b!0m3*0-Kv*U!7zO6#)yXy!N_mRmW2{c~=FgoV+DfV)+cY$3rV=R!d z`-&PhsgvND%_2?9Cgr>+43mK07jaH~b0@_hUCz`NLq_QeNY_!}3|PMdmBtSS2!cLk zG*l&PZ{!XnHZ|j%KENxvZXGsIxhS4KII^bmvIpu1U~a5fRXZ@VVyv?=6&-5#<_KQ8 zQ}&zk6BlkWC^3J5dQ-?Rdac3ZUaaL_zx(b_e`?3@6t|l8{@+rl-Gk<$@M({?ZJW&L z_Y5IBG0V{g0nt}^)YG+9QLw#$2Ax}^yhK~CMUEF`08mg(@`M8moC zqInyOKAV<=U}r@P8C1ZCiVvUC;hk)S_}t*=r?;@#I-Ss8k4x{D*l67$%;2mG+i0*) z6B)1)bUS4EP{s$%^5KC6!f*;rI)+mJ`|lv2m4o9$n=fF_k@f=i*Y+#JIzQuNeaA7Q9ul`h-ydICkOC@=TdoSGubq$i z|H`Wp+|*8htWjKG6P#DL;rPApw}X0r4=YWEoo1wq;ZNHb^tflU9W}*H%T+6l*gsS` z_?H(OU&Q*3Zf(!xpZ*Hm&h`ZF%DmX{{!`BAlw4tTa^f@HIsU7Vk)lICFW!1mN7WoV z82%8r#It(WV#WptU#|Ua+xo+UR5rsiW&cVs4ZcOU=A~SC%+7)U?!mr#yG57a#HJkZ ziGnXxrakFq5epqNDeDB9qs$Hz?;bH?*Obsd?E0syCU}FY`gDvU&$x$@9`2*Md!Qkr z$O5*mkpU;q)|qegW|Alry0*=*!9I?fnbKe2(z_>s)-@@2ZXqV^l6<%d;BLOw1Y>e@+dW~I~M9GKLbt=maqy-gh5 zxMy2EOK}|WK%#Wps95;Rl^S*^f}P|1#pJL^M{ft&WIsZ`;O-Q`*W&3id??C>Al#6y zw{YLbPF~Y;YWzt)D=qxHUWCTQH>N_?H38}7t?gCCsMS}P8)g!Amxe%EvEu0Dc=`$S zapycRF4lMH8PWuds9B!C^Y7+0Z{b17&ATnA+~Hla0dDDWJx+E`WLLWRsi)-}X9#3( z?T47md%njzl*cjrk@sOkUv5tor`!Gy^r5Em95sqEzy&(SR&Gvi7HV`co-69AIsudK!I3kFz-Y*? zxu@mem)Dqr*a|Xze&6E&v;m>ES5cU@SA?#bC!LSab1vht$w{lrhcM*k;ZO9Q6y~eY z>O)SeO}~qR5`A4?%|KIg>Yp*3Wz2V9NE!1Efnj_nGG9(|rTG+KSGWEV(&Px|I4CLO zsS%gZbliDs#LdANS{tJ_P)0is2jOn1j;XOE!QL!jCu6$sC81Kh6aNYVx$!R^Eq$yx zEQS%};Pj~Z&gW|nlIyNbIjuEH;N?1FzX3c%z3)o&S?)YXpyfIdVA3jDq{2=pWto}C z3POxL?7)6UR%^O0adFJ35&$Qrf@}c&FPM)b`bO@4?CWl&9Dk)}DyEm{KO&+I4YZ5k zaf@oRv7lbQ@W@f$ELtIWA58*;;fz7L4z3>G009U&<4V4D)f11??WZEo`%-$A{;co8 z^t^tz|I~kH8+SxK#HT}m*57boS7dtY<$93b(2Y7j8#Ic1&^EsMQ|q^)`)ElR zboug*6*K0TIF;yZW;uxdyvnS)pOHc3I9zgRs(3={-+Qq=IhvX;NW&yl^o2fVLa{T= z?MdigC?)bcSuA#4|JRM-a1sR#%mx0p>XIa!gxiWd&bAtrJX|MrB&@1Hr}Ov`MZAi= ztQyse$%%0jRH;H^vwd zhp&JG=;4<7?DDb9XIdkxO>2_fu*YleRnX(hRYgE&%DaY4sT?#$Q7lBS@oYF$uId5m zZ;~q$b&?A;q%r7tS61tediDiO(%H1K$%wz1J;m4YvT}L*#0+I6iGo(fiYb}vds{-T zDY$;`u&-yAKO=s+AcbkPDj_H3^ukBFI^;O*$D#b-W4I_Ou$`Ntb@O8LJj&-sM6$7k z^9~z`odLJxQLe3PyZuPKW67Xr`sp>vBPk84M2f3C!ty_t`VfjJNOP0dk+v{#e5}J~ zfq3o(dX(KXm_NJ&%GVc}fOs7dT7<2RU+>3q{a|IeYBw%$WeE6*!fwz@quu-iuZElJ z&ziBXlhU{&f!!~r5Fj3~b#ZkvXd^`&_1&E!kOjBb8>4CvSgYjS1#$~Buvi-iQGl%8 z#DfngU9MJda-o$u&<^oR=_2fYZXtG%#*>dO=4#vRuAzOdctH?d5ycc%OBxyz1=aeG zvrGjR4X8k~d^FN25flJ{b>Kbelg&HGOmCJc9~ENR2|+^Qnb&p1Ssd*|@s=?siu@bn z_>o<U<)D+WHjuz=<$^ z`Tz*f6Ym9k&6h;0w3U+r86^5e+0y8FN;Lc8>Kbjia`0C1HCa~~=aUnE(U9W1l&x0Y zXQq(@gt*R`^_p^3deuR<*S(`dw7~c`q*TF5JEy0$fz29xg54BR;v28gP#IR%`-pnv zV@BR3coG7%h=c>}ag3wtw%t@_hzf{TE@q+jYJtd-hVt&|&*^G{wDVl;kCN{4`c@Y0 zYq1>}cxrzXfZIE6LJ|lx>z7Mz~0CW*q(2)~+I8TMsY!@{irlWd+uc zGF|B~(?gnVqXyd_1zyS`G_6LJ*QY+;+$>Zbgg{YuGp(z+(m@`(`s7aewcY~}(liwe za&_39>-33uy3DKt^<8dHF}GHTe{+sm5NWD%xPn3lH1xZjbY1+o_fU*rr<#eX!3hv}^>5X!#(8qk>%S#MJXjkIDSeAFQ5J+|2W z=8bWg=HHc?j<_3-VLot7%ykxD9Mt4`lDFddUe>+jwCScxEM`{-=y;a5OuD+aMCOY) z-y1ZlT|xW$-tP6S4l2xb{?qZ(cNP&cdBUKsmb-c5)%bVoY_LTm5AvrbsJYcrh4bHJ z0AEwE{F3y-D^%8?jL-THvzIj4lNWIRMB%O7JExW59w_S8nK|h01|cPJ2`A6+L3vZI zQtSA)RnWXAk_WGD=mv*hjFk0r#TLj?>i#?)JQhwTwc*btB{h%3B-QQ=_-7t?^Ea|o z_wAyn3%=53&P^JtHl-cLQ-Lu?3#n$Q`x#|bfX))Hh_v4b^TV;JUu!ARrir(*dFL942P#6)px|B{_TsSpG#)EKRS0)epN6ICfR=W1p*> z@>W6J;Iq+oF)uy5@yD7lMbu?G^yqHpV!1QWDt1aqthTrUiy=kX-^4BUK%2=_Iy!>w z5+~9(>x?+Y>F%c!>Y&FBF#!Zv%KP=Y+OCda4#p!D+1c&a=JT}q3;m0hEd8#_^8#~C zWMo@9yJ7&EUuWTE(YIK_UYXoGzdf*;x6d5Kn;J>2h$ri)ZKf=_Hvffwiw}Fa_MA$% zY=nu^$|?i4EV6JVt7HhQzmRW;V)7(6$T$xAJlr9Locx2zxc_U~poHgui+1EoI`W$Y z&nwi8+TcB2D1;qo=kHuDWaU z2j{mUnfjjr-!yW6o$neI7|kl)x^!ym0a!8K!w=tf-F<={t)E3KVGuQ=(7O;I0xlR( z`h#tktnXAY1da>`FP=s8-?bYMU z9@Qw1OfiARt7XfI=0GFyEQqZlFcu>gc=}F&Vdu>;MiFaJAj&iV)lCAN_qn64+WDob z_gYYXE)(}Frla+ShXyLJ5fxK*LUKulX$%d2tu0ly*(nz8klE_{xAY;HLUvGFc{ z#y<#FL$Yf$9^q!Vq9{})bi|G4}GNa9B&Oj4t}H~>f4~ zYiyi!*gOBy(54$(IcXqF9%INUTlSQc*qxb?uc@0B&>Of>m%qyl%ApWGyUv^RhF?ryY_xvn1S+Z|4H*O6jCaoXhBx=Ad9 zcXk&kjJJy0sOZcVW%c9tsy&S-uN#FIq`5LT?W*|F5;l55T|B86AY~A?)0YkMD{eD| zxZoS#w3Sukc5SQ>>c3N`L*ifjq>jaHN3jU)hey$%HtDf;=8f!3(v_s{EpcEgFrb9y zDYzFA77GiLvCs9n^yd6Ks02RL`iAC@7MUIj;5%`pzRg!2&64OP4(aZH=8L$^SP@R>r~)w2J}-Ijxm};evx`*ne%4%+-3rpv zeV$=I+C0X(|1r-w#+2kVVMVt9<&0|3C=g<}`H(v7PtE#Jy+K zuxZ6AKZ*TJ=@-t3?3V`;QjH=SDGAZ3QR6ASV}jt-G5pMC_gyJ{XC)M^+g1H=?|jxq z?qi>0I7nBvZgvIUateJ55n4irW8$Lm*WO9~_|g4GN6)!Dv+jcy^CEgbWAq05=6uMr zpKve8Pdw!H&|h~kDhs<43`u)EElcA5BfZ%fqSSVgQ+1TyXtK9;fvZkUDOCAEqSx$u zYrfR$gYazW>1sgcALu(3#A)Vrl981aeIE9)s(+=|_mZhy6yoDEa6OU@`qBYZ-+KEC zonVXMBER14{aAUW>6^6vbLI!)GBmLYy;jxxW34GE(#r1^*?7K83heMWt$XOV{cCv3 zeP7k-Pqi;Xvug+7=|)|p&sTi49Db$@no(=3o;8D4#vu1xM5NMr)==U7nCn<;z)^MC zE|ptDa}m!*&V?%dx~5u>PJRc8rOg}c91?4F{Pv|;&U{RP&7^LY@vNX@(H8aou7S|Z;FNn1UoEoIm z_4+RdCu0efoj&mqZBGvJZ$A=bHM1ujIP4zxyk7&m4S;kI8SlU9pQ~{v)(5Aj%^g91^Zk z8*LTTyzS}EUgPl9FIlk~UQU4*s$g|VEY82X-s5Z9l_uOL_UdKT=P&I4RoPeszk8BI zpSJmfu{pF`mtIcjgF3dBW31DIRl#n_x%^1ffZN_etvcpypo!#NWUf$hN(gL`!=m|N06;n^;`?F-9z#x%jD8McEvqO>QaMw$vYS5%k5HkXGm0U7GIR zj0Z0?ZBBc?M?!`u#cwTM?J|C3sG}8Rts~-RdAO=l>qR$6s|avwIq|@5PQq`is+1z- zt;~)eZ&avBmk~UH&w6joyfA#G7eGPAO&D3}&F+^0U+8WSI#L0GV{5v8D^tXb;jNy{ zXCIJuYyhG&h4r?(^mkXuu$PG0-$j(nzC0?jK**R$JEerDGN}o3NZuf@d^|*c+c#L@ z;mIDsh+t#M4^s;Kozf6~#Fe2g@^wCE1nEHpov_&qU-rL~Cz!#9c#lfk?L99w@yyqQ z)+Q2)cwnJpjUV5{CTZX|VqDHsROGYDnlh_A4$}fp|09?wHO{FnAIZ~()!%PsVI@S> zndOQK*-Gu-zl}uHcA|G8$WS8!7ft%~lWt1vai;f?6u~2<^g8={s>Zo1RR`~u$bP^2 z)bZTowt+Yh=0^$$(0W-~(hY!HGjL$xb|0IrlX*Kt^CpOLQdd+)p6UK00pO1>`f{P) zzoC>{kfgNpSN&J1(~g9$BVCT50h%io%)~S2kNZqY5T`Qh8$7tCuxh-?nw)W`P)e998|*4$>Zv$nF;+oseWHS7Yk6Msb${;yDKq$ z1lvBR7|Y=+4fiwZe;1Rd!KRG+THS-EFV^$B%{lQMY}H3meH&fx+-%RGrWDXv#s-zNriZiX6Nf~xDv=Xto7GM7Vm3;=(~8aMeSvDpLnsYGXd&o zFWLzqYWT-hSs7E&MGO28l~$^~dP@%Y-J=gTZa;0KM1?A+wu6(+i;e#>Zq0Y5Tv%R( zo3xlkG`x5nTTKe|Y=v&GIu_fE!vpny|47EJqGH%}`juJw-Az+{k+FP@FYymIX+!v( z1FvYJUlc!}`OutCq?$0#u|)Yd&B*ta)CQwUtWF4#v{9AkgVY z6Oz-5orUhvfO(Ut7Zx+Nvk1H0RZNU*Q*86OZ~K2Aw8#SilkI%3^HZW8g z4SoKOk;$~L?)kd-ig_jcloiABDae#O57QiF*m0V>+*5S52%SC~ONYSj>)U>zF?2yN zHHBC0XE4jE^4g;yuey?)zBi>Vdz5hNywgtaZ&{NXQjujA00mZ0{GhrNy8NTX12B%4Sy4&%f zU>bWw!}d&~_A2MA4nGx;VS-`fdt~pc_l8G*@<7Df|M)3YWiQpZ%RF|z%@Mhbzs;St z1I{UV9v;la2m`%o<;XbGlJRS3yc#NSU|rFQ%O{M^y=OsUSsZe_%g{CqnD@zl<1bD{ zl$MR;h_(Ed;nzq$i0p=PM@EEDo@DK$XI|8VkK|5sB}ST=pA* zGn8Kn!FbBbCKHKc&%s=Q-X_mV{>I+7(uRZmd&5ts!8i)q$G7KMobR@t#A@{Wbk)2& z(xq+WLiWZ;50Ocp?CNW(e170^;GR_^=y+8>uu;Xiwq`AbNJ~$*#W+^IW(8d8iJi#3 zG6*j9)DR=<>*DG%^-BC=>tKQaWt?;zVB_tx_b`&~6`~4>Rmv2o`V06X|p zGbf%*F*OFl8!mP{KTY4?DQcFr?|rCR2{#dHy!DUaO2MsHUm_9W<*K z_x*S7q$}=KE|#2)_gU) z)ER`T)&T{eUnBAdOn2ZOCJM(<$m=-&K#t1;rQY{d8YpbS2i~|_vw_tVU>5BR_$ zBCu%UBXPZHq34Uof4OP>*SroN3tQ#$SX~g<$qc!@H#(J#A;v|;`zAkg!Vz8um+wuk zC)f0z&duw1o{aPJm(Jm|=0%!9s*l1yq(r2C&uHdSshh&{tB4#0&0?M2dk2}<&>SLV z*yd%$S%p-=QLzdI0=lXu_H_*I!BIf5tyPt@)0>S}y|^WN=aPVYX>ePRz9vi=eF{5r|EEd|T6?Y2EQN1L;r@fc zX+qX3{@B7{{10UTORnfxk2_bLYircZa@tjs-~PVK2r8bqdut) z$IKNW&L>rhqLVn8!Q$5@k=`-h^@OXJ#2rt%?8`h9dha>^8%bYcAR)(+0*t27YP_oE z2qmkrL%;Xsv!;(#S_=%|iO|o$YB1;>nots+?Rp{G|K>h=aN|2gM=8<-Y~cbywyt z8waDQmp2mTA5LSnW|M`Z+zgm3F12XT=rD! z&2oFbO`x?Y8?4wi^nCP;ZSq$6&edkYQz#!!&+@!6N2}S?=8TDtL+`k+FS}cMLpFXb zpX(E3Er>kXFR~sxH*5AKk~uDLtxuj{ZJiP8nUep7pQi5Ghn7+xvZ9vpNeVyismFiB zH*~Tb@dT}Z%Y5lWOd+t&JiVkq3;n>w0nVOUoa7{1tnj%H(V~=saLF6s*XnC!&^P=K z6ARNN!#se)XYc8}5$hVY{dq+oZ&acLYf%5ahs^u5ZtK%L|`4$1W zI)##qBVdlyf88SKaQOWtK1Oviu@2N82lgJm966UxyS@Np212UK1VPEbjIo1`r~B$m zC{DeR^rL_FnK&*H^oK5}Iv>CiYG0^8Y5AqS{G_t-C;8F&@rCHGy&pfI^vq1{f}Mvq zLdrgtEQGd;lWz# zS74uihcj3Zd87fH?`0wO6m(`gx!@Q>N5^hBu*!YyQ5+ccxCcE<`Kk13#wQNTe2K}S zRxDW`qK;xyp^m_+jvbY6AaLGgQu&ouSoiIuEm}LSNh-7k7oh?v$V$TvfIBVQX$2 z?8lV#Z*NbMj!xT;(=ry07icgH9owy1HE^2rqnYvvw^`@cHk-W&YyU|zJig>d21m}& zF+xUCh}%@S{A%`g@962h(F?V2rV(cT3CZ}Nyy=_tiuc*W$7<7+}S=e|E1{PyAAgtliu@OF^)FKGCx_o+p3f-I-hvaU+2 z=vL_(3zHz$9|If14t2<7MQovMsOOopIQ*%-lSRtm zNi`*ZBxD&l%0vEAq!T5w38@@U>vh6R0nW_%>c83t}UOy&o0(+Nn--v zSevP`HN9*Z^$Wx`9%g~TxQTggQdX@lMXLh2KUl9ufl~0ReO{FPRlMG6_*B6)#I>|Q zElU~t-IB<ym8o4#O{%X_)`={VQ6Rx7NkDFb)h!Cy^`taJrLw$2h37zl@oyJ5~q|D=?NC zMUF-Y%@aK~61bSKC7S;su=+F1+Z)zXq6>l)u(Fu6iMWx!d-tE~YXdS_ntvEKM9OGZ-%HzpAK-bU4FMTI5x0L>P zzjgovym>rzq;)563ip?E-49d?)#Bv%|1{DTYfME7WWV4rGnaMD-^%MuPO9{cHwYCk z$ot-E-NqT-ivu%!ItPgLhn5ssQv)h%gFvhhRdv=#sPk4s80_-A>zw4p?O>XefNPVi zLb^km&!V{@*Oyd-cdt`o`|j@R6B?xdVa(aGjcB=TguiRnl=o03nR9>nVMA!vRZpM3 zgb6RNzVe;V`9}(-Gye@6DP=x`&2O6~)!$`$RHi;F`TuRn@UW4U*kQ~8cX5S~;u_*6 zvH)&&+Hby5I$@*-Co#;>ZqBFkd>(HKt6S!x$x})KSZj==pC4O_!T@Ed&#s!ih%_=w zq`t+s%lSQu52tbeJsX}I@g0Z=b=gaSnjH7*fY7EYc2(&*$5V~}c2@BDiYw&uZfZ~jVR)#bphW=Z98bxAzZMCZr> zvi106UU0gl&nv|mC9Ie%9N5=Rx)4 z1U3dToErGi|2jpxrS7mDh9-sRjytj@I~Blprr?@B3A|z0So&6cy+`nbhYrXcuV;ux zyeL(9FQ@D}8a8RktvDX@B=K5P4$<>J0Np?$zeJ<@=h5Q;5OK=>VdQ~Edq;!RY1+hS zK{8Cq+G5*X3Y3l1J}W2Cn>j_tesp2aQAV~9IM0tmE@fk*1~ zl6@>I(iWvg`_ZV4bbJ(ey38FB0dE!!I|giew(W1WBISqlZ0|%L3s_&!chP!v!@KG1 z%`H8PqdzJ~O2C6IplERPv=#DdE>!$f?z7p-xK35Rr=DFhV#T&5{|>acw*zGMR_}+; zU?3CH#9s^350qEIi!rFXf_U|DB!ln=mVltZ2GP;|HU=og!dho2<(Ud&zc0o+9 zEL5fxwDt4>HtvP8YmYbsAHFJKV}3jm09}Q2{aUyFhz|W{ho_dv4l-vHSfm@|V^Hm!Ri1fcnB90$~3IDm*+k#tZ^D zA8hDVNrrpC4)Vo9oFc7_1#BSTaA9BJ#@@RAR}dH{ z5=Q`-R33q$yw$?F`N65FvlA0%hEt+~RW9sb+rG`S4sF8bkmx-1st*LTVca(@A0_l$ zrXNtge9)#tVsa32bb;Qw03}|)=`>0>)K@awU%h8k*~)XaJRZb2XtzXrNtgj z_a_9{DPK-_Mo$7DM}`GGA^;$?K~DjI@P_8`Dv##0f3)jqhS*I>4Q9QJe|At8g-@`_ zP(uRT9T}uf3IrqA8i{oVjtbE ztfH^%f?3vphVL}Ja>qlnSq-};tlZWOy< zKn91K;sd0f5=X3&f#?3moTXTPZw-UM_*b@Hqh*{PErpel&%4XaEUZX}ijelz$S5F3 zzN2)76FCb`gKk5$ihL%i)b{z-Ksy*mlaTkV^I-T>+jx&3Bvmhqng{@n*yX-oMYzqu z?k1$v8}XLEiHpp^quzuaRCACuqpYIpP=tag$rmdwV*F18hW=_$lk)^iXAAv95CLF} zhK{g`zBSy1zk2wF{CGDPYa9qi1WZiaJs5+*4(A)j1hlJo1VBGnrA|6IJR%Ys@C*}R z`vm3p3!Y;5mfJV{uCP7s-g0X4je-n56y`e-XnQ@Qj7MS!5pXON`r8Q9LB7R$bPc5H zwFn?UU)*<*w;f<6tl!B$q!Dbf6E7j1s#K=7C7uG&%Nl*XEkgimkzgk62JWeL3Yrww zoC?7j<`KmLr!6G`_Q)6&1kj4%+7gcIOX+vqw~iWg+>pX~*wzIAjVF#-lVMfV9YJFa zXffalNK^ET26P|Seb2##XnlcRDw|)7yYmwhBcP~yB1K4W1x*{K)c9faLt4kJzN_@U znnnE9`lZ*)2sGXa;Z5tYWf0>^*=Vd`I1^DpU{!P+C_hx~OVr&=m@c zYlcWAbHL0({%zXObr)8%nUxzKqq znaBkF9eVvP8^+>Ja{sj0dtHHNxOcxsjry(c?NP{x2fmyP#JD00^%yR5KlI_Bu2)^Rna2R$qXX&EoP!b|k@QoKvda zK%HmFLXzfQI-T~ZVvY%7$@q{MGzEHeuUtFSJm)Ix23c2KrZ$VP;(`+}{NN8`4xQ8a9G>mHF~0 zbR9y(hD>ZmP$lm?YR1GwIb3_-M;zBm{9{50)P`KjFm+GhHp~-y)WFA`0j4YtYS)+q zs6r220pG>eq5mCmin@C13On;pd*MJte!r%VjNdmmDv%NYB}LOtmpfksOaQdBs6oi4 z5de6A91H)}xItNNq&hF|Ma}>;q!7Q;*K*tpydy#pc`d0dZ%+~c*HIgFa3=d~D~gl5 z#x1YV&`?zjVl3DN{jlOpFGPuO92iIeL>1AP(u==y@`55YNq~t507L;m+cqot!!(`M zd7IOo&HGTpZ;Mu>GNN&n&UyFB8ql7-F+bzIx2_O`={NWR*~W)URE<6cJ7xSVI%8z@`HN+kjU0Ji_*^6q6ZvymGCfHl#QW`QFBa&C-BmcRTC zBOi%Ytnfq;f&n`5&gIWZ*8@cc_73-dv&ezW3R>0IYJiQ&gL|r!yoVY87%{II?Q%C1 zZEHl2A_A0AnrVGV9!Z{^-C-&?>;KfCyyAIcerKb@>^JeIP`|YZ$$EHr5n6!Ti(x@N13uBl1mMp#kR2 z*aqj&$Y%gD3daOMv&HdiQL_dB*ROL_um%tSO}7D5T3r zOl%503;?lGiwbi%|$YvlAy$m!*Gb1f>T zYXEGO-jzoku?V2P(C;E07LhJcZ!u%{WIP^ezq+5ouK11j{)*Zfb(E#Hi4I0ORJ=(EVq|wSfqsE;2(Uh`2-1JNerYy8M=y^eBXhDwG=5MM)byQ8n^4tbU>=dbizsAOebX zs#HSl@6ZIm7vV<$l$O&3ts?*wdWTR4OaSaGeT@Lf=ae5?t?YGmkA2q$0p>{NKC87J zeVn(H1Sm1&TdxH$`6~k#0U$9Mz(oLHPrp)LXA9@m$f*EOH+b&BzzNm>5UYWKpEn%+ zAe@I_J`xjm3D4+9#2gO&r)ih=q1-qjoqhv(6z>>V!1n0K^l9fS`+hdgB%JSHg<~ zQgE%kWq(vi^r|ubH?uG&cYFRT^6pFU=FzKFowBty6)LYV*W)#DtUw`}006s~6KdG) zZ$$v$yMu)X0w6U7ps*@&i~%u?mWdJ+dCKp|t-g0Ez@1!XZ&)Q88TP)iFMY!S8__@z zkZr~=dUsWB5kIpQ09II$7tso%kP)Gjn_o=4juR7i32UqyG7G}_?RjXgxC2G1lky2X z=n;nL9N36ojDOU>zcAwbm9S>~ajKYfQ zl>W!v5dgUb$aK=XB0nZHy0r2{q4{+dN~in}nY~y}C71wcu?xVi{G|~9*hWqD?i>M- zTXrOYujC&OkX#`|xhF*2pGPP0V$yB+(b~OVPz#tl3*xZvgsh15o80q~ zWs&cLTQi^8+EfZ}mHqXwNo{|EHQyDKcGhduOLqTTD8F+3jzoFvAIhvNZg;eaiMs+G z9YEWY=%|DLxQsI7Kpua)-vah1x=?klr5Y+&U)!&eb@%j5*z<6-|K1{>tr7KAM z1nfYm3|W2uhVfr&n0F-&RJ(^}OIN`4(fVzEB+sDnwH~=v>R!brk-w(+9U`T&QY*T& z@YKe01sl`pl-v}6D$z)}#i(5aAT}=1{=fMe0JT#wwg=K-!)!s72gr0Plr(Ahj_wyb zus47u844c4B4b{<0qg82F_I}k6WI^DxPB#=0p9f3mIN>_JCLhTNgLkO!0e^UzCz*j z8$FivOBfkOPrG0+76dsKwh^D|z*~zcbmT4fdK32#zh`mJyF{(rOARaTBQn6gkO4cU zT{!6#`nwWzyae<*aX&w%J<}dZkrzEiEI?RnwTp3AsIb-=)`y}{;GE>%SY2KPTOOw? z#GVmcKmLy8zq{Y52RaY|yAiLnPMoF}0R#X<0ML#qmWMmPTd))X02NSwX3nFp)0M3O z03%@3Fn}Q`QH*&{3Yo0064?SIGNkUbA_G@96kkwYsYSOxcTmTmZf;R#xHkc+ce83n zz7kPs3oWtEvyw82ioN72e6M|^6#Y=qKiayt%IF6$;;-QCH>-YMFbUv!fxuWctA!)a z`sk;6eZ&-6`QI4}tG-72CSxW@;j`s%4O5w#co4uMcy^mh(@td2-w4kTQ4+Q3U}4Yx z-ZC&baL^A^6aj!T6z7Z8mr+oWF;B`XlCx!x z&kzbMNpwOW-3%Z081JOSRhjkTGIpdM~1|sI}$vxq#oX?5cR#LPt(_M zIiy|J^nJpED6W&^DX`1clv$Ler z#I^y1^TY5?^s|hb695XB^4gL(!nZ6zMTK&sSpGzCG*_%ty4d;FmraWY0(W=$r9V6! z&<`L10?&L!{?dFU#yn12lrM1k493=gesWlY*U7Go0S_bq91rfZRF(v_h6DO5g3rC+ z6#WHQdBxAXeWskec3K8U0!&QYEnK?<<c4O@vc=j_KWC;9fr~m^Y8f z>E&H9ZGUusv==AP6#&xyDM+8nvSIv}Tqc_~GeY4o#B_y9qkf0V(Kk9k#AV7cODoEc zXN#Os8ELAJC~Jq!Xd^E7iHn~aZVJF2DXXzJ0wC6c;5b}pY6?K+g(C$LN^91!9N6J+ zNpTLbCR(7#m^U8OW?e#N<)t!WL+&2*yi;H`5*C};uKLX>CqxnDo(D$K3PH;7DYRpV74;5Bl;Gbfh z^KfO(b7kW9&}Xh_(hNjPYXz*VhZS|Nz7pATo#g_kmys$;a^v{n*f8Q^=>R2NKoD_| zX`RzZfM{}Bo~)O(1vsY-_kK$F-JN3J(f#!XBD5BYMe@B|7jK4 z9>&`Ma>jowZ*Cn~D2)J!H6J>5z)Le7ngGBH9;C>I#QBZ`V+{?`Ci6rLsaqQBq)%o1 z8x$L($irUq$Orj4TEhYHkVwp}l4%8&7HR5XuI8&cq>KNEE{?Bw>4}%@OMZ=CJ)L*g- z-%1FolO_PPUHrnNk9kJAFeI1&pu86+REj&J&_=&D{`CcaH{WA*Wn1?CVCt5=lHrGB zIczF&LYdX}WoEg(aSkaoI!hwS=hT4SmDNwkSCY1gG2by?XvM`7VefQKK0A+eC+Pjm zApy>&_z7R>znGQ67e#!Lm(x^MWqetfMuh&gP$h4Fc~CPfQ0C|g?EN>g!NMQPm&{w? zTu%CQ;*R0TrLC5{ZF|6vAPvA-r-%_zuFG|W?rw6v^n*~&K3P+t=9mcy&Wwmde{nz_yD4{ggHQg)%GmuSCR?k=us<<9DkZ8@chm?W2vRlnyXj zSK#X4j^B$~DT5D%`6}z|sVl_JTcqW5f?09!4UDCuv9x|VICEPiw z$bwU&2{Gc!F*15P62zjCvfl3mqsFADZj&KEkxtZrcpsH-EA0`w!o%R47ax}@Wqj|v zo@92@7prU%AV-OVx&PSsuaYh!Z5E-hHv#|`9;;Bl>kIx}tkVIOtzku}RUIUPVj#;Cr*XhL9wX+G8u~V#pfBdF$iaVAb~TAHv@iFGoR2)mx2L1ONr4zBTiK34m0-?ZQz60H1wq z?~$(6PhnREUfpJil5?)5&vhSjH}4Qdo^tfLi7{WBFQcF=L!L=A34kPgwR%;Wm)j-X z_XTu2F%{ltOCS)XazwjbdMLD&p}&L~>fgLI03susRNnE~?@Zhq?9*=y=Or)-!sppG zE&PT++q78oN}8`D6!bxY>&l^BE2N$nX_2=K`mouC4d_pYja(^2(VgNV*rJI_^QDW) zj!>xi_3!F|IZnapS91&JvpdbNQq-pQwg_upj-=NBD5(G<;ei04m8U9-G0W9xwF>|~ zPmO;}0DM8mwju!1-GHqrB~p3OB}kXpzRt1bPjo|(F^`n4X7yx}03|X~B4CmLQD{Y5 z&!v~3M8g7l_B+v_$-M+4VO=x~bjm>+Z%N_iDw}%Ym?I)k--JAZn79RFN$1zh@Y4v{ zCnh?=DmZd|w!QT*q!M;vOfR+hrf|C!uHB3<{*9cvCfzSx%o-ia5*UQkBWnP_JM>4( zx22r^qTdzvc>a%o3_cX*tF7NSB07n3D4j6o_)Z*0XHRGW#ToVCX5dfJbnjc1}a%oDcG4Da~m~&tg43i||?D?Uh zzai$3jt1qmMBs5P6B7)hlu?h;QBex2arvmQ5i1=hD~fPHiBHE(o#8ohSQKqx=0hRH zdjS~Go1PSGNGp_n92P@Cz$2pUUkmQp@eB=RjU^f8o|w1`a9$5dj$smPbIdU9_-aT4 z*Eaf(^;k}P4OVHx{Sy&J;dX6~W4TW?6m-RgPA}=_;J`Kl@*tJCCLdQ{h4EinUrmI< ziO_Jb_`{ICgh3a%W$q4;0yLRM`UrH1Gy_0C<5vi&5)nA;je z0Bk&7)L1~Df%MRar>Ah@o&j1Oyq0Y-g+LJi2C>hC<5eavWWR_3kfcu%Oq))$(*7b7>(sIX!a~{YXC%R z6@wncJp{m(22c4Lx&}b%qBgAqJOAG}PraRWHpR*7U3#t zF*eSDn9nb~-#AowdjBWx6=3{3uMvIL$+I2wsazgc?lA5)2~I~KMlSk%4w-AQM|$NV zvXY+dLeKwZ@(ts^WPo7PX3AMJ5=BXhGaBRE^Ui%oXS!cPUIjhI*s2m4)oTnAm3bs= zrhb-Z&jJC!MF7MesP1UJ2w;qUrMm#6%1HJgjs!3r?TOUi<`(=!22wY@^N&(sPqTTm z^GVTeIzWK{wbv)b#^Ame`2+BF8JNr&(Fb#VKbI!|rXAbe#O@n6%E!jNvnhEK6PW;B zPAJB^!4UzpBi`1A#}NP=xvt17M(Xix`M7?G8yjNRk@~*3ZsCuC3LnfwtvI{A@s-AW zb$z|iLRY}~=6KZvO>bkX!i-&sZOr|Z>+>)L;6?Bq=Wyf@04;h$ec^Ztz~;rqJ;k`l z`uQ389`Ql2q&mm0iwxV=Bw(Dd+w~PmxhG>Iz=Hu|m`BMjPDB}ZPFd+Jd$FcwNQe4hewsl|%y2m9CrLb|M6bk5{M0@25m;4507$#-kUBK9Syi9!O+J^4)&k z_|d)X#U>{17^dB+>^?3=mMPaRmNH#i8e{y^_XMd&$0C+E=*a(CIZy?(B6=L_4mO`; z1l*x3a5BJ40fAp({AXv8-dojWct2X9thM#ED2K&{uCSFf_ADKO${rxcEDgRVGO?#= zUoZyg%TwrcVc8kRf58HQn#a|P;A=;C=^6mt?rMu&0JbzaIIb`FH<1?skSB-&5+J{; zUej%*9$+Azpvs3pY+$G7d)CbzuW71Pq=6b(Uu|Bbh)^Z1hgL|9ekyBsO|%f8nzh1>^ z|7rr@+eV>uAfcUUQ*03cP?=;t!+UH*pfoaG17PbSOnJCECefl_rnhI{O7Vz4OJTsk z+qhNRdJOdtt%kqe?Yqzpo|sz|M$dPabWgFZ2YYXDL?aWX!>YudXjsaGY!p zfv_b{34%7Jv)guDLHCMH7IVF9-ZPKl(d z6A-$_v0=9a9oI&Dd4-Vy%ZErk&kf|pq3vrvIa=uou~3lUa7G2$eYy<4w(!Ik<*Ue? zx+mONQi|5PLf0`iB3w%53RQ3q;`Q)n{NcZ&lvK~Cyq#_fz?7Dfbz%DWRs=u}W6TX8 zyF1xYYXFoi0+?}8`iGIi?gWq^rVqhRna+4g6B*bu$Q&LC(9*KUWuDFTekQoh^RV*G z^Q(;V&8=I|`&KjFkY)a}7v-Vi%A4{2FJ1TnTNh4DbcKDk*KIB_Dd4m0@xI&RW(OJ5 zATuT~u0e#6^xL>X{gp|pxND9^6}&YRjIp)Gf8mG;wxn ze8yAhR`KVE3naHSdg$!D`G8D37JO2V(^h~4fVK>Vvrh?*!fNn4yVGswhY&<0ao;y8 zc(br~RTO*vH~lck~Ao-uN4UpL1ycIWMJdK8qc0`P02(N`JF43@y57D>rHXJc?!V9 z#QngLV*WZ9DvSW|NHHOfY6FY$Z(bKGo$=-;Nj)K=a#O8vA`eH5jWvdUMe^-@Cszt; zOtweD5F(&3O~ZcEmI91JoBbQ1d({MhwuA8$0M1^HDMrojJ|zHBYI$VzO14^9071XL zneVi`LRj_LsM2Une_Ja2h=dtsOX9)|NI8Mk6Z@Vh_i&U+*7`d5NQ!gqWS$Sne=TjQ zlupad&}>UU?pbCE#L2R+_2i8HVAzgRx?U_W{ypOX-DptA!DHx^co4X7UwA;Q=m~k} zBQo&-;JyCjd5?L~Ey0&zSa*Wr>h(Oz(5@941DYz!kApql^r9@%|5)pZuYs)0{6_lv z)=i?kxO2A2xKt^JhT;z=%cTk8%>F;~_7O1b|S|QT0{i#Y)W^ zH}=-K!Juq2#=m{vxJi`lJk(=^uS58R0Dd+h?L9W}iHRx@M-I2ruD6}u5XVmx64w{Z z-$%y3B>@=H()RC)e1D^`*WdakFwTcs2df{&#q0`*7WtQzb34C_jbtrzg` z1OL_n+b*M14sgEo1={lNy?2UD3-o+jOqn|Tykg>pdK_yLhuf*TPgpaieqws$(G6p5W zV>%fAxc%b1LZJ526AXckzf)4rrUL?lwDs6H-4bWi753)M%D-X!m((x0&lv?^LZha> z*mbhAnCeN%2!EQDe+#v zP=Z?HKXRz3-kL&c>tyqxi>LQtl&-Lqp3_GkNZocANHPwSpv4>Niu?hOYci!I04*by zCdK%dK#>nQGHlg-(16aFyuPlX{A-iS?tJPV>CuL>GJomb1FOOh@<@rF9c2epMaq)4 zv}yWJ+&7fO&#_w%K-#g_fEfKYB_{U7AzjiU%atMZnTcDM*!#>fwu#UwVU*qVM8_4F(VAB^V9>} zo#Adw0d_EB4CYeyxjLW8(7e7zB!C6{zP(z^R=+yhSdIg>*>YRxt+*hgm}8Q>;Yqp_ zyXI3BK-UX%x|9gptF*HC#)1&*ZUIyok)|CT|BFma?10q;l-(7>k?w1|jD5cZsg%(L%U?Q_?7#pz9Ck2L+*n09^EzCN?=Bb3Kec>WueU5)$D72XO|^vjr< zjs0&T%L_8b&%x^dIs)Lk2p>3_Uy;w8P2CJz1OTB!gJaxCwiOAWU)u2hRjiE2Rs_IS zndqb@0ImY|l$M4~&s`88*ZUZ2yEi(^p)WQMRvMs=m0Qmm|W~c6srn@IL|JLA_Rs{8G8cZerq|V4iR2ExV=H)!d61=Te!~KVK-2!XJ@i zd7l^>5CH&F0JaSJ6q&#s+w~N-UIPGsr^dg2=^OBia0Ec=f)fz{!YI zw0Y$ulNJ<{PFqsgTg2%5Q#imYQLYW~j-vbukI=pj)%a>hp<@L03}S#W6qd(@KF;Re zLgWCAKzS-3_NoAPELh5x=%pN|v|KMG9*uGH)I->bi2(qkSx*7b#{VlO^n^2@S^LSf zcWwM%(J`BE=o$dRk`c5KbE7Nt1ypW$zsNjKPk&IdH|4u(QCI8VjWLvK#P;&FHs(w7 z#U8K{M^6oEjD93fub=qzNL+BM`%zTRwi%gUl(8eNP1&GwKLP*~0Gxd+$g%F|wFW>e zoxTy80MP3IB*%rcBU(8EAW^XpOqWkj^zwX@dg9e^lE#j&EQ_gaJ8=G%1Z^VQ!S2~eMH4?xkjx7oZxqZ%n4(u`5e7@?R? zz2Igyt3VM|xJ^oQRAH&Z_Hi5)GcmCrR-+5rv|9tfrew(7C7(&Zz^s8Yx?L#R2MH`L zY1f93gy2Jx_+;%lrYjuiSjjfxH@xH};tU?qOGeGvVkB!F9CTs9hCQyU@Ry&7p4huR}am^1Oxi43*i>VGslB&nrjb7v;{_X4NLAJ z62Ov>oZLCO@uNbzw^*Ny&u#YGM8T-_{0H^c82=R&W?$PKgXmhtyOMW7oe)8;Zr8%u zxlF3)sZ7C2s))R0T46(^@~OTw5b~JMboTL zQL8b>mX}jeQ(?LnJJAR^hW4;X0B{-as1Hy}0-POt6{;%GF@Q6QQw{xSC7ZhjE=`OC zpyj+}gJI(F5YzMUGY)NIiyRx-CKU4eqhsC+M*wW~VMoY+2gow|Y2xie#dX&h|FvX8 zMRj+9N4_==Lx_OFG-~5qwJ2y!xi-*_ZoTFmjQ~g)|1pKCDO#=pfJO-K9R93tfL;ti z696w_B>}JuTY2gE^@LJx+Xhp;O-w)7`PA_X;VTIWjO5@;h?Z;5D|7xJQXvzm>*x-- z@k-^x?>)eJy*CWyDq~)VF!noBu0XcTeD+6|q{EF|0L~Y??=5}@9D~GNS2B`;q&?uF zmbS|zO0!kK75V*WI(s54V+6L01sm$jjBhsIH2x z-xW!3`~5*A0M~yXsVi)aciDt9lW>o0Y@UpQu;;&|UMy>nPi}sV=c~k)t`K`})JB@r z@eVl0=(t~scnL8(n0(*j0@;mq%bP0f3to zH8r>!rT0P9#=pKHnhel4=e>wGC5}{Jlj$GqdV5_Y?4jMLbw+CsPaJEWPp~+gsbxl; zJs$Zjqc>Im-g@&-gvq#Ldw-MI{lMR21A4AP*bRz2n^akb^GHBzD4>gB=iGKSAi#k4 z|gtjKIiy2Q**$fIoO~t6C#u2!EVUMYSrIn}Tc0Oy!| zG{+U|WNX|^gVG5#6GIqxD`tpPwmwNrT4 zH$~qBeM5dV?>~t=5+T~$9D7sxIF*{#>0;6)Zlq`;PNQp@*oVL_W(?(0V|-UW@Z1=T z?0Khm*Mm0Dmx*B5ct*zbRYq=?`Q2?U>A%#?;MN9)@=kR6p{eX(#2YNvfIWpE_wpGV z1!qzLCMIeCOZaI69~r*poRF@~%g8POb}KzFn7#Hfx{VNGPMB+>o_}c9cn5$ZBLE%- zjDfu8KGqoi70Gw!n>Y}?`9OE2zz0;n5F#hqv_o4UZfX<#0 z0NB{3hBp?BYvT9<0>2@8ha008MS$G^V#vE0%lM;*-isNCI4>nvY*|m0!9>&)TVA78 zpkqV=8K{F&l_?dm7F==F*VFt)+z*1*!Zja~zNE>Ax?^63+Fb#-;;i+VEyM(ZzGJTv zcYG>1fAu_w=!`WKLg*e4${j-kqAp+hN3XPg>B8&;O9C8W=uAws!&T6&jo=xFh#0do z25v82w~U|vquPU$*_Q?SrBE#lJZ8Z<25-1s7ki60yyK-Ex<|s)82=R`$Yz!T&;Oax z^l`2%ES62tuZi-N{S!~Xo^ITxbHr>~aHw-x5@7$eEh?T^z5m+L4FmvZ{FjtTh1)G` zuQdShd5-DOHw5nnu%)jiby=%enja7?16Xh=JF)8uT2(Z0SHLZ2moZ+LVNZPkMNi}w zIGkaY|2BbJDVlVE{TF(@@_{u78MKoI^9ML~qsH2w1q3GAb ziqICUs|}f@LFH6}2>`tPAD2I6Gjr#f+|kx+0Dz&+_5Sb~rU3j$_z?i9GPf&U=5DCW z)Tx5;Wi;xgyrMSBjht#I$;lBj{%fE2)_1N@jvPao8rP*|7kTsv39(mKDIx)SVXHqa zQAEed?rtF%{aD#&NFpJSvaN(4W-Cx$T*gSnJ0i0^FWmWnz~~~>#KeK%#w~2rOaB<% z-GeQsXM0BV1f(AHH+Y~zjTV!Y-0mP=2vUk0vnC{*3Xo%Fqfv%dYJ^Ky(Q9q@t#aSwh_2A_X7NU`;` zl;!MnHB1fUdvwb%P^={Bq|N>J7B3pqzNoJBXuAT~hrrbDJr$7x*}=2&_ps!LBJcQk zN!jQb8}f{RV$I0l$z%U)bSG2+9ssT_O6HoV7u1tox%W*1VPfJ=Aw}3ugkbtncwxP8 zaGUnP^n$H?TKv66yn$(j;^qq>jp zp(`li6L9FciOEUH_l_v9p-!eCSGP-JbQ~^KHq*fC7l~9%eJ+Z8)3Ogj8=^vLyA}w5 zFX8~>J_oG`0M7X5m<@R02!IBQ0Gh3*?2ou*l$S!S##_|0mBV>-&}ussdmd1OfGR`y zonz4HfZq6ql@=A)#f+g`YK-s7hm<(cy50&r;$3h3SD6KO)=kuih_C`_X=^JmI#XO7#Q z0FoJV6qOgT5&(LwJ8kd66abE!pp4QmMXrPZpv?jwgJy$YF)Kjdh@uDp5#ErH*JZW= zw28wHo2A7oiIfxM;7dq#W5HSkQREF!`Q`1!z7AT&E+?u`&;gycA0eVW`(9?KbReLQ zM0g-fU()QcELr)$%V6$_4P)6+?^1WHOCae3qpzS&ufAAc$@QlMoWc?iO@okg{oHSi z?;}=vhbStE@o6huI7ll7$viPJ2(03O)gloY22|KTdSwj8)|kAB(FA19{W?Hb*aSQH z>^H)_&jp3mlJS9#789zjc#mN2t}Db|2pe(~??TR((xQ-zT1x6kq*dwi;%B*b%6#AA z0=7;lav)NiG7%sfTgm4dt7Xf@l$G4hz zmep%noGr6A&wkF#6P~CkzHI@`3Yi#*7zYx|F!jQdj4A&{}cUje0$|3D7E$bbLw9`v;K@b5zrvPlRP+R47K`jB``JtF|v5N)hZ4va%(fcH7GC<#~=SaDEql(!E z(7xI@(RX3%CgrA}sf$Dg*LsYxH1+329Ej4|cjS9e1O_@fK%e+xV;*_-v-06c0O9{s zyU2$`gu|(E&~u6-T$azs2nZryPI6ykp8H^NLKJ1P0A1t6#3KP4?erP<=A%cReXs9T zF8|POiBZ{t1xlsxL#St4u0zE0U;2A{eQ|~HPoq+@^9g*xl&C{&$pdW8x(9WM@!5zCK*gj>oF4i&i7gU<9x64)t0M>?5vG0gY;See+sus;+C12bgEykw*mN~ za#9x~tH^ye$S`y)nLb8T?#*GV%f9Cv9 zFv`9c_@Vs%%p(Gn%PK|Rb+;xax?oEOqiq|L0;nNJ`s*W_gYK7yf&=kWG_c=5lO z9ee=O>(FZrfS9YDUOemupl{e~ByXy0bP^%}6e6cpwpJO;iD#~|iP=rcwfb z&({jpG<9Bi58uBAMt#J}9~KF)5s6S!FMcS3Yn;GXZjZ#zy&|`Y8rCKyWBhwIMQ8Y< zR_6M@!Sm#U!J^#WV^GovYtv7XY4*S(8Z#Q)4RSOTZQ`WZ1UCw)b;w%&-lH%zY*jAQLl?)ClRnGSAMx%`6X1q+Ltai;}sLI zS(V3txBtTg0H+{P<9-iOP=yi#fYa#kF+4ip7e^PvIUV}{FR|XRR}sJ9r5xC!QAXTc z%g}qh-S6u*VZvX?ZNP$&Dr#Z#N^QOeANX|1lv0CSRXJA~zcbJT~>%zpFc45Q-${Vr(1Eot&R zbsjT70zIlN#)8BOKFGuMnfSB+4o&td%740viPJy=sdsxxU%xnwKU3{wdZQ_9C_~E`9OJkN{>uhb0Ig*FN5rE>%Eo1wC+N-2*`{vA6Nx z3OOXe9^*DrWLBnDkpNWL63q7+($)^m9*`dbIKUDirD%6_Rm-wDT6!GoCi- zF12?c{c0$vTZg@p7z&IR4}=x9-4I-(yJ}cC?YXPe6BGTR3@P>k-}RqhEFzY(w-u{t z%jWbGBH2`@DF8sj0RaFcfIDkmb^$8!{d!$H zZP((p?KlIUmbYd6cR>vSKshsfOpRC5OA|9qjPJCN09kBhH z77}eZc}x(zzc8J&j_g7~5o6bDrvNS>K749$(#86n{`)FVJF;Y<#>X>?b@t>0PIFd$ z=>f)BId{?MYU_D61!Pu7D$2PzgUO)I7?+|skp3P8Ov+DyO4!P6Cjhc9K&j#Vc~W7j zM(c|94z9(vtZsuV{;{+uRVg*6uoP1e0fCAgo6zoWdDNM77M! z`;DXnYpj(R#jgs~ zhL7a(u&b$Llg;j1D|F=NSX%I>`Xh#H-#L)CV&VMWFDxb>R!Fwr9`Q+@Zy;o*3dVYP z@b-CqcZ%LMy!iFOH%c?Q2WjZRf-uHOw-qxN#-%I;ifMrJdX4e1d1U|${;CaV1vfBU zZ{Dy2!p71tG(bQ(8Gt0Z{**OD6Nm;WU8G>Qw0h>1tTAKgmy!RfH2Y92Cz}#=e0e!$ zz1D=((jsx&T&@<}?!XsI3#yx}!!PH)a-*OyD|RJzma0Fab*bBE+gU&njUbsmf465S z|Mv@si!j|>7agARf4+0A>kZ_xj+Md8iz z-C4$43AE0BwRePT1h*Gcc$77$urHayFN-TnF-i)bnL+|i|C~x(?{it0`(@}L{?UBU z|7Opkr6N)MqC>8IPMugo+>hIZxHZK$gxkmEuy6mQBhthN%0F4GrK2o!9!k-02mj(pULG3Scan?tH#(9G4EG0u=CZC{TAVBBcPQrU$8E$Y@R+wU(;! zGhC)P-4@@GgPZ<}Jd+48yk|r0D?~I(dC$Oji95Q-12C-_sF1dmZp8C*Z6Da}k$HiJ z>xZYV(sOrO($nWbO5R?-U_0*+j>25oOk7s71eK{K#kC6U%~$D932lgr`u?Q55sDn3 zbwrE)zRJ^*OAAka^=JDQI@zAl{5)=BQ@~@Zv*}BI`KG@BO5Em(78c>|Fnm~kvOZoq zVu*>#c}%U01L!AI&xRW@o`_izR+tHYlKx5e7-H#4mF-~=98;B^b|U1reH^k_yUj#~B5-`a zhZS<5+#Y$T0FDsUp_BRhib33+KAoStJry@+6trP&9I>8Jg267=bQ?rCIXVrIMHo+{ z`yD<4)2KE2ay^%osF4sxee)?0s|b~`?Kd`&X+Whhcfg#a` zPA-)EW~-k5H|52%2R(=+W#a%%DddB-()sGT197307Xf3>V*3-`_K0rQJx?G0qjA0a zCHkN}HkFhf>BR_&8G7C4zPOpQHH1~BjFwam@`H|1;M3H#6dzEVOk>USe3XnbD><~0*PrA!51kB_eUT!bmK z^$gD5{HnRYhaeszRVOh|Hf-2$;y2~a$q^_&ZjMcKM<&5qr|~ghDjp0Px#d{9zhs&Y zh6!6_)GAL6ICGLmQ8VNn6y7x@#y*#>2+n8i0%!8S?ww+3zlQ32*NA1_g%?$3-K*n2 zxk%v3s3-~zJq2``w!8&K*-Z2P8ocDu!eRP_FoGm!MylPI_~LjJ4^+XB$1qzsNl1<- z0V>#1;&j{t2gust>^~}`&uOG=-F;X}F{aC#M*q1b7kg3dK{#Kbn4anW(Ox6I>(|gz z7Zp)9f6SRCu4EzBdf&ome4ZDRdoAj?5_QZkv>i6#i2qoCCx*|)Jxm0JC$Hn-#TGoL&CUI&Q&mVgab}z}vv?5rilR(M`4xwYHb6OEa6rGe zZ^VR2dj&Fa{Q+s?^(^B5#r~mOoLO0tRR7a1oz)9T(a{i zuCl1ZKV$0KKXUBNl#hRDDEp4Lhhv_O1jIG}mfIfu>#erk4}pGZ6V9T?$(j3k zk-Yl4SGQgI??^Jgi)nXpcu#-CDt|xCqdd1CET|Il*rTe!f~vm(*?<*^WU=<|jKhcx zGJ3!j#_W%QI>JlA`oGP*k|PlS=v<;d9++=PX*oP+38pk~OrQ9Zq=fx8Rqt627F0_u z)U)vdKaS}i8dbULkgss7n$PEQ@(&faf5T zVJCBur%(q5#%MQUWuCMXv57)`@cVhZUf+#H|Fg<*yz-4w3M%G=&HdE2LKZO`1`f$j zV2F4Mf&!D{fXJvA0`Qi9_pp?M9Zs_6Iu#X)%L2-G+OABDjY&1lb=XL`H2rg+Q8LS` z1VJS(o^j$%WG@>l0qe&qD#|hB%?E!>1b!@6`I5S-_|*bNmcSwVfxBT}_mX@p`430+ z3++y8O3{Ms^rYgP^j7P-Xs<%|*u8OBz!j4unqMthT@F1F2$y`1gx3>;-v2^z3I31m=TE=UMaaP$z89Sq zl9im0$Ve@K^trXGSLiC#8`9-YKh%P(DDGqAzu7m~xzw$Y(7lgxxvA!&1cAz+#Xo!< zu2FKxV`pN%^Zgm6(Y^y++_PehN!Q|>uU&LRB)!dA7Zc5H&^B_yYoq~weCzr)ks?0J z;c$Lb7r8d*4bK;R`QaBTxQeuqTU1XTj!yTYY*@?f{3l~58EqbZqu8K0D`!G*Y8tI| zocUn+k$o;Fil}TxBG5!Y?}2DOc?9v~S@t0#j!#%%9@!V+CxLTCBqb2U+AbRQmPUmr z)N_2|G^dp^e~-L_b*ZYdEJBlm>iAQT1}SELhm5aKV8k9{_Wj3ICMMfCP7Rvlt2Xfy zjDl@f$od0BOz3GZOoMc7T6}{H(GC zRsHx52YcIY@wny9-^!6|S%zn*oqNQRL7RBliLS%*RvR>1v*>qpuJX)xw{9XXII!W! zz0Wb3xcW=ty{tSY475{Qlk}s7rg@#vke!nN$2weudvWDU_p<3Ty?hYDbVItl*(#M+ z!Ks4L2mUT+;RfoKZiE6{u^jnd*~(smfvmYnMD>^Uu$=Smqpc1zPkSD7th8l=MStBLW162#nEl3lssUIS?Gn<&UT8r6Ar%I3ha@Z>psLBze z<`(2%elqF1v1ePUZOnem$;9$Y;D3blWT~&JV5i@<4ino2*5E?LOr>8 zg|em<8ra}mivM(=5chbK&Hd9~rj~}&f%Dn#s6n`AzTe-O-Ltg_N<<<=e3#f5coV3; zB0R(W+I94pr5J#nEeccV8LVq@zgyhc8{$ni>2{)w5|S%;(4(My?&dDl0}W3 zcLxu7M4?Cyh3yfzFJKDCxzx1WjoZBnbku}Aha<9<7s@!ee0O&1Q31X~<#7u_U~Er8 z73GvZ-O?Kk+@1Is)*T_3+R*otgT9CLwSv}+4HZV#@Q*MvaMtHKu$6o5;BKKm{TPpu zki(v6M!e7hPh2=C%h5ubeoIBmA+=`N^M}F!Q-jl@Jxz=_v3xqhi5wLuJFfg@R%T2- zC|l53rtwu_uJv~)`@fie6alh+8JS@7u2}QTLpNA9(U~pZ3`wbpiGL%R8-ilbojLJbOfBPLj4@ed_F0dC3Yc}AS+NJ%r=V?CWEJu5ZSR?i2S}BBS-2kTOKM#lY)Mt@AJMv345__Fa^|kz< zi@vv9y2e&E%q{XjRwaCP4MVHf>$r+`;0^~NkzeTpOzWTG(Nb9+N^0a z>%(*0WaI~I+YBN0&7=|3RgRL?_qsVNfvJrDZden_6<&B@?^8r-9c32BW^V5Y=U*+3 zHo~2{&7*~PTbqSP#Ca;FE4FeA4Qa1ZA~$Kb%YHoOvX4`zsN zXaHvu9Il8Fo9VOL@<_ZdgCCDSzP4oGT0M5@D>X2dWRg1=6kAh(b!c z|9H7Lz5%e}s!Ny2ItAxn&x#u0z{sDg0ZkFmy|1=#ocogaSG&c1ejO6$Py8P|X#%l% z>7%0vLn7`iesBdG=}Vs!U)$`~oNxutAoj~s3G{Hu+%O<=5fJcDVe9&AKmNq;#-HK{ zOwFTWp5Ln>0wW%3txc2%P161GVJMIv{G6|k%_+|ix-rQ3@Am_k#~%8P=_)UHtqs#t zcU?~kb*=@ji~8qJbk02 zLa-S=#IM+-t-ar7dP9dz9A?~c9wQfEHQLf4SyQTc-}T@9#de+5RAyJz=16OE3{xqNUb0+FbqY4vJzKjl>@d9# zDY1Js<5%0b*garz^MP`xxfE7Npv8T9gGaFIO_p4B2d$9-G?Ii+0I)>p=OeR~iys!< zx9JUUX~W_OfRs3k^qmAogXbev=Jk>w`d;;y3kEIIZuZq9%PC4WR(QtBZZeh|eJy9Z zN3BaSy#<0w^Q%-q9RAtpBPx$-Ix<@)O?o7#yvd#5L^YJ~{olsfK&9J?)qu0qe2zsXl5PbQ^sLWSTi zKC!|$pS!7Tg0F;_o{?0*eeLVLU%;~fy##B94j#e95-63Sw<(W|<7NKy0)qvr)F4FX z!qo*Q6(A5T?Hrw6ZlI$pu>1a~(`ki*{*{}eD@|9Q75Kro84v8e`0^9bxu;lKl&S7&p&&$0`I&msL< zChWwH+>%QmX%*53Mgph{RYMTtFw>ytZZsXynay7W)5CTDV}uuyi~!7%RDfa_4W?%I z@q_cuq;(;;;Ga=JW{%t44y6n`cxE}foW@dJBV0ox*DO(^PFSHWk2N#EvyM39QNmF8(B%P9m6#L5kQLCK;FO7S1c-!R@-Ol_g z(<)T?4cOWF9{HJxu|k;*uz9t|I<$suH^`7KuOlrfCO1V{PlE{~48e+s%fHhjd|z!) z9Lh4SAM$D!F6*QIpl9$o){sGN}J?bbdM`3pW-ozZ)&iu;k9>QP1=Lo7_Lk z2HM~DKJG{%3Q#@$jsmiwQ0_}fv$*9Uz#R@I7|(Ynn6C6~sQKZ^mH*N!IIPSCWn7u< z6{05}dipos!0o#A@^&})ryXq55yQW?^@xTW#X(8~O8xPb?^BKTN2@azr_IEgpA8OCDn zA~#P&1g~I@D+bFXi@s9F#WNxnoBqAniEUB7>Ox=vTRsiai!PCPDy9M@p)OlTo0g|4 z^GZ1-cX~=PW`|F;KNt}Oc8lpOLg)+~SXE~_=w1w4u?;tlGXogTXOgA0Y(mp8V_&2C zzTeHHZ|x%b8z7Bi&=4|!0N(vYRKLA_gdH}<2TSK<0--tA^ zKIATZi=d1?VHJlBcL#dVQbs{ zk(Kr;pMMNu;^3NiOo*MX6zGl?2!STuk#J$GyQ+wT~WcW=v*Uuugx8#QQ}5yqF*u>%@`1WL@% z5)>W|pmzyMu00>&8XNgkV(SXO@I~L75RCs>!3+fj^y~;g-+%vtobDsE=o5OpyqFMU z(5=0swQ))C6AO;}`|nvCqWWEFBtP59-Ob|1A9Nn=5oyj85rz*uMsUtDYQa56Ol!r( zZ1-l45!AK<>Q}qFXiesL>Nl$1aZ5yc0pHsBs|qbxY+AcFsAGMkd)5ot(UzUcyWh$F zReJG5RcPcVJX>xI3h)=`9#!6!aY@=vVs76#SoEvXX{k@UW zeX2!%2SyCtMq+_oi~%gD(IbPG&40htgt!%wN-zl`W4-tX$pbgol6JW!%@;(t< z6j$Wg`&?-*(mSg3SndXjqU^?Ipq+!rP$PI1HE@9Z!7-UJ9HB0ji!sTdGNKmmGV6c797N?$k-yA7woZ6gj zpF0&cau{?B>=$5Ba+7_i@WD8wRefHpRX$2=_OH3>65MQsTc&0`B2j9AzW8 z`!zOu&$${xe}TboAG>Er&h#hUmA(lb8n7_4li_X+v41yIyyyFJ7w&I>KZ(}&v)i>` zm2B&(a_ZR$J1Zb>O7LvUyVy!SzFdvyGyBKL&-YyecNzGi36X2BSoae>cf%e8HBi0% zY>KD{6Y@QLz%4`<=<0BJs448CH4RuNl_>p!rhDRJ|neeHjZSg6lv=>+Exkc z;Rk6NYLP%v^C}Aq@sr>bn7Yjv87R{BNCcaWA4jcgBxdR;i&zEsmNodG!yjw*5<-Cg zeBGrQGNL8-{l4!{mB+*#f+&*X4bM*Yt!ECE6m(?DuXQJD9H)f$lkW(yE|+0iPOo}{ zLI?n^LEM=OG9eNNQ8DW}IyNiFe~D6}0)Y|YV+({Afqym$csBjq3_m)D zc!r=+N7~!AZfAIV$z8AF%#5kPk@+_lpLut95|*%DeVpkw9!`piLTOUh9u1wVdZzNl>zb^B(3poIu9=``bq zolSHKR`<~K=mWqKNWA>|&E#TabY_@d=+gSUD4lM;&jWU1RVr|?`H`N8VTEsg$^(A= zD&7?CUwp`(?;HFl3I)BCaw5!mqVTr*!|WJ7RuD5;X}2W9>nuu_oM7z_PqyE-xsap> zxb>(p12e?~=80>0VLpl5=NGN@bQ>Yu@OjxO#y}JWV&!W)e_>P_ z+@hInUcTR35%`!#zbFc0IN@oM^ed}I-fe`t^1WKd_cPCWDnq!@GERsyDK!3>zRB(rnys>0$#>J5mSQb-5q3Zgj77O>ECrj>VQBL_D&=>J= z_I1fRjMz+(k9-y4HF(FRwuk||3fNBkx?TZ__wa&vCT#D2?N3qT5SQGs$jP8*_V|BJ z1)>{q6z7!(ev8>j4a5@=g|pZhw9Q*#=YW&wxx@C;ipegJ zzyhox`{Ol3;X%D?+BP669aprg9ulrboi~7QZ!}W)oAXO40O$wqi9|e#M>JJl8NF@z)j3!f&OC98--Zs(TYTKGyt_) z^2}uVX>Pxk$oFfv0fR|rj+z;x9p`Y}fRS}IfQGvhbv`O_o|BidBSon#*t-!Jmz$RQ z6K#wMSf^D3f1HO8x zFsZD)qRj{GY&|8E-i!81b6Z&NN2z&=6MBR-?+Cj67;*x9Wa_Pb+{zLDCL$SHENc?b z%|fQ^_I#+_`uyulFj!@SK`@SoK3bJV-~-2J(D@?6N;NF&p^;@*OVqFltrvXv3k^(k zEa~07OP0ixH6q;h$ZqlN1B~|=)U1LSjoJ_r+roe%rmRZWc3~S^A~rx&v30@Y@h_Au zb~~F+yCN=am|FJT{kicHab2jmA@DCZ45t(YE~GURBX5J8z-oi-Rm2HVBh`(WB&09N zZTt~_4fXlmdBNA09-29(T?xHBSuWV#3yLImoZ+HRBI2z+uzMQ}zDCUEd^RQcg8ZXN z$86UF-g%V>L&dk-$qaL4z z)up)4GgGiZ@5SM(v2#`mu3*(OdqZ!f)rM zMQebKm+~!ZXy!<9H_b6pe`#Qi_Eh==$IG<4&1KDUdA?<>miZ`NBmHd+9q+>j7qmKO zl}ZildPc&0ql``PjCs+rGdf2|7%=I`YDFE~i`-Jd=OjZN+4 z6S!ux52XDu`dbaADIf6toRVZ*CYwQo=LJ0z<A;I0cI$<8Rc7+X&&C)3+va*SKSrF*I#9#ps?GcU?j5V3 zNczRHnLy1dhMyek`gsi0%bu7<{?H*^_eW*`$Zc#%Xjej>XWVo6ySrvZc6Onl#H;_< zQTN-^=#n=RV@EG$@vG3kW*Uc3~g!pyxS17 zDE(8_g=dZG{aq&K3~sMk4_aYK;eV&U2-W`*E~nyQ4WD1tF%<;^P5Y_JVW)g0x^RMHFq%^;R}oa7m;yjyd*PN zDVd{gMovNi3H(C$&`?Mt6M5T1b(?aK61MuhUB6B1W%|ugV$HI1kh$x>KGX zqp0p~lZmc?Ee9g>9v}g;KYYo!EPCFRFK&%zMQZHWW=W|uGi}$yHa)2LoX~`zu}@M` z{ukgL-_CY#XFA|ONTj=j%PS}>M3`U)I53*d7!tGL5&uDz9hi*W?k@2?I=6s~ZCQf31vl2zAa0QdYWP%XtFtli{0Qy2 z9quWgf9lbszUaTGpvP)xPgvIm0XnCf_M(;w3I>4lq0AXNHUe<|@wKdhGDwAiMvY~x?}A(LSt?ap z1D&sQ-g-n;+lP0%*oE|98V~_@tNU1^k0afe)>h>)S|O0IEfrZ2wgE7XLO?)>a)_9> z)MNZ9`-Z4HgY8;ljSM#XDh*)PdhM5y_+FT3|AR(LYO+ZX{xr&D=6g5Xx3da8AQy)* zp#n73X48PSvw+S!D5il^S)xDih84(Pf|f83?{J`^x%LOiBE42W0tu!mX=7j>$$LTb zLXZ&;xD81kIOhJ$^qtJKfO=sC07r5(b+cK0D~y=jQvV}>ql|*)WdD5)!~7$d zLT{ZbEz~iEWTi&5t~}h|U%VEl1}F(~T`uC#Mt@;@F81H)y#40~?7j44%{@t+C(l1g zxWA@}DE(bPn8gO1N5z|f-Z`Z~4q|bXRSck*uZ|Q<2QWTj4>$trimu_&5SyW<@6LQM z_Vs>!*Y$|)#V@;S^eAh4rO43R=;(|(n7&FXmhpvqnhBM!p;`Q6 z`yTT2*tO1}%e|>2$NVDqY)n9}tKg$_9a^@}#PxXxs2BM$n#2RXRj0wU?TY}g(~?ZI zgZ=)KH%-MZ-gxMr<)!s9laou^q=#+ZTi=36GFxW`Zz%)@cW9c@m8MT>CZWt{ImVS` zJd{s!G=IK^BFaPf1?#%2P!h4@$LQNUhHjz}5#i)*NBKnvb+xlm+xQ_&3DcLZQ^}8R zN>mRckD|)8ycb^Iqpy130?|RX15$1j2H5LplE65E+D<%EC8uE4OXFqq-0NQJ;pBRe zS^Tbl7^gkR`Ry6BSML#4ew{(d`byJ%vM8R%*Z{@?3l~umiJOKO*l&k^4vP-)c5y4f zG*<^mVJ1)eD=1EdgYJ!!$e&-Cx`3m|&@LEBxBjVWrZ!uJY)-cmSiJMQwWUp2$ z77=K5C3@t~Xr`VcAm9LLN+diozAbfgNj)VkcP+a<+BZGfMxy_;S&2NZcOqxQ8dl3Q zTP_xB=;$u)W}?|A+?k!N%6vX;%|;NiyY=x5ig$m!Jn(TZ09rE!v!Y`0XmfTDz#xCn z1S`iMo~B{62o|`p7E&-piIRhr0Qi0+hu|2=JV08}*--#EF^#X9C_5-#pLY8tuxUKC zv+WCt!8!2xV5Q;E9^3>+glo?`?&|l!jv;&ceYBM9`CA~Epmz~(TJFOLcCMc$)32nw zMBIh8hVlmvtGD>VW<^X&qrQrMc5~&Pk&Fk(OiGbM1|-X^+_F?SgKa5sximWJ2zQtq zJ55!AL#Uu2k4DaF?D6akC-^Ale4T&XK+-oQMr_rc5*4nj>_6%eR0LZ0N?)%r`_Vs9 z!ZikMdxeCw?Fs(83((h9Lj-)Awx8*#18L^UAG68rpMd;jQV%xrWk=xrLTwd85-XQ} z$)C(_ZV98;Bez|MecRlBiGyyftPicN)ENPA<2U10V@r`#KH>b@mF z{uUlxU7A9%L!Z6_v`>}skOj*Pmf5tQe|CTn64gl+=!3&T6zC%oVrr_t&OEozuPzL) zXSg_H?;~5vB9Iv;XDXtsCkG<}DjFTHcWnmFG_8`Xg$McQpB+_FBPN@OwIU00lN@cG zNB!*T+FQLE3O7;4#ijmaCS5DvCgQg1PC|+=jc`l0#LIT_l%>`KK9H9he(1CUKDHeZ zL;XYy4ad-GR?tpXmT$i@Jbrryu^OmO{B${jTqBQ&KxX~!V+uRk4kI^mqYJPk2TFX~ z{7GJ79$2?tC~NBP0vw%-AC&K29!eu}n_yOVeym`v{2X)vGrD3>HXnFSZXRR(f4_&l zsYMHXNC{w`q@Lm3JbM{?Ma{^p<=AXkawC3rbr864mm@M|?P`_Edpuq7-a9?N=Eg9T!6^YvF--RC3V5Ri;wqxX{X7ZtFXw%|_FHGmE|ZQx7$V|s&kaTuU$7>g6&?dm6Z+ZSY`?PUBRqyr(%h|P zQ`mHABL#16&!t66*t9?^3|Bzj{BcLlo2Ti!z#+n zu>4P;-DTA^w`zd%c6k2Rn&11%Zi(Zie7B(d;HN98cQxl2|H4^bGorhogtyjS!8N>E z9JBBJ$2Ueqpzsu`9^`2@3Z5Z%0X;8y+b}wiRHt z9M1!u6Q|L^ygfATR5kH3p4wW4LOtd;^1LjHP`Tu8efs)&s_w%IfHoq8*qoLHa(6zb z_9iwcHw%1#{%gwl(W`P&r^W|s*k|)s$cQvOel46L;rBMBf&BR4S^nEL^Bc`r5+hD3 zKio@0=%b!7`)I>R?w6j$1^5(}pCShA*@CpLJlkizAHF?Z7iSK%q4oql9&+Z6Vp1B# z5poyb?@gcZR-3Ag?Dq-#IKS#;a*~JTKtYs>UC=Vj!RK5}SihkKFi&XXn$skb8-W9N zJF5YlYmUGOcTSG@bxxO#>XIQfnA(?=B$-L3cMV66C9QEzCt47lu@54~p4Bdp|5Wul3|232XFMU#-5I z5f}xxR4t4A9m%-@ags7VCD^8AWB0O@dBfBW{o|^EV&fvg(QI~kjvC8s74vp_Kf|x2 z6N`Tk{=ck3JY#3<;TeN|g`^!c)XQ3oF=>I0UM_9}kO@wK^)1B*=h*0>y$~d0zqbRa zet@$yP;yU*J_bNbt5A`BI~UyH0Y$AP>ENXsQ7gMQ;=4?{((FB!t#`?2*_GKuMM-^_ z4OSwOkZmVW?nF51;q4+j{0}2gYEd^cl9rW^aP61a-l6v`nm56EosJ4$DNYF6#JglD z_eOfZjTlgF@*0sOGqc03UqBx#$|@bRZv*~~D3H}_soabPJe`k2bEn|e6_|R+Wh{+!Y@p|W@>J+J`yOc=YAb79lXaM>&laW;fOGAzw9D+J+cd&Qk9kXvoY1< zC8=gH{dY0Jw34cE1z%`r3pWdeDy=SE-vUmK$00Mjx>?%V3hNR6N|MU49xPWF0<7vM z&fjf+c-pR~NH7iMl_qXxjT-giRW=hU+T~zUY z@s_RSwtETxxZU``7ruupcfCI{BBV_H{Ib~uZL?0?rU||H6>_~|@46I@X%EF3<%v~X zwoE}+=#_x$LGj;yj&og8G*m?C6$iO>Ed;vQzXd3?QP$5CDG^2B5%A(Dbsii6rI;;J zBg+ys)ot{;Yp%(S;e@{r&v+lDH9U@V1mHNeeBA8cRW(;D z1I+)Ou@E~&t&4upSA2b)?ye?GKsDK{eFe`yjyXs8=w;0j^wjQC^9dq03>vONc2haet9LZ?P-$zkPN19Lw&`d*G+-?WUv)|u4IKEw@8 zet_xA%YeT3PoIJDRrNPQ*Z|?b&!p5~9E4BvCaN4$-$q983ZEIQw0~E`-E8A2u!A<6 zUNenTFGr|zPSY&0I`to-M-&Cm7Xvu{tM**HU=~IqK<_(`kop!dFYW%MBSHXIjG3?q zet`B|4^uvFQGQ>+SXW|<_ODC9&v)`go^h|{`stKud@~T9PVvE0^;C1TrBzbM(Q^q^ zB()Llygz8*=c1T+MKD)A^S9p_c7|@V^~0;RdL7NfTglb$GO?LcgLn-loI&HPB=1-a zQ|3P(&Tz`3VZ53YoOo@})v(m?5J<}%%=Dji7H*DmS}OK=_OtmGe>Bb$7I5_=zB^dH zawrT)i;L!&La}>2Hf0^f1VlPsp(ms*U6?lCNp%+z>FxAz6}T35BjHG%X43Rti11DO!?H+X|L3g z?U(V9!Up`t`h3xEYS*)Dzcnm~)Ah7z9)^3&!mYm+Ttb=9Gu@Z{;-8VMc*nPD1EIJ3 zB!mNYb9|KDieFSC`l$KdD!Q$u=9Lv%Fw>D}FnKL2Uk9pw#Tp^9Jk2|rd!v_0zBD5^ zG{vG zH!q7%>G$k~c7Xr3~^v z{BNqMIw>44J6?2K5G&v6DbnA064t*d|1e^8fQYm(9AhU*nWvs3$B>E|w-uoy)SRs; zIK^BC%m?(e1Xmk9;WsKuCrbtZQdLN8&Z=Y z#$}kokrhBz2Bgt*f1niVzey_WDaKYADd{95ja2SlavgQ|ye0&J z%I-qxV+2D28;HoDF9rtw7b#6p!oh3HFH(RcldNx#%zqYig9wO#o}KVb?XpJXNL4gO zXTjA*NC{ck@6bRDVGsdwCtHiTu#2@7zcb|Jy$O9H#k18udx)CGbWpZb-AKs$m z=W7X}?EQL@y)>ZR$-1xqp5WT6Bb-l{BYT;Xv?SXEMvlX?~~0PqJOs|G9@kRn+1ldPr0#%%-9-S3NI` z!2S6U8p0UHNb6pDK+7v-2+)z+y%g|Z)%?R0`ORJVtrt@7rgiyE{fH zNREnds0Kt`vj<)dlQ zs+m=YG=OyZBfbqySeL2vR(-%n2lz+>q!)5l8-L_x+nXEl#;$_L&+$JVqe?T&>@){P z(+25ocFnzz(3R1~Sv{ujJu$gqmtbLd-7!-6%eq6*D@C1<~-@%HG=2)q#=Wf(g3}5+UIwuSJLC&(*B2l zEneU}Fo}dn$FQ!W#tR%p2{j}wZ?*Kp)6kkB@CiDE%nR&9{jNjRL(zmi&8N9#(&SEi z4-(Rzt{`8jOB)2GRkp;N)8tt-`satSukUA}b^?u@_la#o<}9B8;QZ$P1(;njEKjIx z%`H>9)+`1r>$I8TU-P{qGDoSPQKnNo%iscR|6MiJagE{E8}|gC=gFa`q^ZzJHAOjr z4#IiAOgyKvaud>3s+*XksXgw?exs4=j2Cm8ACn7Y( z{&0OaZo0AY`_g4a1Gy65`E%--tW$B3Q};d3JkLCa{)IcY<0!bAp(XNf_|{5QHwtMa zL(hG{ZyTr?-HUbW_j`Dz;i{*5#@DE0j`Op6V}P&xLEr&04^+0X!;O|QitNBYr5TGp z(o=f4bMX2<9xcHE6N#Mu>CaB@wXLuy;`3LHx+(N$(cDz22oDf^@aOMJDan_4b*<=A zGl0%LJuoP><5VQ5%Jkv86XTiap=hwS`i@RNZ+b*&Dgf}hk6v~or|zY9aR^~4(N$w& zpG6^T#RX|3-RAU{n%%`pBwT2&_(60@#yh5GtzL3-**KQk%i2b-8UPTRtq<>P2`|mB z;=rI;W6ozH*a|eSHGC0sVqmNEQmI7qAorM-RFq*PueQJcTB;cJ%qL^`X+Sxqhm@z;A$A5Rj0NlB5Y9Z^^L1=P2)BQP54vH)zjAtElSd7Yl4%El zsSg`&vWl5@o&oCWEJa3xV;Z*mF5@U!$F!DWC~&|rT{&~%AnC^|m+3ohWdP{(2RyGT zISY6=B5+_Dy#?lKAl3e7If;GZMPPH)ca)7eyT3JL&g;{7aqDK-O{J4!W1x+^xbk80 zUpp5D?fdd&Ll@AbU^40m)`}0Zw65TN+xfLM}XLai;XqrG9Bg zspH^u+CX;@G;R5r!4ePxLPI}80dBwLZ?urmJoPQuk+sT=%~xt-o)9!OYfi7T9%&gi zzvYXB%Ry^xzv=ZQ@N+--29$GJ$^pn#;Qr8>gHg&WvC*u-R}pRO>+#vQpM z!qd|xkjPS$SMsEhTl}7Y7GH?HkWmtA0uv{>m)7KkL)sw~IvWAUBcukIy5uZrHyCUA zD{YC&KFnS=0B~D7bN=C`x;Xi!ZJtI5gO@f5-ZKDr;NA!jd3k*AKR)pQ$}GdTqd{dZ z7S#UU(b>m$-4Hdhv(!LN6f+t!qiU~a_2EU(9|>w`qv#jBSu#ZJ**JYG1w;q&Vd2aW zylVwL@J{b8>x)jF)R~j`f+(bkg37I2)5XufB!{eLQZHBJ$>?DDIOJ$@SiGM(>U6_T zv}LvXRt!27_tf|1^3bw{BaJ3tGHtrs1TpsYe<6rJ*f(?8O(5CR(1B{KG3R;`zqlF{e4+k7H?9(XmnPSWSNE00caTh>*pO%jSkIzRKQAG z5=W8y!wd=x%p2>8lZf*;KwCD#>inSKa;*TjcGpz~@HGqiHNvH%$XfrYHqxVns&vk6 zBER7?y=~+I?no0EUs=Q7v01hr2ElE>s zQmx#Y`$*<3Aj&vP`#j+Iz&A$y!UuX799^zo5E(|gN&f=%rNjHE&p|+E55akX137+g z7$zm!uc>$_OOa{0l$_jVGFv_?BN=X#x{%U)w&R%QuW1ZY zhS}9g623nrH54Gx>!>3Ce%eChz29ZgF~!McUJu3*NT)HZdy`}^*9!ZAX~qY4YqwB;Wcl5Z?|+B-iJpKe6ztX zQWy%yZfJB(VS9dZGFP%X@xc(e&?jp-U~{WzNeH4d?)Ii2u8@aqy}Q5J%R)($n@Hky7I}oSqA-0RY>!_W;%mbD*tIYoZpOD$QRK<= zaZ*FH=5c*5pR0#LSCi|LG`~6WY!{liYsM!Oa4Xqmi-7AT`Qn#QlV-ts^5x5Mu1Pz) zO7|>Pq)c`rIe^lB9FKWBV*MZwDu<<%Ye7VsZ0~hiQ$}36wNTB^b1E3b@F9%*=|I6; zDwPOnNM+tz@7HB3>mTFcdcP$mwSw$S4}ZVhKDu%^1+6jmt)0I~vZcOCGrjitU!2pM zpwK&jHG?6ca}M-FnQg0RT@co1X(@mP>5yM>S4594mS>oHCX>rygjrH-EOwQq5hVBp z`Wryww(|qAsJAy0H_B@Wqp`+48#($}$?g$VK_i`)D}}V%<^7%S=zm=Irelg&LP_ob z2e|{e#Ht^Ddf#QCq*Z``NOSsg(bV6n3|h;gvHeukKp{lu7Me1kRM#DCW=E>wTDz^{ zYb@?|_lfq+tq7OTJhb%VFo*Q<`mZR9hXw5wtPeQ!5&w&IG zJvkKd0!6|C0yIwv9|8@GJ92%1Twkp(GO`a0Aa^%H+b!a0hIb|tE?O)BS8uDUA;=*l z37bWt0!aUCD;8|!EvICC{_`7mHOZJ`h-fcR7@Zz!`hmZt3dbM(>SPg8LPxPHjJC54 zK+q_}@+RLO{fJ{r8d8_8dKIbEWrCR@OrqT5tpg>=fq*UxxlnOwf#lYiedsNC7M{6G zr!!aX0TY}c8h9CgLdsdbl=ONj9kTbn;kB*9)$zRg?zqkOVp5s92hVeUt;tZ!V0eTK zBkIL~W}1k}41n)PPTY0buflgF8A960JqVj^A{#l+xyUUnY}G4t%@3z-xmB?Ak#~NE z1S=&`d&JD`*8LRSfeWh04ka_B3s=$lS@teI!zb3fLV;2{$4u2C!x<>7Jh^x$3p64C zi{c^%PvMtTK^%TkqA=&@g@fL+|33(g&z&4MfjR8urrLSHi*=qUd0U)Z$z4}ZnEBT$ zlWB*e^5Cqvq2hu+EDJ>n&P5c2kTcKC!*jc5p#bAsqn^Su<>zt&7Qlf9 ziJWcG=TsWQ$Ir%yVy0*zv-g|61f*!zW=Ic8oKgraZgjs1JgR2RbaTNUjm>Xh9-+yPrH@9bg(Z~Twk!J^HdPnAI=v#?m^|dd z-7lGFUn1}WkHKip$D`l&5KyP%N4*7(M2($@Fw~iyT_n+o-6RN$x{+|p!;@4v0hch- zuxmU;^x<);gha(TV)Pc`09CIz&!#sjk87kKV9TJTx}I7}0Z3n{$Vc9)C7Co~eK;tv z!GzjNUm~%!xV87M^*nyOFP-d~^17l*M!w!LjDs@!iTMrM6SH&rC{i0vO3$Ix^n8Vk zR4uTbX}|5Zg4^m@hF`9K7g)xe@0|BgbR3typRthdB;SCu#E{R^C2FJiWFRSlnDONB zlVx(XYmm{8DMvrrVz296XAH~gDKFCPk!!sEUo!0r+rowmHO~^J$51#$v=cPo1%~bq|h+H37V@Z7I=xFA`2WdF)Im@x)lq?e+F$z*S2R_re*HcAt4l> zNDGmabV9Pm#|n{KH8B~z%<*-+9FPY&HmJswqxf8`cECOd8wg-^(|)*Z4-xxQ`033X z>{d`K2;fbias!c&dhm2u9k7K2w5-N6m;a}FZ8H0^?O_q`KKRU9^?xVE9ystzCYgiq ztzOoaA9rO){U-Iw5OkuF`ET6Yt-ckD*JLn(Aq3IjkvkgoAXo)$naZ#6@ao2bT#|d~ zLu+LtcYZ3RnR))>Ke5!EA&ZoDq7FbUA!YGkVeK4Cc72@=8S%%A-~q*Jo@s01z7LM1Y;6L+zI-fQ$jET<|12&gcJ6%v(LZP|Us?3> zuw-5CoJV}?oDjcZ?r`(r#(x4F3h++qWCkR@^DDwT8b1fC0hSX(GA^i4MuDEh;`Tdp z0&NqL-T1F$?DAU0Oxk6^OC41^qYf!Oo`GXxbB50AtJ}8fwh6M=t{55(vfDq`b$1Nl z{aai-V@mjW zNtdAoL{2SQ>&BEhpkv({WZwce>;a%1xcsGWCl%G&#~@r2N=1|5Tce0+Ry2SNxH0!d zNcD38QQx*!oO&9Jl{iXy1RvG&`D4U(%3v=GqRtC*`Zn^*DIEX}i= z7UkVty5)KF=fy?qBh@VF8`Sk7K`MN2FjIK3cMUcJWJ~x{JPJ9pi zJ)ssef7oeADHy%6_{dkU72+~{{I%&$Iqp|*JsO6n>NEz@za?ZY(lFIDvdm(7a{47bXhoTxwhoP0d z_fa+X+?k1!&aBC)hNOebwB> z_1DCERm&b8YbN? zU51_bOm7%j_ws<8XK|<1kKs;|mQ1-9O{rNCZ>gzDf0~ftTRgOaBISzM}T#PnJ{rYbS4C`IWA;xxl7RuCV zN14i40UmEj{Q9-5%DWY&?nTV3LS@wcn9!(yvmpK4$6_|&Z}nN5X{WQO7VOf8qzhQ6 ze4E?4HvGKM4ENAAvOW3=a1c2^h2j?$`TqVj%WjrZmE#LefcYg_bCF)%P)jF`&Z+$9uvSe52 zjw*jaY3Dq=V0II*)o$P5p?dXbEGD6W7Ak2EP}ZGU$KTXOuk&1b;2iF7AI7t!Tw&Or zettgl$2aY3MQcQ;J4&YyGZocAL}sI`7O5G*Q|!M*qS5$TR;0k=i&rh+p=ws~p%>V( zSXpK4Fl33Iv&wk(ks15LUBXEF6So`y*Pa+)-JqtB1JGC^ashGh472}E6*TXXlR4^P zma^dhOv>8D1i!B{`Vk?iYlw+fWpZi&c^7DL$hC6&tUbjAWPz~DPj1trgb&Ax8*~_V zlBuF_!9_!sN9Syk;vCAodTDMS$lXDO>pO?GetkII5n*XJdK=M!a=*$)q9&KKSH6c! zp+^=KH7F`R`9>@8aV2-q(B{ptVme?i^FicJT`g|!oM>5@9(jZhuCqX}WXq6ss-Oed zXDU5r9>wZZXVI{Z8}BPSTIu*#Wm!4UHb&SYlR?f3y=N|m2+?mGzZ2r3gc7VzXKyTQ-EQ@so zq-oB7r`v?sGyB)eklvEQ%4g)i)t=V&1xtF)jxy9nNK!8+>?g@*DJ)r9c=JO(?ary+ z6?@Yj9XY-n#qP1hwK?-D2&&zxZRCmk_o9YZQkLt0fWDyWg?Vtm|Mw?DKBgZrCgYD{ zi2pJ6A1gfxsF~9NR=EGea!hElZN|&@w)$zATFQC4)4qfb2b{yV!JEk@XdH5z ziPqlgh)GlM9DoZpzx5^!s`(=PtsX_LJ`f=PU+a$Z?@Ve4l4NM!gP0W|&ia@Jtbuv4 zfwFcWF6>fE=4(C|R_OC%%5{ZmUyEnjIe0TX|De=ZaVTp*JC6fE;m{Ep%fx#N?K zb+_EuRor)*9h64x8F(-yDWkK)6cZhojW_TD58K=>ZSGUURS7$uCwIq)h>A#K1A7#{ zWEL0x+t2*u&Bv-z3Dbh-ZF>Che7_)U$JOY7fAFH4v7y(cVFz`D#Fe^1HFe9# zhLMo9sRdbo!$9(ra;vJ_ds3Qx79B63w~@{AibnU;PF{Ze5*4F<%=XHT68)T8OVA1s zV%Rm6ag(H4a`>nITYnC|m$`?kq|*{4)`P5dq>Lv3FwYz^;@0 zxEI`PnYZAQ9ISk3M;@B)IJsr|v59)m@PrbE!S1^OZgDq08kMY?$q&hNr@nl4?Smk< zFxp)YF=uvrKlfn7h;AZR8s2vAL+>hT@9 zfEiW&^wYn0AW|O$!B||+biP#E0Xo>1UwqCN0V}+nqXi&~g-Wl8EhYA`a=>e+S<8}XD!tPZx{CBe2T`3aJD2+% z6?>Nh(o~y;i}~^pQfCNleIV1OkxZ(&jzutA`>plT`k-8@^^ zh!rc)Q7CZ{{CIH%mLOt(t_kKCPYP@tbZ zky?A&&~uaOpHDysY$%s4uxbjQ=FGv733zn$Li|zmg1w1452RKDbb6hdT!3DkxBE3 zZ#fUK@5z>0c)seD&(i8v#SuqqVfB7tl;;;%nNMHpff@@wWJP>AkNTXlZ)^gi{n>dgjv zfR{!UN^SuBU!3rrNwZf+@RY$N6n<59gtZWBQI%+ioqXI5%=Ga%*pN`Zt()yH_&dw! zA2t;;rtJW~A{}q$=`ZZuD=p#)~rN{=mKNl`B`fm1zs+ zxO9%u=gwU}=2PA%8IIKx86%*Y=T{)3mdwBuKvaFY)~sTG`MtN5V)S1x3%i@&^4kdP z-{Jrq1KHsRrBCy~y#fhtA@$bZ=6UdnLAGgS3QIssDUy zW}fz|sDovhyeBX`#b$%O@^qxfh*ESSyg73AF;f$5W{uh9uaebC-q@)b`M;5>nl z{gjOMLv0_o%24O#-|-7ikE8a<0U9rv`SNSr=DWRTgz1U8L$6pr>X-<4ZerV4so>!2 ze({vO4yl}1dWSli>~UxJcbE)`CzGMRXlsAbi z-^V*fJcw^%ICG1>DY6 z))by5%Ke%Uw}*g`Q~uKTPu64aMP1atNdYkqB8Ap&LIZTNobi9I)_-CRLzXl-Z{FBp zO6g#s)J#i*7F0Yy3ue+tGlcG|-~tk3?FQL0>&SIIPL3x)?Kh=-`6j^~SN5s1X+toD zXK^`>oFfNm(GWEfgip-n>q}vV0Z;VLVhiNI2`a~NX%$$0)Ai6`f*kpghregreFly4 zn97n~QQ$FV;H`Reh$&P=uRyWL#c%Z;R`d6J_7(Jpt={ zpQo$*k39_?pb`#k=`tv^MrH8@hYy}Ri1vcLkN9IeQH}I2x45$+ORe0N#&x6nI~be5 z_mjlf7F0JLb@BMUUY$tcivME`HTg)-f2&TtYD?;%hAPCfe8Wq#(+Z|U> z3QX?}m!C-n&A!po9$pSUkOpk|AVp-C2utb0Oz|@p-X(IX>70Gb*HpII$yIJ;R$Ai$ZGhV|1=q!2srG7$U|w)v}>I?vY~)89{FQ3Y^+Bd6TG7)w;*=>>j2T?z$9&&V`e{T%^r>dx1Ep{n((lvL>DIqV~& zY5e%N^v*jB!6Y5P@Wzjv${A9f4)L8nD%3-445 z5pD%t9_q)AcFg==#Gf}~0*HG#K4!*6$Nnz3biduzcaXi{;s`ebfHZ@TAp8&VY zIE0{vw0~S1B5pN_ILa~FZ~-Q=8J}O3o|z9J;t&#ZXkuCQQBuL%@jA(<%Z6L8rtZJe zzWsc4Rd+uUGLvL(c`S4G-Tr)@L^FyNu=Yyf0>t>wo&CQdh#C!rTje<1x* z#us^Se1cQ@2r~;|r1_;A3YD{~B!p8tt`VEN6(DDHJH#`~;K5GN|8iIu~A2F<}8#8u|siaTxyEb(pD*jnJy0y z%ws~2Tms2)VU^mc<^93CCddpXuj7I!e4(IVfj19T%lxuVT=;MN8m{q$a|Y+}vzS8W z%j0pTeG96oN6`##pU zC1s;Fj{T;`pHIUOd`GBa!kLXE*QAQ+XcztM(?@hmS0rz}A?Y?URZNjRPO5Dm^pK?0 z02Nf@sKSuf>)`bsZsDGY?#Sf@%_Yi?g8A=^0{r{sSq_(~Z8)1vEpf*kC;*I>TEAT! zrGb!&()JNKfhR%IYPacEX#Y+Fv!4+h z9@Me?c7YV8HeMN+`5gzogs}Z@y!(uI_q^t<40!85J|ov%=?-^JrcdjCJ=CGMgJW}z zd)qa_v?74kadB@f4?9|@DiTTHaCs!GqjKtHT5&fH(C>+{kt6pXx?D>~aMxU|wn-q% z$`K&7mI9k`+Q!Z@(`Q?Ku%+Bmt@;RwnI|TVK%tV--dr$$1C(X1E;klq_wA}h=lY?9 zD^b)3+}!m$z67gh``o+FNzhCWi(BFhe)yVuU+>*KG;Pf&{iUPfi;7>f;J?y}wj^fC zeDD(~Vny7@1Fx+-q?KdgwH0f=3LMAr_+XeJMz2Lqv`LugT&} zvO!cw@BMc<^!nIwU>ILm#lw#?TtEvkaw01{Rj4-EU}LqX9Uy-Jku*DO2Y$w8Ttc~P z6C%2y#Sw{@AK^D&UX9>NbylKxo-C(IubtK^hMvJX9UuKTFLu};X9=d=oVN_BVH|iN z1F>CpBNY_MQlu(AAg7ExJDyh@*9v$t%yNCt<6a{DEGH-J%z_NskxCe|W(>2y9e6aJ zOh=+TzMqcO#r{_YZ`qrX{qV|`64>t341*eqH42{C83yX@Jj8ye8Rt=a-r_E`cl>7EJd3d9Plv1CPi-N`D-G@GF z{Qip-<=q-5{-Ak5iTOiVhXesTVm+ejAorRg)wdU`QFCXsChUCJcpZI33xmJK^J)v_ zQmUh_sN5kqf;&4nD5>3RpHou{YfXNbuBHUqaS`=nfHl6YKUsv>cM@5|+$NB4vK*c; zA#PoKUyf#asM+_Qw5qZEyR^Zy+ReNHu0-F*7Vc?@lZH;V4{I?MFVEQ|OS_uDJ0qZy znrbNMQy(!`M*w&C1hO=rP|2VqWuN%Z=9QzC-ZE_=* z%#ISU|BiDQl}Jk?1ddqb#Bt(M}I*J6TR?Gw7a$&*HA#fCq9xQ&BFxi`hB zs7!gX+v5th$?{(o^75^|^aKfC1FSrZa^#-K)`b3rKXD!#iv+MUAM-e{Jp@z-(^`S7$ez>v!&C~JE~TQ#@|pme&UYs8QO{SKFF9hYkl_YQmqA2H6=J=DWN(Y;GqjD(ssbgwZSB-1q%hNHy37cA4EGk(}e`j5uq-s3CD3;^s>f zN(X_7-4bI~9o=tIM{|macppnZ|H0G9lv#0wnJ|mbJP5|fU`d?{P6!9J+l%Tx@mnu3 zQ<0;wWiKDoz2mI`4E2>R+eXyrnf>boG&9ka#C!PtR1^9}Sa00=P9C#%gQ0OLY}|>_TK|K;qef?x$x+{%6+4L+()*q<%@XV))Y1 zT=V?mV#TciC@>n^ZmdGg8$vDq5>Me+ejr$M63`g{JND$hi>YcmyjV$m(QC#X<{pp6 zoX3KUW9PX+;PUw>?x?T~jD2a7)gtC~1)F&j!|9d2y*(H5uza@CLF>nL9gix{_JY#PlBNJAD+`chv{?zG6jD$4$`pW2A z^ZP7+qATvBFf>+Ua35K6W%@yNFQU$Ox7ovOP?Yowvenv9niv4!1C6@Kt+%!tx(^J{ z3^#MMual3Z*qxCpFs~<={877E)w@&om|axI-L%d{(zj^0gc4#ZzVNdyB0T)}Qx#6IXND^f^G!`gOogQ( zGq(z~+>8S6^js}WrO5FdBpjZv__R_zn@b=#7Pdck9UZIFyPNo5y(p$$c{%m49};#O-ZQgdg!{}boYJq$^pWXmSR8?r z_Tr}U+KAZakfS8K^WK^GSB9wDyHo>Gjf3qDyTf0=^zHRJ1mBBn*LbrH*&S6u<=1lT z;u0^YEmMn+I-I;-@SjjRVyYnHOr)H*1MUu~$xSZ_BM++G=zQTygk+NTlgXhZGv8g# zB)3sblxIp4^hkvq%YZvN{6j=exz*hrI?VkvpHZd9&f6NgqZiPFoJ%JMAVx0w!^bGB zcG0c3QZ!WbU!jFz%l}AX?++K@V~XW})0@iA^(k|W3*#h02Q5p;G-NSmXpNu-+@Clg zkc>AyHuGf?2l@+1Yu%sqhx|m|k6x?^Hs;#>Ysh+Uor}Z$=1OR)JEk-tY?>2&_#KM1 z8};v2KlgQ?-b}ci#v2cr;6gt(Y0)L!b)NID(R-FW426%TQGq^a7kJ6^}bS}ZW% zcT|Vq$^z3yWcC(?uM?=!!J1E$grZs$v4=$8d+K{!7Gxjj9a#BnwQZ_xoH$UetVvNH zm3ZwdN8L4)zSPeiIFDi7Xw8^c(_;fwSuwdpToWisWyE+b%>lP>1a^kxhT!iW{S+_{ z@ZI(|wM_SmH^5G!Om`Z_ypGfjli{mYLw!&j7wrq&u=A5rP=P@?tlDji0hocsp*Ptiz^%pnD{CIxMhLk1UT{jX=kU(D{e7ETl%at#JueBDXf z;AqzuW99zGz{7jV>2LgBJj}T|;31GXAg^981u)-mL#^jiLyhi*ynyMm74=PyO7obc zdE6Q@K7M|3uaJTyGjj4g{D?t9N_wC01``8N%X7o_iv#e^Wf{I%Yxk)cjiR!EwC?%~ z|7AK%%cHuJ=@PBBQ>;vY^$tu&!|9`_efJ4wS2Up;Ty5&Sg)1jZ5??k;i4klBF+z5c z1hQDc659=_u68czWJL-k!I>RhvWnjF07m5_I9m=12{G-5 zyj={!|GeKPEJS>7Nc-Yj_dcOE5c^yHAnQPo8+UaV5&bgl!Idvh2NVR~o?&t#eFukL zD}zKaC*sLZJS-1bCYmlBlk@KO({nP<{Ti^a;HUA4w86a2sQMZn)JmvjTu!9q$39C9 z-=P2)w#R`RWQeB@_gkk^oNs#VZ+Z0dW-tA4xRhQ~WQfdFa0yjmV)i&4;;P1VT%1o* zUpGixeuH^+N=A0=O2wO_ZR%NVC%iJF=upR*@(q#^?t91C`H~{Ie9V*u&UFyI$sxUn zUbDcVwq%xn@2%(m+vX6jXBn<_UpNv1vR?8gfCz@}K*8T`@0yl+plM z*`HMx*oA>=`K`NjYgVhq1F{roSHN$Qza2QmRH?Dwh_EbX1&{sR^x?z`S^{CIc zi|@4Y=8GwUab#ukB9s8zr88vZh+foTMqJcHn6zPaR_~afB^HxS6|TGTrEi0}uX*%onBP;pl{jD


{jC5dwLOVe1`-bBH(F$vG&a`XS4^ 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..2ac9068e0d20f013307a2c1cc4bfa78f24861c66 GIT binary patch literal 1169 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=YKdz^NlIc#s#S7PDv)9@GB7gM zH2@-u5CbDC6B8?A3vB}fD+7bZ^?#F5H00)|WTsW(*6`tz>wTaG4Y&;@nYpROC5gEO zxb-Z5o&waue8|(qF{FZV?UdWy(xEcP_ou(xJUPW&ddu#ct9NWrI<57s?GhK4R;|Di zey*2|*2L5MtWSQ^c=yJsFg}0!~?fa#wn1+`5}LImg`iq@U4u<9~ja zj^u2x`zg(5e8%#5@xJHf`}Qw>@NZ#2^s=%khG9#}y*9eOk6#(PeA=D-O9d;goXgtv zvn{d5W>FT~h+%lHEoqQU1HwncH8D+=rSey&q|B#2<2Hw&ycU@ zDctNbS#-l2n{}b}hpxQw-k{QV?VVQ8Whb@i^*a;a*ZIxKXAQVo`q}sJ7AwoiYdKpk zAAfMeJRm?RCwGst)~?F*Nqx@V&jZS?1;*I$p4`dQT0A4IZR?$5x3_%lW~y*8U)$bl zbS#7Wvwza+O-;_1y9?_7Jvh+0)W+fQ`TmKvKgIIi{^_gvSmmZ$I>f>C30k6PuLw zgxWkfeC-a;qpP9`S$v+S0=|2DgnZh0|82hsYgv%mZ55Bmo)_y&x1^_8AK1qqd@jG( zYmI?!>WsdQDU%Ob^j^GRyQ1ihrmWyRDWzrgD_6+$mI^WKw4R=$JZG!2$hww=^Jm!{ zcF=ZS>S*OGr2gTHL=J<*rIqYbC#U+}vVScm%UC(7!E54`gjj~_OzT6tj2CkUt!DJ; zVOCrDWzxE&&i|8zBXuOo6$~=7)VNQsYkI@9>b-pC5n0BbrUTXYzemaU)%|Ba%zeJ; zInRu6Esjr?FScENF1b}Ise4aUX`ve zZ2kM*gz!hWePr&5)h;bci`7?DN{!2w&%69@(k^|zYegQ~dtTkxJ^fh2zo!aWTgxwh zIqPe1ZgTbQoIuyBVJ{ux#9y&BDs4M-pnBQ9@SpEruV&BQ{qn%qtLIODsNLb)Jb&@+ z$Mw4tTwSi%DQ=w~7`^Dd@{)o@+6H%*nOn-wKD;3A4{ySy6P?DUtRcYc%;4$j=d#Wz Gp$PyiZxV?B literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0a01485fd1f68ed3685eae4e7f5310b3e26d7d0 GIT binary patch literal 2967 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�zE^$^31nAIP3q01 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..eaa1de13c6cea5b6b619d5c568d522a69f6b9f9f GIT binary patch literal 8997 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

WX^q&D$># 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0a01485fd1f68ed3685eae4e7f5310b3e26d7d0 GIT binary patch literal 2967 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 szeF`F``_kP-Nmiy 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 I5@tXD2YHXj%>V!Z literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-50x50@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..a26cd0886121c4f825c098822ab45d3744203def GIT binary patch literal 3996 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-50x50@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8d860f1544f1cc0ea4998826a7cf34f9139f2ad8 GIT binary patch literal 10909 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≺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 I5@tXD2YHXj%>V!Z literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5872355d9b98500ce12083ea9c95a3aab6f14d GIT binary patch literal 25638 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-72x72@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..efac11baadf5d1e18a0f8de1d4d7756f0a5d1bd6 GIT binary patch literal 6770 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 DE=aI{ literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..e8ac9032f80cf64a59f98f6abd4503b3136963cc GIT binary patch literal 7415 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 W_nq(c4F5e1v9#6opEs#N-u(}lr~0%2 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e1859a0df8f72cbe3470860452bc70cc6eba6e3d GIT binary patch literal 20128 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f863a9232f3c05e10360ccf190c3a6748d8b3857 GIT binary patch literal 22732 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

  • wnif?2G)f0! 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fab..05843b52 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-40x40@3x.png","scale":"3x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-50x50@2x.png","scale":"2x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-40x40@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9b0ddb1deab583e5b5102493aa332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10932 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Sptube + Spotube CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -60,5 +60,7 @@ This app require access to the device camera NSMicrophoneUsageDescription This app does not require access to the device microphone - + UIApplicationSupportsIndirectInputEvents + + diff --git a/ios/build/.last_build_id b/ios/build/.last_build_id new file mode 100644 index 00000000..ee73fd53 --- /dev/null +++ b/ios/build/.last_build_id @@ -0,0 +1 @@ +6f5ed64a4065df2d43bfb5b18863018c \ No newline at end of file diff --git a/ios/dev-Info.plist b/ios/dev-Info.plist new file mode 100644 index 00000000..022d5419 --- /dev/null +++ b/ios/dev-Info.plist @@ -0,0 +1,66 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Spotube Dev + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + spotube + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsForMedia + + + NSCameraUsageDescription + This app require access to the device camera + NSMicrophoneUsageDescription + This app does not require access to the device microphone + NSPhotoLibraryUsageDescription + This app require access to the photo library + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/nightly-Info.plist b/ios/nightly-Info.plist new file mode 100644 index 00000000..5ba9991f --- /dev/null +++ b/ios/nightly-Info.plist @@ -0,0 +1,66 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Spotube + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + spotube + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsForMedia + + + CADisableMinimumFrameDurationOnPhone + + UIStatusBarHidden + + NSPhotoLibraryUsageDescription + This app require access to the photo library + NSCameraUsageDescription + This app require access to the device camera + NSMicrophoneUsageDescription + This app does not require access to the device microphone + UIApplicationSupportsIndirectInputEvents + + + diff --git a/ios/stable-Info.plist b/ios/stable-Info.plist new file mode 100644 index 00000000..5ba9991f --- /dev/null +++ b/ios/stable-Info.plist @@ -0,0 +1,66 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Spotube + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + spotube + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsForMedia + + + CADisableMinimumFrameDurationOnPhone + + UIStatusBarHidden + + NSPhotoLibraryUsageDescription + This app require access to the photo library + NSCameraUsageDescription + This app require access to the device camera + NSMicrophoneUsageDescription + This app does not require access to the device microphone + UIApplicationSupportsIndirectInputEvents + + + diff --git a/pubspec.yaml b/pubspec.yaml index 16cdc2b6..36a2f398 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -152,6 +152,7 @@ flutter: - LICENSE flutter_launcher_icons: + ios: true android: true image_path: "assets/spotube-logo.png" adaptive_icon_foreground: "assets/spotube-logo-foreground.jpg" From d1ed56926d27043684f44d69cf98a08815ddb830 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 3 Jan 2024 14:08:06 +0600 Subject: [PATCH 113/131] chore: remove build-IPA.yml as no longer needed --- .github/workflows/build-iPA.yml | 42 --------------------------------- 1 file changed, 42 deletions(-) delete mode 100644 .github/workflows/build-iPA.yml diff --git a/.github/workflows/build-iPA.yml b/.github/workflows/build-iPA.yml deleted file mode 100644 index 72e68774..00000000 --- a/.github/workflows/build-iPA.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build iPA -on: workflow_dispatch - -jobs: - build: - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@master - - uses: actions/checkout@v4 - - name: submodules-init - uses: snickerbockers/submodules-init@v4 - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - - name: Build - run: | - cp .env.example .env - flutter pub get && dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns - flutter build ios --release --no-codesign --flavor dev - flutter build ios --release --no-codesign --flavor stable - flutter build ios --release --no-codesign --flavor nightly - ln -sf ./build/ios/iphoneos Payload - zip -r9 spotube-dev.ipa Payload/dev.app - zip -r9 spotube-stable.ipa Payload/stable.app - zip -r9 spotube-nightly.ipa Payload/nightly.app - - name: Upload spotube-dev.ipa - uses: actions/upload-artifact@v4 - with: - name: "spotube-dev.ipa" - path: "spotube-dev.ipa" - - name: Upload spotube-stable.ipa - uses: actions/upload-artifact@v4 - with: - name: "spotube-stable.ipa" - path: "spotube-stable.ipa" - - name: Upload spotube-nightly.ipa - uses: actions/upload-artifact@v4 - with: - name: "spotube-nightly.ipa" - path: "spotube-nightly.ipa" From 988a975bf1a675df0cfc7b17776bcec74c67f1f2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jan 2024 14:14:15 +0600 Subject: [PATCH 114/131] feat(deep-link): add track opening page --- lib/collections/routes.dart | 10 + .../player/player_track_details.dart | 11 +- lib/components/shared/links/link_text.dart | 3 + .../shared/track_tile/track_options.dart | 4 +- .../shared/track_tile/track_tile.dart | 4 +- lib/hooks/configurators/use_deep_linking.dart | 8 + lib/pages/artist/artist.dart | 2 +- lib/pages/track/track.dart | 227 ++++++++++++++++++ lib/services/queries/queries.dart | 2 + lib/services/queries/tracks.dart | 16 ++ pubspec.lock | 56 +++-- 11 files changed, 317 insertions(+), 26 deletions(-) create mode 100644 lib/pages/track/track.dart create mode 100644 lib/services/queries/tracks.dart diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 7816f204..3e2c42e0 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -17,6 +17,7 @@ import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/about.dart'; import 'package:spotube/pages/settings/logs.dart'; +import 'package:spotube/pages/track/track.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/components/shared/spotube_page_route.dart'; import 'package:spotube/pages/artist/artist.dart'; @@ -144,6 +145,15 @@ final router = GoRouter( ); }, ), + GoRoute( + path: "/track/:id", + pageBuilder: (context, state) { + final id = state.pathParameters["id"]!; + return SpotubePage( + child: TrackPage(trackId: id), + ); + }, + ), ], ), GoRoute( diff --git a/lib/components/player/player_track_details.dart b/lib/components/player/player_track_details.dart index d6f275fa..66cb9ef5 100644 --- a/lib/components/player/player_track_details.dart +++ b/lib/components/player/player_track_details.dart @@ -4,6 +4,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/links/link_text.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -44,10 +45,12 @@ class PlayerTrackDetails extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), - Text( + LinkText( playback.activeTrack?.name ?? "", + "/track/${playback.activeTrack?.id}", + push: true, overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyMedium?.copyWith( + style: theme.textTheme.bodyMedium!.copyWith( color: color, ), ), @@ -66,8 +69,10 @@ class PlayerTrackDetails extends HookConsumerWidget { flex: 1, child: Column( children: [ - Text( + LinkText( playback.activeTrack?.name ?? "", + "/track/${playback.activeTrack?.id}", + push: true, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.bold, color: color), ), diff --git a/lib/components/shared/links/link_text.dart b/lib/components/shared/links/link_text.dart index 217b247d..d7b00b72 100644 --- a/lib/components/shared/links/link_text.dart +++ b/lib/components/shared/links/link_text.dart @@ -8,6 +8,7 @@ class LinkText extends StatelessWidget { final TextAlign? textAlign; final TextOverflow? overflow; final String route; + final int? maxLines; final T? extra; final bool push; @@ -19,6 +20,7 @@ class LinkText extends StatelessWidget { this.extra, this.overflow, this.style = const TextStyle(), + this.maxLines, this.push = false, }) : super(key: key); @@ -37,6 +39,7 @@ class LinkText extends StatelessWidget { overflow: overflow, style: style, textAlign: textAlign, + maxLines: maxLines, ); } } diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/shared/track_tile/track_options.dart index 8405d6ea..724bc029 100644 --- a/lib/components/shared/track_tile/track_options.dart +++ b/lib/components/shared/track_tile/track_options.dart @@ -43,12 +43,14 @@ class TrackOptions extends HookConsumerWidget { final bool userPlaylist; final String? playlistId; final ObjectRef?>? showMenuCbRef; + final Widget? icon; const TrackOptions({ Key? key, required this.track, this.showMenuCbRef, this.userPlaylist = false, this.playlistId, + this.icon, }) : super(key: key); void actionShare(BuildContext context, Track track) { @@ -207,7 +209,7 @@ class TrackOptions extends HookConsumerWidget { break; } }, - icon: const Icon(SpotubeIcons.moreHorizontal), + icon: icon ?? const Icon(SpotubeIcons.moreHorizontal), headings: [ ListTile( dense: true, diff --git a/lib/components/shared/track_tile/track_tile.dart b/lib/components/shared/track_tile/track_tile.dart index 961f29c9..c3b03f3c 100644 --- a/lib/components/shared/track_tile/track_tile.dart +++ b/lib/components/shared/track_tile/track_tile.dart @@ -193,8 +193,10 @@ class TrackTile extends HookConsumerWidget { children: [ Expanded( flex: 6, - child: Text( + child: LinkText( track.name!, + "/track/${track.id}", + push: true, maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart index be6facf9..3b7ec3f3 100644 --- a/lib/hooks/configurators/use_deep_linking.dart +++ b/lib/hooks/configurators/use_deep_linking.dart @@ -48,6 +48,11 @@ void useDeepLinking(WidgetRef ref) { ), ); break; + case "track": + router.push( + "/track/${url.pathSegments.last}", + ); + break; default: break; } @@ -80,6 +85,9 @@ void useDeepLinking(WidgetRef ref) { case "spotify:artist": await router.push("/artist/$endSegment"); break; + case "spotify:track": + await router.push("/track/$endSegment"); + break; case "spotify:playlist": await router.push( "/playlist/$endSegment", diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 92470397..d511cb97 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -35,7 +35,7 @@ class ArtistPage extends HookConsumerWidget { ), extendBodyBehindAppBar: true, body: Builder(builder: (context) { - if (artistQuery.hasError) { + if (artistQuery.hasError && artistQuery.data == null) { return Center(child: Text(artistQuery.error.toString())); } return Skeletonizer( diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart new file mode 100644 index 00000000..14052c10 --- /dev/null +++ b/lib/pages/track/track.dart @@ -0,0 +1,227 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/heart_button.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/components/shared/links/link_text.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/components/shared/track_tile/track_options.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:spotube/extensions/constrains.dart'; + +class TrackPage extends HookConsumerWidget { + final String trackId; + const TrackPage({ + Key? key, + required this.trackId, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final ThemeData(:textTheme, :colorScheme) = Theme.of(context); + final mediaQuery = MediaQuery.of(context); + + final playlist = ref.watch(ProxyPlaylistNotifier.provider); + final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); + + final isActive = playlist.activeTrack?.id == trackId; + + final trackQuery = useQueries.tracks.track(ref, trackId); + + final track = trackQuery.data ?? FakeData.track; + + void onPlay() async { + if (isActive) { + audioPlayer.pause(); + } else { + await playlistNotifier.load([track], autoPlay: true); + } + } + + return Scaffold( + appBar: const PageWindowTitleBar( + automaticallyImplyLeading: true, + backgroundColor: Colors.transparent, + ), + extendBodyBehindAppBar: true, + body: Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: UniversalImage.imageProvider( + TypeConversionUtils.image_X_UrlString( + track.album!.images, + placeholder: ImagePlaceholder.albumArt, + ), + ), + fit: BoxFit.cover, + colorFilter: ColorFilter.mode( + colorScheme.surface.withOpacity(0.5), + BlendMode.srcOver, + ), + alignment: Alignment.topCenter, + ), + ), + ), + ), + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Skeletonizer( + enabled: trackQuery.isLoading, + child: Container( + alignment: Alignment.topCenter, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + colorScheme.surface, + Colors.transparent, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.2, 1], + ), + ), + child: SafeArea( + child: Wrap( + spacing: 20, + runSpacing: 20, + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: TypeConversionUtils.image_X_UrlString( + track.album!.images, + placeholder: ImagePlaceholder.albumArt, + ), + height: 200, + width: 200, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: mediaQuery.smAndDown + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + track.name!, + style: textTheme.titleLarge, + ), + const Gap(10), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(SpotubeIcons.album), + const Gap(5), + Flexible( + child: LinkText( + track.album!.name!, + '/album/${track.album!.id}', + push: true, + extra: track.album, + ), + ), + ], + ), + const Gap(10), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(SpotubeIcons.artist), + const Gap(5), + TypeConversionUtils + .artists_X_ClickableArtists( + track.artists!, + ), + ], + ), + const Gap(10), + ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 350), + child: Row( + mainAxisSize: mediaQuery.smAndDown + ? MainAxisSize.max + : MainAxisSize.min, + children: [ + const Gap(5), + if (!isActive && + !playlist.tracks.contains(track)) + OutlinedButton.icon( + icon: const Icon(SpotubeIcons.queueAdd), + label: Text(context.l10n.queue), + onPressed: () { + playlistNotifier.addTrack(track); + }, + ), + const Gap(5), + if (!isActive && + !playlist.tracks.contains(track)) + IconButton.outlined( + icon: + const Icon(SpotubeIcons.lightning), + tooltip: context.l10n.play_next, + onPressed: () { + playlistNotifier + .addTracksAtFirst([track]); + }, + ), + const Gap(5), + IconButton.filled( + tooltip: isActive + ? context.l10n.pause_playback + : context.l10n.play, + icon: Icon( + isActive + ? SpotubeIcons.pause + : SpotubeIcons.play, + color: colorScheme.onPrimary, + ), + onPressed: onPlay, + ), + const Gap(5), + if (mediaQuery.smAndDown) + const Spacer() + else + const Gap(20), + TrackHeartButton(track: track), + TrackOptions( + track: track, + userPlaylist: false, + ), + const Gap(5), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/services/queries/queries.dart b/lib/services/queries/queries.dart index cc3ce132..30c23268 100644 --- a/lib/services/queries/queries.dart +++ b/lib/services/queries/queries.dart @@ -4,6 +4,7 @@ import 'package:spotube/services/queries/category.dart'; import 'package:spotube/services/queries/lyrics.dart'; import 'package:spotube/services/queries/playlist.dart'; import 'package:spotube/services/queries/search.dart'; +import 'package:spotube/services/queries/tracks.dart'; import 'package:spotube/services/queries/user.dart'; import 'package:spotube/services/queries/views.dart'; @@ -17,6 +18,7 @@ class Queries { final search = const SearchQueries(); final user = const UserQueries(); final views = const ViewsQueries(); + final tracks = const TracksQueries(); } const useQueries = Queries._(); diff --git a/lib/services/queries/tracks.dart b/lib/services/queries/tracks.dart new file mode 100644 index 00000000..52bab984 --- /dev/null +++ b/lib/services/queries/tracks.dart @@ -0,0 +1,16 @@ +import 'package:fl_query/fl_query.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/hooks/spotify/use_spotify_query.dart'; + +class TracksQueries { + const TracksQueries(); + + Query track(WidgetRef ref, String id) { + return useSpotifyQuery( + "track/$id", + (spotify) => spotify.tracks.get(id), + ref: ref, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 526898d5..b4182d12 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -543,10 +543,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_picker: dependency: "direct main" description: @@ -1255,6 +1255,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "04be76c4a4bb50f14904e64749237e541e7c7bcf7ec0b196907322ab5d2fc739" + url: "https://pub.dev" + source: hosted + version: "9.0.16" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: b06739349ec2477e943055aea30172c5c7000225f79dad4702e2ec0eda79a6ff + url: "https://pub.dev" + source: hosted + version: "1.0.5" lints: dependency: transitive description: @@ -1307,10 +1323,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" media_kit: dependency: "direct main" description: @@ -1379,10 +1395,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" metadata_god: dependency: "direct main" description: @@ -1595,10 +1611,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" plugin_platform_interface: dependency: transitive description: @@ -1635,10 +1651,10 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "266ca5be5820feefc777793d0a583acfc8c40834893c87c00c6c09e2cf58ea42" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.1" provider: dependency: transitive description: @@ -1780,10 +1796,10 @@ packages: dependency: transitive description: name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_platform_interface: dependency: transitive description: @@ -1804,10 +1820,10 @@ packages: dependency: transitive description: name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shelf: dependency: transitive description: @@ -2217,10 +2233,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" watcher: dependency: transitive description: @@ -2233,10 +2249,10 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: edc8a9573dd8c5a83a183dae1af2b6fd4131377404706ca4e5420474784906fa url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.4.0" web_socket_channel: dependency: transitive description: @@ -2249,10 +2265,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" wikipedia_api: dependency: "direct main" description: From c203ac69ee74ba8722dae3da4b47761cd8d59c34 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jan 2024 20:02:27 +0600 Subject: [PATCH 115/131] fix: search page vertical scrollbar moves on horizontal scroll #1017 --- .../horizontal_playbutton_card_view.dart | 86 ++++++++++--------- 1 file changed, 45 insertions(+), 41 deletions(-) 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 index d00e5c4b..2075acbb 100644 --- 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 @@ -55,48 +55,52 @@ class HorizontalPlaybuttonCardView extends HookWidget { ), SizedBox( height: height, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: items.isEmpty - ? ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: 5, - itemBuilder: (context, index) { - return AlbumCard(FakeData.albumSimple); - }, - ) - : InfiniteList( - scrollController: scrollController, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(vertical: 8.0), - itemCount: items.length, - onFetchData: onFetchMore, - loadingBuilder: (context) => Skeletonizer( - enabled: true, - child: AlbumCard(FakeData.albumSimple), - ), - isLoading: isLoadingNextPage, - hasReachedMax: !hasNextPage, - itemBuilder: (context, index) { - 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), + child: NotificationListener( + // disable multiple scrollbar to use this + onNotification: (notification) => true, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: items.isEmpty + ? ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: 5, + itemBuilder: (context, index) { + return AlbumCard(FakeData.albumSimple); + }, + ) + : InfiniteList( + scrollController: scrollController, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(vertical: 8.0), + itemCount: items.length, + onFetchData: onFetchMore, + loadingBuilder: (context) => Skeletonizer( + enabled: true, + child: AlbumCard(FakeData.albumSimple), ), - _ => const SizedBox.shrink(), - }; - }), + isLoading: isLoadingNextPage, + hasReachedMax: !hasNextPage, + itemBuilder: (context, index) { + 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(), + }; + }), + ), ), ), ], From 5509cae91c8b1f5cb9fac179060f477397a4a27f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jan 2024 20:26:57 +0600 Subject: [PATCH 116/131] fix(android): download failing for permission issues #1015 --- .../download_manager/chunked_download.dart | 1 - .../download_manager/download_manager.dart | 22 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/services/download_manager/chunked_download.dart b/lib/services/download_manager/chunked_download.dart index b2849a3c..9e5e0a98 100644 --- a/lib/services/download_manager/chunked_download.dart +++ b/lib/services/download_manager/chunked_download.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; import 'package:spotube/models/logger.dart'; final logger = getLogger("ChunkedDownload"); diff --git a/lib/services/download_manager/download_manager.dart b/lib/services/download_manager/download_manager.dart index 904f06cf..d7a42430 100644 --- a/lib/services/download_manager/download_manager.dart +++ b/lib/services/download_manager/download_manager.dart @@ -6,6 +6,8 @@ import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/services/download_manager/chunked_download.dart'; import 'package:spotube/services/download_manager/download_request.dart'; @@ -77,7 +79,18 @@ class DownloadManager { logger.d("[DownloadManager] $url"); final file = File(savePath.toString()); - partialFilePath = savePath + partialExtension; + + final tmpDirPath = await Directory( + path.join( + (await getTemporaryDirectory()).path, + "spotube-downloads", + ), + ).create(recursive: true); + + partialFilePath = path.join( + tmpDirPath.path, + path.basename(savePath) + partialExtension, + ); partialFile = File(partialFilePath); final fileExist = await file.exists(); @@ -111,7 +124,9 @@ class DownloadManager { await ioSink.addStream(partialChunkFile.openRead()); await partialChunkFile.delete(); await ioSink.close(); - await partialFile.rename(savePath); + + await partialFile.copy(savePath); + await partialFile.delete(); setStatus(task, DownloadStatus.completed); } @@ -125,7 +140,8 @@ class DownloadManager { ); if (response.statusCode == HttpStatus.ok) { - await partialFile.rename(savePath); + await partialFile.copy(savePath); + await partialFile.delete(); setStatus(task, DownloadStatus.completed); } } From 29f162c80100fcb9ad51ee3103f04cf2facc17e9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 5 Jan 2024 21:25:23 +0600 Subject: [PATCH 117/131] chore: remove the sp_key cookie requirement as no longer necessary --- lib/components/desktop_login/login_form.dart | 15 +--- lib/l10n/app_ar.arb | 2 - lib/l10n/app_bn.arb | 2 - lib/l10n/app_ca.arb | 2 - lib/l10n/app_de.arb | 2 - lib/l10n/app_en.arb | 6 +- lib/l10n/app_es.arb | 2 - lib/l10n/app_fa.arb | 2 - lib/l10n/app_fr.arb | 2 - lib/l10n/app_hi.arb | 2 - lib/l10n/app_it.arb | 2 - lib/l10n/app_ja.arb | 2 - lib/l10n/app_nl.arb | 2 - lib/l10n/app_pl.arb | 2 - lib/l10n/app_pt.arb | 2 - lib/l10n/app_ru.arb | 2 - lib/l10n/app_tr.arb | 2 - lib/l10n/app_uk.arb | 2 - lib/l10n/app_zh.arb | 2 - lib/pages/mobile_login/mobile_login.dart | 7 +- untranslated_messages.json | 87 +++++++++++++++++++- 21 files changed, 92 insertions(+), 57 deletions(-) diff --git a/lib/components/desktop_login/login_form.dart b/lib/components/desktop_login/login_form.dart index f2b183f4..5abb9524 100644 --- a/lib/components/desktop_login/login_form.dart +++ b/lib/components/desktop_login/login_form.dart @@ -17,7 +17,6 @@ class TokenLoginForm extends HookConsumerWidget { final authenticationNotifier = ref.watch(AuthenticationNotifier.provider.notifier); final directCodeController = useTextEditingController(); - final keyCodeController = useTextEditingController(); final mounted = useIsMounted(); final isLoading = useState(false); @@ -37,23 +36,13 @@ class TokenLoginForm extends HookConsumerWidget { keyboardType: TextInputType.visiblePassword, ), const SizedBox(height: 10), - TextField( - controller: keyCodeController, - decoration: InputDecoration( - hintText: context.l10n.spotify_cookie("\"sp_key (or sp_gaid)\""), - labelText: context.l10n.cookie_name_cookie("sp_key (or sp_gaid)"), - ), - keyboardType: TextInputType.visiblePassword, - ), - const SizedBox(height: 20), FilledButton( onPressed: isLoading.value ? null : () async { try { isLoading.value = true; - if (keyCodeController.text.isEmpty || - directCodeController.text.isEmpty) { + if (directCodeController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(context.l10n.fill_in_all_fields), @@ -63,7 +52,7 @@ class TokenLoginForm extends HookConsumerWidget { return; } final cookieHeader = - "sp_dc=${directCodeController.text.trim()}; sp_key=${keyCodeController.text.trim()}"; + "sp_dc=${directCodeController.text.trim()}"; authenticationNotifier.setCredentials( await AuthenticationCredentials.fromCookie( diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 2bdde72a..20b6a47c 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -177,11 +177,9 @@ "step_2": "الخطوة 2", "step_2_steps": "1. بمجرد تسجيل الدخول، اضغط على F12 أو انقر بزر الماوس الأيمن > فحص لفتح أدوات تطوير المتصفح.\n2. ثم انتقل إلى علامة التبويب \"التطبيقات\" (Chrome وEdge وBrave وما إلى ذلك.) أو علامة التبويب \"التخزين\" (Firefox وPalemoon وما إلى ذلك..)\n3. انتقل إلى قسم \"ملفات تعريف الارتباط\" ثم القسم الفرعي \"https://accounts.spotify.com\"", "step_3": "الخطوة 3", - "step_3_steps": "انسخ قيم \"sp_dc\" و \"sp_key\" (أو sp_gaid) الكويز", "success_emoji": "نجاح 🥳", "success_message": "لقد قمت الآن بتسجيل الدخول بنجاح باستخدام حساب Spotify الخاص بك. عمل جيد يا صديقي!", "step_4": "الخطوة 4", - "step_4_steps": "قم بلصق قيم \"sp_dc\" و \"sp_key\" (أو sp_gaid) المنسوخة في الحقول المعنية", "something_went_wrong": "هناك خطأ ما", "piped_instance": "مثيل خادم Piped", "piped_description": "مثيل خادم Piped الذي سيتم استخدامه لمطابقة المقطوعة", diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 39f8a1ee..74dc200d 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -175,11 +175,9 @@ "step_2": "ধাপ 2", "step_2_steps": "১. একবার আপনি লগ ইন করলে, ব্রাউজার ডেভটুল খুলতে F12 বা মাউসের রাইট ক্লিক > \"Inspect to open Browser DevTools\" টিপুন।\n২. তারপর \"Application\" ট্যাবে যান (Chrome, Edge, Brave etc..) অথবা \"Storage\" Tab (Firefox, Palemoon etc..)\n৩. \"Cookies \" বিভাগে যান তারপর \"https://accounts.spotify.com\" উপবিভাগে যান", "step_3": "ধাপ 3", - "step_3_steps": "\"sp_dc\" এবং \"sp_key\" (অথবা sp_gaid) কুকিজের মান কপি করুন", "success_emoji": "আমরা সফল🥳", "success_message": "এখন আপনি সফলভাবে আপনার Spotify অ্যাকাউন্ট দিয়ে লগ ইন করেছেন। সাধুভাত আপনাকে", "step_4": "ধাপ 4", - "step_4_steps": "কপি করা \"sp_dc\" এবং \"sp_key\" (অথবা sp_gaid) এর মান সংশ্লিষ্ট ফিল্ডে পেস্ট করুন", "something_went_wrong": "কিছু ভুল হয়েছে", "piped_instance": "Piped সার্ভার এড্রেস", "piped_description": "গান ম্যাচ করার জন্য ব্যবহৃত পাইপড সার্ভার", diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index 15ca9e31..2c952457 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -175,11 +175,9 @@ "step_2": "Pas 2", "step_2_steps": "1. Una vegada que hagi iniciat sessió, premi F12 o faci clic dret amb el ratolí > Inspeccionar per obrir les eines de desenvolulpador del navegador.\n2. Després vagi a la pestanya \"Application\" (Chrome, Edge, Brave, etc.) o \"Storage\" (Firefox, Palemoon, etc.)\n3. Vagi a la secció \"Cookies\" i després a la subsecció \"https://accounts.spotify.com\"", "step_3": "Pas 3", - "step_3_steps": "Copiï els valors de les Cookies \"sp_dc\" i \"sp_key\" (o sp_gaid)", "success_emoji": "Èxit! 🥳", "success_message": "Ara has iniciat sessió amb èxit al teu compte de Spotify. Bona feina!", "step_4": "Pas 4", - "step_4_steps": "Enganxi els valors coppiats de \"sp_dc\" i \"sp_key\" (o sp_gaid) en els camps respectius", "something_went_wrong": "Quelcom ha sortit malament", "piped_instance": "Instància del servidor Piped", "piped_description": "La instància del servidor Piped a utilitzar per la coincidència de cançons", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 1a13e4a1..59f832ea 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -175,11 +175,9 @@ "step_2": "Schritt 2", "step_2_steps": "1. Wenn du angemeldet bist, drücke F12 oder klicke mit der rechten Maustaste > Inspektion, um die Browser-Entwicklertools zu öffnen.\n2. Gehe dann zum \"Anwendungs\"-Tab (Chrome, Edge, Brave usw.) oder zum \"Storage\"-Tab (Firefox, Palemoon usw.)\n3. Gehe zum Abschnitt \"Cookies\" und dann zum Unterabschnitt \"https://accounts.spotify.com\"", "step_3": "Schritt 3", - "step_3_steps": "Kopiere die Werte der Cookies \"sp_dc\" und \"sp_key\" (oder sp_gaid)", "success_emoji": "Erfolg🥳", "success_message": "Jetzt bist du erfolgreich mit deinem Spotify-Konto angemeldet. Gut gemacht, Kumpel!", "step_4": "Schritt 4", - "step_4_steps": "Füge die kopierten Werte von \"sp_dc\" und \"sp_key\" (oder sp_gaid) in die entsprechenden Felder ein", "something_went_wrong": "Etwas ist schiefgelaufen", "piped_instance": "Piped-Serverinstanz", "piped_description": "Die Piped-Serverinstanz, die zur Titelzuordnung verwendet werden soll", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bebfafac..82877ea1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -177,11 +177,11 @@ "step_2": "Step 2", "step_2_steps": "1. Once you're logged in, press F12 or Mouse Right Click > Inspect to Open the Browser devtools.\n2. Then go the \"Application\" Tab (Chrome, Edge, Brave etc..) or \"Storage\" Tab (Firefox, Palemoon etc..)\n3. Go to the \"Cookies\" section then the \"https://accounts.spotify.com\" subsection", "step_3": "Step 3", - "step_3_steps": "Copy the values of \"sp_dc\" and \"sp_key\" (or sp_gaid) Cookies", + "step_3_steps": "Copy the value of \"sp_dc\" Cookie", "success_emoji": "Success🥳", - "success_message": "Now you're successfully Logged In with your Spotify account. Good Job, mate!", + "success_message": "Now you've successfully Logged in with your Spotify account. Good Job, mate!", "step_4": "Step 4", - "step_4_steps": "Paste the copied \"sp_dc\" and \"sp_key\" (or sp_gaid) values in the respective fields", + "step_4_steps": "Paste the copied \"sp_dc\" value", "something_went_wrong": "Something went wrong", "piped_instance": "Piped Server Instance", "piped_description": "The Piped server instance to use for track matching", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 2fecd8f1..e04b4798 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -175,11 +175,9 @@ "step_2": "Paso 2", "step_2_steps": "1. Una vez que hayas iniciado sesión, presiona F12 o haz clic derecho con el ratón > Inspeccionar para abrir las herramientas de desarrollo del navegador.\n2. Luego ve a la pestaña \"Application\" (Chrome, Edge, Brave, etc.) o \"Storage\" (Firefox, Palemoon, etc.)\n3. Ve a la sección \"Cookies\" y luego la subsección \"https://accounts.spotify.com\"", "step_3": "Paso 3", - "step_3_steps": "Copia los valores de las Cookies \"sp_dc\" y \"sp_key\" (o sp_gaid)", "success_emoji": "¡Éxito! 🥳", "success_message": "Ahora has iniciado sesión con éxito en tu cuenta de Spotify. ¡Buen trabajo!", "step_4": "Paso 4", - "step_4_steps": "Pega los valores copiados de \"sp_dc\" y \"sp_key\" (o sp_gaid) en los campos respectivos", "something_went_wrong": "Algo salió mal", "piped_instance": "Instancia del servidor Piped", "piped_description": "La instancia del servidor Piped a utilizar para la coincidencia de pistas", diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 84b9b448..c9586cde 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -177,11 +177,9 @@ "step_2": "گام 2", "step_2_steps": "1. پس از ورود به سیستم، F12 یا کلیک راست ماوس > Inspect را فشار دهید تا ابزارهای توسعه مرورگر باز شود..\n2. سپس به تب \"Application\" (Chrome, Edge, Brave etc..) یا \"Storage\" Tab (Firefox, Palemoon etc..)\n3. به قسمت \"Cookies\" و به پخش \"https://accounts.spotify.com\" بروید", "step_3": "گام 3", - "step_3_steps": "کپی کردن مقادیر \"sp_dc\" و \"sp_key\" (یا sp_gaid) کوکی", "success_emoji": "موفقیت🥳", "success_message": "اکنون با موفقیت با حساب اسپوتیفای خود وارد شده اید", "step_4": "مرحله 4", - "step_4_steps": "مقدار کپی شده را \"sp_dc\" and \"sp_key\" (یا sp_gaid) در فیلد مربوط پر کنید", "something_went_wrong": "اشتباهی رخ داده", "piped_instance": "مشکل در ارتباط با سرور", "piped_description": "مشکل در ارتباط با سرور در دریافت آهنگ ها", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 82997bad..0c3eb653 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -175,11 +175,9 @@ "step_2": "Étape 2", "step_2_steps": "1. Une fois connecté, appuyez sur F12 ou clic droit de la souris > Inspecter pour ouvrir les outils de développement du navigateur.\n2. Ensuite, allez dans l'onglet \"Application\" (Chrome, Edge, Brave, etc.) ou l'onglet \"Stockage\" (Firefox, Palemoon, etc.)\n3. Allez dans la section \"Cookies\", puis dans la sous-section \"https://accounts.spotify.com\"", "step_3": "Étape 3", - "step_3_steps": "Copiez les valeurs des cookies \"sp_dc\" et \"sp_key\" (ou sp_gaid)", "success_emoji": "Succès🥳", "success_message": "Vous êtes maintenant connecté avec succès à votre compte Spotify. Bon travail, mon ami!", "step_4": "Étape 4", - "step_4_steps": "Collez les valeurs copiées de \"sp_dc\" et \"sp_key\" (ou sp_gaid) dans les champs respectifs", "something_went_wrong": "Quelque chose s'est mal passé", "piped_instance": "Instance pipée", "piped_description": "L'instance de serveur Piped à utiliser pour la correspondance des pistes", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 4bfff3da..dd27dabf 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -175,11 +175,9 @@ "step_2": "2 चरण", "step_2_steps": "1. जब आप लॉगिन हो जाएँ, तो F12 दबाएं या माउस राइट क्लिक> निरीक्षण करें ताकि ब्राउज़र डेवटूल्स खुलें।\n2. फिर ब्राउज़र के \"एप्लिकेशन\" टैब (Chrome, Edge, Brave आदि) या \"स्टोरेज\" टैब (Firefox, Palemoon आदि) में जाएं\n3. \"कुकीज़\" अनुभाग में जाएं फिर \"https: //accounts.spotify.com\" उप-अनुभाग में जाएं", "step_3": "स्टेप 3", - "step_3_steps": "\"sp_dc\" और \"sp_key\" (या sp_gaid) कुकीज़ के मान कॉपी करें", "success_emoji": "सफलता🥳", "success_message": "अब आप अपने स्पॉटिफाई अकाउंट से सफलतापूर्वक लॉगइन हो गए हैं। अच्छा काम किया!", "step_4": "स्टेप 4", - "step_4_steps": "कॉपी की गई \"sp_dc\" और \"sp_key\" (या sp_gaid) मानों को संबंधित फील्ड में पेस्ट करें", "something_went_wrong": "कुछ गलत हो गया", "piped_instance": "पाइप्ड सर्वर", "piped_description": "पाइप किए गए सर्वर", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 033bb516..3680933a 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -177,11 +177,9 @@ "step_2": "Passo 2", "step_2_steps": "1. Quando sei acceduto premi F12 o premi il tasto destro del Mouse > Ispeziona per aprire gli strumenti di sviluppo del browser.\n2. Vai quindi nel tab \"Applicazione\" (Chrome, Edge, Brave etc..) o tab \"Archiviazione\" (Firefox, Palemoon etc..)\n3. Vai nella sezione \"Cookies\" quindi nella sezione \"https://accounts.spotify.com\"", "step_3": "Passo 3", - "step_3_steps": "Copia il valore dei cookie \"sp_dc\" e \"sp_key\" (o sp_gaid)", "success_emoji": "Successo🥳", "success_message": "Ora hai correttamente effettuato il login al tuo account Spotify. Bel lavoro, amico!", "step_4": "Passo 4", - "step_4_steps": "Incolla i valori copiati di \"sp_dc\" e \"sp_key\" (o sp_gaid) nei campi rispettivi", "something_went_wrong": "Qualcosa è andato storto", "piped_instance": "Istanza Server Piped", "piped_description": "L'istanza server Piped da usare per il match della tracccia", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index ac23728b..39e0dad8 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -175,11 +175,9 @@ "step_2": "ステップ 2", "step_2_steps": "1. ログインしたら、F12を押すか、マウス右クリック > 調査(検証)でブラウザの開発者ツール (devtools) を開きます。\n2. アプリケーション (Application) タブ (Chrome, Edge, Brave など) またはストレージタブ (Firefox, Palemoon など)\n3. Cookies 欄を選択し、https://accounts.spotify.com の枝を選びます", "step_3": "ステップ 3", - "step_3_steps": "sp_dc と sp_key (または or sp_gaid) の値 (Value) をコピーします", "success_emoji": "成功🥳", "success_message": "アカウントへのログインに成功しました。よくできました!", "step_4": "ステップ 4", - "step_4_steps": "コピーした sp_dc と sp_key (または or sp_gaid) の値をそれぞれの入力欄に貼り付けます", "something_went_wrong": "何か誤りがあります", "piped_instance": "Piped サーバーのインスタンス", "piped_description": "曲の一致に使う Piped サーバーのインスタンス", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 6e50c461..23eee51b 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -177,11 +177,9 @@ "step_2": "Stap 2", "step_2_steps": "1. Zodra je bent aangemeld, druk je op F12 of klik je met de rechtermuisknop > Inspect om de Browser devtools te openen.\n2. Ga vervolgens naar het tabblad \"Toepassing\" (Chrome, Edge, Brave enz..) of naar het tabblad \"Opslag\" (Firefox, Palemoon enz..).\n3. Ga naar de sectie \"Cookies\" en vervolgens naar de subsectie \"https://accounts.spotify.com\".", "step_3": "Stap 3", - "step_3_steps": "Kopieer de waarden van \"sp_dc\" en \"sp_key\" (of sp_gaid) Cookies", "success_emoji": "Succes🥳", "success_message": "Je bent nu succesvol ingelogd met je Spotify account. Goed gedaan, maat!", "step_4": "Stap 4", - "step_4_steps": "Plak de gekopieerde \"sp_dc\" en \"sp_key\" (of sp_gaid) waarden in de respectievelijke velden", "something_went_wrong": "Er ging iets mis", "piped_instance": "Piped-serverinstantie", "piped_description": "De Piped-serverinstantie die moet worden gebruikt voor het matchen van sporen", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index dd173a37..4ae48338 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -175,11 +175,9 @@ "step_2": "Krok 2", "step_2_steps": "1. Jeśli jesteś zalogowany, naciśnij klawisz F12 lub Kliknij prawym przyciskiem myszy > Zbadaj, aby odtworzyć narzędzia developerskie.\n2. Następnie przejdź do zakładki \"Application\" (Chrome, Edge, Brave etc..) lub zakładki \"Storage\" (Firefox, Palemoon etc..)\n3. Przejdź do sekcji \"Cookies\" a następnie do pod-sekcji \"https://accounts.spotify.com\"", "step_3": "Krok 3", - "step_3_steps": "Skopiuj wartości \"sp_dc\" i \"sp_key\" (lub sp_gaid) Ciasteczek", "success_emoji": "Sukces!🥳", "success_message": "Udało ci się zalogować! Dobra robota, stary!", "step_4": "Krok 4", - "step_4_steps": "Wklej wartości \"sp_dc\" i \"sp_key\" (lub sp_gaid) do odpowiednich pul.", "something_went_wrong": "Coś poszło nie tak 🙁", "piped_instance": "Instancja serwera Piped", "piped_description": "Instancja serwera Piped używana jest do dopasowania utworów.", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 705217c1..5ea4cca0 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -175,11 +175,9 @@ "step_2": "Passo 2", "step_2_steps": "1. Uma vez logado, pressione F12 ou clique com o botão direito do mouse > Inspecionar para abrir as ferramentas de desenvolvimento do navegador.\n2. Em seguida, vá para a guia \"Aplicativo\" (Chrome, Edge, Brave, etc.) ou \"Armazenamento\" (Firefox, Palemoon, etc.)\n3. Acesse a seção \"Cookies\" e depois a subseção \"https://accounts.spotify.com\"", "step_3": "Passo 3", - "step_3_steps": "Copie os valores dos Cookies \"sp_dc\" e \"sp_key\" (ou sp_gaid)", "success_emoji": "Sucesso🥳", "success_message": "Agora você está logado com sucesso em sua conta do Spotify. Bom trabalho!", "step_4": "Passo 4", - "step_4_steps": "Cole os valores copiados \"sp_dc\" e \"sp_key\" (ou sp_gaid) nos campos correspondentes", "something_went_wrong": "Algo deu errado", "piped_instance": "Instância do Servidor Piped", "piped_description": "A instância do servidor Piped a ser usada para correspondência de faixas", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 32415863..24120d62 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -175,11 +175,9 @@ "step_2": "Шаг 2", "step_2_steps": "1. После входа в систему нажмите F12 или щелкните правой кнопкой мыши > «Проверить», чтобы открыть инструменты разработчика браузера.\n2. Затем перейдите на вкладку \"Application\" (Chrome, Edge, Brave и т.д..) or \"Storage\" (Firefox, Palemoon и т.д..)\n3. Перейдите в раздел \"Cookies\", а затем в подраздел \"https://accounts.spotify.com\"", "step_3": "Шаг 3", - "step_3_steps": "Скопируйте значения \"sp_dc\" и \"sp_key\" (или sp_gaid) Cookies", "success_emoji": "Успешно 🥳", "success_message": "Теперь вы успешно вошли в свою учетную запись Spotify. Отличная работа, приятель!", "step_4": "Шаг 4", - "step_4_steps": "Вставьте скопированные \"sp_dc\" и \"sp_key\" (или sp_gaid) значения в соответствующие поля", "something_went_wrong": "Что-то пошло не так", "piped_instance": "Экземпляр сервера Piped", "piped_description": "Серверный экземпляр Piped для сопоставления треков", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 63646af6..e6b0ce34 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -177,11 +177,9 @@ "step_2": "2. Adım", "step_2_steps": "1. Giriş yaptıktan sonra, Tarayıcı devtools.\n2'yi açmak için F12'ye basın veya Fare Sağ Tıklaması > İncele'ye basın. Ardından \"Uygulama\" Sekmesine (Chrome, Edge, Brave vb.) veya \"Depolama\" Sekmesine (Firefox, Palemoon vb.) gidin\n3. \"Çerezler\" bölümüne ve ardından \"https://accounts.spotify.com\" alt bölümüne gidin", "step_3": "3. Adım", - "step_3_steps": "\"sp_dc\" ve \"sp_key\" (veya sp_gaid) Çerezlerinin değerlerini kopyalayın", "success_emoji": "Başarılı🥳", "success_message": "Şimdi Spotify hesabınızla başarılı bir şekilde oturum açtınız. İyi iş, dostum!", "step_4": "4. Adım", - "step_4_steps": "Kopyalanan \"sp_dc\" ve \"sp_key\" (veya sp_gaid) değerlerini ilgili alanlara yapıştırın", "something_went_wrong": "Bir şeyler ters gitti", "piped_instance": "Piped Sunucu Örneği", "piped_description": "Parça eşleştirme için kullanılacak Piped sunucu örneği", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 2ae29237..a5199a04 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -177,11 +177,9 @@ "step_2": "Крок 2", "step_2_steps": "1. Після входу натисніть F12 або клацніть правою кнопкою миші > Інспектувати, щоб відкрити інструменти розробки браузера.\n2. Потім перейдіть на вкладку 'Програма' (Chrome, Edge, Brave тощо) або вкладку 'Сховище' (Firefox, Palemoon тощо).\n3. Перейдіть до розділу 'Кукі-файли', а потім до підрозділу 'https://accounts.spotify.com'", "step_3": "Крок 3", - "step_3_steps": "Скопіюйте значення кукі-файлів 'sp_dc' та 'sp_key' (або sp_gaid)", "success_emoji": "Успіх🥳", "success_message": "Тепер ви успішно ввійшли у свій обліковий запис Spotify. Гарна робота, друже!", "step_4": "Крок 4", - "step_4_steps": "Вставте скопійовані значення 'sp_dc' та 'sp_key' (або sp_gaid) у відповідні поля", "something_went_wrong": "Щось пішло не так", "piped_instance": "Примірник сервера Piped", "piped_description": "Примірник сервера Piped, який використовуватиметься для зіставлення треків", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 85b57724..30f4a82c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -175,11 +175,9 @@ "step_2": "步骤 2", "step_2_steps": "1. 一旦你已经完成登录, 按 F12 键或者鼠标右击网页空白区域 > 选择“检查”以打开浏览器开发者工具(DevTools)\n2. 然后选择 \"应用(Application)\" 标签页(Chrome, Edge, Brave 等基于 Chromium 的浏览器) 或 \"存储(Storage)\" 标签页 (Firefox, Palemoon 等基于 Firefox 的浏览器))\n3. 选择 \"Cookies\" 栏目然后选择 \"https://accounts.spotify.com\" 子栏目", "step_3": "步骤 3", - "step_3_steps": "复制名称为 \"sp_dc\" 和 \"sp_key\" (或 sp_gaid) 的值(Cookie Value)", "success_emoji": "成功🥳", "success_message": "你已经成功使用 Spotify 登录。干得漂亮!", "step_4": "步骤 4", - "step_4_steps": "将 \"sp_dc\" 与 \"sp_key\" (或 sp_gaid) 的值分别复制后粘贴到对应的区域", "something_went_wrong": "某些地方出现了问题", "piped_instance": "管道服务器实例", "piped_description": "管道服务器实例用于匹配歌曲", diff --git a/lib/pages/mobile_login/mobile_login.dart b/lib/pages/mobile_login/mobile_login.dart index 7ab0ea2a..8b9bce4c 100644 --- a/lib/pages/mobile_login/mobile_login.dart +++ b/lib/pages/mobile_login/mobile_login.dart @@ -55,12 +55,7 @@ class WebViewLogin extends HookConsumerWidget { final cookies = await CookieManager.instance().getCookies(url: action); final cookieHeader = - cookies.fold("", (previousValue, element) { - if (element.name == "sp_dc" || element.name == "sp_key") { - return "$previousValue; ${element.name}=${element.value}"; - } - return previousValue; - }); + "sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}"; authenticationNotifier.setCredentials( await AuthenticationCredentials.fromCookie(cookieHeader), diff --git a/untranslated_messages.json b/untranslated_messages.json index 9e26dfee..59b26614 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1 +1,86 @@ -{} \ No newline at end of file +{ + "ar": [ + "step_3_steps", + "step_4_steps" + ], + + "bn": [ + "step_3_steps", + "step_4_steps" + ], + + "ca": [ + "step_3_steps", + "step_4_steps" + ], + + "de": [ + "step_3_steps", + "step_4_steps" + ], + + "es": [ + "step_3_steps", + "step_4_steps" + ], + + "fa": [ + "step_3_steps", + "step_4_steps" + ], + + "fr": [ + "step_3_steps", + "step_4_steps" + ], + + "hi": [ + "step_3_steps", + "step_4_steps" + ], + + "it": [ + "step_3_steps", + "step_4_steps" + ], + + "ja": [ + "step_3_steps", + "step_4_steps" + ], + + "nl": [ + "step_3_steps", + "step_4_steps" + ], + + "pl": [ + "step_3_steps", + "step_4_steps" + ], + + "pt": [ + "step_3_steps", + "step_4_steps" + ], + + "ru": [ + "step_3_steps", + "step_4_steps" + ], + + "tr": [ + "step_3_steps", + "step_4_steps" + ], + + "uk": [ + "step_3_steps", + "step_4_steps" + ], + + "zh": [ + "step_3_steps", + "step_4_steps" + ] +} From a76ee0acf2615a41b0527163cbd7e1d20fed226d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 8 Jan 2024 22:57:03 +0600 Subject: [PATCH 118/131] chore: new liked tracks image --- assets/liked-tracks.jpg | Bin 0 -> 27058 bytes lib/components/library/user_playlists.dart | 30 +++++++++--------- .../sections/header/flexible_header.dart | 3 +- lib/pages/playlist/liked_playlist.dart | 6 +--- 4 files changed, 17 insertions(+), 22 deletions(-) create mode 100644 assets/liked-tracks.jpg diff --git a/assets/liked-tracks.jpg b/assets/liked-tracks.jpg new file mode 100644 index 0000000000000000000000000000000000000000..62dad65ea112e49db84afab026803502c221a249 GIT binary patch literal 27058 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 literal 0 HcmV?d00001 diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index a65c6d0e..32e91ed6 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -37,21 +37,21 @@ class UserPlaylists extends HookConsumerWidget { ); final likedTracksPlaylist = useMemoized( - () => PlaylistSimple() - ..name = context.l10n.liked_tracks - ..description = context.l10n.liked_tracks_description - ..type = "playlist" - ..collaborative = false - ..public = false - ..id = "user-liked-tracks" - ..images = [ - Image() - ..height = 300 - ..width = 300 - ..url = - "https://t.scdn.co/images/3099b3803ad9496896c43f22fe9be8c4.png" - ], - [context.l10n]); + () => PlaylistSimple() + ..name = context.l10n.liked_tracks + ..description = context.l10n.liked_tracks_description + ..type = "playlist" + ..collaborative = false + ..public = false + ..id = "user-liked-tracks" + ..images = [ + Image() + ..height = 300 + ..width = 300 + ..url = "assets/liked-tracks.jpg" + ], + [context.l10n], + ); final playlists = useMemoized( () { diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/shared/tracks_view/sections/header/flexible_header.dart index e16ccbff..19241dc6 100644 --- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart +++ b/lib/components/shared/tracks_view/sections/header/flexible_header.dart @@ -1,6 +1,5 @@ import 'dart:ui'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -62,7 +61,7 @@ class TrackViewFlexHeader extends HookConsumerWidget { clipBehavior: Clip.hardEdge, decoration: BoxDecoration( image: DecorationImage( - image: CachedNetworkImageProvider(props.image), + image: UniversalImage.imageProvider(props.image), fit: BoxFit.cover, ), ), diff --git a/lib/pages/playlist/liked_playlist.dart b/lib/pages/playlist/liked_playlist.dart index 5972a303..1fb2e1dc 100644 --- a/lib/pages/playlist/liked_playlist.dart +++ b/lib/pages/playlist/liked_playlist.dart @@ -4,7 +4,6 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/tracks_view/track_view.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:spotube/services/queries/queries.dart'; -import 'package:spotube/utils/type_conversion_utils.dart'; class LikedPlaylistPage extends HookConsumerWidget { final PlaylistSimple playlist; @@ -20,10 +19,7 @@ class LikedPlaylistPage extends HookConsumerWidget { return InheritedTrackView( collectionId: playlist.id!, - image: TypeConversionUtils.image_X_UrlString( - playlist.images, - placeholder: ImagePlaceholder.collection, - ), + image: "assets/liked-tracks.jpg", pagination: PaginationProps( hasNextPage: false, isLoading: false, From 3d344bdcddb193e7faf4d292bc5a5286e858d5f6 Mon Sep 17 00:00:00 2001 From: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Date: Thu, 11 Jan 2024 05:07:36 +0100 Subject: [PATCH 119/131] docs: Readme.md rephrasing (#914) * Rephrased README.md * Update README.md Put extra emphasis on the fact that Shows and Podcasts can ONLY be accessed with spotify premium * One line fix in README.md last change was unnecessary :) --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2736d1f1..e637f6c6 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ Spotube Logo An open source, cross-platform Spotify client compatible across multiple platforms
    -utilizing Spotify's data API and YouTube (or Piped.video or JioSaavn) as an audio source,
    +utilizing Spotify's data API and YouTube, Piped.video or JioSaavn as an audio source,
    eliminating the need for Spotify Premium -Btw it's not another Electron app😉 +Btw it's not just another Electron app 😉
    Visit the website Discord Server @@ -26,7 +26,7 @@ Btw it's not another Electron app😉 ## 🌃 Features - 🚫 No ads, thanks to the use of public & free Spotify and YT Music APIs¹ -- ⬇️ Downloadable tracks +- ⬇️ Freely downloadable tracks - 🖥️ 📱 Cross-platform support - 🪶 Small size & less data usage - 🕵️ Anonymous/guest login @@ -40,13 +40,13 @@ Btw it's not another Electron app😉 ### ❌ Unsupported features -- 🗣️ **Spotify Shows & Podcasts:** Shows and Podcasts can **never be supported** because the audio tracks are _only_ available on Spotify and accessing them would require Spotify Premium. +- 🗣️ **Spotify Shows & Podcasts:** Shows and Podcasts will **never be supported** because the audio tracks are _only_ available on Spotify and accessing them would require Spotify Premium. - 🎧 **Spotify Listen Along:** [Coming soon!](https://github.com/KRTirtho/spotube/issues/8) ## 📜 ⬇️ Installation guide -New releases usually appear after 3-4 months.
    -This handy table lists all methods you can use to install Spotube: +New versions usually release every 3-4 months.
    +This handy table lists all the methods you can use to install Spotube: From 27057ea0c8d83c9701057c18b473f1af4e4e82be Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 22 Jan 2024 17:20:30 +0600 Subject: [PATCH 120/131] fix(macos): system tray shows name and sidebar weird gap #1083 --- .vscode/launch.json | 6 + lib/collections/assets.gen.dart | 3 + lib/components/root/sidebar.dart | 2 +- .../configurators/use_init_sys_tray.dart | 2 +- macos/Podfile | 2 +- macos/Podfile.lock | 135 ++++++++++++------ macos/Runner.xcodeproj/project.pbxproj | 17 +-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- 8 files changed, 112 insertions(+), 57 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9add0735..7a1e8b9b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,6 +6,12 @@ "type": "dart", "request": "launch", "program": "lib/main.dart", + }, + { + "name": "spotube (mobile)", + "type": "dart", + "request": "launch", + "program": "lib/main.dart", "args": [ "--flavor", "dev" diff --git a/lib/collections/assets.gen.dart b/lib/collections/assets.gen.dart index ac39cf68..d7149834 100644 --- a/lib/collections/assets.gen.dart +++ b/lib/collections/assets.gen.dart @@ -34,6 +34,8 @@ class Assets { AssetGenImage('assets/bengali-patterns-bg.jpg'); static const AssetGenImage branding = AssetGenImage('assets/branding.png'); static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png'); + static const AssetGenImage likedTracks = + AssetGenImage('assets/liked-tracks.jpg'); static const AssetGenImage placeholder = AssetGenImage('assets/placeholder.png'); static const AssetGenImage spotubeHeroBanner = @@ -74,6 +76,7 @@ class Assets { bengaliPatternsBg, branding, emptyBox, + likedTracks, placeholder, spotubeHeroBanner, spotubeLogoForeground, diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index ac5233ed..9b3fd3ed 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -159,7 +159,7 @@ class Sidebar extends HookConsumerWidget { margin: EdgeInsets.only( bottom: 10, left: 0, - top: kIsMacOS ? 35 : 5, + top: kIsMacOS ? 0 : 5, ), padding: const EdgeInsets.symmetric(horizontal: 6), decoration: BoxDecoration( diff --git a/lib/hooks/configurators/use_init_sys_tray.dart b/lib/hooks/configurators/use_init_sys_tray.dart index db4964ce..8080bea6 100644 --- a/lib/hooks/configurators/use_init_sys_tray.dart +++ b/lib/hooks/configurators/use_init_sys_tray.dart @@ -25,7 +25,7 @@ void useInitSysTray(WidgetRef ref) { } final enabled = !playlist.isFetching; systemTray.value = await DesktopTools.createSystemTrayMenu( - title: DesktopTools.platform.isLinux ? "" : "Spotube", + title: DesktopTools.platform.isWindows ? "Spotube" : "", iconPath: "assets/spotube-logo.png", windowsIconPath: "assets/spotube-logo.ico", items: [ diff --git a/macos/Podfile b/macos/Podfile index fe733905..049abe29 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.13' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 99c0177d..65fe3535 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,101 +1,146 @@ PODS: + - app_links (1.0.0): + - FlutterMacOS - audio_service (0.14.1): - FlutterMacOS - audio_session (0.0.1): - FlutterMacOS - - audioplayers_darwin (0.0.1): + - device_info_plus (0.0.1): - FlutterMacOS - - bitsdojo_window_macos (0.0.1): + - file_selector_macos (0.0.1): - FlutterMacOS - - connectivity_plus_macos (0.0.1): + - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - - ReachabilitySwift - FlutterMacOS (1.0.0) - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) - - macos_ui (0.1.0): + - local_notifier (0.1.0): - FlutterMacOS - - metadata_god (0.0.1): + - media_kit_libs_macos_audio (1.0.4): - FlutterMacOS - - package_info_plus_macos (0.0.1): + - media_kit_native_event_loop (1.0.0): - FlutterMacOS - - path_provider_macos (0.0.1): + - metadata_god (0.0.1) + - package_info_plus (0.0.1): - FlutterMacOS - - ReachabilitySwift (5.0.0) - - shared_preferences_macos (0.0.1): + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - screen_retriever (0.0.1): + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter - FlutterMacOS - sqflite (0.0.2): - FlutterMacOS - FMDB (>= 2.7.5) + - system_theme (0.0.1): + - FlutterMacOS + - system_tray (0.0.1): + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS + - window_manager (0.2.0): + - FlutterMacOS + - window_size (0.0.2): + - FlutterMacOS DEPENDENCIES: + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`) - - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) - - connectivity_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus_macos/macos`) + - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - macos_ui (from `Flutter/ephemeral/.symlinks/plugins/macos_ui/macos`) + - local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`) + - media_kit_libs_macos_audio (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_audio/macos`) + - media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`) - metadata_god (from `Flutter/ephemeral/.symlinks/plugins/metadata_god/macos`) - - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`) + - system_tray (from `Flutter/ephemeral/.symlinks/plugins/system_tray/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) SPEC REPOS: trunk: - FMDB - - ReachabilitySwift EXTERNAL SOURCES: + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos audio_service: :path: Flutter/ephemeral/.symlinks/plugins/audio_service/macos audio_session: :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos - audioplayers_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos - bitsdojo_window_macos: - :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos - connectivity_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus_macos/macos + device_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + flutter_secure_storage_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: :path: Flutter/ephemeral - macos_ui: - :path: Flutter/ephemeral/.symlinks/plugins/macos_ui/macos + local_notifier: + :path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos + media_kit_libs_macos_audio: + :path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_audio/macos + media_kit_native_event_loop: + :path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos metadata_god: :path: Flutter/ephemeral/.symlinks/plugins/metadata_god/macos - package_info_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos - path_provider_macos: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos - shared_preferences_macos: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + screen_retriever: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + system_theme: + :path: Flutter/ephemeral/.symlinks/plugins/system_theme/macos + system_tray: + :path: Flutter/ephemeral/.symlinks/plugins/system_tray/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + window_size: + :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos SPEC CHECKSUMS: + app_links: 4481ed4d71f384b0c3ae5016f4633aa73d32ff67 audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9 audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 - audioplayers_darwin: dcad41de4fbd0099cb3749f7ab3b0cb8f70b810c - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 - connectivity_plus_macos: f6e86fd000e971d361e54b5afcadc8c8fa773308 - FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 + device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 + flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - macos_ui: 125c911559d646194386d84c017ad6819122e2db - metadata_god: 55a71136c95eb75ec28142f6fbfc2bcff6f881b1 - package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c - path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff + media_kit_libs_macos_audio: 3871782a4f3f84c77f04d7666c87800a781c24da + media_kit_native_event_loop: 7321675377cb9ae8596a29bddf3a3d2b5e8792c5 + metadata_god: eceae399d0020475069a5cebc35943ce8562b5d7 + package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea - url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc + system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 -PODFILE CHECKSUM: a884f6dd3f7494f3892ee6c81feea3a3abbf9153 +PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 9b86152a..f7711c83 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -208,7 +208,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -261,6 +261,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -409,7 +410,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -431,7 +432,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -489,7 +490,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -536,7 +537,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -558,7 +559,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -579,7 +580,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.14; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 741e68bc..8f69f0c6 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Mon, 22 Jan 2024 17:51:12 +0600 Subject: [PATCH 121/131] fix: artist page error #1018 --- .../spotify_endpoints.dart | 56 +++++++++++++++++++ lib/services/queries/artist.dart | 7 ++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart index 4a55130a..0510e69a 100644 --- a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart +++ b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart @@ -162,4 +162,60 @@ class CustomSpotifyEndpoints { result["tracks"].map((track) => Track.fromJson(track)).toList(), ); } + + Future artist({required String id}) async { + final pathQuery = "$_baseUrl/artists/$id"; + + final res = await _client.get( + Uri.parse(pathQuery), + headers: { + "content-type": "application/json", + if (accessToken.isNotEmpty) "authorization": "Bearer $accessToken", + "accept": "application/json", + }, + ); + + final data = jsonDecode(res.body); + + return Artist.fromJson(_purifyArtistResponse(data)); + } + + Future> relatedArtists({required String id}) async { + final pathQuery = "$_baseUrl/artists/$id/related-artists"; + + final res = await _client.get( + Uri.parse(pathQuery), + headers: { + "content-type": "application/json", + if (accessToken.isNotEmpty) "authorization": "Bearer $accessToken", + "accept": "application/json", + }, + ); + + final data = jsonDecode(res.body); + + return List.castFrom( + data["artists"] + .map((artist) => Artist.fromJson(_purifyArtistResponse(artist))) + .toList(), + ); + } + + Map _purifyArtistResponse(Map data) { + if (data["popularity"] != null) { + data["popularity"] = data["popularity"].toInt(); + } + if (data["followers"]?["total"] != null) { + data["followers"]["total"] = data["followers"]["total"].toInt(); + } + if (data["images"] != null) { + data["images"] = data["images"].map((e) { + e["height"] = e["height"].toInt(); + e["width"] = e["width"].toInt(); + return e; + }).toList(); + } + + return data; + } } diff --git a/lib/services/queries/artist.dart b/lib/services/queries/artist.dart index 1b939c82..5ccc4955 100644 --- a/lib/services/queries/artist.dart +++ b/lib/services/queries/artist.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart'; import 'package:spotube/hooks/spotify/use_spotify_query.dart'; +import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/wikipedia/wikipedia.dart'; import 'package:wikipedia_api/wikipedia_api.dart'; @@ -15,9 +16,10 @@ class ArtistQueries { WidgetRef ref, String artist, ) { + final customSpotify = ref.watch(customSpotifyEndpointProvider); return useSpotifyQuery( "artist-profile/$artist", - (spotify) => spotify.artists.get(artist), + (spotify) => customSpotify.artist(id: artist), ref: ref, ); } @@ -125,10 +127,11 @@ class ArtistQueries { WidgetRef ref, String artist, ) { + final customSpotify = ref.watch(customSpotifyEndpointProvider); return useSpotifyQuery, dynamic>( "artist-related-artist-query/$artist", (spotify) { - return spotify.artists.relatedArtists(artist); + return customSpotify.relatedArtists(id: artist); }, ref: ref, ); From 59e0e6bb659b70831f6e0ae064100381c57f149c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 22 Jan 2024 18:03:08 +0600 Subject: [PATCH 122/131] fix: track pad horizontal scrolling not working --- .../horizontal_playbutton_card_view.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 index 2075acbb..dc9d30da 100644 --- 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 @@ -60,10 +60,7 @@ class HorizontalPlaybuttonCardView extends HookWidget { onNotification: (notification) => true, child: ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, + dragDevices: PointerDeviceKind.values.toSet(), ), child: items.isEmpty ? ListView.builder( From a8e9b824f33add8f6a83f0d147e889eb6beeb442 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 22 Jan 2024 19:02:10 +0600 Subject: [PATCH 123/131] fix: alternative searched sources doesn't play #1059 --- assets/jiosaavn.png | Bin 0 -> 14045 bytes lib/collections/assets.gen.dart | 2 + lib/collections/spotube_icons.dart | 1 + .../player/sibling_tracks_sheet.dart | 54 ++++++++++++++++-- lib/services/sourced_track/enums.dart | 2 +- .../sourced_track/sources/jiosaavn.dart | 25 ++++++-- lib/services/sourced_track/sources/piped.dart | 27 +++++++-- .../sourced_track/sources/youtube.dart | 27 +++++++-- 8 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 assets/jiosaavn.png diff --git a/assets/jiosaavn.png b/assets/jiosaavn.png new file mode 100644 index 0000000000000000000000000000000000000000..4d2d46e4b079fa9d1a60ef10a3aab3dd96750b90 GIT binary patch literal 14045 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 literal 0 HcmV?d00001 diff --git a/lib/collections/assets.gen.dart b/lib/collections/assets.gen.dart index d7149834..2587800e 100644 --- a/lib/collections/assets.gen.dart +++ b/lib/collections/assets.gen.dart @@ -34,6 +34,7 @@ class Assets { AssetGenImage('assets/bengali-patterns-bg.jpg'); static const AssetGenImage branding = AssetGenImage('assets/branding.png'); static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png'); + static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png'); static const AssetGenImage likedTracks = AssetGenImage('assets/liked-tracks.jpg'); static const AssetGenImage placeholder = @@ -76,6 +77,7 @@ class Assets { bengaliPatternsBg, branding, emptyBox, + jiosaavn, likedTracks, placeholder, spotubeHeroBanner, diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 00010aae..c6acd669 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -109,4 +109,5 @@ abstract class SpotubeIcons { static const normalize = FeatherIcons.barChart2; static const wikipedia = SimpleIcons.wikipedia; static const discord = SimpleIcons.discord; + static const youtube = SimpleIcons.youtube; } diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index cf1429b9..181c363a 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart' hide Offset; +import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; @@ -19,10 +20,28 @@ import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/models/video_info.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; +import 'package:spotube/services/sourced_track/sources/piped.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +final sourceInfoToIconMap = { + YoutubeSourceInfo: const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)), + JioSaavnSourceInfo: Container( + height: 30, + width: 30, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(90), + image: DecorationImage( + image: Assets.jiosaavn.provider(), + fit: BoxFit.cover, + ), + ), + ), + PipedSourceInfo: const Icon(SpotubeIcons.piped), +}; + class SiblingTracksSheet extends HookConsumerWidget { final bool floating; const SiblingTracksSheet({ @@ -64,17 +83,34 @@ class SiblingTracksSheet extends HookConsumerWidget { return []; } - final results = await youtubeClient.search.search(searchTerm.trim()); + final resultsYt = await youtubeClient.search.search(searchTerm.trim()); + final resultsJioSaavn = + await jiosaavnClient.search.songs(searchTerm.trim()); - return await Future.wait( - results.map(YoutubeVideoInfo.fromVideo).mapIndexed((i, video) async { + final searchResults = await Future.wait([ + ...resultsJioSaavn.results.mapIndexed((i, song) async { + final siblingType = JioSaavnSourcedTrack.toSiblingType(song); + return siblingType.info; + }), + ...resultsYt + .map(YoutubeVideoInfo.fromVideo) + .mapIndexed((i, video) async { final siblingType = await YoutubeSourcedTrack.toSiblingType(i, video); return siblingType.info; }), - ); + ]); + final activeSourceInfo = + (playlist.activeTrack! as SourcedTrack).sourceInfo; + return searchResults + ..removeWhere((element) => element.id == activeSourceInfo.id) + ..insert( + 0, + activeSourceInfo, + ); }, [ searchTerm, searchMode.value, + playlist.activeTrack, ]); final siblings = useMemoized( @@ -104,6 +140,7 @@ class SiblingTracksSheet extends HookConsumerWidget { final itemBuilder = useCallback( (SourceInfo sourceInfo) { + final icon = sourceInfoToIconMap[sourceInfo.runtimeType]; return ListTile( title: Text(sourceInfo.title), leading: Padding( @@ -118,7 +155,12 @@ class SiblingTracksSheet extends HookConsumerWidget { borderRadius: BorderRadius.circular(5), ), trailing: Text(sourceInfo.duration.toHumanReadableString()), - subtitle: Text(sourceInfo.artist), + subtitle: Row( + children: [ + if (icon != null) icon, + Text(" • ${sourceInfo.artist}"), + ], + ), enabled: playlist.isFetching != true, selected: playlist.isFetching != true && sourceInfo.id == @@ -137,7 +179,7 @@ class SiblingTracksSheet extends HookConsumerWidget { [playlist.isFetching, playlist.activeTrack, siblings], ); - var mediaQuery = MediaQuery.of(context); + final mediaQuery = MediaQuery.of(context); return SafeArea( child: ClipRRect( borderRadius: borderRadius, diff --git a/lib/services/sourced_track/enums.dart b/lib/services/sourced_track/enums.dart index 48ce1cbd..e47ee6bd 100644 --- a/lib/services/sourced_track/enums.dart +++ b/lib/services/sourced_track/enums.dart @@ -15,4 +15,4 @@ enum SourceQualities { low, } -typedef SiblingType = ({SourceInfo info, SourceMap? source}); +typedef SiblingType = ({T info, SourceMap? source}); diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart index a447b0c1..281be998 100644 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -12,6 +12,19 @@ import 'package:spotube/extensions/string.dart'; final jiosaavnClient = JioSaavnClient(); +class JioSaavnSourceInfo extends SourceInfo { + JioSaavnSourceInfo({ + required super.id, + required super.title, + required super.artist, + required super.thumbnail, + required super.pageUrl, + required super.duration, + required super.artistUrl, + required super.album, + }); +} + class JioSaavnSourcedTrack extends SourcedTrack { JioSaavnSourcedTrack({ required super.ref, @@ -70,7 +83,7 @@ class JioSaavnSourcedTrack extends SourcedTrack { static SiblingType toSiblingType(SongResponse result) { final SiblingType sibling = ( - info: SourceInfo( + info: JioSaavnSourceInfo( artist: [ result.primaryArtists, if (result.featuredArtists.isNotEmpty) ", ", @@ -155,12 +168,16 @@ class JioSaavnSourcedTrack extends SourcedTrack { @override Future swapWithSibling(SourceInfo sibling) async { - if (sibling.id == sourceInfo.id || - siblings.none((s) => s.id == sibling.id)) { + if (sibling.id == sourceInfo.id) { return null; } - final newSourceInfo = siblings.firstWhere((s) => s.id == sibling.id); + // a sibling source that was fetched from the search results + final isStepSibling = siblings.none((s) => s.id == sibling.id); + + final newSourceInfo = isStepSibling + ? sibling + : siblings.firstWhere((s) => s.id == sibling.id); final newSiblings = siblings.where((s) => s.id != sibling.id).toList() ..insert(0, sourceInfo); diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index 0778a7cf..f9e4368d 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -22,6 +22,19 @@ final pipedProvider = Provider( }, ); +class PipedSourceInfo extends SourceInfo { + PipedSourceInfo({ + required super.id, + required super.title, + required super.artist, + required super.thumbnail, + required super.pageUrl, + required super.duration, + required super.artistUrl, + required super.album, + }); +} + class PipedSourcedTrack extends SourcedTrack { PipedSourcedTrack({ required super.ref, @@ -71,7 +84,7 @@ class PipedSourcedTrack extends SourcedTrack { ref: ref, siblings: [], source: toSourceMap(manifest), - sourceInfo: SourceInfo( + sourceInfo: PipedSourceInfo( id: manifest.id, artist: manifest.uploader, artistUrl: manifest.uploaderUrl, @@ -122,7 +135,7 @@ class PipedSourcedTrack extends SourcedTrack { } final SiblingType sibling = ( - info: SourceInfo( + info: PipedSourceInfo( id: item.id, artist: item.channelName, artistUrl: "https://www.youtube.com/${item.channelId}", @@ -233,12 +246,16 @@ class PipedSourcedTrack extends SourcedTrack { @override Future swapWithSibling(SourceInfo sibling) async { - if (sibling.id == sourceInfo.id || - siblings.none((s) => s.id == sibling.id)) { + if (sibling.id == sourceInfo.id) { return null; } - final newSourceInfo = siblings.firstWhere((s) => s.id == sibling.id); + // a sibling source that was fetched from the search results + final isStepSibling = siblings.none((s) => s.id == sibling.id); + + final newSourceInfo = isStepSibling + ? sibling + : siblings.firstWhere((s) => s.id == sibling.id); final newSiblings = siblings.where((s) => s.id != sibling.id).toList() ..insert(0, sourceInfo); diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index 2bcd6e3e..c4105d75 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -17,6 +17,19 @@ final officialMusicRegex = RegExp( caseSensitive: false, ); +class YoutubeSourceInfo extends SourceInfo { + YoutubeSourceInfo({ + required super.id, + required super.title, + required super.artist, + required super.thumbnail, + required super.pageUrl, + required super.duration, + required super.artistUrl, + required super.album, + }); +} + class YoutubeSourcedTrack extends SourcedTrack { YoutubeSourcedTrack({ required super.source, @@ -64,7 +77,7 @@ class YoutubeSourcedTrack extends SourcedTrack { ref: ref, siblings: [], source: toSourceMap(manifest), - sourceInfo: SourceInfo( + sourceInfo: YoutubeSourceInfo( id: item.id.value, artist: item.author, artistUrl: "https://www.youtube.com/channel/${item.channelId}", @@ -117,7 +130,7 @@ class YoutubeSourcedTrack extends SourcedTrack { } final SiblingType sibling = ( - info: SourceInfo( + info: YoutubeSourceInfo( id: item.id, artist: item.channelName, artistUrl: "https://www.youtube.com/channel/${item.channelId}", @@ -217,12 +230,16 @@ class YoutubeSourcedTrack extends SourcedTrack { @override Future swapWithSibling(SourceInfo sibling) async { - if (sibling.id == sourceInfo.id || - siblings.none((s) => s.id == sibling.id)) { + if (sibling.id == sourceInfo.id) { return null; } - final newSourceInfo = siblings.firstWhere((s) => s.id == sibling.id); + // a sibling source that was fetched from the search results + final isStepSibling = siblings.none((s) => s.id == sibling.id); + + final newSourceInfo = isStepSibling + ? sibling + : siblings.firstWhere((s) => s.id == sibling.id); final newSiblings = siblings.where((s) => s.id != sibling.id).toList() ..insert(0, sourceInfo); From 682e88e0c55bc0f4708bc0b4681b129e5c61c999 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 22 Jan 2024 19:09:57 +0600 Subject: [PATCH 124/131] fix: releases section is empty when user doesn't follow any artists #1104 --- .../home/sections/new_releases.dart | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/components/home/sections/new_releases.dart b/lib/components/home/sections/new_releases.dart index 77481de1..0f4a046a 100644 --- a/lib/components/home/sections/new_releases.dart +++ b/lib/components/home/sections/new_releases.dart @@ -21,16 +21,21 @@ class HomeNewReleasesSection extends HookConsumerWidget { userArtistsQuery.data?.map((s) => s.id!).toList() ?? const []; final albums = useMemoized( - () => newReleases.pages - .whereType>() - .expand((page) => page.items ?? const []) - .where((album) { - return album.artists - ?.any((artist) => userArtists.contains(artist.id!)) == - true; - }) - .map((album) => TypeConversionUtils.simpleAlbum_X_Album(album)) - .toList(), + () { + final allReleases = newReleases.pages + .whereType>() + .expand((page) => page.items ?? const []) + .map((album) => TypeConversionUtils.simpleAlbum_X_Album(album)); + + final userArtistReleases = allReleases.where((album) { + return album.artists + ?.any((artist) => userArtists.contains(artist.id!)) == + true; + }).toList(); + + if (userArtistReleases.isEmpty) return allReleases.toList(); + return userArtistReleases; + }, [newReleases.pages], ); From 79839329b0970acccb0c566a31eee508adbc8557 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 23 Jan 2024 22:44:00 +0600 Subject: [PATCH 125/131] feat: add spotify friends activity (#1130) * feat: add spotify friend endpoint * feat: add friend activity in home screen * fix: when no friends, dummy UI still shows giving the user a false hope of friendship :'( --- lib/collections/fake.dart | 32 +++++ lib/components/home/sections/friends.dart | 95 ++++++++++++ .../home/sections/friends/friend_item.dart | 136 ++++++++++++++++++ .../shared/page_window_title_bar.dart | 4 +- lib/l10n/app_en.arb | 3 +- lib/main.dart | 4 +- lib/models/spotify_friends.dart | 111 ++++++++++++++ lib/models/spotify_friends.g.dart | 65 +++++++++ lib/pages/home/home.dart | 2 + .../spotify_endpoints.dart | 14 +- lib/services/queries/user.dart | 13 ++ untranslated_messages.json | 51 ++++--- 12 files changed, 507 insertions(+), 23 deletions(-) create mode 100644 lib/components/home/sections/friends.dart create mode 100644 lib/components/home/sections/friends/friend_item.dart create mode 100644 lib/models/spotify_friends.dart create mode 100644 lib/models/spotify_friends.g.dart diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart index 10cf2819..8f5f9e8b 100644 --- a/lib/collections/fake.dart +++ b/lib/collections/fake.dart @@ -1,5 +1,6 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/track.dart'; +import 'package:spotube/models/spotify_friends.dart'; abstract class FakeData { static final Image image = Image() @@ -164,4 +165,35 @@ abstract class FakeData { ..icons = [image] ..id = "1" ..name = "category"; + + static final friends = SpotifyFriends( + friends: [ + for (var i = 0; i < 3; i++) + SpotifyFriendActivity( + user: const SpotifyFriend( + name: "name", + imageUrl: "imageUrl", + uri: "uri", + ), + track: SpotifyActivityTrack( + name: "name", + artist: const SpotifyActivityArtist( + name: "name", + uri: "uri", + ), + album: const SpotifyActivityAlbum( + name: "name", + uri: "uri", + ), + context: SpotifyActivityContext( + name: "name", + index: i, + uri: "uri", + ), + imageUrl: "imageUrl", + uri: "uri", + ), + ), + ], + ); } diff --git a/lib/components/home/sections/friends.dart b/lib/components/home/sections/friends.dart new file mode 100644 index 00000000..ef24b8d5 --- /dev/null +++ b/lib/components/home/sections/friends.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:spotube/collections/fake.dart'; +import 'package:spotube/components/home/sections/friends/friend_item.dart'; +import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; +import 'package:spotube/models/spotify_friends.dart'; +import 'package:spotube/services/queries/queries.dart'; + +class HomePageFriendsSection extends HookConsumerWidget { + const HomePageFriendsSection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final friendsQuery = useQueries.user.friendActivity(ref); + final friends = friendsQuery.data?.friends ?? FakeData.friends.friends; + + final groupCount = useBreakpointValue( + sm: 3, + xs: 2, + md: 4, + lg: 5, + xl: 6, + xxl: 7, + ); + + final friendGroup = friends.fold>>( + [], + (previousValue, element) { + if (previousValue.isEmpty) { + return [ + [element] + ]; + } + + final lastGroup = previousValue.last; + if (lastGroup.length < groupCount) { + return [ + ...previousValue.sublist(0, previousValue.length - 1), + [...lastGroup, element] + ]; + } + + return [ + ...previousValue, + [element] + ]; + }, + ); + + if (!friendsQuery.isLoading && + (!friendsQuery.hasData || friendsQuery.data!.friends.isEmpty)) { + return const SliverToBoxAdapter( + child: SizedBox.shrink(), + ); + } + + return Skeletonizer.sliver( + enabled: friendsQuery.isLoading, + child: SliverMainAxisGroup( + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Friends', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ), + SliverToBoxAdapter( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final group in friendGroup) + Row( + children: [ + for (final friend in group) + Padding( + padding: const EdgeInsets.all(8.0), + child: FriendItem(friend: friend), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/components/home/sections/friends/friend_item.dart b/lib/components/home/sections/friends/friend_item.dart new file mode 100644 index 00000000..fcdadab7 --- /dev/null +++ b/lib/components/home/sections/friends/friend_item.dart @@ -0,0 +1,136 @@ +import 'package:fl_query_hooks/fl_query_hooks.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/models/spotify_friends.dart'; +import 'package:spotube/provider/spotify_provider.dart'; + +class FriendItem extends HookConsumerWidget { + final SpotifyFriendActivity friend; + const FriendItem({ + Key? key, + required this.friend, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final ThemeData( + textTheme: textTheme, + colorScheme: colorScheme, + ) = Theme.of(context); + + final queryClient = useQueryClient(); + final spotify = ref.watch(spotifyProvider); + + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: colorScheme.surfaceVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(15), + ), + constraints: const BoxConstraints( + minWidth: 300, + ), + height: 80, + child: Row( + children: [ + CircleAvatar( + backgroundImage: UniversalImage.imageProvider( + friend.user.imageUrl, + ), + ), + const Gap(8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + friend.user.name, + style: textTheme.bodyLarge, + ), + RichText( + text: TextSpan( + style: textTheme.bodySmall, + children: [ + TextSpan( + text: friend.track.name, + recognizer: TapGestureRecognizer() + ..onTap = () { + context.push("/track/${friend.track.id}"); + }, + ), + const TextSpan(text: " • "), + const WidgetSpan( + child: Icon( + SpotubeIcons.artist, + size: 12, + ), + ), + TextSpan( + text: " ${friend.track.artist.name}", + recognizer: TapGestureRecognizer() + ..onTap = () { + context.push( + "/artist/${friend.track.artist.id}", + ); + }, + ), + const TextSpan(text: "\n"), + TextSpan( + text: friend.track.context.name, + recognizer: TapGestureRecognizer() + ..onTap = () async { + context.push( + "/${friend.track.context.path}", + extra: !friend.track.context.path + .startsWith("album") + ? null + : await queryClient.fetchQuery( + "album/${friend.track.album.id}", + () => spotify.albums.get( + friend.track.album.id, + ), + ), + ); + }, + ), + const TextSpan(text: " • "), + const WidgetSpan( + child: Icon( + SpotubeIcons.album, + size: 12, + ), + ), + TextSpan( + text: " ${friend.track.album.name}", + recognizer: TapGestureRecognizer() + ..onTap = () async { + final album = + await queryClient.fetchQuery( + "album/${friend.track.album.id}", + () => spotify.albums.get( + friend.track.album.id, + ), + ); + if (context.mounted) { + context.push( + "/album/${friend.track.album.id}", + extra: album, + ); + } + }, + ), + ], + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart index d8e20184..4f522e0c 100644 --- a/lib/components/shared/page_window_title_bar.dart +++ b/lib/components/shared/page_window_title_bar.dart @@ -2,14 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/utils/platform.dart'; import 'package:titlebar_buttons/titlebar_buttons.dart'; import 'dart:math'; import 'package:flutter/foundation.dart' show kIsWeb; -import 'dart:io' show Platform, exit; +import 'dart:io' show Platform; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; -import 'package:local_notifier/local_notifier.dart'; class PageWindowTitleBar extends StatefulHookConsumerWidget implements PreferredSizeWidget { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 82877ea1..6b61cbae 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -284,5 +284,6 @@ "discord_rich_presence": "Discord Rich Presence", "browse_all": "Browse All", "genres": "Genres", - "explore_genres": "Explore Genres" + "explore_genres": "Explore Genres", + "friends": "Friends" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index f96920a1..b6afa85c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -228,7 +228,9 @@ class SpotubeState extends ConsumerState { builder: (context, child) { return DevicePreview.appBuilder( context, - DragToResizeArea(child: child!), + DesktopTools.platform.isDesktop + ? DragToResizeArea(child: child!) + : child, ); }, themeMode: themeMode, diff --git a/lib/models/spotify_friends.dart b/lib/models/spotify_friends.dart new file mode 100644 index 00000000..b386fb81 --- /dev/null +++ b/lib/models/spotify_friends.dart @@ -0,0 +1,111 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'spotify_friends.g.dart'; + +@JsonSerializable(createToJson: false) +class SpotifyFriend { + final String uri; + final String name; + final String imageUrl; + + const SpotifyFriend({ + required this.uri, + required this.name, + required this.imageUrl, + }); + + factory SpotifyFriend.fromJson(Map json) => + _$SpotifyFriendFromJson(json); + + String get id => uri.split(":").last; +} + +@JsonSerializable(createToJson: false) +class SpotifyActivityArtist { + final String uri; + final String name; + + const SpotifyActivityArtist({required this.uri, required this.name}); + + factory SpotifyActivityArtist.fromJson(Map json) => + _$SpotifyActivityArtistFromJson(json); + + String get id => uri.split(":").last; +} + +@JsonSerializable(createToJson: false) +class SpotifyActivityAlbum { + final String uri; + final String name; + + const SpotifyActivityAlbum({required this.uri, required this.name}); + + factory SpotifyActivityAlbum.fromJson(Map json) => + _$SpotifyActivityAlbumFromJson(json); + + String get id => uri.split(":").last; +} + +@JsonSerializable(createToJson: false) +class SpotifyActivityContext { + final String uri; + final String name; + final num index; + + const SpotifyActivityContext({ + required this.uri, + required this.name, + required this.index, + }); + + factory SpotifyActivityContext.fromJson(Map json) => + _$SpotifyActivityContextFromJson(json); + + String get id => uri.split(":").last; + String get path => uri.split(":").skip(1).join("/"); +} + +@JsonSerializable(createToJson: false) +class SpotifyActivityTrack { + final String uri; + final String name; + final String imageUrl; + final SpotifyActivityArtist artist; + final SpotifyActivityAlbum album; + final SpotifyActivityContext context; + + const SpotifyActivityTrack({ + required this.uri, + required this.name, + required this.imageUrl, + required this.artist, + required this.album, + required this.context, + }); + + factory SpotifyActivityTrack.fromJson(Map json) => + _$SpotifyActivityTrackFromJson(json); + + String get id => uri.split(":").last; +} + +@JsonSerializable(createToJson: false) +class SpotifyFriendActivity { + SpotifyFriend user; + SpotifyActivityTrack track; + + SpotifyFriendActivity({required this.user, required this.track}); + + factory SpotifyFriendActivity.fromJson(Map json) => + _$SpotifyFriendActivityFromJson(json); +} + +@JsonSerializable(createToJson: false) +class SpotifyFriends { + List friends; + + SpotifyFriends({required this.friends}); + + factory SpotifyFriends.fromJson(Map json) => + _$SpotifyFriendsFromJson(json); +} diff --git a/lib/models/spotify_friends.g.dart b/lib/models/spotify_friends.g.dart new file mode 100644 index 00000000..4a32dd09 --- /dev/null +++ b/lib/models/spotify_friends.g.dart @@ -0,0 +1,65 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'spotify_friends.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SpotifyFriend _$SpotifyFriendFromJson(Map json) => + SpotifyFriend( + uri: json['uri'] as String, + name: json['name'] as String, + imageUrl: json['imageUrl'] as String, + ); + +SpotifyActivityArtist _$SpotifyActivityArtistFromJson( + Map json) => + SpotifyActivityArtist( + uri: json['uri'] as String, + name: json['name'] as String, + ); + +SpotifyActivityAlbum _$SpotifyActivityAlbumFromJson( + Map json) => + SpotifyActivityAlbum( + uri: json['uri'] as String, + name: json['name'] as String, + ); + +SpotifyActivityContext _$SpotifyActivityContextFromJson( + Map json) => + SpotifyActivityContext( + uri: json['uri'] as String, + name: json['name'] as String, + index: json['index'] as num, + ); + +SpotifyActivityTrack _$SpotifyActivityTrackFromJson( + Map json) => + SpotifyActivityTrack( + uri: json['uri'] as String, + name: json['name'] as String, + imageUrl: json['imageUrl'] as String, + artist: SpotifyActivityArtist.fromJson( + json['artist'] as Map), + album: + SpotifyActivityAlbum.fromJson(json['album'] as Map), + context: SpotifyActivityContext.fromJson( + json['context'] as Map), + ); + +SpotifyFriendActivity _$SpotifyFriendActivityFromJson( + Map json) => + SpotifyFriendActivity( + user: SpotifyFriend.fromJson(json['user'] as Map), + track: + SpotifyActivityTrack.fromJson(json['track'] as Map), + ); + +SpotifyFriends _$SpotifyFriendsFromJson(Map json) => + SpotifyFriends( + friends: (json['friends'] as List) + .map((e) => SpotifyFriendActivity.fromJson(e as Map)) + .toList(), + ); diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 9b33a66c..eb2ddb94 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -3,6 +3,7 @@ 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/home/sections/featured.dart'; +import 'package:spotube/components/home/sections/friends.dart'; import 'package:spotube/components/home/sections/genres.dart'; import 'package:spotube/components/home/sections/made_for_user.dart'; import 'package:spotube/components/home/sections/new_releases.dart'; @@ -31,6 +32,7 @@ class HomePage extends HookConsumerWidget { HomeNewReleasesSection(), ], ), + const HomePageFriendsSection(), const SliverSafeArea(sliver: HomeMadeForUserSection()), ], ), diff --git a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart index 0510e69a..e27b701b 100644 --- a/lib/services/custom_spotify_endpoints/spotify_endpoints.dart +++ b/lib/services/custom_spotify_endpoints/spotify_endpoints.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:spotify/spotify.dart'; +import 'package:spotube/models/spotify_friends.dart'; class CustomSpotifyEndpoints { static const _baseUrl = 'https://api.spotify.com/v1'; @@ -163,6 +164,18 @@ class CustomSpotifyEndpoints { ); } + Future getFriendActivity() async { + final res = await _client.get( + Uri.parse("https://guc-spclient.spotify.com/presence-view/v1/buddylist"), + headers: { + "content-type": "application/json", + "authorization": "Bearer $accessToken", + "accept": "application/json", + }, + ); + return SpotifyFriends.fromJson(jsonDecode(res.body)); + } + Future artist({required String id}) async { final pathQuery = "$_baseUrl/artists/$id"; @@ -174,7 +187,6 @@ class CustomSpotifyEndpoints { "accept": "application/json", }, ); - final data = jsonDecode(res.body); return Artist.fromJson(_purifyArtistResponse(data)); diff --git a/lib/services/queries/user.dart b/lib/services/queries/user.dart index 40799c1e..82af600f 100644 --- a/lib/services/queries/user.dart +++ b/lib/services/queries/user.dart @@ -3,7 +3,9 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/hooks/spotify/use_spotify_query.dart'; +import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class UserQueries { @@ -37,4 +39,15 @@ class UserQueries { ref: ref, ); } + + Query friendActivity(WidgetRef ref) { + final customSpotify = ref.read(customSpotifyEndpointProvider); + return useSpotifyQuery( + "friend-activity", + (spotify) { + return customSpotify.getFriendActivity(); + }, + ref: ref, + ); + } } diff --git a/untranslated_messages.json b/untranslated_messages.json index 59b26614..2fd7ceca 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,86 +1,103 @@ { "ar": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "bn": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "ca": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "de": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "es": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "fa": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "fr": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "hi": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "it": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "ja": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "nl": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "pl": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "pt": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "ru": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "tr": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "uk": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ], "zh": [ "step_3_steps", - "step_4_steps" + "step_4_steps", + "friends" ] } From e58e18de33d7bc6fb0e4ddd7ccf6ea14472642b1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 23 Jan 2024 23:13:13 +0600 Subject: [PATCH 126/131] fix: better error message for failing to find lyrics #1085 --- lib/pages/lyrics/plain_lyrics.dart | 2 +- lib/pages/lyrics/synced_lyrics.dart | 2 +- lib/services/queries/lyrics.dart | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index f6eaa5d5..0ac1ac66 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -73,7 +73,7 @@ class PlainLyrics extends HookConsumerWidget { return const ShimmerLyrics(); } else if (lyricsQuery.hasError) { return Text( - "Sorry, no Lyrics were found for `${playlist.activeTrack?.name}` :'(\n${lyricsQuery.error.toString()}", + "Sorry, no Lyrics were found for `${playlist.activeTrack?.name}` :'(\n${lyricsQuery.error.toString().replaceAll("Exception: ", "")}", style: textTheme.bodyLarge?.copyWith( color: palette.bodyTextColor, ), diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 04d7c04a..0dc0b1f1 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -190,7 +190,7 @@ class SyncedLyrics extends HookConsumerWidget { else if (playlist.activeTrack != null && (timedLyricsQuery.hasError)) Text( - "Sorry, no Lyrics were found for `${playlist.activeTrack?.name}` :'(\n${timedLyricsQuery.error.toString()}", + "Sorry, no Lyrics were found for `${playlist.activeTrack?.name}` :'(\n${timedLyricsQuery.error.toString().replaceAll("Exception: ", "")}", style: bodyTextTheme, ) else if (isUnSyncLyric == true) diff --git a/lib/services/queries/lyrics.dart b/lib/services/queries/lyrics.dart index faa5bdec..618f960f 100644 --- a/lib/services/queries/lyrics.dart +++ b/lib/services/queries/lyrics.dart @@ -63,8 +63,8 @@ class LyricsQueries { /// Special thanks to [raptag](https://github.com/raptag) for discovering this /// jem - Query spotifySynced(WidgetRef ref, Track? track) { - return useSpotifyQuery( + Query spotifySynced(WidgetRef ref, Track? track) { + return useSpotifyQuery( "spotify-synced-lyrics/${track?.id}}", (spotify) async { if (track == null) { From a6cb78380d57ea9f3f68c82aee2c211e38287813 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 24 Jan 2024 00:22:14 +0600 Subject: [PATCH 127/131] chore: show icon with error msg #1085 --- lib/collections/spotube_icons.dart | 1 + lib/l10n/app_en.arb | 3 +- lib/pages/lyrics/plain_lyrics.dart | 23 ++++++++++--- lib/pages/lyrics/synced_lyrics.dart | 21 ++++++++---- untranslated_messages.json | 51 +++++++++++++++++++---------- 5 files changed, 71 insertions(+), 28 deletions(-) diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index c6acd669..65e6c1a0 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -41,6 +41,7 @@ abstract class SpotubeIcons { static const clock = FeatherIcons.clock; static const lyrics = Icons.lyrics_rounded; static const lyricsOff = Icons.lyrics_outlined; + static const noLyrics = Icons.music_off_outlined; static const logout = FeatherIcons.logOut; static const login = FeatherIcons.logIn; static const dashboard = FeatherIcons.grid; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6b61cbae..07df5f06 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -285,5 +285,6 @@ "browse_all": "Browse All", "genres": "Genres", "explore_genres": "Explore Genres", - "friends": "Friends" + "friends": "Friends", + "no_lyrics_available": "Sorry, unable find lyrics for this track" } \ No newline at end of file diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index 0ac1ac66..bee5114d 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -1,12 +1,15 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/lyrics/zoom_controls.dart'; import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart'; import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; @@ -72,10 +75,22 @@ class PlainLyrics extends HookConsumerWidget { if (lyricsQuery.isLoading || lyricsQuery.isRefreshing) { return const ShimmerLyrics(); } else if (lyricsQuery.hasError) { - return Text( - "Sorry, no Lyrics were found for `${playlist.activeTrack?.name}` :'(\n${lyricsQuery.error.toString().replaceAll("Exception: ", "")}", - style: textTheme.bodyLarge?.copyWith( - color: palette.bodyTextColor, + return Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.no_lyrics_available, + style: textTheme.bodyLarge?.copyWith( + color: palette.bodyTextColor, + ), + textAlign: TextAlign.center, + ), + const Gap(26), + const Icon(SpotubeIcons.noLyrics, size: 60), + ], ), ); } diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 0dc0b1f1..ddef1c65 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotify/spotify.dart' hide Offset; @@ -7,6 +8,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/lyrics/zoom_controls.dart'; import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart'; import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart'; import 'package:spotube/components/lyrics/use_synced_lyrics.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; @@ -188,12 +190,19 @@ class SyncedLyrics extends HookConsumerWidget { child: ShimmerLyrics(), ) else if (playlist.activeTrack != null && - (timedLyricsQuery.hasError)) - Text( - "Sorry, no Lyrics were found for `${playlist.activeTrack?.name}` :'(\n${timedLyricsQuery.error.toString().replaceAll("Exception: ", "")}", - style: bodyTextTheme, - ) - else if (isUnSyncLyric == true) + (timedLyricsQuery.hasError)) ...[ + Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(16), + child: Text( + context.l10n.no_lyrics_available, + style: bodyTextTheme, + textAlign: TextAlign.center, + ), + ), + const Gap(26), + const Icon(SpotubeIcons.noLyrics, size: 60), + ] else if (isUnSyncLyric == true) Expanded( child: Center( child: RichText( diff --git a/untranslated_messages.json b/untranslated_messages.json index 2fd7ceca..45a6df11 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -2,102 +2,119 @@ "ar": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "bn": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "ca": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "de": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "es": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "fa": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "fr": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "hi": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "it": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "ja": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "nl": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "pl": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "pt": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "ru": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "tr": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "uk": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ], "zh": [ "step_3_steps", "step_4_steps", - "friends" + "friends", + "no_lyrics_available" ] } From 7c0689c056f75a27de33d2a03cec47836ec4de16 Mon Sep 17 00:00:00 2001 From: Meenbeese Date: Tue, 23 Jan 2024 22:07:33 -0500 Subject: [PATCH 128/131] docs: Reword and improve the support message (#1084) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e637f6c6..11f1db2e 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Btw it's not just another Electron app 😉 - 📖 Open source/libre software - 🔉 Playback control is done locally, not on the server -**¹** It is still **recommended** to support the creators by watching/liking/subscribing to the artists' YouTube channels or liking their tracks on Spotify (or purchasing a Spotify Premium subscription too). +**¹** It is still **recommended** to support creators by engaging with their YouTube channels/Spotify tracks (or preferably by buying their merch/concert tickets/physical media). ### ❌ Unsupported features From c3ebf56ac149b0af8815a5533fe6c386df743440 Mon Sep 17 00:00:00 2001 From: Nabraj Khadka <40161692+iamnabink@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:37:49 +0545 Subject: [PATCH 129/131] =?UTF-8?q?feat(translations):=20add=20Nepali=20(?= =?UTF-8?q?=E0=A4=A8=E0=A5=87=E0=A4=AA=E0=A4=BE=E0=A4=B2=E0=A5=80)=20trans?= =?UTF-8?q?lations=20(#1111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * actions: Add Up for grab checkbox to issue templates. (#1074) * docs: update copyright year in README.md (#1100) year changed from 2023 to 2024 * feat(translations): add Nepali (नेपाली) translations --------- Co-authored-by: Taha Ghadirian Co-authored-by: Muhammad Jawad <70428284+m-Jawa-d@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.yml | 7 + .github/ISSUE_TEMPLATE/new_feature.yml | 9 +- README.md | 2 +- lib/collections/language_codes.dart | 8 +- lib/l10n/app_ne.arb | 288 +++++++++++++++++++++++++ lib/l10n/l10n.dart | 1 + 6 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 lib/l10n/app_ne.arb diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e0031d17..64ee89d2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -71,3 +71,10 @@ body: description: Anything else you'd like to include? validations: required: false + - type: checkboxes + attributes: + label: Self grab + description: If you are a developer and want to work on this issue yourself, you can check this box and wait for maintainer response. We welcome contributions! + options: + - label: I'm ready to work on this issue! + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/new_feature.yml b/.github/ISSUE_TEMPLATE/new_feature.yml index 9742f91f..7f02ea38 100644 --- a/.github/ISSUE_TEMPLATE/new_feature.yml +++ b/.github/ISSUE_TEMPLATE/new_feature.yml @@ -35,4 +35,11 @@ body: label: Additional information description: Anything else you'd like to include? validations: - required: false \ No newline at end of file + required: false + - type: checkboxes + attributes: + label: Self grab + description: If you are a developer and want to work on this issue yourself, you can check this box and wait for maintainer response. We welcome contributions! + options: + - label: I'm ready to work on this issue! + required: false \ No newline at end of file diff --git a/README.md b/README.md index 11f1db2e..791d5da0 100644 --- a/README.md +++ b/README.md @@ -304,4 +304,4 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [dart_discord_rpc](https://github.com/alexmercerind/dart_discord_rpc) - Discord Rich Presence for Flutter & Dart apps & games. -

    © Copyright Spotube 2023

    +

    © Copyright Spotube 2024

    diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index de6f6d1c..4554de63 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -452,10 +452,10 @@ abstract class LanguageLocals { // name: "North Ndebele", // nativeName: "isiNdebele", // ), - // "ne": const ISOLanguageName( - // name: "Nepali", - // nativeName: "नेपाली", - // ), + "ne": const ISOLanguageName( + name: "Nepali", + nativeName: "नेपाली", + ), // "ng": const ISOLanguageName( // name: "Ndonga", // nativeName: "Owambo", diff --git a/lib/l10n/app_ne.arb b/lib/l10n/app_ne.arb new file mode 100644 index 00000000..9fca9ea4 --- /dev/null +++ b/lib/l10n/app_ne.arb @@ -0,0 +1,288 @@ +{ + "guest": "अतिथि", + "browse": "ब्राउज़ गर्नुहोस्", + "search": "खोजी गर्नुहोस्", + "library": "पुस्तकालय", + "lyrics": "गीतको शब्द", + "settings": "सेटिङ", + "genre_categories_filter": "शैली वा शैलीहरू फिल्टर गर्नुहोस्...", + "genre": "शैली", + "personalized": "व्यक्तिगत", + "featured": "विशेष", + "new_releases": "नयाँ रिलिज", + "songs": "गीतहरू", + "playing_track": "{track} बज्यो", + "queue_clear_alert": "यो हालको कतारलाई हटाउँछ। {track_length} ट्र्याकहरू हटाईन्छ\nके तपाईं जारी राख्न चाहनुहुन्छ?", + "load_more": "थप लोड गर्नुहोस्", + "playlists": "प्लेलिस्टहरू", + "artists": "कलाकारहरू", + "albums": "आल्बमहरू", + "tracks": "ट्र्याकहरू", + "downloads": "डाउनलोडहरू", + "filter_playlists": "तपाईंको प्लेलिस्टहरू फिल्टर गर्नुहोस्...", + "liked_tracks": "मन परेका ट्र्याकहरू", + "liked_tracks_description": "तपाईंको मन परेका सबै ट्र्याकहरू", + "create_playlist": "प्लेलिस्ट बनाउनुहोस्", + "create_a_playlist": "प्लेलिस्ट बनाउनुहोस्", + "update_playlist": "प्लेलिस्ट अपडेट गर्नुहोस्", + "create": "बनाउनुहोस्", + "cancel": "रद्द गर्नुहोस्", + "update": "अपडेट गर्नुहोस्", + "playlist_name": "प्लेलिस्टको नाम", + "name_of_playlist": "प्लेलिस्टको नाम", + "description": "विवरण", + "public": "सार्वजनिक", + "collaborative": "सहकारी", + "search_local_tracks": "स्थानीय ट्र्याकहरू खोजी गर्नुहोस्...", + "play": "बजाउनुहोस्", + "delete": "मेटाउनुहोस्", + "none": "कुनै पनि होइन", + "sort_a_z": "A-Zमा क्रमबद्ध गर्नुहोस्", + "sort_z_a": "Z-Aमा क्रमबद्ध गर्नुहोस्", + "sort_artist": "कलाकारबाट क्रमबद्ध गर्नुहोस्", + "sort_album": "आल्बमबाट क्रमबद्ध गर्नुहोस्", + "sort_tracks": "ट्र्याकहरूलाई क्रमबद्ध गर्नुहोस्", + "currently_downloading": "हाल डाउनलोड गर्दैछ ({tracks_length})", + "cancel_all": "सब रद्द गर्नुहोस्", + "filter_artist": "कलाकारहरूलाई फिल्टर गर्नुहोस्...", + "followers": "{followers} अनुयायीहरू", + "add_artist_to_blacklist": "कलाकारलाई कालोसूचीमा थप्नुहोस्", + "top_tracks": "शीर्ष ट्र्याकहरू", + "fans_also_like": "अनुयायीहरू पनि लाइक गर्छन्", + "loading": "लोड हुँदैछ...", + "artist": "कलाकार", + "blacklisted": "कालोसूचीमा", + "following": "फल्लो गर्दै", + "follow": "फल्लो गर्नुहोस्", + "artist_url_copied": "कलाकार URL क्लिपबोर्डमा प्रतिलिपि गरिएको छ", + "added_to_queue": "{tracks} ट्र्याकहरूलाई कतारमा थपिएको छ", + "filter_albums": "आल्बमहरूलाई फिल्टर गर्नुहोस्...", + "synced": "सिङ्क गरिएको", + "plain": "साधा", + "shuffle": "शफल", + "search_tracks": "ट्र्याकहरू खोजी गर्नुहोस्...", + "released": "रिलिज गरिएको", + "error": "त्रुटि {error}", + "title": "शीर्षक", + "time": "समय", + "more_actions": "थप कार्यहरू", + "download_count": "डाउनलोड ({count})", + "add_count_to_playlist": "प्लेलिस्टमा थप्नुहोस् ({count})", + "add_count_to_queue": "कतारमा थप्नुहोस् ({count})", + "play_count_next": "प्लेगरी गर्नुहोस् ({count})", + "album": "आल्बम", + "copied_to_clipboard": "{data} क्लिपबोर्डमा प्रतिलिपि गरिएको छ", + "add_to_following_playlists": "{track} लाई तलका प्लेलिस्टमा थप्नुहोस्", + "add": "थप्नुहोस्", + "added_track_to_queue": "{track} लाई कतारमा थपिएको छ", + "add_to_queue": "कतारमा थप्नुहोस्", + "track_will_play_next": "{track} अरूलाई पहिलोमा बज्नेछ", + "play_next": "पछिबजाउनुहोस्", + "removed_track_from_queue": "{track} लाई कतारबाट हटाइएको छ", + "remove_from_queue": "कतारबाट हटाउनुहोस्", + "remove_from_favorites": "पसन्दीदामा बाट हटाउनुहोस्", + "save_as_favorite": "पसन्दीदा बनाउनुहोस्", + "add_to_playlist": "प्लेलिस्टमा थप्नुहोस्", + "remove_from_playlist": "प्लेलिस्टबाट हटाउनुहोस्", + "add_to_blacklist": "कालोसूचीमा थप्नुहोस्", + "remove_from_blacklist": "कालोसूचीबाट हटाउनुहोस्", + "share": "साझा गर्नुहोस्", + "mini_player": "मिनि प्लेयर", + "slide_to_seek": "अगाडि वा पछाडि खोजी गर्नका लागि स्लाइड गर्नुहोस्", + "shuffle_playlist": "प्लेलिस्ट शफल गर्नुहोस्", + "unshuffle_playlist": "प्लेलिस्ट शफल नगर्नुहोस्", + "previous_track": "पूर्व ट्र्याक", + "next_track": "अरू ट्र्याक", + "pause_playback": "प्लेब्याक रोक्नुहोस्", + "resume_playback": "प्लेब्याक पुनः सुरु गर्नुहोस्", + "loop_track": "ट्र्याकलाई दोहोरोपट्टी बजाउनुहोस्", + "repeat_playlist": "प्लेलिस्ट पुनः बजाउनुहोस्", + "queue": "कतार", + "alternative_track_sources": "वैकल्पिक ट्र्याक स्रोतहरू", + "download_track": "ट्र्याक डाउनलोड गर्नुहोस्", + "tracks_in_queue": "कतारमा {tracks} ट्र्याकहरू", + "clear_all": "सब मेटाउनुहोस्", + "show_hide_ui_on_hover": "हवर गरेपछि UI देखाउनुहोस्/लुकाउनुहोस्", + "always_on_top": "सधैं टपमा राख्नुहोस्", + "exit_mini_player": "मिनि प्लेयर बाट बाहिर निस्कनुहोस्", + "download_location": "डाउनलोड स्थान", + "account": "खाता", + "login_with_spotify": "तपाईंको Spotify खातासँग लगइन गर्नुहोस्", + "connect_with_spotify": "Spotify सँग जडान गर्नुहोस्", + "logout": "बाहिर निस्कनुहोस्", + "logout_of_this_account": "यो खाताबाट बाहिर निस्कनुहोस्", + "language_region": "भाषा र क्षेत्र", + "language": "भाषा", + "system_default": "सिस्टम पूर्वनिर्धारित", + "market_place_region": "बजार स्थान", + "recommendation_country":"सिफारिस गरिएको देश", + "appearance": "दृष्टिकोण", + "layout_mode": "लेआउट मोड", + "override_layout_settings": "अनुकूलित प्रतिकृयात्मक लेआउट मोड सेटिङ्गहरू", + "adaptive": "अनुकूलित", + "compact": "संकुचित", + "extended": "बढाइएको", + "theme": "थिम", + "dark": "गाढा", + "light": "प्रकाश", + "system": "सिस्टम", + "accent_color": "एक्सेन्ट रङ्ग", + "sync_album_color": "एल्बम रङ्ग सिङ्क गर्नुहोस्", + "sync_album_color_description": "एल्बम कला को प्रमुख रङ्गलाई एक्सेन्ट रङ्गको रूपमा प्रयोग गर्दछ", + "playback": "प्लेब्याक", + "audio_quality": "आडियो गुणस्तर", + "high": "उच्च", + "low": "न्यून", + "pre_download_play": "पूर्व-डाउनलोड र प्ले गर्नुहोस्", + "pre_download_play_description": "आडियो स्ट्रिम गर्नु नगरी बाइटहरू डाउनलोड गरी बजाउँछ (उच्च ब्यान्डविथ उपयोगकर्ताहरूको लागि सिफारिस गरिएको)", + "skip_non_music": "गीतहरू बाहेक कुनै अनुष्ठान छोड्नुहोस् (स्पन्सरब्लक)", + "blacklist_description": "कालोसूची गीत र कलाकारहरू", + "wait_for_download_to_finish": "कृपया हालको डाउनलोड समाप्त हुन लागि पर्खनुहोस्", + "desktop": "डेस्कटप", + "close_behavior": "बन्द व्यवहार", + "close": "बन्द गर्नुहोस्", + "minimize_to_tray": "ट्रेमा कम गर्नुहोस्", + "show_tray_icon": "सिस्टम ट्रे आइकन देखाउनुहोस्", + "about": "बारेमा", + "u_love_spotube": "हामीले थाहा पारेका छौं तपाईंलाई Spotube मन पर्छ", + "check_for_updates": "अपडेटहरूको लागि जाँच गर्नुहोस्", + "about_spotube": "Spotube को बारेमा", + "blacklist": "कालोसूची", + "please_sponsor": "कृपया स्पन्सर/डोनेट गर्नुहोस्", + "spotube_description": "Spotube, एक हल्का, समृद्ध, स्वतन्त्र Spotify क्लाइयन", + "version": "संस्करण", + "build_number": "निर्माण नम्बर", + "founder": "संस्थापक", + "repository": "पुनरावलोकन स्थल", + "bug_issues": "त्रुटि + समस्याहरू", + "made_with": "❤️ 2021-2024 बाट बनाइएको", + "kingkor_roy_tirtho": "किङ्कोर राय तिर्थो", + "copyright": "© 2021-{current_year} किङ्कोर राय तिर्थो", + "license": "लाइसेन्स", + "add_spotify_credentials": "सुरु हुनका लागि तपाईंको स्पटिफाई क्रेडेन्शियल थप्नुहोस्", + "credentials_will_not_be_shared_disclaimer": "चिन्ता नगर्नुहोस्, तपाईंको कुनै पनि क्रेडेन्शियलहरूले कसैले संग्रह वा साझा गर्नेछैन", + "know_how_to_login": "कसरी लगिन गर्ने भन्ने थाहा छैन?", + "follow_step_by_step_guide": "चरणबद्ध मार्गदर्शनमा साथी बनाउनुहोस्", + "spotify_cookie": "Spotify {name} कुकी", + "cookie_name_cookie": "{name} कुकी", + "fill_in_all_fields": "कृपया सबै क्षेत्रहरू भर्नुहोस्", + "submit": "पेश गर्नुहोस्", + "exit": "बाहिर निस्कनुहोस्", + "previous": "पूर्ववत", + "next": "अरू", + "done": "गरिएको", + "step_1": "कदम 1", + "first_go_to": "पहिलो, जानुहोस्", + "login_if_not_logged_in": "र लगइन/साइनअप गर्नुहोस् जुन तपाईंले लगइन गरेनन्", + "step_2": "कदम 2", + "step_2_steps": "1. एकबार तपाईं लगइन गरे पछि, F12 थिच्नुहोस् वा माउस राइट क्लिक गर्नुहोस् > इन्स्पेक्ट गर्नुहोस् भने ब्राउजर डेभटुलहरू खुलाउनका लागि।\n2. तपाईंको \"एप्लिकेसन\" ट्याबमा जानुहोस् (Chrome, Edge, Brave इत्यादि) वा \"स्टोरेज\" ट्याबमा जानुहोस् (Firefox, Palemoon इत्यादि)\n3. तपाईंको इन्सेक्ट गरेको ब्राउजर डेभटुलहरूमा \"कुकीहरू\" खण्डमा जानुहोस् अनि \"https://accounts.spotify.com\" उपकोणमा जानुहोस्", + "step_3": "कदम 3", + "step_3_steps": "\"sp_dc\" र \"sp_key\" (वा sp_gaid) कुकीहरूको मानहरू प्रतिलिपि गर्नुहोस्", + "success_emoji": "सफलता 🥳", + "success_message": "हाम्रो सानो भाइ, अब तपाईं सफलतापूर्वक आफ्नो Spotify खातामा लगइन गरेका छौं। राम्रो काम गरेको!", + "step_4": "कदम 4", + "step_4_steps": "प्रतिलिपि गरेको \"sp_dc\" र \"sp_key\" (वा sp_gaid) मानहरूलाई आफ्नो ठाउँमा पेस्ट गर्नुहोस्", + "something_went_wrong": "केहि गल्ति भएको छ", + "piped_instance": "पाइपड सर्भर इन्स्ट्यान्स", + "piped_description": "गीत मिलाउको लागि प्रयोग गर्ने पाइपड सर्भर इन्स्ट्यान्स", + "piped_warning": "तिनीहरूमध्ये केहि ठिक गर्न सक्छ। यसलाई आफ्नो जोखिममा प्रयोग गर्नुहोस्", + "generate_playlist": "प्लेलिस्ट बनाउनुहोस्", + "track_exists": "ट्र्याक {track} पहिले नै छ", + "replace_downloaded_tracks": "सबै डाउनलोड गरिएका ट्र्याकहरूलाई परिवर्तन गर्नुहोस्", + "skip_download_tracks": "सबै डाउनलोड गरिएका ट्र्याकहरूलाई छोड्नुहोस्", + "do_you_want_to_replace": "के तपाईंले वर्तमान ट्र्याकलाई परिवर्तन गर्न चाहनुहुन्छ?", + "replace": "परिवर्तन गर्नुहोस्", + "skip": "छोड्नुहोस्", + "select_up_to_count_type": "{count} {type} सम्म चयन गर्नुहोस्", + "select_genres": "जनरहरू चयन गर्नुहोस्", + "add_genres": "जनरहरू थप्नुहोस्", + "country": "देश", + "number_of_tracks_generate": "बनाउनका लागि ट्र्याकहरूको संख्या", + "acousticness": "एकोस्टिकनेस", + "danceability": "नृत्यक्षमता", + "energy": "ऊर्जा", + "instrumentalness": "साजा रहेकोता", + "liveness": "प्राणिकता", + "loudness": "शोर", + "speechiness": "भाषण", + "valence": "मानसिक स्वभाव", + "popularity": "लोकप्रियता", + "key": "कुञ्जी", + "duration": "अवधि (सेकेण्ड)", + "tempo": "गति (बीपीएम)", + "mode": "मोड", + "time_signature": "समय हस्ताक्षर", + "short": "सानो", + "medium": "मध्यम", + "long": "लामो", + "min": "न्यून", + "max": "अधिक", + "target": "लक्ष्य", + "moderate": "मध्यस्थ", + "deselect_all": "सबै छान्नुहोस्", + "select_all": "सबै चयन गर्नुहोस्", + "are_you_sure": "के तपाईं सुनिश्चित हुनुहुन्छ?", + "generating_playlist": "तपाईंको विशेष प्लेलिस्ट बनाइएको छ...", + "selected_count_tracks": "{count} ट्र्याकहरू छन् चयन गरिएका", + "download_warning": "यदि तपाईं सबै ट्र्याकहरूलाई बल्कमा डाउनलोड गर्छनु हो भने तपाईं स्पष्ट रूपमा साङ्गीत चोरी गरिरहेका छन् र यो साङ्गीतको रचनात्मक समाजलाई क्षति पनि पुर्याउँछ। उमेराइएको छ कि तपाईं यसको बारेमा जागरूक छिनुहुन्छ। सधैं, कला गर्दै र कलाकारको कडा परम्परा समर्थन गर्दै आइन्छ।", + "download_ip_ban_warning": "बितिएका डाउनलोड अनुरोधहरूका कारण तपाईंको आइपीले YouTube मा ब्लक हुन सक्छ। आइपी ब्लक भनेको कम्तीमा 2-3 महिनासम्म तपाईं त्यस आइपी यन्त्रबाट YouTube प्रयोग गर्न सक्नुहुन्छ। र यदि यो हुँदैछ भने स्पट्यूबले यसलाई कसैले गरेको बारेमा कुनै दायित्व लिन्छैन।", + "by_clicking_accept_terms": "'स्वीकृत' गरेर तपाईं निम्नलिखित निर्वाचन गर्दैछिन्:", + "download_agreement_1": "म मन्ने छु कि म साङ्गीत चोरी गरिरहेको छु। म बुरो हुँ", + "download_agreement_2": "म कहिल्यै कहिल्यै तिनीहरूलाई समर्थन गर्नेछु र म यो तिनीहरूको कला किन्ने पैसा छैन भने मा मात्र यो गरेको छु", + "download_agreement_3": "म पूरा रूपमा जान्छु कि मेरो आइपी YouTube मा ब्लक हुन सक्छ र म मन्छेहरूले मेरो चासोबाट भएको कुनै दुर्घटनामा स्पट्यूब वा तिनीहरूको मालिकहरू/सहयोगीहरूलाई दायित्वी ठान्छुँभन्ने पूर्ण जानकारी छैन", + "decline": "अस्वीकृत", + "accept": "स्वीकृत", + "details": "विवरण", + "youtube": "YouTube", + "channel": "च्यानल", + "likes": "लाइकहरू", + "dislikes": "असुनुहरू", + "views": "हेरिएको", + "streamUrl": "स्ट्रिम यूआरएल", + "stop": "रोक्नुहोस्", + "sort_newest": "नयाँ थपिएकोमा क्रमबद्ध गर्नुहोस्", + "sort_oldest": "पुरानो थपिएकोमा क्रमबद्ध गर्नुहोस्", + "sleep_timer": "सुत्ने टाइमर", + "mins": "{minutes} मिनेटहरू", + "hours": "{hours} घण्टाहरू", + "hour": "{hours} घण्टा", + "custom_hours": "कस्टम घण्टाहरू", + "logs": "लगहरू", + "developers": "डेभेलपर्स", + "not_logged_in": "तपाईंले लगइन गरेका छैनौं", + "search_mode": "खोज मोड", + "audio_source": "अडियो स्रोत", + "ok": "ठिक छ", + "failed_to_encrypt": "एन्क्रिप्ट गर्न सकिएन", + "encryption_failed_warning": "स्पट्यूबले तपाईंको डेटा सुरक्षित रूपमा स्टोर गर्नका लागि एन्क्रिप्ट गर्न खोजेको छ। तर यसले गरेको छैन। यसले असुरक्षित स्टोरेजमा फल्लब्याक गर्दछ\nयदि तपाईंले लिनक्स प्रयोग गरिरहेका छन् भने कृपया सुनिश्चित गर्नुहोस् कि तपाईंले कुनै सीक्रेट-सर्भिस (गोनोम-किरिङ, केडीइ-वालेट, किपासेक्ससि इत्यादि) इन्स्टल गरेका छौं", + "querying_info": "जानकारी हेर्दै...", + "piped_api_down": "पाइपड एपीआई डाउन छ", + "piped_down_error_instructions": "पाइपड इन्स्ट्यान्स {pipedInstance} हाल डाउन छ\n\nजीसनै इन्स्ट्यान्स परिवर्तन गर्नुहोस् वा 'एपीआई प्रकार' लाइ YouTube आफिसियल एपीआईमा परिवर्तन गर्नुहोस्\n\nपरिवर्तनपछि एप्लिकेसन पुन: सुरु गर्नुहोस्", + "you_are_offline": "तपाईं वर्तमान अफलाइन हुनुहुन्छ", + "connection_restored": "तपाईंको इन्टरनेट कनेक्सन पुन: स्थापित भएको छ", + "use_system_title_bar": "सिस्टम शीर्षक पट्टी प्रयोग गर्नुहोस्", + "crunching_results": "परिणामहरू कपालबाट पीस्दै...", + "search_to_get_results": "परिणामहरू प्राप्त गर्नका लागि खोज्नुहोस्", + "use_amoled_mode": "कृष्ण ब्ल्याक गाढा थिम प्रयोग गर्नुहोस्", + "pitch_dark_theme": "एमोलेड मोड", + "normalize_audio": "अडियो सामान्य गर्नुहोस्", + "change_cover": "कवर परिवर्तन गर्नुहोस्", + "add_cover": "कवर थप्नुहोस्", + "restore_defaults": "पूर्वनिर्धारितहरू पुनः स्थापित गर्नुहोस्", + "download_music_codec": "साङ्गीत कोडेक डाउनलोड गर्नुहोस्", + "streaming_music_codec": "स्ट्रिमिङ साङ्गीत कोडेक", + "login_with_lastfm": "लास्ट.एफ.एम सँग लगइन गर्नुहोस्", + "connect": "जडान गर्नुहोस्", + "disconnect_lastfm": "लास्ट.एफ.एम डिसकनेक्ट गर्नुहोस्", + "disconnect": "डिसकनेक्ट", + "username": "प्रयोगकर्ता नाम", + "password": "पासवर्ड", + "login": "लगइन", + "login_with_your_lastfm": "तपाईंको लास्ट.एफ.एम खातामा लगइन गर्नुहोस्", + "scrobble_to_lastfm": "लास्ट.एफ.एम मा स्क्रबल गर्नुहोस्", + "go_to_album": "आल्बममा जानुहोस्", + "discord_rich_presence": "डिस्कर्ड धनी उपस्थिति", + "browse_all": "सबै हेर्नुहोस्", + "genres": "शैलीहरू", + "explore_genres": "शैलीहरू अन्वेषण गर्नुहोस्" +} \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 47e5eb99..335be545 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -21,6 +21,7 @@ class L10n { const Locale('es', 'ES'), const Locale("fa", "IR"), const Locale('fr', 'FR'), + const Locale('ne', 'NP'), const Locale('hi', 'IN'), const Locale('it', 'IT'), const Locale('ja', 'JP'), From 9b289605c7e803605c83b81e62ecc089ea668b60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:59:20 +0600 Subject: [PATCH 130/131] chore(deps): bump shared_preferences from 2.2.1 to 2.2.2 (#796) Bumps [shared_preferences](https://github.com/flutter/packages/tree/main/packages/shared_preferences) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/flutter/packages/releases) - [Commits](https://github.com/flutter/packages/commits/shared_preferences-v2.2.2/packages/shared_preferences) --- updated-dependencies: - dependency-name: shared_preferences dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index b4182d12..2278f185 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1772,10 +1772,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" shared_preferences_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 36a2f398..4798aeec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -83,7 +83,7 @@ dependencies: url: https://github.com/KRTirtho/scrobblenaut.git ref: dart-3-support scroll_to_index: ^3.0.1 - shared_preferences: ^2.0.11 + shared_preferences: ^2.2.2 sidebarx: ^0.15.0 skeleton_text: ^3.0.1 smtc_windows: ^0.1.1 From ab09ae60f06930299e1f17138931fd1604530af4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:00:35 +0600 Subject: [PATCH 131/131] chore(deps): bump visibility_detector from 0.3.3 to 0.4.0+2 (#797) Bumps [visibility_detector](https://github.com/google/flutter.widgets/tree/master/packages) from 0.3.3 to 0.4.0+2. - [Commits](https://github.com/google/flutter.widgets/commits/HEAD/packages) --- updated-dependencies: - dependency-name: visibility_detector dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 2278f185..5141b46b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2225,10 +2225,10 @@ packages: dependency: "direct main" description: name: visibility_detector - sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.4.0+2" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4798aeec..d49c02a1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,7 +94,7 @@ dependencies: url_launcher: ^6.1.7 uuid: ^3.0.7 version: ^3.0.2 - visibility_detector: ^0.3.3 + visibility_detector: ^0.4.0+2 window_manager: ^0.3.1 window_size: git: