From 187bdaf75c4e1ba36d9a06ba167d601cd9514041 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 31 Aug 2023 21:27:07 +0600 Subject: [PATCH 01/14] chore: trying to fix windows memory leak --- lib/services/queries/playlist.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index 25da6199..70b9ebe7 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -138,6 +138,10 @@ class PlaylistQueries { (lastPageData.items?.length ?? 0) < 10 || lastPageData.isLast ? null : lastPage + 1, + retryConfig: RetryConfig.withConstantDefaults( + maxRetries: 1, + retryDelay: const Duration(seconds: 5), + ), ref: ref, ); } From a5de69c65a73204c80f14bba25aeab8bb18169a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 19:26:40 +0600 Subject: [PATCH 02/14] chore(deps): bump actions/checkout from 3 to 4 (#699) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/spotube-publish-binary.yml | 6 +++--- .github/workflows/spotube-release-binary.yml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index 85c26e01..12a2f99b 100644 --- a/.github/workflows/spotube-publish-binary.yml +++ b/.github/workflows/spotube-publish-binary.yml @@ -22,12 +22,12 @@ jobs: runs-on: ubuntu-22.04 if: contains(inputs.jobs, 'flathub') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: repository: KRTirtho/com.github.KRTirtho.Spotube token: ${{ secrets.FLATHUB_TOKEN }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: path: spotube @@ -50,7 +50,7 @@ jobs: runs-on: ubuntu-22.04 if: contains(inputs.jobs, 'aur') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dsaltares/fetch-gh-release-asset@master with: diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index 31b0ebb6..cd94ef67 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -32,7 +32,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2.10.0 with: cache: true @@ -102,7 +102,7 @@ jobs: linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2.10.0 with: cache: true @@ -191,7 +191,7 @@ jobs: android: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2.10.0 with: cache: true @@ -266,7 +266,7 @@ jobs: macos: runs-on: macos-12 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2.10.0 with: cache: true From c987ea78414f094dead2c2b35ddf8ae83a70f2fe Mon Sep 17 00:00:00 2001 From: Joshua Samenfink Date: Fri, 8 Sep 2023 05:52:59 +0200 Subject: [PATCH 03/14] fix: add missing dependency in debian package (#704) * chore: fill missing translations * chore: vscode filenesting * fix: add missing dependency in debian package * fixes https://github.com/KRTirtho/spotube/issues/703 --------- Co-authored-by: Kingkor Roy Tirtho --- .vscode/settings.json | 30 ++++++---- lib/l10n/app_bn.arb | 7 ++- lib/l10n/app_ca.arb | 8 ++- lib/l10n/app_de.arb | 7 ++- lib/l10n/app_es.arb | 7 ++- lib/l10n/app_fr.arb | 7 ++- lib/l10n/app_hi.arb | 7 ++- lib/l10n/app_ja.arb | 7 ++- lib/l10n/app_pl.arb | 7 ++- lib/l10n/app_pt.arb | 9 ++- lib/l10n/app_ru.arb | 3 +- lib/l10n/app_zh.arb | 7 ++- linux/packaging/deb/make_config.yaml | 1 + untranslated_messages.json | 83 +--------------------------- 14 files changed, 82 insertions(+), 108 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c4917255..c12c492a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,19 @@ { - "cmake.configureOnOpen": false, - "cSpell.words": [ - "acousticness", - "danceability", - "instrumentalness", - "Mpris", - "riverpod", - "speechiness", - "Spotube", - "winget" - ] -} + "cmake.configureOnOpen": false, + "cSpell.words": [ + "acousticness", + "danceability", + "instrumentalness", + "Mpris", + "riverpod", + "speechiness", + "Spotube", + "winget" + ], + "editor.formatOnSave": true, + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "pubspec.yaml": "pubspec.lock,analysis_options.yaml,.packages,.flutter-plugins,.flutter-plugins-dependencies", + "README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md", + } +} \ No newline at end of file diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 98e165aa..3a5e3cdd 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -254,5 +254,10 @@ "ok": "ঠিক আছে", "failed_to_encrypt": "এনক্রিপ্ট করা ব্যর্থ হয়েছে", "encryption_failed_warning": "Spotube আপনার তথ্যগুলি নিরাপদভাবে স্টোর করতে এনক্রিপশন ব্যবহার করে। কিন্তু এটি ব্যর্থ হয়েছে। তাই এটি অনিরাপদ স্টোরে ফলফল হবে\nযদি আপনি Linux ব্যবহার করেন, তবে দয়া করে নিশ্চিত হউন যে আপনার কোনও সিক্রেট-সার্ভিস gnome-keyring, kde-wallet, keepassxc ইত্যাদি ইনস্টল করা আছে", - "querying_info": "তথ্য অনুসন্ধান করা হচ্ছে" + "querying_info": "তথ্য অনুসন্ধান করা হচ্ছে", + "piped_api_down": "পাইপড API ডাউন আছে", + "piped_down_error_instructions": "বর্তমানে পাইপড ইনস্ট্যান্স {pipedInstance} ডাউন আছে\n\nইনস্ট্যান্স পরিবর্তন করুন অথবা 'API টাইপ' পরিবর্তন করুন অফিসিয়াল ইউটিউব API হতে\n\nপরিবর্তনের পরে অ্যাপটি পুনরায় চালানোর নিশ্চিত করুন", + "you_are_offline": "আপনি বর্তমানে অফলাইন", + "connection_restored": "আপনার ইন্টারনেট সংযোগ পুনরুদ্ধার হয়েছে", + "use_system_title_bar": "সিস্টেম শিরোনাম বার ব্যবহার করুন" } \ No newline at end of file diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index 11413af2..0e7814fb 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -253,5 +253,11 @@ "youtube_api_type": "Tipus d'API de YouTube", "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" + "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", + "piped_api_down": "La API de Piped no està operativa", + "piped_down_error_instructions": "La instància de Piped {pipedInstance} no està operativa en aquest moment\n\nCanvieu la instància o canvieu el 'Tipus d'API' a l'API oficial de YouTube\n\nAssegureu-vos de reiniciar l'aplicació després del canvi", + "you_are_offline": "Actualment no teniu connexió a internet", + "connection_restored": "S'ha restablert la connexió a internet", + "use_system_title_bar": "Utilitza la barra de títol del sistema", + "querying_info": "Consultant informació..." } \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index d84333ec..6f711a2b 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -254,5 +254,10 @@ "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", - "querying_info": "Abfrageinformationen..." + "querying_info": "Abfrageinformationen...", + "piped_api_down": "Die Piped API ist ausgefallen", + "piped_down_error_instructions": "Die Piped-Instanz {pipedInstance} ist derzeit nicht verfügbar\n\nEntweder ändern Sie die Instanz oder wechseln Sie den 'API-Typ' zur offiziellen YouTube API\n\nStellen Sie sicher, dass Sie die App nach der Änderung neu starten", + "you_are_offline": "Sie sind derzeit offline", + "connection_restored": "Ihre Internetverbindung wurde wiederhergestellt", + "use_system_title_bar": "System-Titelleiste verwenden" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 101ba1b8..b1e242ca 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -254,5 +254,10 @@ "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", - "querying_info": "Consultando información..." + "querying_info": "Consultando información...", + "piped_api_down": "La API de Piped no está disponible", + "piped_down_error_instructions": "La instancia de Piped {pipedInstance} no está funcionando en este momento\n\nCambie la instancia o cambie el 'Tipo de API' a la API oficial de YouTube\n\nAsegúrese de reiniciar la aplicación después del cambio", + "you_are_offline": "Actualmente estás sin conexión", + "connection_restored": "Se ha restablecido tu conexión a internet", + "use_system_title_bar": "Usar la barra de título del sistema" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 302e4740..207bc7d7 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -254,5 +254,10 @@ "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", - "querying_info": "Interrogation des info..." + "querying_info": "Interrogation des info...", + "piped_api_down": "L'API Piped est hors service", + "piped_down_error_instructions": "L'instance Piped {pipedInstance} est actuellement indisponible\n\nChangez soit l'instance, soit le 'Type d'API' pour utiliser l'API officielle de YouTube\n\nN'oubliez pas de redémarrer l'application après la modification", + "you_are_offline": "Vous êtes actuellement hors ligne", + "connection_restored": "Votre connexion internet a été rétablie", + "use_system_title_bar": "Utiliser la barre de titre système" } \ No newline at end of file diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index e60286f4..85b55ff7 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -254,5 +254,10 @@ "ok": "ठीक है", "failed_to_encrypt": "एन्क्रिप्ट करने में विफल रहा", "encryption_failed_warning": "Spotube आपके डेटा को सुरक्षित रूप से स्टोर करने के लिए एन्क्रिप्शन का उपयोग करता है। लेकिन इसमें विफल रहा। इसलिए, यह असुरक्षित स्टोरेज पर फॉलबैक करेगा\nयदि आप Linux का उपयोग कर रहे हैं, तो कृपया सुनिश्चित करें कि आपके पास gnome-keyring, kde-wallet, keepassxc आदि जैसी कोई सीक्रेट-सर्विस इंस्टॉल की गई है", - "querying_info": "जानकारी प्राप्त करना" + "querying_info": "जानकारी प्राप्त करना", + "piped_api_down": "पाइप्ड एपीआई डाउन है", + "piped_down_error_instructions": "पाइप्ड इंस्टेंस {pipedInstance} वर्तमान में डाउन है\n\nइंस्टेंस बदलें या 'एपीआई प्रकार' को आधिकृत YouTube एपीआई में बदलें\n\nपरिवर्तन के बाद ऐप को फिर से चालने की सुनिश्चित करें", + "you_are_offline": "आप वर्तमान में ऑफ़लाइन हैं", + "connection_restored": "आपका इंटरनेट कनेक्शन बहाल हो गया है", + "use_system_title_bar": "सिस्टम शीर्षक पट्टी का उपयोग करें" } \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 9efd59ba..4b552e9e 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -254,5 +254,10 @@ "ok": "分かりました", "failed_to_encrypt": "暗号化に失敗しました", "encryption_failed_warning": "Spotubeはデータを安全に保存するために暗号化を使用しています。しかし、失敗しました。したがって、安全でないストレージにフォールバックします\nLinuxを使用している場合は、gnome-keyring、kde-wallet、keepassxcなどのシークレットサービスがインストールされていることを確認してください", - "querying_info": "情報を取得中..." + "querying_info": "情報を取得中...", + "piped_api_down": "Piped APIがダウンしています", + "piped_down_error_instructions": "Pipedインスタンス{pipedInstance}は現在ダウンしています\n\nインスタンスを変更するか、'APIタイプ'を公式のYouTube APIに変更してください\n\n変更後にアプリを再起動してください", + "you_are_offline": "現在、オフラインです", + "connection_restored": "インターネット接続が復旧しました", + "use_system_title_bar": "システムタイトルバーを使用する" } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 641f5a62..982f815e 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -255,6 +255,9 @@ "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.)", "querying_info": "Szukam informacji...", - "piped_api_down": "Piped API jest wyłączone", - "piped_down_error_instructions": "Instancja Piped {pipedInstance} jest obecnie wyłączona.\n\nJednak możesz zmienić instancję lub 'Typ API' na oficialne API YouTube'a.\n\nUpewnij się, że uruchomiłeś ponownie aplikacje po tej zmianie." + "piped_api_down": "API Piped jest niedostępne", + "piped_down_error_instructions": "Instancja Piped {pipedInstance} jest obecnie niedostępna\n\nZmień instancję lub zmień 'Rodzaj API' na oficjalne API YouTube\n\nUpewnij się, że po zmianie zrestartujesz aplikację", + "you_are_offline": "Obecnie jesteś offline", + "connection_restored": "Twoje połączenie z internetem zostało przywrócone", + "use_system_title_bar": "Użyj paska tytułu systemu" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index af26c335..32dabfba 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -255,6 +255,9 @@ "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", "querying_info": "Consultando informações...", - "piped_api_down": "A API do Piped está fora do ar", - "piped_down_error_instructions": "A instância do Piped {pipedInstance} está fora do ar no momento\n\nMude a instância ou altere o 'Tipo de API' para a API oficial do YouTube\n\nCertifique-se de reiniciar o aplicativo após a alteração" -} + "piped_api_down": "A API do Piped está indisponível", + "piped_down_error_instructions": "A instância do Piped {pipedInstance} está atualmente indisponível\n\nMude a instância ou mude o 'Tipo de API' para a API oficial do YouTube\n\nCertifique-se de reiniciar o aplicativo após a alteração", + "you_are_offline": "Você está offline no momento", + "connection_restored": "Sua conexão com a internet foi restaurada", + "use_system_title_bar": "Usar a barra de título do sistema" +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index e92797a4..9f79ec66 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -258,5 +258,6 @@ "piped_api_down": "Piped API не отвечает", "piped_down_error_instructions": "Экземпляр Piped {pipedInstance} в данный момент недоступен.\n\nВы можете либо изменить экземпляр, либо переключиться на использование официального API YouTube.\n\nНе забудьте перезапустить приложение после внесенных изменений", "you_are_offline": "Нет доступа к сети", - "connection_restored": "Ваше интернет-соединение восстановлено" + "connection_restored": "Ваше интернет-соединение восстановлено", + "use_system_title_bar": "Использовать системную панель заголовка" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 9810e4f2..f5b6f007 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -254,5 +254,10 @@ "ok": "确定", "failed_to_encrypt": "加密失败", "encryption_failed_warning": "Spotube使用加密来安全地存储您的数据。但是失败了。因此,它将回退到不安全的存储\n如果您使用Linux,请确保已安装gnome-keyring、kde-wallet和keepassxc等秘密服务", - "querying_info": "正在查询信息..." + "querying_info": "正在查询信息...", + "piped_api_down": "Piped API不可用", + "piped_down_error_instructions": "当前Piped实例{pipedInstance}不可用\n\n请更改实例或将'API类型'更改为官方YouTube API\n\n更改后请确保重新启动应用程序", + "you_are_offline": "您当前处于离线状态", + "connection_restored": "您的互联网连接已恢复", + "use_system_title_bar": "使用系统标题栏" } \ No newline at end of file diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml index 3d6d75d6..148b9618 100644 --- a/linux/packaging/deb/make_config.yaml +++ b/linux/packaging/deb/make_config.yaml @@ -16,6 +16,7 @@ dependencies: - libsecret-1-0 - libnotify-bin - libjsoncpp25 + - libmpv2 essential: false icon: assets/spotube-logo.png diff --git a/untranslated_messages.json b/untranslated_messages.json index 3c52fab3..9e26dfee 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,82 +1 @@ -{ - "bn": [ - "piped_api_down", - "piped_down_error_instructions", - "you_are_offline", - "connection_restored", - "use_system_title_bar" - ], - - "ca": [ - "querying_info", - "piped_api_down", - "piped_down_error_instructions", - "you_are_offline", - "connection_restored", - "use_system_title_bar" - ], - - "de": [ - "piped_api_down", - "piped_down_error_instructions", - "you_are_offline", - "connection_restored", - "use_system_title_bar" - ], - - "es": [ - "piped_api_down", - "piped_down_error_instructions", - "you_are_offline", - "connection_restored", - "use_system_title_bar" - ], - - "fr": [ - "piped_api_down", - "piped_down_error_instructions", - "you_are_offline", - "connection_restored", - "use_system_title_bar" - ], - - "hi": [ - "piped_api_down", - "piped_down_error_instructions", - "you_are_offline", - "connection_restored", - "use_system_title_bar" - ], - - "ja": [ - "piped_api_down", - "piped_down_error_instructions", - "you_are_offline", - "connection_restored", - "use_system_title_bar" - ], - - "pl": [ - "you_are_offline", - "connection_restored", - "use_system_title_bar" - ], - - "pt": [ - "you_are_offline", - "connection_restored", - "use_system_title_bar" - ], - - "ru": [ - "use_system_title_bar" - ], - - "zh": [ - "piped_api_down", - "piped_down_error_instructions", - "you_are_offline", - "connection_restored", - "use_system_title_bar" - ] -} +{} \ No newline at end of file From 142dc498f8f9d26e6b370c9c52f790a20832fc38 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 8 Sep 2023 12:30:52 +0600 Subject: [PATCH 04/14] fix: Windows memory leak due refetchOnStale user-liked-tracks (#705) * chore: refactor CLI stuff to separate service folder * chore: trying to fix memory leak * chore: fix fl_Query_devtools in wrong place * chore: upgrade deps * fix: user liked tracks memory leak due to isStale & updateQueryFn --- lib/components/playlist/playlist_card.dart | 4 +- lib/components/shared/heart_button.dart | 79 ++++++++++++------- .../use_disable_battery_optimizations.dart | 2 +- lib/main.dart | 44 +---------- lib/pages/playlist/playlist.dart | 7 +- lib/services/cli/cli.dart | 46 +++++++++++ lib/services/queries/playlist.dart | 67 ++++++++++------ lib/services/queries/user.dart | 8 ++ pubspec.lock | 58 ++++++++++---- pubspec.yaml | 3 +- 10 files changed, 206 insertions(+), 112 deletions(-) create mode 100644 lib/services/cli/cli.dart diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index be7abfb9..edb374c8 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -62,7 +62,7 @@ class PlaylistCard extends HookConsumerWidget { List fetchedTracks = await queryBowl.fetchQuery( "playlist-tracks/${playlist.id}", - () => useQueries.playlist.tracksOf(playlist.id!, spotify), + () => useQueries.playlist.tracksOf(playlist.id!, spotify, ref), ) ?? []; @@ -83,7 +83,7 @@ class PlaylistCard extends HookConsumerWidget { if (isPlaylistPlaying) return; List fetchedTracks = await queryBowl.fetchQuery( "playlist-tracks/${playlist.id}", - () => useQueries.playlist.tracksOf(playlist.id!, spotify), + () => useQueries.playlist.tracksOf(playlist.id!, spotify, ref), ) ?? []; diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index 2b877ecf..c18ca2e4 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -29,7 +29,7 @@ class HeartButton extends HookConsumerWidget { Widget build(BuildContext context, ref) { final auth = ref.watch(AuthenticationNotifier.provider); - if (auth == null) return Container(); + if (auth == null) return const SizedBox.shrink(); return IconButton( tooltip: tooltip, @@ -57,18 +57,21 @@ class HeartButton extends HookConsumerWidget { } } -({ +typedef UseTrackToggleLike = ({ bool isLiked, Mutation toggleTrackLike, Query me, -}) useTrackToggleLike(Track track, WidgetRef ref) { +}); + +UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) { final me = useQueries.user.me(ref); - final savedTracks = - useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks"); + final savedTracks = useQueries.playlist.likedTracksQuery(ref); - final isLiked = - savedTracks.data?.any((element) => element.id == track.id) ?? false; + final isLiked = useMemoized( + () => savedTracks.data?.any((element) => element.id == track.id) ?? false, + [savedTracks.data, track.id], + ); final mounted = useIsMounted(); @@ -76,28 +79,48 @@ class HeartButton extends HookConsumerWidget { ref, track.id!, onMutate: (isLiked) { - savedTracks.setData( - [ - if (isLiked == true) - ...?savedTracks.data?.where((element) => element.id != track.id) - else - ...?savedTracks.data?..add(track) - ], - ); + print("Toggle Like onMutate: $isLiked"); + + if (isLiked) { + savedTracks.setData( + savedTracks.data + ?.where((element) => element.id != track.id) + .toList() ?? + [], + ); + } else { + savedTracks.setData( + [ + ...?savedTracks.data, + track, + ], + ); + } return isLiked; }, onData: (data, recoveryData) async { + print("Toggle Like onData: $data"); await savedTracks.refresh(); }, onError: (payload, isLiked) { + print("Toggle Like onError: $payload"); if (!mounted()) return; - savedTracks.setData([ - if (isLiked != true) - ...?savedTracks.data?.where((element) => element.id != track.id) - else - ...?savedTracks.data?..add(track), - ]); + if (isLiked != true) { + savedTracks.setData( + savedTracks.data + ?.where((element) => element.id != track.id) + .toList() ?? + [], + ); + } else { + savedTracks.setData( + [ + ...?savedTracks.data, + track, + ], + ); + } }, ); @@ -113,21 +136,21 @@ class TrackHeartButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final savedTracks = - useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks"); - final toggler = useTrackToggleLike(track, ref); - if (toggler.me.isLoading || !toggler.me.hasData) { + final savedTracks = useQueries.playlist.likedTracksQuery(ref); + final (:me, :isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref); + + if (me.isLoading || !me.hasData) { return const CircularProgressIndicator(); } return HeartButton( - tooltip: toggler.isLiked + tooltip: isLiked ? context.l10n.remove_from_favorites : context.l10n.save_as_favorite, - isLiked: toggler.isLiked, + isLiked: isLiked, onPressed: savedTracks.hasData ? () { - toggler.toggleTrackLike.mutate(toggler.isLiked); + toggleTrackLike.mutate(isLiked); } : null, ); diff --git a/lib/hooks/use_disable_battery_optimizations.dart b/lib/hooks/use_disable_battery_optimizations.dart index cf1ad0c1..267655b6 100644 --- a/lib/hooks/use_disable_battery_optimizations.dart +++ b/lib/hooks/use_disable_battery_optimizations.dart @@ -4,7 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/hooks/use_async_effect.dart'; bool _asked = false; -void useDisableBatterOptimizations() { +void useDisableBatteryOptimizations() { useAsyncEffect(() async { if (!DesktopTools.platform.isAndroid || _asked) return; final localStorage = await SharedPreferences.getInstance(); diff --git a/lib/main.dart b/lib/main.dart index a7f37bad..ee6fdf17 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,7 @@ -import 'dart:io'; - -import 'package:args/args.dart'; import 'package:catcher/catcher.dart'; import 'package:device_preview/device_preview.dart'; import 'package:fl_query/fl_query.dart'; +import 'package:fl_query_devtools/fl_query_devtools.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,7 +12,6 @@ import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:media_kit/media_kit.dart'; import 'package:metadata_god/metadata_god.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/intents.dart'; @@ -26,6 +23,7 @@ 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/services/audio_player/audio_player.dart'; +import 'package:spotube/services/cli/cli.dart'; import 'package:spotube/services/connectivity_adapter.dart'; import 'package:spotube/themes/theme.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; @@ -37,41 +35,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; Future main(List rawArgs) async { - final parser = ArgParser(); - - parser.addFlag( - 'verbose', - abbr: 'v', - help: 'Verbose mode', - defaultsTo: !kReleaseMode, - callback: (verbose) { - if (verbose) { - logEnv['VERBOSE'] = 'true'; - logEnv['DEBUG'] = 'true'; - logEnv['ERROR'] = 'true'; - } - }, - ); - parser.addFlag( - "version", - help: "Print version and exit", - negatable: false, - ); - - parser.addFlag("help", abbr: "h", negatable: false); - - final arguments = parser.parse(rawArgs); - - if (arguments["help"] == true) { - print(parser.usage); - exit(0); - } - - if (arguments["version"] == true) { - final package = await PackageInfo.fromPlatform(); - print("Spotube v${package.version}"); - exit(0); - } + final arguments = await startCLI(rawArgs); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); @@ -215,7 +179,7 @@ class SpotubeState extends ConsumerState { }; }, []); - useDisableBatterOptimizations(); + useDisableBatteryOptimizations(); final lightTheme = useMemoized( () => theme(paletteColor ?? accentMaterialColor, Brightness.light), diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index baee0669..722fcb6d 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -55,7 +55,12 @@ class PlaylistView extends HookConsumerWidget { final mediaQuery = MediaQuery.of(context); final meSnapshot = useQueries.user.me(ref); - final tracksSnapshot = useQueries.playlist.tracksOfQuery(ref, playlist.id!); + 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!), diff --git a/lib/services/cli/cli.dart b/lib/services/cli/cli.dart new file mode 100644 index 00000000..61af710e --- /dev/null +++ b/lib/services/cli/cli.dart @@ -0,0 +1,46 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:flutter/foundation.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:spotube/models/logger.dart'; + +Future startCLI(List args) async { + final parser = ArgParser(); + + parser.addFlag( + 'verbose', + abbr: 'v', + help: 'Verbose mode', + defaultsTo: !kReleaseMode, + callback: (verbose) { + if (verbose) { + logEnv['VERBOSE'] = 'true'; + logEnv['DEBUG'] = 'true'; + logEnv['ERROR'] = 'true'; + } + }, + ); + parser.addFlag( + "version", + help: "Print version and exit", + negatable: false, + ); + + parser.addFlag("help", abbr: "h", negatable: false); + + final arguments = parser.parse(args); + + if (arguments["help"] == true) { + print(parser.usage); + exit(0); + } + + if (arguments["version"] == true) { + final package = await PackageInfo.fromPlatform(); + print("Spotube v${package.version}"); + exit(0); + } + + return arguments; +} diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index 70b9ebe7..f3a00eac 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -1,3 +1,6 @@ +import 'dart:io'; +import 'dart:math'; + import 'package:catcher/catcher.dart'; import 'package:fl_query/fl_query.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart'; @@ -10,6 +13,7 @@ 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/pages/library/playlist_generate/playlist_generate.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'; @@ -138,20 +142,51 @@ class PlaylistQueries { (lastPageData.items?.length ?? 0) < 10 || lastPageData.isLast ? null : lastPage + 1, - retryConfig: RetryConfig.withConstantDefaults( - maxRetries: 1, - retryDelay: const Duration(seconds: 5), + ref: ref, + ); + } + + Future> likedTracks( + SpotifyApi spotify, + WidgetRef ref, + ) 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 context = useContext(); + + return useSpotifyQuery, dynamic>( + "user-liked-tracks", + query, + jsonConfig: JsonConfig( + toJson: (tracks) => { + 'tracks': tracks.map((e) => e.toJson()).toList(), + }, + fromJson: (json) => (json['tracks'] as List) + .map( + (e) => Track.fromJson((e as Map).castKeyDeep()), + ) + .toList(), + ), + refreshConfig: RefreshConfig.withDefaults( + context, + // will never make it stale + staleDuration: const Duration(days: 60), ), ref: ref, ); } - Future> tracksOf(String playlistId, SpotifyApi spotify) { - if (playlistId == "user-liked-tracks") { - return spotify.tracks.me.saved.all().then( - (tracks) => tracks.map((e) => e.track!).toList(), - ); - } + Future> tracksOf( + String playlistId, + SpotifyApi spotify, + WidgetRef ref, + ) async { + if (playlistId == "user-liked-tracks") return []; return spotify.playlists.getTracksByPlaylistId(playlistId).all().then( (value) => value.toList(), ); @@ -163,19 +198,7 @@ class PlaylistQueries { ) { return useSpotifyQuery, dynamic>( "playlist-tracks/$playlistId", - (spotify) => tracksOf(playlistId, spotify), - jsonConfig: playlistId == "user-liked-tracks" - ? JsonConfig( - toJson: (tracks) => { - 'tracks': tracks.map((e) => e.toJson()).toList() - }, - fromJson: (json) => (json['tracks'] as List) - .map((e) => Track.fromJson( - (e as Map).castKeyDeep(), - )) - .toList(), - ) - : null, + (spotify) => tracksOf(playlistId, spotify, ref), ref: ref, ); } diff --git a/lib/services/queries/user.dart b/lib/services/queries/user.dart index 63d58afd..89792592 100644 --- a/lib/services/queries/user.dart +++ b/lib/services/queries/user.dart @@ -1,4 +1,5 @@ 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'; @@ -8,6 +9,8 @@ import 'package:spotube/utils/type_conversion_utils.dart'; class UserQueries { const UserQueries(); Query me(WidgetRef ref) { + final context = useContext(); + return useSpotifyQuery( "current-user", (spotify) async { @@ -26,6 +29,11 @@ class UserQueries { } return me; }, + refreshConfig: RefreshConfig.withDefaults( + context, + // will never make it stale + staleDuration: const Duration(days: 60), + ), ref: ref, ); } diff --git a/pubspec.lock b/pubspec.lock index 21c864b6..e596944e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -318,10 +318,10 @@ packages: dependency: "direct main" description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" color: dependency: transitive description: @@ -522,6 +522,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0-alpha.3" + fl_query_devtools: + dependency: "direct main" + description: + name: fl_query_devtools + sha256: f46148364d7fc49fb02ab2d3b2c280e6652edd3984e9fdf14c1b49d4d8473907 + url: "https://pub.dev" + source: hosted + version: "0.1.0-alpha.1" fl_query_hooks: dependency: "direct main" description: @@ -954,10 +962,10 @@ packages: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" introduction_screen: dependency: "direct main" description: @@ -998,6 +1006,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.2" + json_view: + dependency: transitive + description: + name: json_view + sha256: "905c69f9e69d1eab5406b87ab6c10c3706c04c70c6a4959621bd2b43c2d27374" + url: "https://pub.dev" + source: hosted + version: "0.4.2" jwt_decode: dependency: transitive description: @@ -1050,18 +1066,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" media_kit: dependency: "direct main" description: @@ -1610,10 +1626,10 @@ packages: dependency: "direct main" description: name: skeleton_text - sha256: "6e088723b97ddcccfcce45312ce5e385ed1e5139a57afdf574f753d51eaa77f1" + sha256: bacd536bf0664efe1cae53bcbd78c3d4040a120f300f69dc85d83f358471cc6c url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" sky_engine: dependency: transitive description: flutter @@ -1647,10 +1663,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" spotify: dependency: "direct main" description: @@ -1791,10 +1807,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" time: dependency: transitive description: @@ -1967,10 +1983,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6deed8ed625c52864792459709183da231ebf66ff0cf09e69b573227c377efe + sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "11.7.1" watcher: dependency: transitive description: @@ -1979,6 +1995,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -2069,5 +2093,5 @@ packages: source: hosted version: "2.0.1" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1055f4c1..78fc2829 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: file_picker: ^5.2.2 fl_query: ^1.0.0-alpha.3 fl_query_hooks: ^1.0.0-alpha.3 + fl_query_devtools: ^0.1.0-alpha.1 fluentui_system_icons: ^1.1.189 flutter: sdk: flutter @@ -81,7 +82,7 @@ dependencies: scroll_to_index: ^3.0.1 shared_preferences: ^2.0.11 sidebarx: ^0.15.0 - skeleton_text: ^3.0.0 + skeleton_text: ^3.0.1 smtc_windows: ^0.1.0 spotify: ^0.11.0 supabase: ^1.9.9 From d3e1cef8a21ef7d64e74ca4e99b4b57b653b60a7 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 9 Sep 2023 17:37:24 +0600 Subject: [PATCH 05/14] fix: liked tracks card play not working --- lib/components/playlist/playlist_card.dart | 17 ++++++++++----- lib/themes/theme.dart | 1 + pubspec.lock | 24 +++++++++++----------- pubspec.yaml | 7 +++---- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index edb374c8..3fb30952 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -60,11 +60,18 @@ class PlaylistCard extends HookConsumerWidget { return audioPlayer.resume(); } - List fetchedTracks = await queryBowl.fetchQuery( - "playlist-tracks/${playlist.id}", - () => useQueries.playlist.tracksOf(playlist.id!, spotify, ref), - ) ?? - []; + 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), + ) ?? + []; if (fetchedTracks.isEmpty) return; diff --git a/lib/themes/theme.dart b/lib/themes/theme.dart index 7f107d56..e11f0cc2 100644 --- a/lib/themes/theme.dart +++ b/lib/themes/theme.dart @@ -51,6 +51,7 @@ ThemeData theme(Color seed, Brightness brightness) { sliderTheme: SliderThemeData(overlayShape: SliderComponentShape.noOverlay), searchBarTheme: SearchBarThemeData( constraints: const BoxConstraints(maxWidth: double.infinity), + padding: const MaterialStatePropertyAll(EdgeInsets.all(8)), backgroundColor: MaterialStatePropertyAll( Color.lerp( scheme.surfaceVariant, diff --git a/pubspec.lock b/pubspec.lock index e596944e..9d903a25 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -518,26 +518,26 @@ packages: dependency: "direct main" description: name: fl_query - sha256: "64f482fc09eb1166adca232f68772b2b11c616d88bce3208b2753c940ebc9f71" + sha256: "3d71cd1eeb3232efa5e32363a351d74fd9ff07c6eb80aeb672b1970962764945" url: "https://pub.dev" source: hosted - version: "1.0.0-alpha.3" + version: "1.0.0-alpha.4" fl_query_devtools: dependency: "direct main" description: name: fl_query_devtools - sha256: f46148364d7fc49fb02ab2d3b2c280e6652edd3984e9fdf14c1b49d4d8473907 + sha256: "72fac45293902b9f99c726609cd5416573566cce0b7c6e27311efde7fdf1b8b1" url: "https://pub.dev" source: hosted - version: "0.1.0-alpha.1" + version: "0.1.0-alpha.2" fl_query_hooks: dependency: "direct main" description: name: fl_query_hooks - sha256: b0ffc81fb047cbcedd9766776f9c72b95382730ce173226f0695c3f45774b0bc + sha256: "7f0880696666714f77981777509a8aedb765857dcdbdde23e623da20a24c4ae0" url: "https://pub.dev" source: hosted - version: "1.0.0-alpha.3" + version: "1.0.0-alpha.4+1" fluentui_system_icons: dependency: "direct main" description: @@ -702,10 +702,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "0c997763ce06359ee4686553b74def84062e9d6929ac63f61fa02465c1f8e32c" + sha256: "1bd39b04f1bcd217a969589777ca6bd642d116e3e5de65c3e6a8e8bdd8b178ec" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.4.0" flutter_rust_bridge: dependency: transitive description: @@ -901,10 +901,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "71695b2e1dfc22a39f1f9c67b798f8f8f1521f2d0349817d13ccdd5c4cd7acba" + sha256: ad7b877c3687e38764633d221a1f65491bc7a540e724101e9a404a84db2a4276 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.4.0" html: dependency: "direct main" description: @@ -1490,10 +1490,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "0f43c64f1f79c2112c843305a879a746587fb7c1e388f1d4717737796756e2c4" + sha256: a600120d6f213a9922860eea1abc32597436edd5b2c4e73b91410f8c2af67d22 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.4.0" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 78fc2829..a7d43bf1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,9 +34,9 @@ dependencies: duration: ^3.0.12 envied: ^0.3.0 file_picker: ^5.2.2 - fl_query: ^1.0.0-alpha.3 - fl_query_hooks: ^1.0.0-alpha.3 - fl_query_devtools: ^0.1.0-alpha.1 + fl_query: ^1.0.0-alpha.4 + fl_query_hooks: ^1.0.0-alpha.4+1 + fl_query_devtools: ^0.1.0-alpha.2 fluentui_system_icons: ^1.1.189 flutter: sdk: flutter @@ -119,7 +119,6 @@ dev_dependencies: dependency_overrides: http: ^1.1.0 - flutter_hooks: ^0.20.0 flutter: generate: true From e3217436c9985b86c68dab93ea65ee414b32fb49 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 10 Sep 2023 11:47:03 +0600 Subject: [PATCH 06/14] fix: rewind breaks track progress bar (#695) --- lib/hooks/use_progress.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/hooks/use_progress.dart b/lib/hooks/use_progress.dart index 62dccbce..15a979af 100644 --- a/lib/hooks/use_progress.dart +++ b/lib/hooks/use_progress.dart @@ -40,14 +40,14 @@ import 'package:spotube/services/audio_player/audio_player.dart'; } }); + var lastPosition = position.value; + // audioPlayer.positionStream is fired every 200ms and only 1s delay is // enough. Thus only update the position if the difference is more than 1s // Reduces CPU usage - var lastPosition = position.value; - final positionSubscription = audioPlayer.positionStream.listen((event) { - if (event.inMilliseconds > 1000 && - event.inMilliseconds - lastPosition.inMilliseconds < 1000) return; + final diff = event.inMilliseconds - lastPosition.inMilliseconds; + if (event.inMilliseconds > 1000 && diff < 1000 && diff > 0) return; lastPosition = event; position.value = event; From 0df8d9cacee718fbb4cf3ec7b950b489630f3145 Mon Sep 17 00:00:00 2001 From: Victor Tinoco <44973221+victj99@users.noreply.github.com> Date: Sun, 10 Sep 2023 05:39:05 -0500 Subject: [PATCH 07/14] fix: playlist grey page (#707) * chore: fill missing translations * chore: vscode filenesting * fix playlist grey page --------- Co-authored-by: Kingkor Roy Tirtho --- lib/services/queries/playlist.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index f3a00eac..ea53c4b0 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -188,7 +188,7 @@ class PlaylistQueries { ) async { if (playlistId == "user-liked-tracks") return []; return spotify.playlists.getTracksByPlaylistId(playlistId).all().then( - (value) => value.toList(), + (value) => value.where((track) => track.id != null).toList(), ); } From ab0fe5bdfa0fe9a9ea7810080bbc8fc7f087075b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 10 Sep 2023 16:39:21 +0600 Subject: [PATCH 08/14] Playlist info editing (#708) * feat: playlist metadata edit support * refactor: replace file_picker with file_selector --- ios/Runner/Info.plist | 116 +++---- lib/collections/spotube_icons.dart | 2 + lib/components/playlist/playlist_card.dart | 2 + .../playlist/playlist_create_dialog.dart | 297 +++++++++++++----- lib/components/shared/heart_button.dart | 6 + lib/components/shared/playbutton_card.dart | 39 +++ .../track_collection_view.dart | 13 + lib/generated_plugin_registrant.dart | 2 - lib/l10n/app_en.arb | 2 + lib/pages/playlist/playlist.dart | 76 +++-- lib/pages/settings/settings.dart | 7 +- lib/services/mutations/playlist.dart | 111 ++++++- lib/services/queries/playlist.dart | 10 + linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Runner/DebugProfile.entitlements | 3 + macos/Runner/Info.plist | 72 ++--- macos/Runner/Release.entitlements | 3 + pubspec.lock | 144 ++++++++- pubspec.yaml | 4 +- untranslated_messages.json | 57 +++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 24 files changed, 750 insertions(+), 227 deletions(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 59fc0f08..1f0a5e62 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,58 +1,64 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Sptube - 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 - - - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Sptube + 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 + + \ No newline at end of file diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 5503ebb3..7b5221b5 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -94,4 +94,6 @@ abstract class SpotubeIcons { static const noWifi = FeatherIcons.wifiOff; static const wifi = FeatherIcons.wifi; static const window = Icons.window_rounded; + static const user = FeatherIcons.user; + static const edit = FeatherIcons.edit; } diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index 3fb30952..0438e559 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -32,6 +32,7 @@ class PlaylistCard extends HookConsumerWidget { final updating = useState(false); final spotify = ref.watch(spotifyProvider); + final me = useQueries.user.me(ref); return PlaybuttonCard( margin: const EdgeInsets.symmetric(horizontal: 10), @@ -44,6 +45,7 @@ class PlaylistCard extends HookConsumerWidget { isPlaying: isPlaylistPlaying, isLoading: (isPlaylistPlaying && playlistQueue.isFetching) || updating.value, + isOwner: playlist.owner?.id == me.data?.id && me.data?.id != null, onTap: () { ServiceUtils.push( context, diff --git a/lib/components/playlist/playlist_create_dialog.dart b/lib/components/playlist/playlist_create_dialog.dart index b7cee79d..fa960210 100644 --- a/lib/components/playlist/playlist_create_dialog.dart +++ b/lib/components/playlist/playlist_create_dialog.dart @@ -1,106 +1,228 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; +import 'dart:convert'; + +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:form_validator/form_validator.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.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/provider/spotify_provider.dart'; +import 'package:spotube/services/mutations/mutations.dart'; +import 'package:spotube/services/mutations/playlist.dart'; +import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; class PlaylistCreateDialog extends HookConsumerWidget { /// Track ids to add to the playlist final List trackIds; - const PlaylistCreateDialog({ + final String? playlistId; + PlaylistCreateDialog({ Key? key, this.trackIds = const [], + this.playlistId, }) : super(key: key); + final formKey = GlobalKey(); + @override Widget build(BuildContext context, ref) { - final spotify = ref.watch(spotifyProvider); - final playlistName = useTextEditingController(); - final description = useTextEditingController(); - final public = useState(false); - final collaborative = useState(false); - final client = useQueryClient(); - final navigator = Navigator.of(context); + return ScaffoldMessenger( + child: Scaffold( + backgroundColor: Colors.transparent, + body: HookBuilder(builder: (context) { + final userPlaylists = useQueries.playlist.ofMine(ref); + final updatingPlaylist = useMemoized( + () => userPlaylists.pages + .expand((p) => p.items ?? []) + .firstWhereOrNull((playlist) => playlist.id == playlistId), + [ + userPlaylists.pages, + playlistId, + ], + ); - Future onCreate() async { - if (playlistName.text.isEmpty) return; - final me = await spotify.me.get(); - final playlist = await spotify.playlists.createPlaylist( - me.id!, - playlistName.text, - collaborative: collaborative.value, - public: public.value, - description: description.text, - ); - if (trackIds.isNotEmpty) { - await spotify.playlists.addTracks( - trackIds.map((id) => "spotify:track:$id").toList(), - playlist.id!, - ); - } - await client - .getQuery( - "current-user-playlists", - ) - ?.refresh(); - navigator.pop(playlist); - } + final playlistName = useTextEditingController( + text: updatingPlaylist?.name, + ); + final description = useTextEditingController( + text: updatingPlaylist?.description, + ); + final public = useState( + updatingPlaylist?.public ?? false, + ); + final collaborative = useState( + updatingPlaylist?.collaborative ?? false, + ); + final image = useState(null); - return AlertDialog( - title: Text(context.l10n.create_a_playlist), - actions: [ - OutlinedButton( - child: Text(context.l10n.cancel), - onPressed: () { - Navigator.pop(context); - }, - ), - FilledButton( - onPressed: onCreate, - child: Text(context.l10n.create), - ), - ], - content: Container( - width: MediaQuery.of(context).size.width, - constraints: const BoxConstraints(maxWidth: 500), - child: ListView( - shrinkWrap: true, - children: [ - TextField( - controller: playlistName, - decoration: InputDecoration( - hintText: context.l10n.name_of_playlist, - labelText: context.l10n.name_of_playlist, + final isUpdatingPlaylist = playlistId != null; + + final l10n = context.l10n; + final theme = Theme.of(context); + final scaffold = ScaffoldMessenger.of(context); + + final onError = useCallback((error) { + if (error is SpotifyError || error is SpotifyException) { + scaffold.showSnackBar( + SnackBar( + content: Text( + l10n.error(error.message ?? "Epic failure!"), + style: theme.textTheme.bodyMedium!.copyWith( + color: theme.colorScheme.onError, + ), + ), + backgroundColor: theme.colorScheme.error, + ), + ); + } + }, [scaffold, l10n, theme]); + + final playlistCreateMutation = useMutations.playlist.create( + ref, + trackIds: trackIds, + onData: (value) { + Navigator.pop(context); + }, + onError: onError, + ); + + final playlistUpdateMutation = useMutations.playlist.update( + ref, + playlistId: playlistId, + onData: (value) { + Navigator.pop(context); + }, + onError: onError, + ); + + Future onCreate() async { + if (!formKey.currentState!.validate()) return; + + final PlaylistCRUDVariables payload = ( + playlistName: playlistName.text, + collaborative: collaborative.value, + public: public.value, + description: description.text, + base64Image: image.value?.path != null + ? await image.value! + .readAsBytes() + .then((bytes) => base64Encode(bytes)) + : null, + ); + + if (isUpdatingPlaylist) { + await playlistUpdateMutation.mutate(payload); + } else { + await playlistCreateMutation.mutate(payload); + } + } + + return AlertDialog( + title: Text( + isUpdatingPlaylist + ? context.l10n.update_playlist + : context.l10n.create_a_playlist, + ), + actions: [ + OutlinedButton( + child: Text(context.l10n.cancel), + onPressed: () { + Navigator.pop(context); + }, + ), + FilledButton( + onPressed: onCreate, + child: Text( + isUpdatingPlaylist + ? context.l10n.update + : context.l10n.create, + ), + ), + ], + insetPadding: const EdgeInsets.all(8), + content: Container( + width: MediaQuery.of(context).size.width, + constraints: const BoxConstraints(maxWidth: 500), + child: Form( + key: formKey, + child: ListView( + shrinkWrap: true, + children: [ + Center( + child: Stack( + children: [ + UniversalImage( + path: image.value?.path ?? + TypeConversionUtils.image_X_UrlString( + updatingPlaylist?.images, + placeholder: ImagePlaceholder.collection, + ), + height: 200, + ), + Positioned( + bottom: 20, + right: 20, + child: IconButton.filled( + icon: const Icon(SpotubeIcons.edit), + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.primary, + elevation: 2, + shadowColor: theme.colorScheme.onSurface, + ), + onPressed: () async { + final imageFile = await ImagePicker() + .pickImage(source: ImageSource.gallery); + + image.value = imageFile ?? image.value; + }, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + TextFormField( + controller: playlistName, + decoration: InputDecoration( + hintText: context.l10n.name_of_playlist, + labelText: context.l10n.name_of_playlist, + ), + validator: ValidationBuilder().required().build(), + ), + const SizedBox(height: 10), + TextFormField( + controller: description, + decoration: InputDecoration( + hintText: context.l10n.description, + ), + keyboardType: TextInputType.multiline, + maxLines: 5, + ), + const SizedBox(height: 10), + CheckboxListTile( + title: Text(context.l10n.public), + value: public.value, + onChanged: (val) => public.value = val ?? false, + ), + const SizedBox(height: 10), + CheckboxListTile( + title: Text(context.l10n.collaborative), + value: collaborative.value, + onChanged: (val) => collaborative.value = val ?? false, + ), + ], + ), ), ), - const SizedBox(height: 10), - TextField( - controller: description, - decoration: InputDecoration( - hintText: context.l10n.description, - ), - keyboardType: TextInputType.multiline, - maxLines: 5, - ), - const SizedBox(height: 10), - CheckboxListTile( - title: Text(context.l10n.public), - value: public.value, - onChanged: (val) => public.value = val ?? false, - ), - const SizedBox(height: 10), - CheckboxListTile( - title: Text(context.l10n.collaborative), - value: collaborative.value, - onChanged: (val) => collaborative.value = val ?? false, - ), - ], - ), + ); + }), ), ); } @@ -112,7 +234,7 @@ class PlaylistCreateDialogButton extends HookConsumerWidget { showPlaylistDialog(BuildContext context, SpotifyApi spotify) { showDialog( context: context, - builder: (context) => const PlaylistCreateDialog(), + builder: (context) => PlaylistCreateDialog(), ); } @@ -132,11 +254,12 @@ class PlaylistCreateDialogButton extends HookConsumerWidget { } return FilledButton.tonalIcon( - style: FilledButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.primary, - ), - icon: const Icon(SpotubeIcons.addFilled), - label: Text(context.l10n.create_playlist), - onPressed: () => showPlaylistDialog(context, spotify)); + style: FilledButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.primary, + ), + icon: const Icon(SpotubeIcons.addFilled), + label: Text(context.l10n.create_playlist), + onPressed: () => showPlaylistDialog(context, spotify), + ); } } diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index c18ca2e4..4a23cc48 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -159,10 +159,14 @@ class TrackHeartButton extends HookConsumerWidget { class PlaylistHeartButton extends HookConsumerWidget { final PlaylistSimple playlist; + final IconData? icon; + final ValueChanged? onData; const PlaylistHeartButton({ required this.playlist, Key? key, + this.icon, + this.onData, }) : super(key: key); @override @@ -181,6 +185,7 @@ class PlaylistHeartButton extends HookConsumerWidget { refreshQueries: [ isLikedQuery.key, ], + onData: onData, ); if (me.isLoading || !me.hasData) { @@ -193,6 +198,7 @@ class PlaylistHeartButton extends HookConsumerWidget { ? context.l10n.remove_from_favorites : context.l10n.save_as_favorite, color: Colors.white, + icon: icon, onPressed: isLikedQuery.hasData ? () { togglePlaylistLike.mutate(isLikedQuery.data!); diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index 86c3f046..c9daa267 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.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/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_brightness_value.dart'; @@ -28,6 +29,7 @@ class PlaybuttonCard extends HookWidget { final bool isPlaying; final bool isLoading; final String title; + final bool isOwner; const PlaybuttonCard({ required this.imageUrl, @@ -39,6 +41,7 @@ class PlaybuttonCard extends HookWidget { this.onPlaybuttonPressed, this.onAddToQueuePressed, this.onTap, + this.isOwner = false, Key? key, }) : super(key: key); @@ -153,6 +156,42 @@ class PlaybuttonCard extends HookWidget { ), ), ), + 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), + ), + 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, + ), + ), + ], + ), + ); + }), + ), + ), AnimatedPositioned( duration: const Duration(milliseconds: 300), right: end, 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 b4a1314e..1c87d887 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 @@ -4,6 +4,7 @@ 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/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'; @@ -71,6 +72,18 @@ class TrackCollectionView extends HookConsumerWidget { 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 diff --git a/lib/generated_plugin_registrant.dart b/lib/generated_plugin_registrant.dart index dc0cbb0b..a25a1f5f 100644 --- a/lib/generated_plugin_registrant.dart +++ b/lib/generated_plugin_registrant.dart @@ -8,7 +8,6 @@ import 'package:audio_service_web/audio_service_web.dart'; import 'package:audio_session/audio_session_web.dart'; -import 'package:file_picker/_internal/file_picker_web.dart'; import 'package:shared_preferences_web/shared_preferences_web.dart'; import 'package:url_launcher_web/url_launcher_web.dart'; @@ -18,7 +17,6 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart'; void registerPlugins(Registrar registrar) { AudioServiceWeb.registerWith(registrar); AudioSessionWeb.registerWith(registrar); - FilePickerWeb.registerWith(registrar); SharedPreferencesPlugin.registerWith(registrar); UrlLauncherPlugin.registerWith(registrar); registrar.registerMessageHandler(); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 74894612..bdfb7983 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -24,8 +24,10 @@ "liked_tracks_description": "All your liked tracks", "create_playlist": "Create Playlist", "create_a_playlist": "Create a playlist", + "update_playlist": "Update playlist", "create": "Create", "cancel": "Cancel", + "update": "update", "playlist_name": "Playlist Name", "name_of_playlist": "Name of the playlist", "description": "Description", diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 722fcb6d..9c852ace 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -1,6 +1,8 @@ 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: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'; @@ -18,34 +20,8 @@ import 'package:spotube/utils/type_conversion_utils.dart'; class PlaylistView extends HookConsumerWidget { final logger = getLogger(PlaylistView); - final PlaylistSimple playlist; - PlaylistView(this.playlist, {Key? key}) : super(key: key); - - Future playPlaylist( - List tracks, - WidgetRef ref, { - Track? currentTrack, - }) async { - final proxyPlaylist = ref.read(ProxyPlaylistNotifier.provider); - 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); - } - } + final PlaylistSimple playlistSimple; + PlaylistView(this.playlistSimple, {Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { @@ -55,6 +31,10 @@ class PlaylistView extends HookConsumerWidget { 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); @@ -83,6 +63,35 @@ class PlaylistView extends HookConsumerWidget { [proxyPlaylist.activeTrack, tracksSnapshot.data], ); + 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 ownPlaylist = + playlist.owner?.id != null && playlist.owner?.id == meSnapshot.data?.id; + return TrackCollectionView( id: playlist.id!, playingState: isPlaylistPlaying && playlistTrackPlaying @@ -94,8 +103,7 @@ class PlaylistView extends HookConsumerWidget { titleImage: titleImage, tracksSnapshot: tracksSnapshot, description: playlist.description, - isOwned: playlist.owner?.id != null && - playlist.owner!.id == meSnapshot.data?.id, + isOwned: ownPlaylist, onPlay: ([track]) { if (tracksSnapshot.hasData) { if (!isPlaylistPlaying) { @@ -142,7 +150,13 @@ class PlaylistView extends HookConsumerWidget { ); }); }, - heartBtn: PlaylistHeartButton(playlist: playlist), + heartBtn: PlaylistHeartButton( + playlist: playlist, + icon: ownPlaylist ? SpotubeIcons.trash : null, + onData: (data) { + GoRouter.of(context).pop(); + }, + ), onShuffledPlay: ([track]) { final tracks = [...?tracksSnapshot.data]..shuffle(); diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index d4e1e9db..a6efdc8f 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -1,6 +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'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; @@ -11,7 +11,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:piped_client/piped_client.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'; @@ -47,8 +46,8 @@ class SettingsPage extends HookConsumerWidget { }, []); final pickDownloadLocation = useCallback(() async { - final dirStr = await FilePicker.platform.getDirectoryPath( - dialogTitle: context.l10n.download_location, + final dirStr = await getDirectoryPath( + initialDirectory: preferences.downloadLocation, ); if (dirStr == null) return; preferences.setDownloadLocation(dirStr); diff --git a/lib/services/mutations/playlist.dart b/lib/services/mutations/playlist.dart index ee06ad9d..176b5cd8 100644 --- a/lib/services/mutations/playlist.dart +++ b/lib/services/mutations/playlist.dart @@ -1,7 +1,17 @@ import 'package:fl_query/fl_query.dart'; -import 'package:fl_query_hooks/fl_query_hooks.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/services/queries/queries.dart'; + +typedef PlaylistCRUDVariables = ({ + String playlistName, + bool? public, + bool? collaborative, + String? description, + String? base64Image, +}); class PlaylistMutations { const PlaylistMutations(); @@ -11,8 +21,8 @@ class PlaylistMutations { String playlistId, { List? refreshQueries, List? refreshInfiniteQueries, + ValueChanged? onData, }) { - final queryClient = useQueryClient(); return useSpotifyMutation( "toggle-playlist-like/$playlistId", (isLiked, spotify) async { @@ -25,10 +35,12 @@ class PlaylistMutations { }, ref: ref, refreshQueries: refreshQueries, - refreshInfiniteQueries: refreshInfiniteQueries, - onData: (data, recoveryData) async { - await queryClient - .refreshInfiniteQueryAllPages("current-user-playlists"); + refreshInfiniteQueries: [ + ...?refreshInfiniteQueries, + "current-user-playlists", + ], + onData: (data, recoveryData) { + onData?.call(data); }, ); } @@ -47,4 +59,91 @@ class PlaylistMutations { refreshQueries: ["playlist-tracks/$playlistId"], ); } + + Mutation create( + WidgetRef ref, { + List? trackIds, + ValueChanged? onError, + ValueChanged? onData, + }) { + final me = useQueries.user.me(ref); + return useSpotifyMutation( + "create-playlist", + (variable, spotify) async { + final playlist = await spotify.playlists.createPlaylist( + me.data!.id!, + variable.playlistName, + collaborative: variable.collaborative, + description: variable.description, + public: variable.public, + ); + + if (variable.base64Image != null) { + await spotify.playlists.updatePlaylistImage( + playlist.id!, + variable.base64Image!, + ); + } + + if (trackIds != null && trackIds.isNotEmpty) { + await spotify.playlists.addTracks( + trackIds.map((id) => "spotify:track:$id").toList(), + playlist.id!, + ); + } + + return playlist; + }, + refreshInfiniteQueries: [ + "current-user-playlists", + ], + ref: ref, + onError: (error, recoveryData) { + onError?.call(error); + }, + onData: (data, recoveryData) { + onData?.call(data); + }, + ); + } + + Mutation update( + WidgetRef ref, { + String? playlistId, + ValueChanged? onError, + ValueChanged? onData, + }) { + return useSpotifyMutation( + "update-playlist/$playlistId", + (variable, spotify) async { + if (playlistId == null) return; + await spotify.playlists.updatePlaylist( + playlistId, + variable.playlistName, + collaborative: variable.collaborative, + description: variable.description, + public: variable.public, + ); + if (variable.base64Image != null) { + await spotify.playlists.updatePlaylistImage( + playlistId, + variable.base64Image!, + ); + } + }, + refreshQueries: [ + "playlist/$playlistId", + ], + refreshInfiniteQueries: [ + "current-user-playlists", + ], + ref: ref, + onError: (error, recoveryData) { + onError?.call(error); + }, + onData: (data, recoveryData) { + onData?.call(data); + }, + ); + } } diff --git a/lib/services/queries/playlist.dart b/lib/services/queries/playlist.dart index ea53c4b0..0204f9b7 100644 --- a/lib/services/queries/playlist.dart +++ b/lib/services/queries/playlist.dart @@ -203,6 +203,16 @@ class PlaylistQueries { ); } + Query byId(WidgetRef ref, String id) { + return useSpotifyQuery( + "playlist/$id", + (spotify) async { + return await spotify.playlists.get(id); + }, + ref: ref, + ); + } + InfiniteQuery, dynamic, int> featured( WidgetRef ref, ) { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index eef22b2f..9f8f2fd3 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) catcher_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "CatcherPlugin"); catcher_plugin_register_with_registrar(catcher_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); 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); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1d7de67a..c7f0e848 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST catcher + file_selector_linux flutter_secure_storage_linux local_notifier media_kit_libs_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3f63519c..1010a6c4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import audio_service import audio_session import catcher import device_info_plus +import file_selector_macos import flutter_secure_storage_macos import local_notifier import media_kit_libs_macos_audio @@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) CatcherPlugin.register(with: registry.registrar(forPlugin: "CatcherPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin")) diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index 6fc950ef..0aed55d5 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -19,5 +19,8 @@ + + com.apple.security.files.user-selected.read-write + \ No newline at end of file diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 9728552b..19f1c02a 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -1,39 +1,39 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSAllowsArbitraryLoadsForMedia - - - - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsForMedia + + + + \ No newline at end of file diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 4a447fde..dc5df580 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -17,5 +17,8 @@ + + com.apple.security.files.user-selected.read-write + \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 9d903a25..94eccfdd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -338,6 +338,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb + url: "https://pub.dev" + source: hosted + version: "0.3.3+5" crypto: dependency: transitive description: @@ -498,14 +506,70 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" - file_picker: + file_selector: dependency: "direct main" description: - name: file_picker - sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877 + name: file_selector + sha256: "84eaf3e034d647859167d1f01cfe7b6352488f34c1b4932635012b202014c25b" url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "1.0.1" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: d41e165d6f798ca941d536e5dc93494d50e78c571c28ad60cfe0b0fefeb9f1e7 + url: "https://pub.dev" + source: hosted + version: "0.5.0+3" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: b3fbdda64aa2e335df6e111f6b0f1bb968402ed81d2dd1fa4274267999aa32c2 + url: "https://pub.dev" + source: hosted + version: "0.5.1+6" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "182c3f8350cee659f7b115e956047ee3dc672a96665883a545e81581b9a82c72" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: dc6622c4d66cb1bee623ddcc029036603c6cc45c85e4a775bb06008d61c809c1 + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" fixnum: dependency: transitive description: @@ -788,6 +852,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.3" + form_validator: + dependency: "direct main" + description: + name: form_validator + sha256: "8cbe91b7d5260870d6fb9e23acd55d5d1d1fdf2397f0279a4931ac3c0c7bf8fb" + url: "https://pub.dev" + source: hosted + version: "2.1.1" freezed_annotation: dependency: transitive description: @@ -945,6 +1017,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.17" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d32a997bcc4ee135aebca8e272b7c517927aa65a74b9c60a81a2764ef1a0462d + url: "https://pub.dev" + source: hosted + version: "0.8.7+5" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497 + url: "https://pub.dev" + source: hosted + version: "0.8.8+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 + url: "https://pub.dev" + source: hosted + version: "2.9.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" integration_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index a7d43bf1..c1b427cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,6 @@ dependencies: disable_battery_optimization: ^1.1.0+1 duration: ^3.0.12 envied: ^0.3.0 - file_picker: ^5.2.2 fl_query: ^1.0.0-alpha.4 fl_query_hooks: ^1.0.0-alpha.4+1 fl_query_devtools: ^0.1.0-alpha.2 @@ -55,6 +54,7 @@ dependencies: flutter_riverpod: ^2.1.1 flutter_secure_storage: ^8.0.0 flutter_svg: ^1.1.6 + form_validator: ^2.1.1 fuzzywuzzy: ^0.2.0 google_fonts: ^5.1.0 go_router: ^10.0.0 @@ -100,6 +100,8 @@ dependencies: path: plugins/window_size youtube_explode_dart: ^2.0.1 stroke_text: ^0.0.2 + image_picker: ^1.0.4 + file_selector: ^1.0.1 dev_dependencies: build_runner: ^2.3.2 diff --git a/untranslated_messages.json b/untranslated_messages.json index 9e26dfee..74a87896 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1 +1,56 @@ -{} \ No newline at end of file +{ + "bn": [ + "update_playlist", + "update" + ], + + "ca": [ + "update_playlist", + "update" + ], + + "de": [ + "update_playlist", + "update" + ], + + "es": [ + "update_playlist", + "update" + ], + + "fr": [ + "update_playlist", + "update" + ], + + "hi": [ + "update_playlist", + "update" + ], + + "ja": [ + "update_playlist", + "update" + ], + + "pl": [ + "update_playlist", + "update" + ], + + "pt": [ + "update_playlist", + "update" + ], + + "ru": [ + "update_playlist", + "update" + ], + + "zh": [ + "update_playlist", + "update" + ] +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b8983c9c..089930d3 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { CatcherPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("CatcherPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalNotifierPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e066d223..b2b08c8e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST catcher + file_selector_windows flutter_secure_storage_windows local_notifier media_kit_libs_windows_audio From b9d5c70301dd33ec26332e5e9a456ce5bfe73da0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 10 Sep 2023 18:19:47 +0600 Subject: [PATCH 09/14] feat: search loading animation --- lib/collections/spotube_icons.dart | 1 + .../track_collection_heading.dart | 22 +- lib/l10n/app_en.arb | 6 +- lib/pages/search/search.dart | 623 ++++++++++-------- untranslated_messages.json | 44 +- 5 files changed, 397 insertions(+), 299 deletions(-) diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 7b5221b5..4781050d 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -96,4 +96,5 @@ abstract class SpotubeIcons { static const window = Icons.window_rounded; static const user = FeatherIcons.user; static const edit = FeatherIcons.edit; + static const web = FeatherIcons.globe; } 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 index c82b8177..a8a60109 100644 --- 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 @@ -1,5 +1,6 @@ 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'; @@ -103,11 +104,19 @@ class TrackCollectionHeading extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, children: [ - Text( - title, - style: theme.textTheme.titleLarge!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w600, + 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) @@ -125,11 +134,12 @@ class TrackCollectionHeading extends HookConsumerWidget { constraints: BoxConstraints( maxWidth: constrains.mdAndDown ? 400 : 300, ), - child: Text( + child: AutoSizeText( cleanDescription, style: const TextStyle(color: Colors.white), maxLines: 2, overflow: TextOverflow.fade, + minFontSize: 14, ), ), const SizedBox(height: 10), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bdfb7983..fa81450b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -27,7 +27,7 @@ "update_playlist": "Update playlist", "create": "Create", "cancel": "Cancel", - "update": "update", + "update": "Update", "playlist_name": "Playlist Name", "name_of_playlist": "Name of the playlist", "description": "Description", @@ -261,5 +261,7 @@ "piped_down_error_instructions": "The Piped instance {pipedInstance} is currently down\n\nEither change the instance or change the 'API type' to official YouTube API\n\nMake sure to restart the app after change", "you_are_offline": "You are currently offline", "connection_restored": "Your internet connection was restored", - "use_system_title_bar": "Use system title bar" + "use_system_title_bar": "Use system title bar", + "crunching_results": "Crunching results...", + "search_to_get_results": "Search to get results" } \ No newline at end of file diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 9d5e7eed..7ceecd58 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -55,13 +55,295 @@ class SearchPage extends HookConsumerWidget { Future onSearch() async { await Future.wait([ - searchTrack.refreshAll(), - searchAlbum.refreshAll(), - searchPlaylist.refreshAll(), - searchArtist.refreshAll(), - ]); + searchTrack.reset(), + searchAlbum.reset(), + searchPlaylist.reset(), + searchArtist.reset(), + ]).then((_) { + return Future.wait([ + searchTrack.refreshAll(), + searchAlbum.refreshAll(), + searchPlaylist.refreshAll(), + searchArtist.refreshAll(), + ]); + }); } + final queries = [searchTrack, searchAlbum, searchPlaylist, searchArtist]; + final isFetching = queries.every( + (s) => s.isLoadingPage || s.isRefreshingPage || !s.hasPageData, + ) && + 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 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.isLoadingPage) + 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.isRefreshingPage + ? null + : () => searchTrack.fetchNext(), + child: searchTrack.isRefreshingPage + ? 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.isLoadingPage) + 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.isLoadingPage) + 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.isLoadingPage) + const CircularProgressIndicator(), + if (searchAlbum.hasPageError) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + searchAlbum.errors.lastOrNull?.toString() ?? "", + ), + ), + ], + ), + ), + ), + ); + }, + ); + return SafeArea( bottom: false, child: Scaffold( @@ -77,7 +359,7 @@ class SearchPage extends HookConsumerWidget { ), color: theme.scaffoldBackgroundColor, child: TextField( - autofocus: true, + autofocus: queries.none((s) => s.hasPageData), decoration: InputDecoration( prefixIcon: const Icon(SpotubeIcons.search), hintText: "${context.l10n.search}...", @@ -93,283 +375,64 @@ class SearchPage extends HookConsumerWidget { }, ), ), - 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 Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 20, - ), - child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (tracks.isNotEmpty) - Text( - context.l10n.songs, - style: theme.textTheme.titleLarge!, - ), - if (searchTrack.isLoadingPage) - 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.isRefreshingPage - ? null - : () => searchTrack.fetchNext(), - child: searchTrack.isRefreshingPage - ? const CircularProgressIndicator() - : Text(context.l10n.load_more), - ), - ), - if (playlists.isNotEmpty) - 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, - child: Row( - children: [ - ...playlists.mapIndexed( - (i, playlist) { - if (i == - playlists.length - - 1 && - searchPlaylist - .hasNextPage) { - return const ShimmerPlaybuttonCard( - count: 1); - } - return PlaylistCard(playlist); - }, - ), - ], - ), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: searchTerm.isEmpty + ? Column( + children: [ + SizedBox( + height: mediaQuery.size.height * 0.2, + ), + Icon( + SpotubeIcons.web, + size: 120, + color: theme.colorScheme.onBackground + .withOpacity(0.7), + ), + const SizedBox(height: 20), + Text( + context.l10n.search_to_get_results, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + color: theme.colorScheme.onBackground + .withOpacity(0.5), + ), + ), + ], + ) + : isFetching + ? Container( + constraints: BoxConstraints( + maxWidth: mediaQuery.lgAndUp + ? mediaQuery.size.width * 0.5 + : mediaQuery.size.width, + ), + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Text( + context.l10n.crunching_results, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: theme.colorScheme.onBackground + .withOpacity(0.7), ), ), - ), + const SizedBox(height: 20), + const LinearProgressIndicator(), + ], ), - if (searchPlaylist.isLoadingPage) - const CircularProgressIndicator(), - if (searchPlaylist.hasPageError) - Text( - searchPlaylist.errors.lastOrNull - ?.toString() ?? - "", - ), - const SizedBox(height: 20), - if (artists.isNotEmpty) - 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.isLoadingPage) - const CircularProgressIndicator(), - if (searchArtist.hasPageError) - Text( - searchArtist.errors.lastOrNull - ?.toString() ?? - "", - ), - const SizedBox(height: 20), - if (albums.isNotEmpty) - Text( - context.l10n.albums, - style: theme.textTheme.titleMedium!, - ), - 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.isLoadingPage) - const CircularProgressIndicator(), - if (searchAlbum.hasPageError) - Text( - searchAlbum.errors.lastOrNull - ?.toString() ?? - "", - ), - ], - ), - ), - ), - ), - ); - }, - ) + ) + : resultWidget, + ), + ), ], ), ), diff --git a/untranslated_messages.json b/untranslated_messages.json index 74a87896..ec30b430 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,56 +1,78 @@ { "bn": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "ca": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "de": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "es": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "fr": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "hi": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "ja": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "pl": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "pt": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "ru": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "zh": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ] } From 1c50612559a78dce9c108f7e7b816d1b84540fe4 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 10 Sep 2023 21:39:16 +0600 Subject: [PATCH 10/14] fix: limit cover image upload to allowed 256kb size --- .../playlist/playlist_create_dialog.dart | 110 +++++++++++++----- 1 file changed, 79 insertions(+), 31 deletions(-) diff --git a/lib/components/playlist/playlist_create_dialog.dart b/lib/components/playlist/playlist_create_dialog.dart index fa960210..53424914 100644 --- a/lib/components/playlist/playlist_create_dialog.dart +++ b/lib/components/playlist/playlist_create_dialog.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -154,39 +155,85 @@ class PlaylistCreateDialog extends HookConsumerWidget { child: ListView( shrinkWrap: true, children: [ - Center( - child: Stack( - children: [ - UniversalImage( - path: image.value?.path ?? - TypeConversionUtils.image_X_UrlString( - updatingPlaylist?.images, - placeholder: ImagePlaceholder.collection, - ), - height: 200, - ), - Positioned( - bottom: 20, - right: 20, - child: IconButton.filled( - icon: const Icon(SpotubeIcons.edit), - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.surface, - foregroundColor: theme.colorScheme.primary, - elevation: 2, - shadowColor: theme.colorScheme.onSurface, - ), - onPressed: () async { - final imageFile = await ImagePicker() - .pickImage(source: ImageSource.gallery); + FormField( + initialValue: image.value, + onSaved: (newValue) { + image.value = newValue; + }, + validator: (value) { + if (value == null) return null; + final file = File(value.path); - image.value = imageFile ?? image.value; - }, + if (file.lengthSync() > 256000) { + return "Image size should be less than 256kb"; + } + return null; + }, + builder: (field) { + return Center( + child: Stack( + children: [ + UniversalImage( + path: field.value?.path ?? + TypeConversionUtils.image_X_UrlString( + updatingPlaylist?.images, + placeholder: + ImagePlaceholder.collection, + ), + height: 200, + ), + Positioned( + bottom: 20, + right: 20, + child: IconButton.filled( + icon: const Icon(SpotubeIcons.edit), + style: IconButton.styleFrom( + backgroundColor: + theme.colorScheme.surface, + foregroundColor: + theme.colorScheme.primary, + elevation: 2, + shadowColor: theme.colorScheme.onSurface, + ), + onPressed: () async { + final imageFile = await ImagePicker() + .pickImage( + source: ImageSource.gallery); + + if (imageFile != null) { + field.didChange(imageFile); + field.validate(); + field.save(); + } + }, + ), + ), + if (field.hasError) + Positioned( + bottom: 20, + left: 20, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: theme.colorScheme.error, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + field.errorText ?? "", + style: theme.textTheme.bodyMedium! + .copyWith( + color: theme.colorScheme.onError, + ), + ), + ), + ), + ], ), - ), - ], - ), - ), + ); + }), const SizedBox(height: 10), TextFormField( controller: playlistName, @@ -203,6 +250,7 @@ class PlaylistCreateDialog extends HookConsumerWidget { hintText: context.l10n.description, ), keyboardType: TextInputType.multiline, + validator: ValidationBuilder().required().build(), maxLines: 5, ), const SizedBox(height: 10), From d4f99ec89927ea78f070707509ff3222ec402942 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 10 Sep 2023 22:28:38 +0600 Subject: [PATCH 11/14] feat(player_queue): filtering track support --- lib/components/player/player_overlay.dart | 18 +- lib/components/player/player_queue.dart | 276 +++++++++++++++------- 2 files changed, 207 insertions(+), 87 deletions(-) diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart index f4984ad2..889e6609 100644 --- a/lib/components/player/player_overlay.dart +++ b/lib/components/player/player_overlay.dart @@ -100,9 +100,13 @@ class PlayerOverlay extends HookConsumerWidget { child: GestureDetector( onTap: () => GoRouter.of(context).push("/player"), - child: PlayerTrackDetails( - albumArt: albumArt, - color: textColor, + child: Container( + width: double.infinity, + color: Colors.transparent, + child: PlayerTrackDetails( + albumArt: albumArt, + color: textColor, + ), ), ), ), @@ -114,7 +118,9 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipBack, color: textColor, ), - onPressed: playlistNotifier.previous, + onPressed: playlist.isFetching + ? null + : playlistNotifier.previous, ), Consumer( builder: (context, ref, _) { @@ -143,7 +149,9 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipForward, color: textColor, ), - onPressed: playlistNotifier.next, + onPressed: playlist.isFetching + ? null + : playlistNotifier.next, ), ], ), diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index 599da26e..a5dee8c9 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -1,16 +1,21 @@ import 'dart:ui'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/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/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; class PlayerQueue extends HookConsumerWidget { final bool floating; @@ -24,12 +29,11 @@ class PlayerQueue extends HookConsumerWidget { final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final controller = useAutoScrollController(); + final searchText = useState(''); + + final isSearching = useState(false); + final tracks = playlist.tracks; - - if (tracks.isEmpty) { - return const NotFound(vertical: true); - } - final borderRadius = floating ? BorderRadius.circular(10) : const BorderRadius.only( @@ -39,6 +43,27 @@ class PlayerQueue extends HookConsumerWidget { final theme = Theme.of(context); final headlineColor = theme.textTheme.headlineSmall?.color; + final filteredTracks = useMemoized( + () { + if (searchText.value.isEmpty) { + return tracks; + } + return tracks + .map((e) => ( + weightedRatio( + '${e.name!} - ${TypeConversionUtils.artists_X_String(e.artists!)}', + searchText.value, + ), + e + )) + .sorted((a, b) => b.$1.compareTo(a.$1)) + .where((e) => e.$1 > 50) + .map((e) => e.$2) + .toList(); + }, + [tracks, searchText.value], + ); + useEffect(() { if (playlist.active == null) return null; @@ -50,6 +75,10 @@ class PlayerQueue extends HookConsumerWidget { return null; }, []); + if (tracks.isEmpty) { + return const NotFound(vertical: true); + } + return BackdropFilter( filter: ImageFilter.blur( sigmaX: 12.0, @@ -64,89 +93,172 @@ class PlayerQueue extends HookConsumerWidget { color: theme.scaffoldBackgroundColor.withOpacity(0.5), borderRadius: borderRadius, ), - child: Column( - children: [ - Container( - height: 5, - width: 100, - margin: const EdgeInsets.only(bottom: 5, top: 2), - decoration: BoxDecoration( - color: headlineColor, - borderRadius: BorderRadius.circular(20), - ), - ), - Row( + child: CallbackShortcuts( + bindings: { + LogicalKeySet(LogicalKeyboardKey.escape): () { + if (!isSearching.value) { + Navigator.of(context).pop(); + } + isSearching.value = false; + searchText.value = ''; + } + }, + child: LayoutBuilder(builder: (context, constraints) { + return Column( children: [ - const SizedBox(width: 10), - Text( - context.l10n.tracks_in_queue(tracks.length), - style: TextStyle( + Container( + height: 5, + width: 100, + margin: const EdgeInsets.only(bottom: 5, top: 2), + decoration: BoxDecoration( color: headlineColor, - fontWeight: FontWeight.bold, - fontSize: 18, + borderRadius: BorderRadius.circular(20), ), ), - const Spacer(), - 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), - 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), - ), - ], + 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, ), ), - ); - }), - ), - ], + 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 (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: 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: 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); + }, + ), + ); + }, + ), + ), + ], + ); + }), ), ), ); From 1540999f50d7ba78d9706d73127483b98d800d86 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 10 Sep 2023 22:50:44 +0600 Subject: [PATCH 12/14] feat: right click to open track option --- .../adaptive/adaptive_pop_sheet_list.dart | 25 ++ .../shared/track_table/track_options.dart | 409 +++++++++--------- .../shared/track_table/track_tile.dart | 342 ++++++++------- 3 files changed, 413 insertions(+), 363 deletions(-) diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart index 41534cb3..21f56a22 100644 --- a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart @@ -78,6 +78,31 @@ class AdaptivePopSheetList extends StatelessWidget { 'Either icon or child must be provided', ); + Future showPopupMenu(BuildContext context, RelativeRect position) { + final mediaQuery = MediaQuery.of(context); + + return showMenu( + context: context, + useRootNavigator: useRootNavigator, + constraints: BoxConstraints( + maxHeight: mediaQuery.size.height * 0.6, + ), + position: position, + items: children + .map( + (item) => PopupMenuItem( + padding: EdgeInsets.zero, + enabled: false, + child: _AdaptivePopSheetListItem( + item: item, + onSelected: onSelected, + ), + ), + ) + .toList(), + ); + } + @override Widget build(BuildContext context) { final mediaQuery = MediaQuery.of(context); diff --git a/lib/components/shared/track_table/track_options.dart b/lib/components/shared/track_table/track_options.dart index a1bc3fef..96bd8b60 100644 --- a/lib/components/shared/track_table/track_options.dart +++ b/lib/components/shared/track_table/track_options.dart @@ -14,7 +14,6 @@ import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/models/local_track.dart'; -import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart'; @@ -40,9 +39,11 @@ class TrackOptions extends HookConsumerWidget { final Track track; final bool userPlaylist; final String? playlistId; + final ObjectRef?>? showMenuCbRef; const TrackOptions({ Key? key, required this.track, + this.showMenuCbRef, this.userPlaylist = false, this.playlistId, }) : super(key: key); @@ -114,210 +115,216 @@ class TrackOptions extends HookConsumerWidget { return downloadManager.getProgressNotifier(spotubeTrack); }); + final adaptivePopSheetList = AdaptivePopSheetList( + onSelected: (value) async { + switch (value) { + case TrackOptionValue.delete: + await File((track as LocalTrack).path).delete(); + ref.refresh(localTracksProvider); + break; + case TrackOptionValue.addToQueue: + await playback.addTrack(track); + if (context.mounted) { + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + context.l10n.added_track_to_queue(track.name!), + ), + ), + ); + } + break; + case TrackOptionValue.playNext: + playback.addTracksAtFirst([track]); + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + context.l10n.track_will_play_next(track.name!), + ), + ), + ); + break; + case TrackOptionValue.removeFromQueue: + playback.removeTrack(track.id!); + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + context.l10n.removed_track_from_queue( + track.name!, + ), + ), + ), + ); + break; + case TrackOptionValue.favorite: + favorites.toggleTrackLike.mutate(favorites.isLiked); + break; + case TrackOptionValue.addToPlaylist: + actionAddToPlaylist(context, track); + break; + case TrackOptionValue.removeFromPlaylist: + removingTrack.value = track.uri; + removeTrack.mutate(track.uri!); + break; + case TrackOptionValue.blacklist: + if (isBlackListed) { + ref.read(BlackListNotifier.provider.notifier).remove( + BlacklistedElement.track(track.id!, track.name!), + ); + } else { + ref.read(BlackListNotifier.provider.notifier).add( + BlacklistedElement.track(track.id!, track.name!), + ); + } + break; + case TrackOptionValue.share: + actionShare(context, track); + break; + case TrackOptionValue.details: + showDialog( + context: context, + builder: (context) => TrackDetailsDialog(track: track), + ); + break; + case TrackOptionValue.download: + await downloadManager.addToQueue(track); + break; + } + }, + icon: const Icon(SpotubeIcons.moreHorizontal), + headings: [ + ListTile( + dense: true, + leading: AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: TypeConversionUtils.image_X_UrlString(track.album!.images, + placeholder: ImagePlaceholder.albumArt), + fit: BoxFit.cover, + ), + ), + ), + title: Text( + track.name!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Align( + alignment: Alignment.centerLeft, + child: TypeConversionUtils.artists_X_ClickableArtists( + track.artists!, + ), + ), + ), + ], + children: switch (track.runtimeType) { + LocalTrack => [ + PopSheetEntry( + value: TrackOptionValue.delete, + leading: const Icon(SpotubeIcons.trash), + title: Text(context.l10n.delete), + ) + ], + _ => [ + if (!playlist.containsTrack(track)) ...[ + PopSheetEntry( + value: TrackOptionValue.addToQueue, + leading: const Icon(SpotubeIcons.queueAdd), + title: Text(context.l10n.add_to_queue), + ), + PopSheetEntry( + value: TrackOptionValue.playNext, + leading: const Icon(SpotubeIcons.lightning), + title: Text(context.l10n.play_next), + ), + ] else + PopSheetEntry( + value: TrackOptionValue.removeFromQueue, + enabled: playlist.activeTrack?.id != track.id, + leading: const Icon(SpotubeIcons.queueRemove), + title: Text(context.l10n.remove_from_queue), + ), + if (favorites.me.hasData) + PopSheetEntry( + value: TrackOptionValue.favorite, + leading: favorites.isLiked + ? const Icon( + SpotubeIcons.heartFilled, + color: Colors.pink, + ) + : const Icon(SpotubeIcons.heart), + title: Text( + favorites.isLiked + ? context.l10n.remove_from_favorites + : context.l10n.save_as_favorite, + ), + ), + if (auth != null) + PopSheetEntry( + value: TrackOptionValue.addToPlaylist, + leading: const Icon(SpotubeIcons.playlistAdd), + title: Text(context.l10n.add_to_playlist), + ), + if (userPlaylist && auth != null) + PopSheetEntry( + value: TrackOptionValue.removeFromPlaylist, + leading: (removeTrack.isMutating || !removeTrack.hasData) && + removingTrack.value == track.uri + ? const CircularProgressIndicator() + : const Icon(SpotubeIcons.removeFilled), + title: Text(context.l10n.remove_from_playlist), + ), + PopSheetEntry( + value: TrackOptionValue.download, + enabled: !isInQueue, + leading: isInQueue + ? HookBuilder(builder: (context) { + final progress = useListenable(progressNotifier!); + return CircularProgressIndicator( + value: progress.value, + ); + }) + : const Icon(SpotubeIcons.download), + title: Text(context.l10n.download_track), + ), + PopSheetEntry( + value: TrackOptionValue.blacklist, + leading: const Icon(SpotubeIcons.playlistRemove), + iconColor: !isBlackListed ? Colors.red[400] : null, + textColor: !isBlackListed ? Colors.red[400] : null, + title: Text( + isBlackListed + ? context.l10n.remove_from_blacklist + : context.l10n.add_to_blacklist, + ), + ), + PopSheetEntry( + value: TrackOptionValue.share, + leading: const Icon(SpotubeIcons.share), + title: Text(context.l10n.share), + ), + PopSheetEntry( + value: TrackOptionValue.details, + leading: const Icon(SpotubeIcons.info), + title: Text(context.l10n.details), + ), + ] + }, + ); + + //! This is the most ANTI pattern I've ever done, but it works + showMenuCbRef?.value = (relativeRect) { + adaptivePopSheetList.showPopupMenu(context, relativeRect); + }; + return ListTileTheme( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), - child: AdaptivePopSheetList( - onSelected: (value) async { - switch (value) { - case TrackOptionValue.delete: - await File((track as LocalTrack).path).delete(); - ref.refresh(localTracksProvider); - break; - case TrackOptionValue.addToQueue: - await playback.addTrack(track); - if (context.mounted) { - scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - context.l10n.added_track_to_queue(track.name!), - ), - ), - ); - } - break; - case TrackOptionValue.playNext: - playback.addTracksAtFirst([track]); - scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - context.l10n.track_will_play_next(track.name!), - ), - ), - ); - break; - case TrackOptionValue.removeFromQueue: - playback.removeTrack(track.id!); - scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - context.l10n.removed_track_from_queue( - track.name!, - ), - ), - ), - ); - break; - case TrackOptionValue.favorite: - favorites.toggleTrackLike.mutate(favorites.isLiked); - break; - case TrackOptionValue.addToPlaylist: - actionAddToPlaylist(context, track); - break; - case TrackOptionValue.removeFromPlaylist: - removingTrack.value = track.uri; - removeTrack.mutate(track.uri!); - break; - case TrackOptionValue.blacklist: - if (isBlackListed) { - ref.read(BlackListNotifier.provider.notifier).remove( - BlacklistedElement.track(track.id!, track.name!), - ); - } else { - ref.read(BlackListNotifier.provider.notifier).add( - BlacklistedElement.track(track.id!, track.name!), - ); - } - break; - case TrackOptionValue.share: - actionShare(context, track); - break; - case TrackOptionValue.details: - showDialog( - context: context, - builder: (context) => TrackDetailsDialog(track: track), - ); - break; - case TrackOptionValue.download: - await downloadManager.addToQueue(track); - break; - } - }, - icon: const Icon(SpotubeIcons.moreHorizontal), - headings: [ - ListTile( - dense: true, - leading: AspectRatio( - aspectRatio: 1, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - path: TypeConversionUtils.image_X_UrlString( - track.album!.images, - placeholder: ImagePlaceholder.albumArt), - fit: BoxFit.cover, - ), - ), - ), - title: Text( - track.name!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - subtitle: Align( - alignment: Alignment.centerLeft, - child: TypeConversionUtils.artists_X_ClickableArtists( - track.artists!, - ), - ), - ), - ], - children: switch (track.runtimeType) { - LocalTrack => [ - PopSheetEntry( - value: TrackOptionValue.delete, - leading: const Icon(SpotubeIcons.trash), - title: Text(context.l10n.delete), - ) - ], - _ => [ - if (!playlist.containsTrack(track)) ...[ - PopSheetEntry( - value: TrackOptionValue.addToQueue, - leading: const Icon(SpotubeIcons.queueAdd), - title: Text(context.l10n.add_to_queue), - ), - PopSheetEntry( - value: TrackOptionValue.playNext, - leading: const Icon(SpotubeIcons.lightning), - title: Text(context.l10n.play_next), - ), - ] else - PopSheetEntry( - value: TrackOptionValue.removeFromQueue, - enabled: playlist.activeTrack?.id != track.id, - leading: const Icon(SpotubeIcons.queueRemove), - title: Text(context.l10n.remove_from_queue), - ), - if (favorites.me.hasData) - PopSheetEntry( - value: TrackOptionValue.favorite, - leading: favorites.isLiked - ? const Icon( - SpotubeIcons.heartFilled, - color: Colors.pink, - ) - : const Icon(SpotubeIcons.heart), - title: Text( - favorites.isLiked - ? context.l10n.remove_from_favorites - : context.l10n.save_as_favorite, - ), - ), - if (auth != null) - PopSheetEntry( - value: TrackOptionValue.addToPlaylist, - leading: const Icon(SpotubeIcons.playlistAdd), - title: Text(context.l10n.add_to_playlist), - ), - if (userPlaylist && auth != null) - PopSheetEntry( - value: TrackOptionValue.removeFromPlaylist, - leading: (removeTrack.isMutating || !removeTrack.hasData) && - removingTrack.value == track.uri - ? const CircularProgressIndicator() - : const Icon(SpotubeIcons.removeFilled), - title: Text(context.l10n.remove_from_playlist), - ), - PopSheetEntry( - value: TrackOptionValue.download, - enabled: !isInQueue, - leading: isInQueue - ? HookBuilder(builder: (context) { - final progress = useListenable(progressNotifier!); - return CircularProgressIndicator( - value: progress.value, - ); - }) - : const Icon(SpotubeIcons.download), - title: Text(context.l10n.download_track), - ), - PopSheetEntry( - value: TrackOptionValue.blacklist, - leading: const Icon(SpotubeIcons.playlistRemove), - iconColor: !isBlackListed ? Colors.red[400] : null, - textColor: !isBlackListed ? Colors.red[400] : null, - title: Text( - isBlackListed - ? context.l10n.remove_from_blacklist - : context.l10n.add_to_blacklist, - ), - ), - PopSheetEntry( - value: TrackOptionValue.share, - leading: const Icon(SpotubeIcons.share), - title: Text(context.l10n.share), - ), - PopSheetEntry( - value: TrackOptionValue.details, - leading: const Icon(SpotubeIcons.info), - title: Text(context.l10n.details), - ), - ] - }, - ), + child: adaptivePopSheetList, ); } } diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index 7926f55a..9fe13dcc 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -57,174 +58,191 @@ class TrackTile extends HookConsumerWidget { [blacklist, track], ); + final showOptionCbRef = useRef?>(null); + final isPlaying = track.id == playlist.activeTrack?.id; return LayoutBuilder(builder: (context, constrains) { - return HoverBuilder( - permanentState: isPlaying || constrains.smAndDown ? true : null, - builder: (context, isHovering) { - return ListTile( - selected: isPlaying, - onTap: onTap, - onLongPress: onLongPress, - enabled: !isBlackListed, - contentPadding: EdgeInsets.zero, - tileColor: isBlackListed ? theme.colorScheme.errorContainer : null, - horizontalTitleGap: 12, - leadingAndTrailingTextStyle: theme.textTheme.bodyMedium, - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ...?leadingActions, - if (index != null && onChanged == null && constrains.mdAndUp) - SizedBox( - width: 34, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Text( - '$index', - maxLines: 1, - style: theme.textTheme.bodySmall, - textAlign: TextAlign.center, - ), - ), - ) - else if (constrains.smAndDown) - const SizedBox(width: 16), - if (onChanged != null) - Checkbox( - value: selected, - onChanged: onChanged, - ), - Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: AspectRatio( - aspectRatio: 1, - child: UniversalImage( - path: TypeConversionUtils.image_X_UrlString( - track.album?.images, - placeholder: ImagePlaceholder.albumArt, - ), - fit: BoxFit.cover, - ), - ), - ), - Positioned.fill( - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: isHovering - ? Colors.black.withOpacity(0.4) - : Colors.transparent, - ), - ), - ), - Positioned.fill( - child: Center( - child: IconTheme( - data: theme.iconTheme - .copyWith(size: 26, color: Colors.white), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: !isHovering - ? const SizedBox.shrink() - : isPlaying && playlist.isFetching - ? const SizedBox( - width: 26, - height: 26, - child: CircularProgressIndicator( - strokeWidth: 1.5, - color: Colors.white, - ), - ) - : isPlaying - ? Icon( - SpotubeIcons.pause, - color: theme.colorScheme.primary, - ) - : const Icon(SpotubeIcons.play), - ), - ), - ), - ), - ], - ), - ], - ), - title: Row( - children: [ - Expanded( - flex: 6, - child: Text( - track.name!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - if (constrains.mdAndUp) ...[ - const SizedBox(width: 8), - Expanded( - flex: 4, - child: switch (track.runtimeType) { - LocalTrack => Text( - track.album!.name!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - _ => Align( - alignment: Alignment.centerLeft, - child: LinkText( - track.album!.name!, - "/album/${track.album?.id}", - extra: track.album, - push: true, - overflow: TextOverflow.ellipsis, - ), - ) - }, - ), - ], - ], - ), - subtitle: Align( - alignment: Alignment.centerLeft, - child: track is LocalTrack - ? Text( - TypeConversionUtils.artists_X_String( - track.artists ?? [], - ), - ) - : ClipRect( - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 40), - child: TypeConversionUtils.artists_X_ClickableArtists( - track.artists ?? [], - ), - ), - ), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 8), - Text( - Duration(milliseconds: track.durationMs ?? 0) - .toHumanReadableString(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - TrackOptions( - track: track, - playlistId: playlistId, - userPlaylist: userPlaylist, - ), - ], + return Listener( + onPointerDown: (event) { + if (event.buttons != kSecondaryMouseButton) return; + showOptionCbRef.value?.call( + RelativeRect.fromLTRB( + event.position.dx, + event.position.dy, + constrains.maxWidth - event.position.dx, + constrains.maxHeight - event.position.dy, ), ); }, + child: HoverBuilder( + permanentState: isPlaying || constrains.smAndDown ? true : null, + builder: (context, isHovering) { + return ListTile( + selected: isPlaying, + onTap: onTap, + onLongPress: onLongPress, + enabled: !isBlackListed, + contentPadding: EdgeInsets.zero, + tileColor: + isBlackListed ? theme.colorScheme.errorContainer : null, + horizontalTitleGap: 12, + leadingAndTrailingTextStyle: theme.textTheme.bodyMedium, + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ...?leadingActions, + if (index != null && onChanged == null && constrains.mdAndUp) + SizedBox( + width: 34, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + '$index', + maxLines: 1, + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ), + ) + else if (constrains.smAndDown) + const SizedBox(width: 16), + if (onChanged != null) + Checkbox( + value: selected, + onChanged: onChanged, + ), + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: AspectRatio( + aspectRatio: 1, + child: UniversalImage( + path: TypeConversionUtils.image_X_UrlString( + track.album?.images, + placeholder: ImagePlaceholder.albumArt, + ), + fit: BoxFit.cover, + ), + ), + ), + Positioned.fill( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: isHovering + ? Colors.black.withOpacity(0.4) + : Colors.transparent, + ), + ), + ), + Positioned.fill( + child: Center( + child: IconTheme( + data: theme.iconTheme + .copyWith(size: 26, color: Colors.white), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: !isHovering + ? const SizedBox.shrink() + : isPlaying && playlist.isFetching + ? const SizedBox( + width: 26, + height: 26, + child: CircularProgressIndicator( + strokeWidth: 1.5, + color: Colors.white, + ), + ) + : isPlaying + ? Icon( + SpotubeIcons.pause, + color: theme.colorScheme.primary, + ) + : const Icon(SpotubeIcons.play), + ), + ), + ), + ), + ], + ), + ], + ), + title: Row( + children: [ + Expanded( + flex: 6, + child: Text( + track.name!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (constrains.mdAndUp) ...[ + const SizedBox(width: 8), + Expanded( + flex: 4, + child: switch (track.runtimeType) { + LocalTrack => Text( + track.album!.name!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + _ => Align( + alignment: Alignment.centerLeft, + child: LinkText( + track.album!.name!, + "/album/${track.album?.id}", + extra: track.album, + push: true, + overflow: TextOverflow.ellipsis, + ), + ) + }, + ), + ], + ], + ), + subtitle: Align( + alignment: Alignment.centerLeft, + child: track is LocalTrack + ? Text( + TypeConversionUtils.artists_X_String( + track.artists ?? [], + ), + ) + : ClipRect( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 40), + child: TypeConversionUtils.artists_X_ClickableArtists( + track.artists ?? [], + ), + ), + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 8), + Text( + Duration(milliseconds: track.durationMs ?? 0) + .toHumanReadableString(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + TrackOptions( + track: track, + playlistId: playlistId, + userPlaylist: userPlaylist, + showMenuCbRef: showOptionCbRef, + ), + ], + ), + ); + }, + ), ); }); } From d12ea48b97596205d6309012d561ce83e5cbc9c1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 11 Sep 2023 11:05:42 +0600 Subject: [PATCH 13/14] feat: show loading indicator on play track --- lib/components/library/user_local_tracks.dart | 10 ++-- .../track_collection_view.dart | 4 +- .../shared/track_table/track_tile.dart | 52 ++++++++++++------- .../shared/track_table/tracks_table_view.dart | 14 ++--- lib/pages/album/album.dart | 8 +-- lib/pages/artist/artist.dart | 2 +- lib/pages/playlist/playlist.dart | 14 ++--- 7 files changed, 57 insertions(+), 47 deletions(-) diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index a36be283..16692462 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -131,7 +131,7 @@ final localTracksProvider = FutureProvider>((ref) async { class UserLocalTracks extends HookConsumerWidget { const UserLocalTracks({Key? key}) : super(key: key); - void playLocalTracks( + Future playLocalTracks( WidgetRef ref, List tracks, { LocalTrack? currentTrack, @@ -203,10 +203,10 @@ class UserLocalTracks extends HookConsumerWidget { const SizedBox(width: 10), FilledButton( onPressed: trackSnapshot.value != null - ? () { + ? () async { if (trackSnapshot.value?.isNotEmpty == true) { if (!isPlaylistPlaying) { - playLocalTracks( + await playLocalTracks( ref, trackSnapshot.value!, ); @@ -295,8 +295,8 @@ class UserLocalTracks extends HookConsumerWidget { index: index, track: track, userPlaylist: false, - onTap: () { - playLocalTracks( + onTap: () async { + await playLocalTracks( ref, sortedTracks, currentTrack: track, 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 1c87d887..14d9598f 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 @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fl_query/fl_query.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; @@ -27,7 +29,7 @@ class TrackCollectionView extends HookConsumerWidget { final Query, T> tracksSnapshot; final String titleImage; final PlayButtonState playingState; - final void Function([Track? currentTrack]) onPlay; + final Future Function([Track? currentTrack]) onPlay; final void Function([Track? currentTrack]) onShuffledPlay; final void Function() onAddToQueue; final void Function() onShare; diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index 9fe13dcc..757432fe 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -21,7 +23,7 @@ class TrackTile extends HookConsumerWidget { final Track track; final bool selected; final ValueChanged? onChanged; - final VoidCallback? onTap; + final Future Function()? onTap; final VoidCallback? onLongPress; final bool userPlaylist; final String? playlistId; @@ -62,6 +64,10 @@ class TrackTile extends HookConsumerWidget { final isPlaying = track.id == playlist.activeTrack?.id; + final isLoading = useState(false); + + final isSelected = isPlaying || isLoading.value; + return LayoutBuilder(builder: (context, constrains) { return Listener( onPointerDown: (event) { @@ -76,11 +82,18 @@ class TrackTile extends HookConsumerWidget { ); }, child: HoverBuilder( - permanentState: isPlaying || constrains.smAndDown ? true : null, + permanentState: isSelected || constrains.smAndDown ? true : null, builder: (context, isHovering) { return ListTile( - selected: isPlaying, - onTap: onTap, + selected: isSelected, + onTap: () async { + try { + isLoading.value = true; + await onTap?.call(); + } finally { + isLoading.value = false; + } + }, onLongPress: onLongPress, enabled: !isBlackListed, contentPadding: EdgeInsets.zero, @@ -145,22 +158,23 @@ class TrackTile extends HookConsumerWidget { .copyWith(size: 26, color: Colors.white), child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: !isHovering - ? const SizedBox.shrink() - : isPlaying && playlist.isFetching - ? const SizedBox( - width: 26, - height: 26, - child: CircularProgressIndicator( - strokeWidth: 1.5, - color: Colors.white, - ), + 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, ) - : isPlaying - ? Icon( - SpotubeIcons.pause, - color: theme.colorScheme.primary, - ) + : !isHovering + ? const SizedBox.shrink() : const Icon(SpotubeIcons.play), ), ), diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index 58d662f4..2ad6d384 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -27,7 +29,7 @@ final trackCollectionSortState = StateProvider.family((ref, _) => SortBy.none); class TracksTableView extends HookConsumerWidget { - final void Function(Track currentTrack)? onTrackPlayButtonPressed; + final Future Function(Track currentTrack)? onTrackPlayButtonPressed; final List tracks; final bool userPlaylist; final String? playlistId; @@ -58,8 +60,7 @@ class TracksTableView extends HookConsumerWidget { final downloader = ref.watch(downloadManagerProvider.notifier); final apiType = ref.watch(userPreferencesProvider.select((s) => s.youtubeApiType)); - final tableHeadStyle = - const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); + const tableHeadStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 16); final selected = useState>([]); final showCheck = useState(false); @@ -297,7 +298,7 @@ class TracksTableView extends HookConsumerWidget { selected: selected.value.contains(track.id), userPlaylist: userPlaylist, playlistId: playlistId, - onTap: () { + onTap: () async { if (showCheck.value) { final alreadyChecked = selected.value.contains(track.id); if (alreadyChecked) { @@ -314,9 +315,8 @@ class TracksTableView extends HookConsumerWidget { ), ), ); - if (!isBlackListed) { - onTrackPlayButtonPressed?.call(track); - } + if (isBlackListed) return; + await onTrackPlayButtonPressed?.call(track); } }, onLongPress: () { diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index 84e23e8b..a585c9e5 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -85,10 +85,10 @@ class AlbumPage extends HookConsumerWidget { album: album, routePath: "/album/${album.id}", bottomSpace: mediaQuery.mdAndDown, - onPlay: ([track]) { + onPlay: ([track]) async { if (tracksSnapshot.hasData) { if (!isAlbumPlaying) { - playPlaylist( + await playPlaylist( tracksSnapshot.data! .map((track) => TypeConversionUtils.simpleTrack_X_Track(track, album)) @@ -96,7 +96,7 @@ class AlbumPage extends HookConsumerWidget { ref, ); } else if (isAlbumPlaying && track != null) { - playPlaylist( + await playPlaylist( tracksSnapshot.data! .map((track) => TypeConversionUtils.simpleTrack_X_Track(track, album)) @@ -105,7 +105,7 @@ class AlbumPage extends HookConsumerWidget { ref, ); } else { - playback + await playback .removeTracks(tracksSnapshot.data!.map((track) => track.id!)); } } diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 44e40423..e1bbefcb 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -390,7 +390,7 @@ class ArtistPage extends HookConsumerWidget { return TrackTile( index: i, track: track, - onTap: () { + onTap: () async { playPlaylist( topTracks.toList(), currentTrack: track, diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index 9c852ace..1623195b 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -104,22 +104,16 @@ class PlaylistView extends HookConsumerWidget { tracksSnapshot: tracksSnapshot, description: playlist.description, isOwned: ownPlaylist, - onPlay: ([track]) { + onPlay: ([track]) async { if (tracksSnapshot.hasData) { - if (!isPlaylistPlaying) { - playPlaylist( - tracksSnapshot.data!, - ref, - currentTrack: track, - ); - } else if (isPlaylistPlaying && track != null) { - playPlaylist( + if (!isPlaylistPlaying || (isPlaylistPlaying && track != null)) { + await playPlaylist( tracksSnapshot.data!, ref, currentTrack: track, ); } else { - playlistNotifier + await playlistNotifier .removeTracks(tracksSnapshot.data!.map((e) => e.id!)); } } From de335f48342e45a077d6c3202706ef48dfb0a326 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 11 Sep 2023 12:12:05 +0600 Subject: [PATCH 14/14] fix: hour not showing for tracks longer than 60 minutes (#648) --- lib/components/player/player_controls.dart | 19 +++---------------- .../player/sibling_tracks_sheet.dart | 5 ++--- .../shared/track_table/track_tile.dart | 2 +- lib/extensions/duration.dart | 17 ++++++++++++++--- lib/utils/primitive_utils.dart | 11 ----------- 5 files changed, 20 insertions(+), 34 deletions(-) diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart index 7ae4fa82..07a6b7ba 100644 --- a/lib/components/player/player_controls.dart +++ b/lib/components/player/player_controls.dart @@ -7,12 +7,12 @@ import 'package:palette_generator/palette_generator.dart'; 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/models/logger.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/utils/primitive_utils.dart'; class PlayerControls extends HookConsumerWidget { final PaletteGenerator? palette; @@ -113,19 +113,6 @@ class PlayerControls extends HookConsumerWidget { :progressStatic ) = useProgress(ref); - final totalMinutes = PrimitiveUtils.zeroPadNumStr( - duration.inMinutes.remainder(60), - ); - final totalSeconds = PrimitiveUtils.zeroPadNumStr( - duration.inSeconds.remainder(60), - ); - final currentMinutes = PrimitiveUtils.zeroPadNumStr( - position.inMinutes.remainder(60), - ); - final currentSeconds = PrimitiveUtils.zeroPadNumStr( - position.inSeconds.remainder(60), - ); - final progress = useState( useMemoized(() => progressStatic, []), ); @@ -173,8 +160,8 @@ class PlayerControls extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("$currentMinutes:$currentSeconds"), - Text("$totalMinutes:$totalSeconds"), + Text(position.toHumanReadableString()), + Text(duration.toHumanReadableString()), ], ), ), diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index b7d802f9..d4857853 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -9,6 +9,7 @@ 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/extensions/duration.dart'; import 'package:spotube/hooks/use_debounce.dart'; import 'package:spotube/models/matched_track.dart'; import 'package:spotube/models/spotube_track.dart'; @@ -99,9 +100,7 @@ class SiblingTracksSheet extends HookConsumerWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), - trailing: Text( - PrimitiveUtils.toReadableDuration(video.duration), - ), + trailing: Text(video.duration.toHumanReadableString()), subtitle: Text(video.channelName), enabled: playlist.isFetching != true, selected: playlist.isFetching != true && diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index 757432fe..0666b7f9 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -242,7 +242,7 @@ class TrackTile extends HookConsumerWidget { const SizedBox(width: 8), Text( Duration(milliseconds: track.durationMs ?? 0) - .toHumanReadableString(), + .toHumanReadableString(padZero: false), maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/extensions/duration.dart b/lib/extensions/duration.dart index 183fce5f..c8612425 100644 --- a/lib/extensions/duration.dart +++ b/lib/extensions/duration.dart @@ -1,10 +1,21 @@ import 'package:duration/locale.dart'; -import 'package:spotube/utils/primitive_utils.dart'; import 'package:duration/duration.dart'; extension DurationToHumanReadableString on Duration { - String toHumanReadableString() => - "${inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(inSeconds.remainder(60))}"; + String toHumanReadableString({padZero = true}) { + final mm = inMinutes + .remainder(60) + .toString() + .padLeft(2, !padZero && inHours == 0 ? '' : "0"); + final ss = inSeconds.remainder(60).toString().padLeft(2, "0"); + + if (inHours > 0) { + final hh = inHours.toString().padLeft(2, !padZero ? '' : "0"); + return "$hh:$mm:$ss"; + } + + return "$mm:$ss"; + } String format({ DurationTersity tersity = DurationTersity.second, diff --git a/lib/utils/primitive_utils.dart b/lib/utils/primitive_utils.dart index a0e54430..3843601e 100644 --- a/lib/utils/primitive_utils.dart +++ b/lib/utils/primitive_utils.dart @@ -31,17 +31,6 @@ abstract class PrimitiveUtils { } } - static String zeroPadNumStr(int input) { - return input < 10 ? "0$input" : input.toString(); - } - - static String toReadableDuration(Duration duration) { - final hours = duration.inHours; - final minutes = duration.inMinutes % 60; - final seconds = duration.inSeconds % 60; - return "${hours > 0 ? "${zeroPadNumStr(hours)}:" : ""}${zeroPadNumStr(minutes)}:${zeroPadNumStr(seconds)}"; - } - static Future raceMultiple( Future Function() inner, { Duration timeout = const Duration(milliseconds: 2500),