From a2f04454b1775c42b22fac9a1772c6087d51fd8b Mon Sep 17 00:00:00 2001 From: James Harvey <44349936+jmshrv@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:46:10 +0100 Subject: [PATCH 01/21] Fix Flutter 3.24 Android build Looks like yet another issue with Flutter upgrades and old dependencies. Fix taken from https://stackoverflow.com/a/78865504/8532605 --- android/build.gradle | 11 +++++++++++ pubspec.lock | 24 ++++++++++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index bc157bd1a..65722a416 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,6 +9,17 @@ rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } +subprojects { + afterEvaluate { project -> + if (project.plugins.hasPlugin("com.android.application") || + project.plugins.hasPlugin("com.android.library")) { + project.android { + compileSdkVersion 34 + buildToolsVersion "34.0.0" + } + } + } +} subprojects { project.evaluationDependsOn(':app') } diff --git a/pubspec.lock b/pubspec.lock index d1bd7a596..220a43769 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -835,18 +835,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -899,10 +899,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" media_kit: dependency: transitive description: @@ -932,10 +932,10 @@ packages: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -1451,10 +1451,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" time: dependency: transitive description: @@ -1635,10 +1635,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" wakelock_plus: dependency: "direct main" description: From 2d5ff03c680ffcdc64186123ccbe462cb01af366 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 17 Sep 2024 14:02:38 +0200 Subject: [PATCH 02/21] fix overlay on fast scroller - fixes #890 --- .../MusicScreen/alphabet_item_list.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/components/MusicScreen/alphabet_item_list.dart b/lib/components/MusicScreen/alphabet_item_list.dart index fe849bf56..8b52af83b 100644 --- a/lib/components/MusicScreen/alphabet_item_list.dart +++ b/lib/components/MusicScreen/alphabet_item_list.dart @@ -133,16 +133,16 @@ class _AlphabetListState extends State { child: widget.child, )), if (_currentSelected != null && _displayPreview) - FocusOnIt( - onUnfocus: () { - setState(() { - _currentSelected = null; - _displayPreview = false; - }); - }, - child: Positioned( - left: 20, - top: 20, + Positioned( + left: 20, + top: 20, + child: FocusOnIt( + onUnfocus: () { + setState(() { + _currentSelected = null; + _displayPreview = false; + }); + }, child: GestureDetector( onTap: () { setState(() { From e64e4b571b060f9d814e282ab9d730ced47b08c2 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 17 Sep 2024 14:17:01 +0200 Subject: [PATCH 03/21] try to fix linux build assets upload --- .github/workflows/upload-assets.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/upload-assets.yml b/.github/workflows/upload-assets.yml index 3242f22cd..77ab0baa6 100644 --- a/.github/workflows/upload-assets.yml +++ b/.github/workflows/upload-assets.yml @@ -58,22 +58,24 @@ jobs: && cp assets/finamp.desktop.m4 build/linux/x64/release/ \ && cp assets/com.unicornsonlsd.finamp.metainfo.xml build/linux/x64/release/ # archive bundle and generate checksum + - run: GITHUB_REF="${{ github.ref }}" + - run: VERSION="${ref#refs/tags/}" - run: | - tar -czf finamp-${{ github.ref }}-linux-release.tar.gz --directory build/linux/x64/release/ bundle icons finamp.desktop.m4 com.unicornsonlsd.finamp.metainfo.xml \ - && sha256sum finamp-${{ github.ref }}-linux-release.tar.gz > finamp-${{ github.ref }}-linux-release.tar.gz.sha256sum + tar -czf finamp-${VERSION}-linux-release.tar.gz --directory build/linux/x64/release/ bundle icons finamp.desktop.m4 com.unicornsonlsd.finamp.metainfo.xml \ + && sha256sum finamp-${VERSION}-linux-release.tar.gz > finamp-${VERSION}-linux-release.tar.gz.sha256sum - uses: actions/upload-artifact@v4 with: - name: finamp-${{ github.ref }}-linux-release.tar.gz - path: finamp-${{ github.ref }}-linux-release.tar.gz + name: finamp-${VERSION}-linux-release.tar.gz + path: finamp-${VERSION}-linux-release.tar.gz compression-level: 0 # no compression - uses: actions/upload-artifact@v4 with: - name: finamp-${{ github.ref }}-linux-release.tar.gz.sha256sum - path: finamp-${{ github.ref }}-linux-release.tar.gz.sha256sum + name: finamp-${VERSION}-linux-release.tar.gz.sha256sum + path: finamp-${VERSION}-linux-release.tar.gz.sha256sum compression-level: 0 # no compression - name: Upload release archive uses: alexellis/upload-assets@0.4.1 env: GITHUB_TOKEN: ${{ github.token }} with: - asset_paths: '["./finamp-${{ github.ref }}-linux-release.tar.gz", "./finamp-${{ github.ref }}-linux-release.tar.gz.sha256sum"]' + asset_paths: '["./finamp-${VERSION}-linux-release.tar.gz", "./finamp-${VERSION}-linux-release.tar.gz.sha256sum"]' From 60e722f28338016c83c19d9d91217d45b296e908 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 17 Sep 2024 14:20:34 +0200 Subject: [PATCH 04/21] bump version to 0.9.11 --- assets/com.unicornsonlsd.finamp.metainfo.xml | 16 ++++++++++++++++ pubspec.yaml | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/assets/com.unicornsonlsd.finamp.metainfo.xml b/assets/com.unicornsonlsd.finamp.metainfo.xml index 158894ccf..30e706d87 100644 --- a/assets/com.unicornsonlsd.finamp.metainfo.xml +++ b/assets/com.unicornsonlsd.finamp.metainfo.xml @@ -39,6 +39,21 @@ com.unicornsonlsd.finamp.desktop + + https://github.com/jmshrv/finamp/releases/tag/0.9.11-beta + +

This is a hotfix for a bug introduced with 0.9.10

+

+ Bug Fixes +

+
    +
  • Fix white overlay preventing further interaction when using the fast scroller / alphabet list
  • +
+

+ Thank you for using Finamp! +

+
+
https://github.com/jmshrv/finamp/releases/tag/0.9.10-beta @@ -49,6 +64,7 @@
  • Added setting to keep the screen on
  • Accessibility improvements
  • More lyrics screen customizations
  • +

    Bug Fixes

    diff --git a/pubspec.yaml b/pubspec.yaml index f5079a944..5c26f4395 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.9.10+110 +version: 0.9.11+111 environment: sdk: ">=3.3.0 <4.0.0" @@ -189,7 +189,7 @@ msix_config: display_name: Finamp publisher_display_name: jmshrv identity_name: com.unicornsonlsd.finamp - msix_version: 0.9.10.0 + msix_version: 0.9.11.0 logo_path: ./images/finamp_cropped.png trim_logo: false # don't force logo to be square, it will be stretched capabilities: internetClientServer From 7da719aa55dc020441b09b0f770fa87635789f14 Mon Sep 17 00:00:00 2001 From: James Harvey <44349936+jmshrv@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:41:20 +0100 Subject: [PATCH 05/21] Disable drawer swipe gesture on iOS Since iOS "bounces" when it gets to the end of a list, it felt very weird --- ios/Podfile.lock | 20 +++++++++++++- ios/Runner/AppDelegate.swift | 2 +- lib/screens/music_screen.dart | 50 +++++++++++++++++++--------------- macos/Podfile.lock | 26 +++++++++++++++++- macos/Runner/AppDelegate.swift | 2 +- 5 files changed, 74 insertions(+), 26 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1c1cca925..f6ced7f86 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,10 +1,14 @@ PODS: + - app_set_id (1.2.0): + - Flutter - audio_service (0.0.1): - Flutter - audio_session (0.0.1): - Flutter - background_downloader (0.0.1): - Flutter + - battery_plus (1.0.0): + - Flutter - CropViewController (2.6.1) - device_info_plus (0.0.1): - Flutter @@ -91,11 +95,15 @@ PODS: - SwiftyGif (5.4.4) - url_launcher_ios (0.0.1): - Flutter + - wakelock_plus (0.0.1): + - Flutter DEPENDENCIES: + - app_set_id (from `.symlinks/plugins/app_set_id/ios`) - audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - background_downloader (from `.symlinks/plugins/background_downloader/ios`) + - battery_plus (from `.symlinks/plugins/battery_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - DKImagePickerController (= 4.3.4) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -110,6 +118,7 @@ DEPENDENCIES: - share_plus (from `.symlinks/plugins/share_plus/ios`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) SPEC REPOS: trunk: @@ -123,12 +132,16 @@ SPEC REPOS: - SwiftyGif EXTERNAL SOURCES: + app_set_id: + :path: ".symlinks/plugins/app_set_id/ios" audio_service: :path: ".symlinks/plugins/audio_service/ios" audio_session: :path: ".symlinks/plugins/audio_session/ios" background_downloader: :path: ".symlinks/plugins/background_downloader/ios" + battery_plus: + :path: ".symlinks/plugins/battery_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -155,11 +168,15 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqflite/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: + app_set_id: a4d12ebbc7915f987b4a04983b4c0104c64d5e02 audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207 background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf + battery_plus: 1ff2e16ba75af2a78387f65476057a390b47885e CropViewController: 58fb440f30dac788b129d2a1f24cffdcb102669c device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKCamera: a902b66921fca14b7a75266feb8c7568aa7caa71 @@ -181,7 +198,8 @@ SPEC CHECKSUMS: sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 PODFILE CHECKSUM: 047c0919aa274fcdf0ce568f883473a4587eda02 -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 893fdb806..a76d52ae3 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/lib/screens/music_screen.dart b/lib/screens/music_screen.dart index 9271e0225..b97bb6e27 100644 --- a/lib/screens/music_screen.dart +++ b/lib/screens/music_screen.dart @@ -318,29 +318,35 @@ class _MusicScreenState extends ConsumerState child: getFloatingActionButton(sortedTabs.toList()), ), body: Builder(builder: (context) { - return TransparentRightSwipeDetector( - action: () { - if (_tabController?.index == 0 && - !FinampSettingsHelper.finampSettings.disableGesture) { - Scaffold.of(context).openDrawer(); - } - }, - child: TabBarView( - controller: _tabController, - physics: FinampSettingsHelper.finampSettings.disableGesture - ? const NeverScrollableScrollPhysics() - : const AlwaysScrollableScrollPhysics(), - dragStartBehavior: DragStartBehavior.down, - children: sortedTabs - .map((tabType) => MusicScreenTabView( - tabContentType: tabType, - searchTerm: searchQuery, - view: _finampUserHelper.currentUser?.currentView, - refresh: refreshMap[tabType], - )) - .toList(), - ), + final child = TabBarView( + controller: _tabController, + physics: FinampSettingsHelper.finampSettings.disableGesture + ? const NeverScrollableScrollPhysics() + : const AlwaysScrollableScrollPhysics(), + dragStartBehavior: DragStartBehavior.down, + children: sortedTabs + .map((tabType) => MusicScreenTabView( + tabContentType: tabType, + searchTerm: searchQuery, + view: _finampUserHelper.currentUser?.currentView, + refresh: refreshMap[tabType], + )) + .toList(), ); + + if (Platform.isAndroid) { + return TransparentRightSwipeDetector( + action: () { + if (_tabController?.index == 0 && + !FinampSettingsHelper.finampSettings.disableGesture) { + Scaffold.of(context).openDrawer(); + } + }, + child: child, + ); + } + + return child; }), ), ); diff --git a/macos/Podfile.lock b/macos/Podfile.lock index da5789d11..c40c5a8e4 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,8 +1,12 @@ PODS: + - app_set_id (1.2.0): + - FlutterMacOS - audio_service (0.14.1): - FlutterMacOS - audio_session (0.0.1): - FlutterMacOS + - battery_plus (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) @@ -22,12 +26,18 @@ PODS: - sqflite (0.0.3): - Flutter - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - wakelock_plus (0.0.1): + - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS DEPENDENCIES: + - app_set_id (from `Flutter/ephemeral/.symlinks/plugins/app_set_id/macos`) - audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) + - battery_plus (from `Flutter/ephemeral/.symlinks/plugins/battery_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - isar_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos`) @@ -37,13 +47,19 @@ DEPENDENCIES: - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) EXTERNAL SOURCES: + app_set_id: + :path: Flutter/ephemeral/.symlinks/plugins/app_set_id/macos audio_service: :path: Flutter/ephemeral/.symlinks/plugins/audio_service/macos audio_session: :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos + battery_plus: + :path: Flutter/ephemeral/.symlinks/plugins/battery_plus/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos FlutterMacOS: @@ -62,12 +78,18 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + wakelock_plus: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: + app_set_id: a356c3d63b33bb53ee5c22ca6508fed8bfbd682e audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9 audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 + battery_plus: 922e3d9686072259c8fbce6c4238561f769990ae device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a @@ -77,8 +99,10 @@ SPEC CHECKSUMS: screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index d53ef6437..8e02df288 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true From 14e38f8415186d25543c68eff642daafc221b832 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 18 Sep 2024 22:25:27 +0200 Subject: [PATCH 06/21] actually fix linux build assets upload --- .github/workflows/upload-assets.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/upload-assets.yml b/.github/workflows/upload-assets.yml index 77ab0baa6..02c0d78c2 100644 --- a/.github/workflows/upload-assets.yml +++ b/.github/workflows/upload-assets.yml @@ -58,24 +58,22 @@ jobs: && cp assets/finamp.desktop.m4 build/linux/x64/release/ \ && cp assets/com.unicornsonlsd.finamp.metainfo.xml build/linux/x64/release/ # archive bundle and generate checksum - - run: GITHUB_REF="${{ github.ref }}" - - run: VERSION="${ref#refs/tags/}" - run: | - tar -czf finamp-${VERSION}-linux-release.tar.gz --directory build/linux/x64/release/ bundle icons finamp.desktop.m4 com.unicornsonlsd.finamp.metainfo.xml \ - && sha256sum finamp-${VERSION}-linux-release.tar.gz > finamp-${VERSION}-linux-release.tar.gz.sha256sum + tar -czf finamp-${{ github.ref_name }}-linux-release.tar.gz --directory build/linux/x64/release/ bundle icons finamp.desktop.m4 com.unicornsonlsd.finamp.metainfo.xml \ + && sha256sum finamp-${{ github.ref_name }}-linux-release.tar.gz > finamp-${{ github.ref_name }}-linux-release.tar.gz.sha256sum - uses: actions/upload-artifact@v4 with: - name: finamp-${VERSION}-linux-release.tar.gz - path: finamp-${VERSION}-linux-release.tar.gz + name: finamp-${{ github.ref_name }}-linux-release.tar.gz + path: finamp-${{ github.ref_name }}-linux-release.tar.gz compression-level: 0 # no compression - uses: actions/upload-artifact@v4 with: - name: finamp-${VERSION}-linux-release.tar.gz.sha256sum - path: finamp-${VERSION}-linux-release.tar.gz.sha256sum + name: finamp-${{ github.ref_name }}-linux-release.tar.gz.sha256sum + path: finamp-${{ github.ref_name }}-linux-release.tar.gz.sha256sum compression-level: 0 # no compression - name: Upload release archive uses: alexellis/upload-assets@0.4.1 env: GITHUB_TOKEN: ${{ github.token }} with: - asset_paths: '["./finamp-${VERSION}-linux-release.tar.gz", "./finamp-${VERSION}-linux-release.tar.gz.sha256sum"]' + asset_paths: '["./finamp-${{ github.ref_name }}-linux-release.tar.gz", "./finamp-${{ github.ref_name }}-linux-release.tar.gz.sha256sum"]' From 36e65c7d67f5023a3b1f0ff87990d778ec643c0c Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 18 Sep 2024 22:52:15 +0200 Subject: [PATCH 07/21] shuffle only selected library when offline - closes #819 --- lib/services/audio_service_helper.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index e883125a6..e212912c9 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -27,9 +27,12 @@ class AudioServiceHelper { // This is a bit inefficient since we have to get all of the songs and // shuffle them before making a sublist, but I couldn't think of a better // way. - items = (await _isarDownloader.getAllSongs()) - .map((e) => e.baseItem!) - .toList(); + items = (await _isarDownloader.getAllSongs( + viewFilter: _finampUserHelper.currentUser?.currentView?.id, + nullableViewFilters: + FinampSettingsHelper.finampSettings.showDownloadsWithUnknownLibrary + )).map((e) => e.baseItem!) + .toList(); items.shuffle(); if (items.length - 1 > FinampSettingsHelper.finampSettings.songShuffleItemCount) { From b97e7410780e37dbe0f66442de392fa000ceb7fa Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 18 Sep 2024 23:14:26 +0200 Subject: [PATCH 08/21] fix queue source when offline and tapping track --- lib/components/AlbumScreen/song_list_tile.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index ed6af4bb7..4ee8d61b9 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -306,8 +306,12 @@ class _SongListTileState extends ConsumerState (element) => element.id == widget.item.id) : await widget.index, source: QueueItemSource( - name: const QueueItemSourceName( - type: QueueItemSourceNameType.mix), + name: QueueItemSourceName( + type: widget.item.name != null + ? QueueItemSourceNameType.mix + : QueueItemSourceNameType.instantMix, + localizationParameter: widget.item.name ?? "", + ), type: QueueItemSourceType.allSongs, id: widget.item.id, ), From fa4a500536ea6d901c01d7a0e29f60392321a692 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 19 Sep 2024 22:27:19 +0200 Subject: [PATCH 09/21] fix durations for track longer than 1h - closes #893 --- lib/components/now_playing_bar.dart | 7 ++----- lib/components/print_duration.dart | 14 +++++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 3af542007..2ffb00191 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:audio_service/audio_service.dart'; import 'package:finamp/color_schemes.g.dart'; import 'package:finamp/components/AddToPlaylistScreen/add_to_playlist_button.dart'; +import 'package:finamp/components/print_duration.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/current_track_metadata_provider.dart'; import 'package:finamp/services/feedback_helper.dart'; @@ -400,11 +401,7 @@ class NowPlayingBar extends ConsumerWidget { child: Row( children: [ Text( - // '0:00', - playbackPosition!.inHours >= - 1.0 - ? "${playbackPosition?.inHours.toString()}:${((playbackPosition?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - : "${playbackPosition?.inMinutes.toString()}:${((playbackPosition?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + printDuration(playbackPosition, leadingZeroes: false), style: TextStyle( fontSize: 14, fontWeight: diff --git a/lib/components/print_duration.dart b/lib/components/print_duration.dart index 9e6ffe9b7..2c06fc6ca 100644 --- a/lib/components/print_duration.dart +++ b/lib/components/print_duration.dart @@ -1,21 +1,25 @@ /// Flutter doesn't have a nice way of formatting durations for some reason so I stole this code from StackOverflow -String printDuration(Duration? duration, {bool isRemaining = false}) { +String printDuration(Duration? duration, { + bool isRemaining = false, + bool leadingZeroes = true, +}) { if (duration == null) { return "00:00"; } String twoDigits(int n) => n.toString().padLeft(2, "0"); - String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); + final minutes = duration.inMinutes.remainder(60); + String twoDigitMinutes = leadingZeroes ? twoDigits(minutes) : minutes.toString(); String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); String durationString; if (duration.inHours >= 1) { - String twoDigitHours = twoDigits(duration.inHours); + String twoDigitHours = leadingZeroes ? twoDigits(duration.inHours) : duration.inHours.toString(); durationString = "$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds"; + } else { + durationString = "$twoDigitMinutes:$twoDigitSeconds"; } - durationString = "$twoDigitMinutes:$twoDigitSeconds"; - if (isRemaining) { durationString = "-$durationString"; } From 62ee5eefb05c8ded73b5fe0c9ed17a669f4ff9c3 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 24 Sep 2024 20:27:20 +0200 Subject: [PATCH 10/21] add missing tooltips button, remove phantom buttons --- .../AlbumScreen/download_button.dart | 88 ++++++++----------- lib/components/now_playing_bar.dart | 1 + 2 files changed, 39 insertions(+), 50 deletions(-) diff --git a/lib/components/AlbumScreen/download_button.dart b/lib/components/AlbumScreen/download_button.dart index c1fdc71ae..ffadbc96e 100644 --- a/lib/components/AlbumScreen/download_button.dart +++ b/lib/components/AlbumScreen/download_button.dart @@ -85,57 +85,45 @@ class DownloadButton extends ConsumerWidget { tooltip: parentTooltip, ), ); - var deleteButton = Semantics.fromProperties( - properties: SemanticsProperties( - label: AppLocalizations.of(context)!.deleteItem, - button: true, - ), - container: true, - child: IconButton( - icon: const Icon(Icons.delete), - // If offline, we don't allow the user to delete items. - // If we did, we'd have to implement listeners for MusicScreenTabView so that the user can't delete a parent, go back, and select the same parent. - // If they did, AlbumScreen would show an error since the item no longer exists. - // Also, the user could delete the parent and immediately redownload it, which will either cause unwanted network usage or cause more errors because the user is offline. - onPressed: () { - showDialog( - context: context, - builder: (context) => ConfirmationPromptDialog( - promptText: AppLocalizations.of(context)!.deleteDownloadsPrompt( - item.baseItem?.name ?? "", item.baseItemType.name), - confirmButtonText: - AppLocalizations.of(context)!.deleteDownloadsConfirmButtonText, - abortButtonText: - AppLocalizations.of(context)!.deleteDownloadsAbortButtonText, - onConfirmed: () async { - try { - await downloadsService.deleteDownload(stub: item); - GlobalSnackbar.message((scaffold) => - AppLocalizations.of(scaffold)!.downloadsDeleted); - } catch (error) { - GlobalSnackbar.error(error); - } - }, - onAborted: () {}, - ), - ); - // .whenComplete(() => checkIfDownloaded()); - }, - ) + var deleteButton = IconButton( + icon: const Icon(Icons.delete), + tooltip: AppLocalizations.of(context)!.deleteItem, + // If offline, we don't allow the user to delete items. + // If we did, we'd have to implement listeners for MusicScreenTabView so that the user can't delete a parent, go back, and select the same parent. + // If they did, AlbumScreen would show an error since the item no longer exists. + // Also, the user could delete the parent and immediately redownload it, which will either cause unwanted network usage or cause more errors because the user is offline. + onPressed: () { + showDialog( + context: context, + builder: (context) => ConfirmationPromptDialog( + promptText: AppLocalizations.of(context)!.deleteDownloadsPrompt( + item.baseItem?.name ?? "", item.baseItemType.name), + confirmButtonText: + AppLocalizations.of(context)!.deleteDownloadsConfirmButtonText, + abortButtonText: + AppLocalizations.of(context)!.deleteDownloadsAbortButtonText, + onConfirmed: () async { + try { + await downloadsService.deleteDownload(stub: item); + GlobalSnackbar.message((scaffold) => + AppLocalizations.of(scaffold)!.downloadsDeleted); + } catch (error) { + GlobalSnackbar.error(error); + } + }, + onAborted: () {}, + ), + ); + // .whenComplete(() => checkIfDownloaded()); + }, ); - var syncButton = Semantics.fromProperties( - properties: SemanticsProperties( - label: AppLocalizations.of(context)!.syncDownloads, - button: true, - ), - container: true, - child: IconButton( - icon: const Icon(Icons.sync), - onPressed: () { - downloadsService.resync(item, viewId); - }, - color: status.outdated ? Colors.orange : null, - ) + var syncButton = IconButton( + icon: const Icon(Icons.sync), + tooltip: AppLocalizations.of(context)!.syncDownloads, + onPressed: () { + downloadsService.resync(item, viewId); + }, + color: status.outdated ? Colors.orange : null, ); if (isOffline) { if (status.isRequired) { diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 2ffb00191..d8c3739d3 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -247,6 +247,7 @@ class NowPlayingBar extends ConsumerWidget { color: Color.fromRGBO(0, 0, 0, 0.3), ), child: IconButton( + tooltip: "Toggle Playback", onPressed: () { FeedbackHelper.feedback( FeedbackType.light); From d9db25a816ebdb530aca29aa6f3f735b3bd40073 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 24 Sep 2024 20:48:38 +0200 Subject: [PATCH 11/21] hide fast scroller from semantics - it's not selectable anyway --- lib/components/MusicScreen/alphabet_item_list.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/components/MusicScreen/alphabet_item_list.dart b/lib/components/MusicScreen/alphabet_item_list.dart index 8b52af83b..34a83f840 100644 --- a/lib/components/MusicScreen/alphabet_item_list.dart +++ b/lib/components/MusicScreen/alphabet_item_list.dart @@ -86,7 +86,6 @@ class _AlphabetListState extends State { onPointerUp: (x) => updateSelected(x.localPosition, Drag.end), behavior: HitTestBehavior.opaque, child: Semantics( - label: 'Alphabet list for fast scrolling', excludeSemantics: true, // replace child semantics with custom semantics child: Column( mainAxisAlignment: MainAxisAlignment.center, From 6ecdb9d6d8834ba238bbd1e0280d0d78eb9fd71c Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 24 Sep 2024 22:20:14 +0200 Subject: [PATCH 12/21] add proper translatable strings for accessibility labels --- .../add_to_playlist_button.dart | 2 +- .../AlbumScreen/download_button.dart | 55 +- lib/components/PlayerScreen/album_chip.dart | 2 +- lib/components/PlayerScreen/artist_chip.dart | 2 +- .../PlayerScreen/player_buttons.dart | 13 +- .../PlayerScreen/player_buttons_more.dart | 3 +- .../player_buttons_repeating.dart | 27 +- .../PlayerScreen/player_buttons_shuffle.dart | 38 +- .../player_screen_album_image.dart | 3 +- .../PlayerScreen/progress_slider.dart | 5 +- .../PlayerScreen/song_name_content.dart | 3 +- lib/components/album_image.dart | 4 +- lib/components/now_playing_bar.dart | 654 +++++++++--------- lib/l10n/app_en.arb | 74 +- 14 files changed, 479 insertions(+), 406 deletions(-) diff --git a/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart b/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart index 58e7ea16a..0de33bace 100644 --- a/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart +++ b/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart @@ -45,7 +45,7 @@ class _AddToPlaylistButtonState extends ConsumerState { return Semantics.fromProperties( properties: SemanticsProperties( label: AppLocalizations.of(context)!.addToPlaylistTooltip, - hint: "Tap to add to playlist. Long press to toggle favorite.", + hint: AppLocalizations.of(context)!.playlistActionsMenuButtonTooltip, button: true, ), excludeSemantics: true, diff --git a/lib/components/AlbumScreen/download_button.dart b/lib/components/AlbumScreen/download_button.dart index ffadbc96e..f03a54ac1 100644 --- a/lib/components/AlbumScreen/download_button.dart +++ b/lib/components/AlbumScreen/download_button.dart @@ -53,37 +53,30 @@ class DownloadButton extends ConsumerWidget { viewId = finampUserHelper.currentUser!.currentViewId!; } - var downloadButton = Semantics.fromProperties( - properties: SemanticsProperties( - label: AppLocalizations.of(context)!.downloadItem, - button: true, - ), - container: true, - child: IconButton( - icon: status == DownloadItemStatus.notNeeded - ? const Icon(Icons.file_download) - : const Icon(Icons.lock), //TODO get better icon - onPressed: () async { - if (isLibrary) { - await showDialog( - context: context, - builder: (context) => ConfirmationPromptDialog( - promptText: AppLocalizations.of(context)! - .downloadLibraryPrompt(item.name), - confirmButtonText: - AppLocalizations.of(context)!.addButtonLabel, - abortButtonText: - MaterialLocalizations.of(context).cancelButtonLabel, - onConfirmed: () => - DownloadDialog.show(context, item, viewId), - onAborted: () {}, - )); - } else { - await DownloadDialog.show(context, item, viewId); - } - }, - tooltip: parentTooltip, - ), + var downloadButton = IconButton( + icon: status == DownloadItemStatus.notNeeded + ? const Icon(Icons.file_download) + : const Icon(Icons.lock), //TODO get better icon + onPressed: () async { + if (isLibrary) { + await showDialog( + context: context, + builder: (context) => ConfirmationPromptDialog( + promptText: AppLocalizations.of(context)! + .downloadLibraryPrompt(item.name), + confirmButtonText: + AppLocalizations.of(context)!.addButtonLabel, + abortButtonText: + MaterialLocalizations.of(context).cancelButtonLabel, + onConfirmed: () => + DownloadDialog.show(context, item, viewId), + onAborted: () {}, + )); + } else { + await DownloadDialog.show(context, item, viewId); + } + }, + tooltip: parentTooltip, ); var deleteButton = IconButton( icon: const Icon(Icons.delete), diff --git a/lib/components/PlayerScreen/album_chip.dart b/lib/components/PlayerScreen/album_chip.dart index e96c94dc9..c8fc5d4e5 100644 --- a/lib/components/PlayerScreen/album_chip.dart +++ b/lib/components/PlayerScreen/album_chip.dart @@ -73,7 +73,7 @@ class _AlbumChipContent extends StatelessWidget { return Semantics.fromProperties( properties: SemanticsProperties( - label: "$albumName (Album)", + label: "$albumName (${AppLocalizations.of(context)!.album})", button: true, ), excludeSemantics: true, diff --git a/lib/components/PlayerScreen/artist_chip.dart b/lib/components/PlayerScreen/artist_chip.dart index 756799841..d06c3b466 100644 --- a/lib/components/PlayerScreen/artist_chip.dart +++ b/lib/components/PlayerScreen/artist_chip.dart @@ -141,7 +141,7 @@ class _ArtistChipContent extends StatelessWidget { return Semantics.fromProperties( properties: SemanticsProperties( - label: "$name (Artist)", + label: "$name (${AppLocalizations.of(context)!.artist})", button: true, ), container: true, diff --git a/lib/components/PlayerScreen/player_buttons.dart b/lib/components/PlayerScreen/player_buttons.dart index 47bf2957d..1217e9608 100644 --- a/lib/components/PlayerScreen/player_buttons.dart +++ b/lib/components/PlayerScreen/player_buttons.dart @@ -7,6 +7,7 @@ import 'package:flutter/semantics.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:get_it/get_it.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../services/media_state_stream.dart'; import '../../services/music_player_background_task.dart'; @@ -34,8 +35,8 @@ class PlayerButtons extends StatelessWidget { if (controller.shouldShow(PlayerHideable.loopShuffleButtons)) PlayerButtonsRepeating(), Semantics.fromProperties( - properties: const SemanticsProperties( - label: "Skip to beginning or previous track", + properties: SemanticsProperties( + label: AppLocalizations.of(context)!.skipToPreviousTrackButtonTooltip, button: true, ), container: true, @@ -51,8 +52,8 @@ class PlayerButtons extends StatelessWidget { ), ), Semantics.fromProperties( - properties: const SemanticsProperties( - label: "Toggle playback", + properties: SemanticsProperties( + label: AppLocalizations.of(context)!.togglePlaybackButtonTooltip, button: true, ), container: true, @@ -86,8 +87,8 @@ class PlayerButtons extends StatelessWidget { ), ), Semantics.fromProperties( - properties: const SemanticsProperties( - label: "Skip to next track", + properties: SemanticsProperties( + label: AppLocalizations.of(context)!.skipToNextTrackButtonTooltip, button: true, ), container: true, diff --git a/lib/components/PlayerScreen/player_buttons_more.dart b/lib/components/PlayerScreen/player_buttons_more.dart index e799618d2..c9540a57d 100644 --- a/lib/components/PlayerScreen/player_buttons_more.dart +++ b/lib/components/PlayerScreen/player_buttons_more.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../models/finamp_models.dart'; @@ -21,7 +22,7 @@ class PlayerButtonsMore extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Semantics( - label: "Track menu", + label: AppLocalizations.of(context)!.trackMenuButtonTooltip, excludeSemantics: true, // replace child semantics with custom semantics container: true, child: IconTheme( diff --git a/lib/components/PlayerScreen/player_buttons_repeating.dart b/lib/components/PlayerScreen/player_buttons_repeating.dart index 2da469dce..e0c852051 100644 --- a/lib/components/PlayerScreen/player_buttons_repeating.dart +++ b/lib/components/PlayerScreen/player_buttons_repeating.dart @@ -23,23 +23,16 @@ class PlayerButtonsRepeating extends StatelessWidget { return StreamBuilder( stream: mediaStateStream, builder: (BuildContext context, AsyncSnapshot snapshot) { - return Semantics.fromProperties( - properties: SemanticsProperties( - label: "${getLocalizedLoopMode(context, queueService.loopMode)}. Tap to toggle.", - button: true, - ), - excludeSemantics: true, - container: true, - child: IconButton( - onPressed: () async { - FeedbackHelper.feedback(FeedbackType.light); - queueService.toggleLoopMode(); - }, - icon: _getRepeatingIcon( - queueService.loopMode, - Theme.of(context).colorScheme.secondary, - )), - ); + return IconButton( + tooltip: "${getLocalizedLoopMode(context, queueService.loopMode)}. ${AppLocalizations.of(context)!.genericToggleButtonTooltip}", + onPressed: () async { + FeedbackHelper.feedback(FeedbackType.light); + queueService.toggleLoopMode(); + }, + icon: _getRepeatingIcon( + queueService.loopMode, + Theme.of(context).colorScheme.secondary, + )); }); } diff --git a/lib/components/PlayerScreen/player_buttons_shuffle.dart b/lib/components/PlayerScreen/player_buttons_shuffle.dart index 367455d7d..896a44692 100644 --- a/lib/components/PlayerScreen/player_buttons_shuffle.dart +++ b/lib/components/PlayerScreen/player_buttons_shuffle.dart @@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:get_it/get_it.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class PlayerButtonsShuffle extends StatelessWidget { final audioHandler = GetIt.instance(); @@ -21,26 +22,29 @@ class PlayerButtonsShuffle extends StatelessWidget { return StreamBuilder( stream: mediaStateStream, builder: (BuildContext context, AsyncSnapshot snapshot) { - return Semantics.fromProperties( - properties: SemanticsProperties( - label: "Playing ${_queueService.playbackOrder == FinampPlaybackOrder.shuffled ? "shuffled" : "in order"}. Tap to toggle.", - button: true, - ), - excludeSemantics: true, - container: true, - child: IconButton( - onPressed: () async { - FeedbackHelper.feedback(FeedbackType.light); - _queueService.togglePlaybackOrder(); - }, - icon: Icon( - (_queueService.playbackOrder == FinampPlaybackOrder.shuffled - ? TablerIcons.arrows_shuffle - : TablerIcons.arrows_right), - ), + return IconButton( + tooltip: getLocalizedPlaybackOrder(context, _queueService.playbackOrder), + onPressed: () async { + FeedbackHelper.feedback(FeedbackType.light); + _queueService.togglePlaybackOrder(); + }, + icon: Icon( + (_queueService.playbackOrder == FinampPlaybackOrder.shuffled + ? TablerIcons.arrows_shuffle + : TablerIcons.arrows_right), ), ); }, ); } + + String getLocalizedPlaybackOrder(BuildContext context, FinampPlaybackOrder playbackOrder) { + switch (playbackOrder) { + case FinampPlaybackOrder.linear: + return AppLocalizations.of(context)!.playbackOrderLinearButtonTooltip; + case FinampPlaybackOrder.shuffled: + return AppLocalizations.of(context)!.playbackOrderShuffledButtonTooltip; + } + } + } diff --git a/lib/components/PlayerScreen/player_screen_album_image.dart b/lib/components/PlayerScreen/player_screen_album_image.dart index 927fd84ac..22a07f335 100644 --- a/lib/components/PlayerScreen/player_screen_album_image.dart +++ b/lib/components/PlayerScreen/player_screen_album_image.dart @@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:get_it/get_it.dart'; import 'package:simple_gesture_detector/simple_gesture_detector.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../services/current_album_image_provider.dart'; import '../../services/favorite_provider.dart'; @@ -35,7 +36,7 @@ class PlayerScreenAlbumImage extends ConsumerWidget { final currentTrack = snapshot.data!.currentTrack; return Semantics( - label: "Artwork for ${currentTrack?.item.title}. Tap to toggle playback. Swipe left or right to switch tracks.", + label: AppLocalizations.of(context)!.playerAlbumArtworkTooltip(currentTrack?.item.title ?? AppLocalizations.of(context)!.unknownName), excludeSemantics: true, // replace child semantics with custom semantics container: true, child: GestureDetector( diff --git a/lib/components/PlayerScreen/progress_slider.dart b/lib/components/PlayerScreen/progress_slider.dart index cdaa8b193..ba9975797 100644 --- a/lib/components/PlayerScreen/progress_slider.dart +++ b/lib/components/PlayerScreen/progress_slider.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../services/music_player_background_task.dart'; @@ -235,7 +236,9 @@ class __PlaybackProgressSliderState final durationFullHours = (widget.mediaItem?.duration?.inHours ?? 0); final durationFullMinutes = (widget.mediaItem?.duration?.inMinutes ?? 0) % 60; final durationSeconds = (widget.mediaItem?.duration?.inSeconds ?? 0) % 60; - return "${positionFullHours > 0 ? "$positionFullHours hours " : ""}${positionFullMinutes > 0 ? "$positionFullMinutes minutes " : ""}$positionSeconds seconds of ${durationFullHours > 0 ? "$durationFullHours hours " : ""}${durationFullMinutes > 0 ? "$durationFullMinutes minutes " : ""}$durationSeconds seconds"; + final positionString = "${positionFullHours > 0 ? "$positionFullHours ${AppLocalizations.of(context)!.hours} " : ""}${positionFullMinutes > 0 ? "$positionFullMinutes ${AppLocalizations.of(context)!.minutes} " : ""}$positionSeconds ${AppLocalizations.of(context)!.seconds}"; + final durationString = "${durationFullHours > 0 ? "$durationFullHours ${AppLocalizations.of(context)!.hours} " : ""}${durationFullMinutes > 0 ? "$durationFullMinutes ${AppLocalizations.of(context)!.minutes} " : ""}$durationSeconds ${AppLocalizations.of(context)!.seconds}"; + return AppLocalizations.of(context)!.timeFractionTooltip(positionString, durationString); }, secondaryTrackValue: widget.mediaItem?.extras?["downloadedSongPath"] == null diff --git a/lib/components/PlayerScreen/song_name_content.dart b/lib/components/PlayerScreen/song_name_content.dart index afeb2f63e..3a12bfaaf 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -7,6 +7,7 @@ import 'package:finamp/screens/player_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:get_it/get_it.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../services/queue_service.dart'; import 'album_chip.dart'; @@ -58,7 +59,7 @@ class SongNameContent extends StatelessWidget { ), child: Semantics.fromProperties( properties: SemanticsProperties( - label: "${currentTrack.item.title} (Title)", + label: "${currentTrack.item.title} (${AppLocalizations.of(context)!.title})", ), excludeSemantics: true, container: true, diff --git a/lib/components/album_image.dart b/lib/components/album_image.dart index e18dcc91e..b7c6e6fd9 100644 --- a/lib/components/album_image.dart +++ b/lib/components/album_image.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:octo_image/octo_image.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../models/jellyfin_models.dart'; import '../services/album_image_provider.dart'; @@ -75,7 +76,8 @@ class AlbumImage extends ConsumerWidget { } return Semantics( - label: "Artwork${item?.name != null ? "for ${item?.name}" : ""}", + // label: item?.name != null ? AppLocalizations.of(context)!.artworkTooltip(item!.name!) : AppLocalizations.of(context)!.artwork, // removed to reduce screen reader verbosity + excludeSemantics: true, child: AspectRatio( aspectRatio: 1.0, child: Align( diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index d8c3739d3..03e002501 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -155,347 +155,353 @@ class NowPlayingBar extends ConsumerWidget { return SafeArea( child: Padding( padding: const EdgeInsets.only(left: 12.0, bottom: 12.0, right: 12.0), - child: SimpleGestureDetector( - onTap: () => Navigator.of(context).pushNamed(PlayerScreen.routeName), - child: Dismissible( - key: const Key("NowPlayingBarDismiss"), - direction: FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.down, - confirmDismiss: (direction) async { - if (direction == DismissDirection.down) { - final queueService = GetIt.instance(); - await queueService.stopPlayback(); - } - return false; - }, - dismissThresholds: const {DismissDirection.down: 0.7}, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: getShadow(context), - //TODO use a PageView instead of a Dismissible, and only wrap dynamic items (not the buttons) - child: Dismissible( - key: const Key("NowPlayingBar"), - direction: FinampSettingsHelper.finampSettings.disableGesture - ? DismissDirection.none - : DismissDirection.horizontal, - confirmDismiss: (direction) async { - if (direction == DismissDirection.endToStart) { - await audioHandler.skipToNext(); - } else { - await audioHandler.skipToPrevious(forceSkip: true); - } - return false; - }, - child: Material( - shadowColor: Theme.of(context) - .colorScheme - .primary - .withOpacity( - Theme.of(context).brightness == Brightness.light - ? 0.75 - : 0.3), - borderRadius: BorderRadius.circular(12.0), - clipBehavior: Clip.antiAlias, - color: Theme.of(context).brightness == Brightness.dark - ? IconTheme.of(context).color!.withOpacity(0.1) - : Theme.of(context).cardColor, - elevation: 8.0, - child: StreamBuilder( - stream: mediaStateStream - .where((event) => event.mediaItem != null), - initialData: MediaState(audioHandler.mediaItem.valueOrNull, - audioHandler.playbackState.value), - builder: (context, snapshot) { - final MediaState mediaState = snapshot.data!; - // If we have a media item and the player hasn't finished, show - // the now playing bar. - if (mediaState.mediaItem != null) { - //TODO move into separate component and share with queue list - return Container( - width: MediaQuery.of(context).size.width, - height: albumImageSize, - padding: EdgeInsets.zero, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: progressBackgroundColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Stack( - alignment: Alignment.center, - children: [ - AlbumImage( - placeholderBuilder: (_) => - const SizedBox.shrink(), - imageListenable: - currentAlbumImageProvider, - borderRadius: BorderRadius.zero, - ), - Container( - width: albumImageSize, - height: albumImageSize, - decoration: const ShapeDecoration( - shape: Border(), - color: Color.fromRGBO(0, 0, 0, 0.3), - ), - child: IconButton( - tooltip: "Toggle Playback", - onPressed: () { - FeedbackHelper.feedback( - FeedbackType.light); - audioHandler.togglePlayback(); - }, - icon: mediaState.playbackState.playing - ? const Icon( - TablerIcons.player_pause, - size: 32, - ) - : const Icon( - TablerIcons.player_play, - size: 32, - ), - color: Colors.white, - )), - ], + child: Semantics.fromProperties( + properties: SemanticsProperties( + label: AppLocalizations.of(context)!.nowPlayingBarTooltip, + button: true, + ), + child: SimpleGestureDetector( + onTap: () => Navigator.of(context).pushNamed(PlayerScreen.routeName), + child: Dismissible( + key: const Key("NowPlayingBarDismiss"), + direction: FinampSettingsHelper.finampSettings.disableGesture + ? DismissDirection.none + : DismissDirection.down, + confirmDismiss: (direction) async { + if (direction == DismissDirection.down) { + final queueService = GetIt.instance(); + await queueService.stopPlayback(); + } + return false; + }, + dismissThresholds: const {DismissDirection.down: 0.7}, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: getShadow(context), + //TODO use a PageView instead of a Dismissible, and only wrap dynamic items (not the buttons) + child: Dismissible( + key: const Key("NowPlayingBar"), + direction: FinampSettingsHelper.finampSettings.disableGesture + ? DismissDirection.none + : DismissDirection.horizontal, + confirmDismiss: (direction) async { + if (direction == DismissDirection.endToStart) { + await audioHandler.skipToNext(); + } else { + await audioHandler.skipToPrevious(forceSkip: true); + } + return false; + }, + child: Material( + shadowColor: Theme.of(context) + .colorScheme + .primary + .withOpacity( + Theme.of(context).brightness == Brightness.light + ? 0.75 + : 0.3), + borderRadius: BorderRadius.circular(12.0), + clipBehavior: Clip.antiAlias, + color: Theme.of(context).brightness == Brightness.dark + ? IconTheme.of(context).color!.withOpacity(0.1) + : Theme.of(context).cardColor, + elevation: 8.0, + child: StreamBuilder( + stream: mediaStateStream + .where((event) => event.mediaItem != null), + initialData: MediaState(audioHandler.mediaItem.valueOrNull, + audioHandler.playbackState.value), + builder: (context, snapshot) { + final MediaState mediaState = snapshot.data!; + // If we have a media item and the player hasn't finished, show + // the now playing bar. + if (mediaState.mediaItem != null) { + //TODO move into separate component and share with queue list + return Container( + width: MediaQuery.of(context).size.width, + height: albumImageSize, + padding: EdgeInsets.zero, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: progressBackgroundColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), ), - Expanded( - child: Stack( + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + alignment: Alignment.center, children: [ - if (FinampSettingsHelper.finampSettings.showProgressOnNowPlayingBar) - Positioned( - left: 0, - top: 0, - child: StreamBuilder( - stream: AudioService.position, - initialData: audioHandler - .playbackState.value.position, - builder: (context, snapshot) { - if (snapshot.hasData) { - playbackPosition = - snapshot.data; - final screenSize = - MediaQuery.of(context).size; - return Container( - // rather hacky workaround, using LayoutBuilder would be nice but I couldn't get it to work... - width: max( - 0, - (screenSize.width - - 3 * - horizontalPadding - - albumImageSize) * - (playbackPosition! - .inMilliseconds / - (mediaState.mediaItem - ?.duration ?? - const Duration( - seconds: - 0)) - .inMilliseconds)), - height: albumImageSize, - decoration: ShapeDecoration( - color: IconTheme.of(context) - .color! - .withOpacity(0.75), - shape: - const RoundedRectangleBorder( - borderRadius: - BorderRadius.only( - topRight: - Radius.circular(12), - bottomRight: - Radius.circular(12), + AlbumImage( + placeholderBuilder: (_) => + const SizedBox.shrink(), + imageListenable: + currentAlbumImageProvider, + borderRadius: BorderRadius.zero, + ), + Container( + width: albumImageSize, + height: albumImageSize, + decoration: const ShapeDecoration( + shape: Border(), + color: Color.fromRGBO(0, 0, 0, 0.3), + ), + child: IconButton( + tooltip: AppLocalizations.of(context)!.togglePlaybackButtonTooltip, + onPressed: () { + FeedbackHelper.feedback( + FeedbackType.light); + audioHandler.togglePlayback(); + }, + icon: mediaState.playbackState.playing + ? const Icon( + TablerIcons.player_pause, + size: 32, + ) + : const Icon( + TablerIcons.player_play, + size: 32, + ), + color: Colors.white, + )), + ], + ), + Expanded( + child: Stack( + children: [ + if (FinampSettingsHelper.finampSettings.showProgressOnNowPlayingBar) + Positioned( + left: 0, + top: 0, + child: StreamBuilder( + stream: AudioService.position, + initialData: audioHandler + .playbackState.value.position, + builder: (context, snapshot) { + if (snapshot.hasData) { + playbackPosition = + snapshot.data; + final screenSize = + MediaQuery.of(context).size; + return Container( + // rather hacky workaround, using LayoutBuilder would be nice but I couldn't get it to work... + width: max( + 0, + (screenSize.width - + 3 * + horizontalPadding - + albumImageSize) * + (playbackPosition! + .inMilliseconds / + (mediaState.mediaItem + ?.duration ?? + const Duration( + seconds: + 0)) + .inMilliseconds)), + height: albumImageSize, + decoration: ShapeDecoration( + color: IconTheme.of(context) + .color! + .withOpacity(0.75), + shape: + const RoundedRectangleBorder( + borderRadius: + BorderRadius.only( + topRight: + Radius.circular(12), + bottomRight: + Radius.circular(12), + ), ), ), + ); + } else { + return Container(); + } + }), + ), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Container( + height: albumImageSize, + padding: const EdgeInsets.only( + left: 12, right: 4), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + currentTrack.item.title, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: + FontWeight.w500, + overflow: TextOverflow + .ellipsis), ), - ); - } else { - return Container(); - } - }), - ), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Container( - height: albumImageSize, - padding: const EdgeInsets.only( - left: 12, right: 4), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - currentTrack.item.title, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: - FontWeight.w500, - overflow: TextOverflow - .ellipsis), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Expanded( - child: Text( - processArtist( - currentTrack - .item.artist, - context), - style: TextStyle( - color: Colors - .white - .withOpacity( - 0.85), - fontSize: 13, - fontWeight: - FontWeight - .w300, - overflow: - TextOverflow - .ellipsis), + const SizedBox(height: 4), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Expanded( + child: Text( + processArtist( + currentTrack + .item.artist, + context), + style: TextStyle( + color: Colors + .white + .withOpacity( + 0.85), + fontSize: 13, + fontWeight: + FontWeight + .w300, + overflow: + TextOverflow + .ellipsis), + ), ), - ), - StreamBuilder( - stream: - AudioService - .position, - initialData: - audioHandler - .playbackState - .value - .position, - builder: (context, snapshot) { - if (snapshot.hasData) { - playbackPosition = - snapshot.data; - final positionFullMinutes = (playbackPosition?.inMinutes ?? 0) % 60; - final positionFullHours = (playbackPosition?.inHours ?? 0); - final positionSeconds = (playbackPosition?.inSeconds ?? 0) % 60; - final durationFullHours = (mediaState.mediaItem?.duration?.inHours ?? 0); - final durationFullMinutes = (mediaState.mediaItem?.duration?.inMinutes ?? 0) % 60; - final durationSeconds = (mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60; - return Semantics.fromProperties( - properties: SemanticsProperties( - label: "${positionFullHours > 0 ? "$positionFullHours hours " : ""}${positionFullMinutes > 0 ? "$positionFullMinutes minutes " : ""}$positionSeconds seconds of ${durationFullHours > 0 ? "$durationFullHours hours " : ""}${durationFullMinutes > 0 ? "$durationFullMinutes minutes " : ""}$durationSeconds seconds", - ), - excludeSemantics: true, - container: true, - child: Row( - children: [ - Text( - printDuration(playbackPosition, leadingZeroes: false), - style: TextStyle( - fontSize: 14, - fontWeight: - FontWeight - .w400, - color: Colors - .white - .withOpacity( - 0.8), - ), - ), - const SizedBox( - width: 2), - Text( - '/', - style: TextStyle( - color: Colors + StreamBuilder( + stream: + AudioService + .position, + initialData: + audioHandler + .playbackState + .value + .position, + builder: (context, snapshot) { + if (snapshot.hasData) { + playbackPosition = + snapshot.data; + final positionFullMinutes = (playbackPosition?.inMinutes ?? 0) % 60; + final positionFullHours = (playbackPosition?.inHours ?? 0); + final positionSeconds = (playbackPosition?.inSeconds ?? 0) % 60; + final durationFullHours = (mediaState.mediaItem?.duration?.inHours ?? 0); + final durationFullMinutes = (mediaState.mediaItem?.duration?.inMinutes ?? 0) % 60; + final durationSeconds = (mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60; + return Semantics.fromProperties( + properties: SemanticsProperties( + label: "${positionFullHours > 0 ? "$positionFullHours hours " : ""}${positionFullMinutes > 0 ? "$positionFullMinutes minutes " : ""}$positionSeconds seconds of ${durationFullHours > 0 ? "$durationFullHours hours " : ""}${durationFullMinutes > 0 ? "$durationFullMinutes minutes " : ""}$durationSeconds seconds", + ), + excludeSemantics: true, + container: true, + child: Row( + children: [ + Text( + printDuration(playbackPosition, leadingZeroes: false), + style: TextStyle( + fontSize: 14, + fontWeight: + FontWeight + .w400, + color: Colors .white .withOpacity( 0.8), - fontSize: 14, - fontWeight: - FontWeight - .w400, + ), + ), + const SizedBox( + width: 2), + Text( + '/', + style: TextStyle( + color: Colors + .white + .withOpacity( + 0.8), + fontSize: 14, + fontWeight: + FontWeight + .w400, + ), ), - ), - const SizedBox( - width: 2), - Text( - // '3:44', - (mediaState.mediaItem?.duration - ?.inHours ?? - 0.0) >= - 1.0 - ? "${mediaState.mediaItem?.duration?.inHours.toString()}:${((mediaState.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - : "${mediaState.mediaItem?.duration?.inMinutes.toString()}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - style: TextStyle( - color: Colors - .white - .withOpacity( - 0.8), - fontSize: 14, - fontWeight: - FontWeight - .w400, + const SizedBox( + width: 2), + Text( + // '3:44', + (mediaState.mediaItem?.duration + ?.inHours ?? + 0.0) >= + 1.0 + ? "${mediaState.mediaItem?.duration?.inHours.toString()}:${((mediaState.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${mediaState.mediaItem?.duration?.inMinutes.toString()}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: TextStyle( + color: Colors + .white + .withOpacity( + 0.8), + fontSize: 14, + fontWeight: + FontWeight + .w400, + ), ), - ), - ], - ), - ); - } else { - return const SizedBox.shrink(); + ], + ), + ); + } else { + return const SizedBox.shrink(); + } } - } - ) - ], - ), - ], + ) + ], + ), + ], + ), ), ), - ), - Row( - mainAxisAlignment: - MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 4.0, right: 4.0), - child: AddToPlaylistButton( - item: currentTrackBaseItem, - queueItem: currentTrack, - color: Colors.white, - size: 28, - visualDensity: - const VisualDensity( - horizontal: -4), + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only( + top: 4.0, right: 4.0), + child: AddToPlaylistButton( + item: currentTrackBaseItem, + queueItem: currentTrack, + color: Colors.white, + size: 28, + visualDensity: + const VisualDensity( + horizontal: -4), + ), ), - ), - ], - ), - ], - ), - ], + ], + ), + ], + ), + ], + ), ), - ), - ], + ], + ), ), - ), - ); - } else { - return const SizedBox.shrink(); - } - }, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), ), ), ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 396fe08eb..7638511fe 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -219,6 +219,8 @@ "@sortOrder": {}, "sortBy": "Sort by", "@sortBy": {}, + "title": "Title", + "@title": {}, "album": "Album", "@album": {}, "albumArtist": "Album Artist", @@ -623,8 +625,16 @@ "@noButtonLabel": {}, "setSleepTimer": "Set Sleep Timer", "@setSleepTimer": {}, + "hours": "Hours", + "@hours": {}, + "seconds": "Seconds", + "@seconds": {}, "minutes": "Minutes", "@minutes": {}, + "timeFractionTooltip": "{currentTime} of {totalTime}", + "@timeFractionTooltip": { + "description": "Tooltip and accessibility label for the track progress. {currentTime} is the current position within the track, as a translated string like '2 minutes 40 seconds', and {totalTime} is the total duration of the track, also a translated string." + }, "invalidNumber": "Invalid Number", "@invalidNumber": {}, "sleepTimerTooltip": "Sleep timer", @@ -676,6 +686,10 @@ "@createButtonLabel": {}, "playlistCreated": "Playlist created.", "@playlistCreated": {}, + "playlistActionsMenuButtonTooltip": "Tap to add to playlist. Long press to toggle favorite.", + "@playlistActionsMenuButtonTooltip": { + "description": "Tooltip for the (currently heart) button that opens the playlist actions menu / playlist picker by default and can toggle the favorite status on long press" + }, "noAlbum": "No Album", "@noAlbum": {}, "noItem": "No Item", @@ -796,6 +810,18 @@ "@bufferDurationSubtitle": {}, "language": "Language", "@language": {}, + "skipToPreviousTrackButtonTooltip": "Skip to beginning or to previous track", + "@skipToPreviousTrackButtonTooltip": { + "description": "Tooltip for the button that skips to the beginning of the current track or to the previous track" + }, + "skipToNextTrackButtonTooltip": "Skip to next track", + "@skipToNextTrackButtonTooltip": { + "description": "Tooltip for the button that skips to the next track" + }, + "togglePlaybackButtonTooltip": "Toggle playback", + "@togglePlaybackButtonTooltip": { + "description": "Tooltip for the button that toggles playback" + }, "previousTracks": "Previous Tracks", "@previousTracks": { "description": "Description in the queue panel for the list of tracks that come before the current track in the queue. The tracks might not actually have been played (e.g. if the user skipped ahead to a specific track)." @@ -918,14 +944,22 @@ "@shuffleAllQueueSource": { "description": "Title for the queue source when the user is shuffling all tracks. Should be capitalized (if applicable) to be more recognizable throughout the UI" }, - "playbackOrderLinearButtonLabel": "Playing in order. Tap to shuffle.", + "playbackOrderLinearButtonLabel": "Playing in order", "@playbackOrderLinearButtonLabel": { "description": "Label for the button that toggles the playback order between linear/in-order and shuffle, while the queue is in linear/in-order mode" }, - "playbackOrderShuffledButtonLabel": "Shuffling tracks. Tap to play in order.", + "playbackOrderLinearButtonTooltip": "Playing in order. Tap to shuffle.", + "@playbackOrderLinearButtonTooltip": { + "description": "Tooltip for the button that toggles the playback order between linear/in-order and shuffle, while the queue is in linear/in-order mode" + }, + "playbackOrderShuffledButtonLabel": "Shuffling tracks", "@playbackOrderShuffledButtonLabel": { "description": "Label for the button that toggles the playback order between linear/in-order and shuffle, while the queue is in shuffle mode" }, + "playbackOrderShuffledButtonTooltip": "Shuffling tracks. Tap to play in order.", + "@playbackOrderShuffledButtonTooltip": { + "description": "Tooltip for the button that toggles the playback order between linear/in-order and shuffle, while the queue is in shuffle mode" + }, "playbackSpeedButtonLabel": "Playing at x{speed} speed", "@playbackSpeedButtonLabel": { "description": "Label for the button that toggles visibility. of the playback speed menu, {speed} is the current playback speed.", @@ -1532,6 +1566,10 @@ }, "description": "Prompt on dialog to confirm a removal from playlist" }, + "trackMenuButtonTooltip": "Track Menu", + "@trackMenuButtonTooltip": { + "description": "Tooltip for the button that opens the track menu" + }, "quickActions": "Quick Actions", "@quickActions": { "description": "Title for the short menu that can be shown when long-pressing on the menu button on the player screen. Currently only used for adding to and removing from playlists." @@ -1648,5 +1686,35 @@ "keepScreenOnWhilePlaying": "While Playing Music", "keepScreenOnWhileLyrics": "While Showing Lyrics", "keepScreenOnWhilePluggedIn": "Keep Screen On only while plugged in", - "keepScreenOnWhilePluggedInSubtitle": "Ignore the Keep Screen On setting if device is unplugged" + "keepScreenOnWhilePluggedInSubtitle": "Ignore the Keep Screen On setting if device is unplugged", + "genericToggleButtonTooltip": "Tap to toggle.", + "@genericToggleButtonTooltip": { + "description": "Tooltip for toggle buttons that can be tapped to toggle their state" + }, + "artwork": "Artwork", + "@artwork": {}, + "artworkTooltip": "Artwork for {title}", + "@artworkTooltip": { + "placeholders": { + "title": { + "type": "String", + "example": "Abbey Road" + } + }, + "description": "Tooltip for album artwork on track and album tiles as well as the album screen" + }, + "playerAlbumArtworkTooltip": "Artwork for {title}. Tap to toggle playback. Swipe left or right to switch tracks.", + "@playerAlbumArtworkTooltip": { + "placeholders": { + "title": { + "type": "String", + "example": "Abbey Road" + } + }, + "description": "Tooltip for the album artwork on the player screen" + }, + "nowPlayingBarTooltip": "Open Player Screen", + "@nowPlayingBarTooltip": { + "description": "Tooltip for the now playing bar at the bottom of the screen" + } } From d1845a38d1d858b0d7f1c06697e618d4750381cf Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 24 Sep 2024 22:30:18 +0200 Subject: [PATCH 13/21] run dart format --- .../add_to_playlist_button.dart | 5 +- ...bum_screen_content_flexible_space_bar.dart | 2 +- .../AlbumScreen/download_dialog.dart | 2 +- .../AlbumScreen/song_list_tile.dart | 16 +- ...eep_screen_on_while_charging_selector.dart | 3 +- .../MusicScreen/alphabet_item_list.dart | 3 +- lib/components/PlayerScreen/album_chip.dart | 4 +- lib/components/PlayerScreen/artist_chip.dart | 3 +- .../PlayerScreen/player_buttons.dart | 9 +- .../player_buttons_repeating.dart | 3 +- .../PlayerScreen/player_buttons_shuffle.dart | 7 +- .../player_screen_album_image.dart | 10 +- .../player_split_screen_scaffold.dart | 3 +- .../PlayerScreen/progress_slider.dart | 39 +- lib/components/PlayerScreen/queue_button.dart | 10 +- lib/components/PlayerScreen/queue_list.dart | 3 +- .../PlayerScreen/queue_source_helper.dart | 5 +- .../PlayerScreen/song_name_content.dart | 3 +- ...me_normalization_ios_base_gain_editor.dart | 3 +- lib/components/album_image.dart | 14 +- lib/components/now_playing_bar.dart | 270 ++++---- lib/components/print_duration.dart | 12 +- lib/main.dart | 2 +- lib/models/finamp_models.dart | 168 +++-- .../customization_settings_screen.dart | 26 +- lib/screens/interaction_settings_screen.dart | 10 +- lib/screens/layout_settings_screen.dart | 10 +- lib/screens/lyrics_screen.dart | 63 +- lib/screens/lyrics_settings_screen.dart | 8 +- lib/screens/player_settings_screen.dart | 3 +- lib/screens/settings_screen.dart | 169 ++--- lib/services/android_auto_helper.dart | 631 +++++++++++------- lib/services/audio_service_helper.dart | 10 +- lib/services/finamp_settings_helper.dart | 5 +- lib/services/jellyfin_api_helper.dart | 9 +- lib/services/playback_history_service.dart | 7 +- lib/services/queue_service.dart | 108 +-- 37 files changed, 974 insertions(+), 684 deletions(-) diff --git a/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart b/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart index 0de33bace..e50178ab0 100644 --- a/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart +++ b/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart @@ -72,12 +72,13 @@ class _AddToPlaylistButtonState extends ConsumerState { return GlobalSnackbar.message((context) => AppLocalizations.of(context)!.notAvailableInOfflineMode); } - + bool inPlaylist = queueItemInPlaylist(widget.queueItem); await showPlaylistActionsMenu( context: context, item: widget.item!, - parentPlaylist: inPlaylist ? widget.queueItem!.source.item : null, + parentPlaylist: + inPlaylist ? widget.queueItem!.source.item : null, usePlayerTheme: true, ); }), diff --git a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart index 2fb1f154d..558b64fde 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -437,4 +437,4 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/components/AlbumScreen/download_dialog.dart b/lib/components/AlbumScreen/download_dialog.dart index 2c9b58cff..5648f0a62 100644 --- a/lib/components/AlbumScreen/download_dialog.dart +++ b/lib/components/AlbumScreen/download_dialog.dart @@ -236,4 +236,4 @@ class _DownloadDialogState extends State { ], ); } -} \ No newline at end of file +} diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 4ee8d61b9..8d2eb95fa 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -281,12 +281,12 @@ class _SongListTileState extends ConsumerState List offlineItems; // If we're on the songs tab, just get all of the downloaded items offlineItems = await downloadsService.getAllSongs( - // nameFilter: widget.searchTerm, - viewFilter: finampUserHelper.currentUser?.currentView?.id, - nullableViewFilters: - settings.showDownloadsWithUnknownLibrary, - onlyFavorites: - settings.onlyShowFavourite && settings.trackOfflineFavorites, + // nameFilter: widget.searchTerm, + viewFilter: finampUserHelper.currentUser?.currentView?.id, + nullableViewFilters: + settings.showDownloadsWithUnknownLibrary, + onlyFavorites: settings.onlyShowFavourite && + settings.trackOfflineFavorites, ); var items = offlineItems @@ -308,8 +308,8 @@ class _SongListTileState extends ConsumerState source: QueueItemSource( name: QueueItemSourceName( type: widget.item.name != null - ? QueueItemSourceNameType.mix - : QueueItemSourceNameType.instantMix, + ? QueueItemSourceNameType.mix + : QueueItemSourceNameType.instantMix, localizationParameter: widget.item.name ?? "", ), type: QueueItemSourceType.allSongs, diff --git a/lib/components/InteractionSettingsScreen/keep_screen_on_while_charging_selector.dart b/lib/components/InteractionSettingsScreen/keep_screen_on_while_charging_selector.dart index e89969c15..a2713ce38 100644 --- a/lib/components/InteractionSettingsScreen/keep_screen_on_while_charging_selector.dart +++ b/lib/components/InteractionSettingsScreen/keep_screen_on_while_charging_selector.dart @@ -15,7 +15,8 @@ class KeepScreenOnWhilePluggedInSelector extends StatelessWidget { builder: (_, box, __) { return SwitchListTile.adaptive( title: Text(AppLocalizations.of(context)!.keepScreenOnWhilePluggedIn), - subtitle: Text(AppLocalizations.of(context)!.keepScreenOnWhilePluggedInSubtitle), + subtitle: Text( + AppLocalizations.of(context)!.keepScreenOnWhilePluggedInSubtitle), value: FinampSettingsHelper.finampSettings.keepScreenOnWhilePluggedIn, onChanged: (value) { FinampSettingsHelper.setKeepScreenOnWhileCharging(value); diff --git a/lib/components/MusicScreen/alphabet_item_list.dart b/lib/components/MusicScreen/alphabet_item_list.dart index 34a83f840..f7da9d606 100644 --- a/lib/components/MusicScreen/alphabet_item_list.dart +++ b/lib/components/MusicScreen/alphabet_item_list.dart @@ -86,7 +86,8 @@ class _AlphabetListState extends State { onPointerUp: (x) => updateSelected(x.localPosition, Drag.end), behavior: HitTestBehavior.opaque, child: Semantics( - excludeSemantics: true, // replace child semantics with custom semantics + excludeSemantics: + true, // replace child semantics with custom semantics child: Column( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( diff --git a/lib/components/PlayerScreen/album_chip.dart b/lib/components/PlayerScreen/album_chip.dart index c8fc5d4e5..77a2a8762 100644 --- a/lib/components/PlayerScreen/album_chip.dart +++ b/lib/components/PlayerScreen/album_chip.dart @@ -88,8 +88,8 @@ class _AlbumChipContent extends StatelessWidget { (album) => Navigator.of(context).pushNamed( AlbumScreen.routeName, arguments: album!.baseItem!)) - : () => jellyfinApiHelper.getItemById(item.albumId!).then((album) => - Navigator.of(context) + : () => jellyfinApiHelper.getItemById(item.albumId!).then( + (album) => Navigator.of(context) .pushNamed(AlbumScreen.routeName, arguments: album)), child: Padding( padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), diff --git a/lib/components/PlayerScreen/artist_chip.dart b/lib/components/PlayerScreen/artist_chip.dart index d06c3b466..244c483f6 100644 --- a/lib/components/PlayerScreen/artist_chip.dart +++ b/lib/components/PlayerScreen/artist_chip.dart @@ -106,7 +106,8 @@ class ArtistChip extends ConsumerWidget { final BaseItemDto? localArtist; if (artist != null && FinampSettingsHelper.finampSettings.showArtistChipImage) { - localArtist = ref.watch(artistItemProvider(artist!.id)).valueOrNull ?? artist; + localArtist = + ref.watch(artistItemProvider(artist!.id)).valueOrNull ?? artist; } else { localArtist = artist; } diff --git a/lib/components/PlayerScreen/player_buttons.dart b/lib/components/PlayerScreen/player_buttons.dart index 1217e9608..40ba65de8 100644 --- a/lib/components/PlayerScreen/player_buttons.dart +++ b/lib/components/PlayerScreen/player_buttons.dart @@ -36,7 +36,8 @@ class PlayerButtons extends StatelessWidget { PlayerButtonsRepeating(), Semantics.fromProperties( properties: SemanticsProperties( - label: AppLocalizations.of(context)!.skipToPreviousTrackButtonTooltip, + label: AppLocalizations.of(context)! + .skipToPreviousTrackButtonTooltip, button: true, ), container: true, @@ -53,7 +54,8 @@ class PlayerButtons extends StatelessWidget { ), Semantics.fromProperties( properties: SemanticsProperties( - label: AppLocalizations.of(context)!.togglePlaybackButtonTooltip, + label: + AppLocalizations.of(context)!.togglePlaybackButtonTooltip, button: true, ), container: true, @@ -88,7 +90,8 @@ class PlayerButtons extends StatelessWidget { ), Semantics.fromProperties( properties: SemanticsProperties( - label: AppLocalizations.of(context)!.skipToNextTrackButtonTooltip, + label: AppLocalizations.of(context)! + .skipToNextTrackButtonTooltip, button: true, ), container: true, diff --git a/lib/components/PlayerScreen/player_buttons_repeating.dart b/lib/components/PlayerScreen/player_buttons_repeating.dart index e0c852051..72a0f890f 100644 --- a/lib/components/PlayerScreen/player_buttons_repeating.dart +++ b/lib/components/PlayerScreen/player_buttons_repeating.dart @@ -24,7 +24,8 @@ class PlayerButtonsRepeating extends StatelessWidget { stream: mediaStateStream, builder: (BuildContext context, AsyncSnapshot snapshot) { return IconButton( - tooltip: "${getLocalizedLoopMode(context, queueService.loopMode)}. ${AppLocalizations.of(context)!.genericToggleButtonTooltip}", + tooltip: + "${getLocalizedLoopMode(context, queueService.loopMode)}. ${AppLocalizations.of(context)!.genericToggleButtonTooltip}", onPressed: () async { FeedbackHelper.feedback(FeedbackType.light); queueService.toggleLoopMode(); diff --git a/lib/components/PlayerScreen/player_buttons_shuffle.dart b/lib/components/PlayerScreen/player_buttons_shuffle.dart index 896a44692..71dcfa654 100644 --- a/lib/components/PlayerScreen/player_buttons_shuffle.dart +++ b/lib/components/PlayerScreen/player_buttons_shuffle.dart @@ -23,7 +23,8 @@ class PlayerButtonsShuffle extends StatelessWidget { stream: mediaStateStream, builder: (BuildContext context, AsyncSnapshot snapshot) { return IconButton( - tooltip: getLocalizedPlaybackOrder(context, _queueService.playbackOrder), + tooltip: + getLocalizedPlaybackOrder(context, _queueService.playbackOrder), onPressed: () async { FeedbackHelper.feedback(FeedbackType.light); _queueService.togglePlaybackOrder(); @@ -38,7 +39,8 @@ class PlayerButtonsShuffle extends StatelessWidget { ); } - String getLocalizedPlaybackOrder(BuildContext context, FinampPlaybackOrder playbackOrder) { + String getLocalizedPlaybackOrder( + BuildContext context, FinampPlaybackOrder playbackOrder) { switch (playbackOrder) { case FinampPlaybackOrder.linear: return AppLocalizations.of(context)!.playbackOrderLinearButtonTooltip; @@ -46,5 +48,4 @@ class PlayerButtonsShuffle extends StatelessWidget { return AppLocalizations.of(context)!.playbackOrderShuffledButtonTooltip; } } - } diff --git a/lib/components/PlayerScreen/player_screen_album_image.dart b/lib/components/PlayerScreen/player_screen_album_image.dart index 22a07f335..ed5ba7956 100644 --- a/lib/components/PlayerScreen/player_screen_album_image.dart +++ b/lib/components/PlayerScreen/player_screen_album_image.dart @@ -36,8 +36,11 @@ class PlayerScreenAlbumImage extends ConsumerWidget { final currentTrack = snapshot.data!.currentTrack; return Semantics( - label: AppLocalizations.of(context)!.playerAlbumArtworkTooltip(currentTrack?.item.title ?? AppLocalizations.of(context)!.unknownName), - excludeSemantics: true, // replace child semantics with custom semantics + label: AppLocalizations.of(context)!.playerAlbumArtworkTooltip( + currentTrack?.item.title ?? + AppLocalizations.of(context)!.unknownName), + excludeSemantics: + true, // replace child semantics with custom semantics container: true, child: GestureDetector( onSecondaryTapDown: (_) async { @@ -58,7 +61,8 @@ class PlayerScreenAlbumImage extends ConsumerWidget { child: SimpleGestureDetector( //TODO replace with PageView, this is just a placeholder onTap: () { - final audioService = GetIt.instance(); + final audioService = + GetIt.instance(); audioService.togglePlayback(); FeedbackHelper.feedback(FeedbackType.selection); }, diff --git a/lib/components/PlayerScreen/player_split_screen_scaffold.dart b/lib/components/PlayerScreen/player_split_screen_scaffold.dart index 2251683d6..8476f4837 100644 --- a/lib/components/PlayerScreen/player_split_screen_scaffold.dart +++ b/lib/components/PlayerScreen/player_split_screen_scaffold.dart @@ -134,8 +134,7 @@ Widget buildPlayerSplitScreenScaffold(BuildContext context, Widget? widget) { arguments: x.arguments); return EmptyRoute(); }, - observers: [KeepScreenOnObserver()] - ), + observers: [KeepScreenOnObserver()]), )), ) ]); diff --git a/lib/components/PlayerScreen/progress_slider.dart b/lib/components/PlayerScreen/progress_slider.dart index ba9975797..1f697dec4 100644 --- a/lib/components/PlayerScreen/progress_slider.dart +++ b/lib/components/PlayerScreen/progress_slider.dart @@ -136,8 +136,10 @@ class _ProgressSliderDuration extends StatelessWidget { @override Widget build(BuildContext context) { final showRemaining = Platform.isIOS || Platform.isMacOS; - final currentPosition = Duration(seconds: (position.inMilliseconds / 1000).round()); - final roundedDuration = Duration(seconds: ((itemDuration?.inMilliseconds ?? 0) / 1000).round()); + final currentPosition = + Duration(seconds: (position.inMilliseconds / 1000).round()); + final roundedDuration = + Duration(seconds: ((itemDuration?.inMilliseconds ?? 0) / 1000).round()); return Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -151,10 +153,9 @@ class _ProgressSliderDuration extends StatelessWidget { Text( printDuration( // display remaining time if on iOS or macOS - showRemaining ? - (roundedDuration - currentPosition) - : roundedDuration - , + showRemaining + ? (roundedDuration - currentPosition) + : roundedDuration, isRemaining: showRemaining, ), style: Theme.of(context).textTheme.bodySmall?.copyWith( @@ -230,15 +231,23 @@ class __PlaybackProgressSliderState .clamp(0, widget.mediaItem!.duration!.inMicroseconds.toDouble()) .toDouble(), semanticFormatterCallback: (double value) { - final positionFullMinutes = Duration(microseconds: value.toInt()).inMinutes % 60; - final positionFullHours = Duration(microseconds: value.toInt()).inHours; - final positionSeconds = Duration(microseconds: value.toInt()).inSeconds % 60; + final positionFullMinutes = + Duration(microseconds: value.toInt()).inMinutes % 60; + final positionFullHours = + Duration(microseconds: value.toInt()).inHours; + final positionSeconds = + Duration(microseconds: value.toInt()).inSeconds % 60; final durationFullHours = (widget.mediaItem?.duration?.inHours ?? 0); - final durationFullMinutes = (widget.mediaItem?.duration?.inMinutes ?? 0) % 60; - final durationSeconds = (widget.mediaItem?.duration?.inSeconds ?? 0) % 60; - final positionString = "${positionFullHours > 0 ? "$positionFullHours ${AppLocalizations.of(context)!.hours} " : ""}${positionFullMinutes > 0 ? "$positionFullMinutes ${AppLocalizations.of(context)!.minutes} " : ""}$positionSeconds ${AppLocalizations.of(context)!.seconds}"; - final durationString = "${durationFullHours > 0 ? "$durationFullHours ${AppLocalizations.of(context)!.hours} " : ""}${durationFullMinutes > 0 ? "$durationFullMinutes ${AppLocalizations.of(context)!.minutes} " : ""}$durationSeconds ${AppLocalizations.of(context)!.seconds}"; - return AppLocalizations.of(context)!.timeFractionTooltip(positionString, durationString); + final durationFullMinutes = + (widget.mediaItem?.duration?.inMinutes ?? 0) % 60; + final durationSeconds = + (widget.mediaItem?.duration?.inSeconds ?? 0) % 60; + final positionString = + "${positionFullHours > 0 ? "$positionFullHours ${AppLocalizations.of(context)!.hours} " : ""}${positionFullMinutes > 0 ? "$positionFullMinutes ${AppLocalizations.of(context)!.minutes} " : ""}$positionSeconds ${AppLocalizations.of(context)!.seconds}"; + final durationString = + "${durationFullHours > 0 ? "$durationFullHours ${AppLocalizations.of(context)!.hours} " : ""}${durationFullMinutes > 0 ? "$durationFullMinutes ${AppLocalizations.of(context)!.minutes} " : ""}$durationSeconds ${AppLocalizations.of(context)!.seconds}"; + return AppLocalizations.of(context)! + .timeFractionTooltip(positionString, durationString); }, secondaryTrackValue: widget.mediaItem?.extras?["downloadedSongPath"] == null @@ -274,7 +283,7 @@ class __PlaybackProgressSliderState // Seek to the new position await _audioHandler .seek(Duration(microseconds: newValue.toInt())); - + // Clear drag value so that the slider uses the play // duration again. if (mounted) { diff --git a/lib/components/PlayerScreen/queue_button.dart b/lib/components/PlayerScreen/queue_button.dart index b6e503564..67432f878 100644 --- a/lib/components/PlayerScreen/queue_button.dart +++ b/lib/components/PlayerScreen/queue_button.dart @@ -13,10 +13,10 @@ class QueueButton extends StatelessWidget { @override Widget build(BuildContext context) { return SimpleButton( - text: AppLocalizations.of(context)!.queue, - icon: TablerIcons.playlist, - onPressed: () { - showQueueBottomSheet(context); - }); + text: AppLocalizations.of(context)!.queue, + icon: TablerIcons.playlist, + onPressed: () { + showQueueBottomSheet(context); + }); } } diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 374b683c7..8deb199c0 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -804,7 +804,8 @@ class _CurrentTrackState extends State { width: (screenSize.width - 2 * horizontalPadding - albumImageSize) * - ((playbackPosition?.inMilliseconds ?? 0) / + ((playbackPosition?.inMilliseconds ?? + 0) / (mediaState?.mediaItem ?.duration ?? const Duration( diff --git a/lib/components/PlayerScreen/queue_source_helper.dart b/lib/components/PlayerScreen/queue_source_helper.dart index 4625c15e8..874f13318 100644 --- a/lib/components/PlayerScreen/queue_source_helper.dart +++ b/lib/components/PlayerScreen/queue_source_helper.dart @@ -102,7 +102,8 @@ Future removeFromPlaylist(BuildContext context, BaseItemDto item, // re-sync playlist to delete removed item if not required anymore final downloadsService = GetIt.instance(); unawaited(downloadsService.resync( - DownloadStub.fromItem(type: DownloadItemType.collection, item: parent), + DownloadStub.fromItem( + type: DownloadItemType.collection, item: parent), null, keepSlow: true)); @@ -112,9 +113,7 @@ Future removeFromPlaylist(BuildContext context, BaseItemDto item, (context) => AppLocalizations.of(context)!.removedFromPlaylist, isConfirmation: true); return true; - } catch (err) { - GlobalSnackbar.error(err); return false; } diff --git a/lib/components/PlayerScreen/song_name_content.dart b/lib/components/PlayerScreen/song_name_content.dart index 3a12bfaaf..1234d6485 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -59,7 +59,8 @@ class SongNameContent extends StatelessWidget { ), child: Semantics.fromProperties( properties: SemanticsProperties( - label: "${currentTrack.item.title} (${AppLocalizations.of(context)!.title})", + label: + "${currentTrack.item.title} (${AppLocalizations.of(context)!.title})", ), excludeSemantics: true, container: true, diff --git a/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_ios_base_gain_editor.dart b/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_ios_base_gain_editor.dart index 72c543e3b..c4ffc55d1 100644 --- a/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_ios_base_gain_editor.dart +++ b/lib/components/VolumeNormalizationSettingsScreen/volume_normalization_ios_base_gain_editor.dart @@ -29,7 +29,8 @@ class _VolumeNormalizationIOSBaseGainEditorState child: TextField( controller: _controller, textAlign: TextAlign.center, - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, signed: true), onChanged: (value) { final valueDouble = double.tryParse(value); diff --git a/lib/components/album_image.dart b/lib/components/album_image.dart index b7c6e6fd9..076c6409b 100644 --- a/lib/components/album_image.dart +++ b/lib/components/album_image.dart @@ -94,14 +94,17 @@ class AlbumImage extends ConsumerWidget { // Because of this, we convert the logical pixels to physical pixels by multiplying by the device's DPI. final MediaQueryData mediaQuery = MediaQuery.of(context); physicalWidth = - (constraints.maxWidth * mediaQuery.devicePixelRatio).toInt(); + (constraints.maxWidth * mediaQuery.devicePixelRatio) + .toInt(); physicalHeight = - (constraints.maxHeight * mediaQuery.devicePixelRatio).toInt(); + (constraints.maxHeight * mediaQuery.devicePixelRatio) + .toInt(); // If using grid music screen view without fixed size tiles, and if the view is resizable due // to being on desktop and using split screen, then clamp album size to reduce server requests when resizing. if ((!(Platform.isIOS || Platform.isAndroid) || usingPlayerSplitScreen) && - !FinampSettingsHelper.finampSettings.useFixedSizeGridTiles && + !FinampSettingsHelper + .finampSettings.useFixedSizeGridTiles && FinampSettingsHelper.finampSettings.contentViewType == ContentViewType.grid) { physicalWidth = @@ -110,7 +113,7 @@ class AlbumImage extends ConsumerWidget { exp((log(physicalHeight) * 3).ceil() / 3).toInt(); } } - + var image = Container( decoration: decoration, child: BareAlbumImage( @@ -123,7 +126,8 @@ class AlbumImage extends ConsumerWidget { imageProviderCallback: themeCallback == null ? null : (image) => themeCallback!( - FinampTheme.fromImageDeferred(image, item?.blurHash)), + FinampTheme.fromImageDeferred( + image, item?.blurHash)), placeholderBuilder: placeholderBuilder), ); return disabled diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 03e002501..bb4172afd 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -48,25 +48,20 @@ class NowPlayingBar extends ConsumerWidget { ]); Color getProgressBackgroundColor(BuildContext context) { - return FinampSettingsHelper.finampSettings.showProgressOnNowPlayingBar ? - Color.alphaBlend( - Theme.of(context).brightness == Brightness.dark - ? IconTheme.of(context) - .color! - .withOpacity(0.35) - : IconTheme.of(context) - .color! - .withOpacity(0.5), - Theme.of(context).brightness == - Brightness.dark - ? Colors.black - : Colors.white - ) : - IconTheme.of(context).color!.withOpacity(0.85); + return FinampSettingsHelper.finampSettings.showProgressOnNowPlayingBar + ? Color.alphaBlend( + Theme.of(context).brightness == Brightness.dark + ? IconTheme.of(context).color!.withOpacity(0.35) + : IconTheme.of(context).color!.withOpacity(0.5), + Theme.of(context).brightness == Brightness.dark + ? Colors.black + : Colors.white) + : IconTheme.of(context).color!.withOpacity(0.85); } Widget buildLoadingQueueBar(BuildContext context, Function()? retryCallback) { - final progressBackgroundColor = getProgressBackgroundColor(context).withOpacity(0.5); + final progressBackgroundColor = + getProgressBackgroundColor(context).withOpacity(0.5); return SimpleGestureDetector( onVerticalSwipe: (direction) { @@ -140,7 +135,6 @@ class NowPlayingBar extends ConsumerWidget { Widget buildNowPlayingBar( BuildContext context, FinampQueueItem currentTrack) { - final audioHandler = GetIt.instance(); Duration? playbackPosition; @@ -151,7 +145,7 @@ class NowPlayingBar extends ConsumerWidget { : null; final progressBackgroundColor = getProgressBackgroundColor(context); - + return SafeArea( child: Padding( padding: const EdgeInsets.only(left: 12.0, bottom: 12.0, right: 12.0), @@ -161,7 +155,8 @@ class NowPlayingBar extends ConsumerWidget { button: true, ), child: SimpleGestureDetector( - onTap: () => Navigator.of(context).pushNamed(PlayerScreen.routeName), + onTap: () => + Navigator.of(context).pushNamed(PlayerScreen.routeName), child: Dismissible( key: const Key("NowPlayingBarDismiss"), direction: FinampSettingsHelper.finampSettings.disableGesture @@ -209,7 +204,8 @@ class NowPlayingBar extends ConsumerWidget { child: StreamBuilder( stream: mediaStateStream .where((event) => event.mediaItem != null), - initialData: MediaState(audioHandler.mediaItem.valueOrNull, + initialData: MediaState( + audioHandler.mediaItem.valueOrNull, audioHandler.playbackState.value), builder: (context, snapshot) { final MediaState mediaState = snapshot.data!; @@ -252,13 +248,16 @@ class NowPlayingBar extends ConsumerWidget { color: Color.fromRGBO(0, 0, 0, 0.3), ), child: IconButton( - tooltip: AppLocalizations.of(context)!.togglePlaybackButtonTooltip, + tooltip: AppLocalizations.of( + context)! + .togglePlaybackButtonTooltip, onPressed: () { FeedbackHelper.feedback( FeedbackType.light); audioHandler.togglePlayback(); }, - icon: mediaState.playbackState.playing + icon: mediaState + .playbackState.playing ? const Icon( TablerIcons.player_pause, size: 32, @@ -274,20 +273,24 @@ class NowPlayingBar extends ConsumerWidget { Expanded( child: Stack( children: [ - if (FinampSettingsHelper.finampSettings.showProgressOnNowPlayingBar) + if (FinampSettingsHelper.finampSettings + .showProgressOnNowPlayingBar) Positioned( left: 0, top: 0, child: StreamBuilder( stream: AudioService.position, initialData: audioHandler - .playbackState.value.position, + .playbackState + .value + .position, builder: (context, snapshot) { if (snapshot.hasData) { playbackPosition = snapshot.data; final screenSize = - MediaQuery.of(context).size; + MediaQuery.of(context) + .size; return Container( // rather hacky workaround, using LayoutBuilder would be nice but I couldn't get it to work... width: max( @@ -301,22 +304,25 @@ class NowPlayingBar extends ConsumerWidget { (mediaState.mediaItem ?.duration ?? const Duration( - seconds: - 0)) + seconds: 0)) .inMilliseconds)), height: albumImageSize, - decoration: ShapeDecoration( - color: IconTheme.of(context) - .color! - .withOpacity(0.75), + decoration: + ShapeDecoration( + color: IconTheme.of( + context) + .color! + .withOpacity(0.75), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topRight: - Radius.circular(12), + Radius.circular( + 12), bottomRight: - Radius.circular(12), + Radius.circular( + 12), ), ), ), @@ -337,7 +343,8 @@ class NowPlayingBar extends ConsumerWidget { padding: const EdgeInsets.only( left: 12, right: 4), child: Column( - mainAxisSize: MainAxisSize.min, + mainAxisSize: + MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: @@ -363,7 +370,8 @@ class NowPlayingBar extends ConsumerWidget { child: Text( processArtist( currentTrack - .item.artist, + .item + .artist, context), style: TextStyle( color: Colors @@ -380,89 +388,118 @@ class NowPlayingBar extends ConsumerWidget { ), ), StreamBuilder( - stream: - AudioService - .position, - initialData: - audioHandler - .playbackState - .value - .position, - builder: (context, snapshot) { - if (snapshot.hasData) { - playbackPosition = - snapshot.data; - final positionFullMinutes = (playbackPosition?.inMinutes ?? 0) % 60; - final positionFullHours = (playbackPosition?.inHours ?? 0); - final positionSeconds = (playbackPosition?.inSeconds ?? 0) % 60; - final durationFullHours = (mediaState.mediaItem?.duration?.inHours ?? 0); - final durationFullMinutes = (mediaState.mediaItem?.duration?.inMinutes ?? 0) % 60; - final durationSeconds = (mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60; - return Semantics.fromProperties( - properties: SemanticsProperties( - label: "${positionFullHours > 0 ? "$positionFullHours hours " : ""}${positionFullMinutes > 0 ? "$positionFullMinutes minutes " : ""}$positionSeconds seconds of ${durationFullHours > 0 ? "$durationFullHours hours " : ""}${durationFullMinutes > 0 ? "$durationFullMinutes minutes " : ""}$durationSeconds seconds", - ), - excludeSemantics: true, - container: true, - child: Row( - children: [ + stream: AudioService + .position, + initialData: + audioHandler + .playbackState + .value + .position, + builder: (context, + snapshot) { + if (snapshot + .hasData) { + playbackPosition = + snapshot + .data; + final positionFullMinutes = + (playbackPosition?.inMinutes ?? + 0) % + 60; + final positionFullHours = + (playbackPosition + ?.inHours ?? + 0); + final positionSeconds = + (playbackPosition?.inSeconds ?? + 0) % + 60; + final durationFullHours = (mediaState + .mediaItem + ?.duration + ?.inHours ?? + 0); + final durationFullMinutes = + (mediaState.mediaItem?.duration?.inMinutes ?? + 0) % + 60; + final durationSeconds = + (mediaState.mediaItem?.duration?.inSeconds ?? + 0) % + 60; + return Semantics + .fromProperties( + properties: + SemanticsProperties( + label: + "${positionFullHours > 0 ? "$positionFullHours hours " : ""}${positionFullMinutes > 0 ? "$positionFullMinutes minutes " : ""}$positionSeconds seconds of ${durationFullHours > 0 ? "$durationFullHours hours " : ""}${durationFullMinutes > 0 ? "$durationFullMinutes minutes " : ""}$durationSeconds seconds", + ), + excludeSemantics: + true, + container: + true, + child: Row( + children: [ Text( - printDuration(playbackPosition, leadingZeroes: false), - style: TextStyle( - fontSize: 14, - fontWeight: - FontWeight - .w400, - color: Colors - .white - .withOpacity( - 0.8), - ), + printDuration( + playbackPosition, + leadingZeroes: + false), + style: + TextStyle( + fontSize: + 14, + fontWeight: + FontWeight.w400, + color: Colors + .white + .withOpacity(0.8), ), - const SizedBox( - width: 2), - Text( - '/', - style: TextStyle( - color: Colors - .white - .withOpacity( - 0.8), - fontSize: 14, - fontWeight: - FontWeight - .w400, ), - ), - const SizedBox( - width: 2), - Text( - // '3:44', - (mediaState.mediaItem?.duration - ?.inHours ?? - 0.0) >= - 1.0 - ? "${mediaState.mediaItem?.duration?.inHours.toString()}:${((mediaState.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" - : "${mediaState.mediaItem?.duration?.inMinutes.toString()}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - style: TextStyle( - color: Colors - .white - .withOpacity( - 0.8), - fontSize: 14, - fontWeight: - FontWeight - .w400, + const SizedBox( + width: + 2), + Text( + '/', + style: + TextStyle( + color: Colors + .white + .withOpacity(0.8), + fontSize: + 14, + fontWeight: + FontWeight.w400, + ), + ), + const SizedBox( + width: + 2), + Text( + // '3:44', + (mediaState.mediaItem?.duration?.inHours ?? 0.0) >= + 1.0 + ? "${mediaState.mediaItem?.duration?.inHours.toString()}:${((mediaState.mediaItem?.duration?.inMinutes ?? 0) % 60).toString().padLeft(2, '0')}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}" + : "${mediaState.mediaItem?.duration?.inMinutes.toString()}:${((mediaState.mediaItem?.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + style: + TextStyle( + color: Colors + .white + .withOpacity(0.8), + fontSize: + 14, + fontWeight: + FontWeight.w400, + ), ), - ), - ], - ), - ); - } else { - return const SizedBox.shrink(); - } - } - ) + ], + ), + ); + } else { + return const SizedBox + .shrink(); + } + }) ], ), ], @@ -474,8 +511,9 @@ class NowPlayingBar extends ConsumerWidget { MainAxisAlignment.end, children: [ Padding( - padding: const EdgeInsets.only( - top: 4.0, right: 4.0), + padding: + const EdgeInsets.only( + top: 4.0, right: 4.0), child: AddToPlaylistButton( item: currentTrackBaseItem, queueItem: currentTrack, diff --git a/lib/components/print_duration.dart b/lib/components/print_duration.dart index 2c06fc6ca..eb9a934af 100644 --- a/lib/components/print_duration.dart +++ b/lib/components/print_duration.dart @@ -1,5 +1,6 @@ /// Flutter doesn't have a nice way of formatting durations for some reason so I stole this code from StackOverflow -String printDuration(Duration? duration, { +String printDuration( + Duration? duration, { bool isRemaining = false, bool leadingZeroes = true, }) { @@ -9,12 +10,15 @@ String printDuration(Duration? duration, { String twoDigits(int n) => n.toString().padLeft(2, "0"); final minutes = duration.inMinutes.remainder(60); - String twoDigitMinutes = leadingZeroes ? twoDigits(minutes) : minutes.toString(); + String twoDigitMinutes = + leadingZeroes ? twoDigits(minutes) : minutes.toString(); String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); String durationString; if (duration.inHours >= 1) { - String twoDigitHours = leadingZeroes ? twoDigits(duration.inHours) : duration.inHours.toString(); + String twoDigitHours = leadingZeroes + ? twoDigits(duration.inHours) + : duration.inHours.toString(); durationString = "$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds"; } else { durationString = "$twoDigitMinutes:$twoDigitSeconds"; @@ -23,6 +27,6 @@ String printDuration(Duration? duration, { if (isRemaining) { durationString = "-$durationString"; } - + return durationString; } diff --git a/lib/main.dart b/lib/main.dart index e76a12faf..298d1e92f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -117,7 +117,7 @@ void main() async { : LocaleHelper.locale.toString()) : "en_US"; await initializeDateFormatting(localeString, null); - + runApp(const Finamp()); } } diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 8404ebbfc..41d13e3f7 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -116,85 +116,87 @@ const _keepScreenOnWhilePluggedIn = true; @HiveType(typeId: 28) class FinampSettings { - FinampSettings({ - this.isOffline = _isOfflineDefault, - this.shouldTranscode = _shouldTranscodeDefault, - this.transcodeBitrate = _transcodeBitrateDefault, - // downloadLocations is required since the other values can be created with - // default values. create() is used to return a FinampSettings with - // downloadLocations. - required this.downloadLocations, - this.androidStopForegroundOnPause = _androidStopForegroundOnPauseDefault, - required this.showTabs, - this.onlyShowFavourite = _isFavouriteDefault, - this.sortBy = SortBy.sortName, - this.sortOrder = SortOrder.ascending, - this.songShuffleItemCount = _songShuffleItemCountDefault, - this.volumeNormalizationActive = _volumeNormalizationActiveDefault, - this.volumeNormalizationIOSBaseGain = - _volumeNormalizationIOSBaseGainDefault, - this.volumeNormalizationMode = _volumeNormalizationModeDefault, - this.contentViewType = _contentViewType, - this.playbackSpeedVisibility = _playbackSpeedVisibility, - this.contentGridViewCrossAxisCountPortrait = - _contentGridViewCrossAxisCountPortrait, - this.contentGridViewCrossAxisCountLandscape = - _contentGridViewCrossAxisCountLandscape, - this.showTextOnGridView = _showTextOnGridView, - this.sleepTimerSeconds = _sleepTimerSeconds, - required this.downloadLocationsMap, - this.useCoverAsBackground = _useCoverAsBackground, - this.playerScreenCoverMinimumPadding = _playerScreenCoverMinimumPadding, - this.hideSongArtistsIfSameAsAlbumArtists = - _hideSongArtistsIfSameAsAlbumArtists, - this.showArtistsTopSongs = _showArtistsTopSongs, - this.bufferDurationSeconds = _bufferDurationSeconds, - required this.tabSortBy, - required this.tabSortOrder, - this.loopMode = _defaultLoopMode, - this.playbackSpeed = _defaultPlaybackSpeed, - this.tabOrder = _tabOrder, - this.autoloadLastQueueOnStartup = _autoLoadLastQueueOnStartup, - this.hasCompletedBlurhashImageMigration = true, - this.hasCompletedBlurhashImageMigrationIdFix = true, - this.hasCompleteddownloadsServiceMigration = true, - this.requireWifiForDownloads = false, - this.onlyShowFullyDownloaded = false, - this.showDownloadsWithUnknownLibrary = true, - this.maxConcurrentDownloads = 10, - this.downloadWorkers = 5, - this.resyncOnStartup = _defaultResyncOnStartup, - this.preferQuickSyncs = true, - this.hasCompletedIsarUserMigration = true, - this.downloadTranscodingCodec, - this.downloadTranscodeBitrate, - this.shouldTranscodeDownloads = _shouldTranscodeDownloadsDefault, - this.shouldRedownloadTranscodes = _shouldRedownloadTranscodesDefault, - this.swipeInsertQueueNext = _swipeInsertQueueNext, - this.useFixedSizeGridTiles = false, - this.fixedGridTileSize = _fixedGridTileSizeDefault, - this.allowSplitScreen = true, - this.splitScreenPlayerWidth = _defaultSplitScreenPlayerWidth, - this.enableVibration = _enableVibration, - this.prioritizeCoverFactor = _prioritizeCoverFactor, - this.suppressPlayerPadding = _suppressPlayerPadding, - this.hidePlayerBottomActions = _hidePlayerBottomActions, - this.reportQueueToServer = _reportQueueToServerDefault, - this.periodicPlaybackSessionUpdateFrequencySeconds = - _periodicPlaybackSessionUpdateFrequencySecondsDefault, - this.showArtistChipImage = _showArtistChipImage, - this.trackOfflineFavorites = _trackOfflineFavoritesDefault, - this.showProgressOnNowPlayingBar = _showProgressOnNowPlayingBarDefault, - this.startInstantMixForIndividualTracks = _startInstantMixForIndividualTracksDefault, - this.showLyricsTimestamps = _showLyricsTimestampsDefault, - this.lyricsAlignment = _lyricsAlignmentDefault, - this.lyricsFontSize = _lyricsFontSizeDefault, - this.showLyricsScreenAlbumPrelude = _showLyricsScreenAlbumPreludeDefault, - this.showStopButtonOnMediaNotification = _showStopButtonOnMediaNotificationDefault, - this.showSeekControlsOnMediaNotification = _showSeekControlsOnMediaNotificationDefault, - this.keepScreenOnOption = _keepScreenOnOption, - this.keepScreenOnWhilePluggedIn = _keepScreenOnWhilePluggedIn - }); + FinampSettings( + {this.isOffline = _isOfflineDefault, + this.shouldTranscode = _shouldTranscodeDefault, + this.transcodeBitrate = _transcodeBitrateDefault, + // downloadLocations is required since the other values can be created with + // default values. create() is used to return a FinampSettings with + // downloadLocations. + required this.downloadLocations, + this.androidStopForegroundOnPause = _androidStopForegroundOnPauseDefault, + required this.showTabs, + this.onlyShowFavourite = _isFavouriteDefault, + this.sortBy = SortBy.sortName, + this.sortOrder = SortOrder.ascending, + this.songShuffleItemCount = _songShuffleItemCountDefault, + this.volumeNormalizationActive = _volumeNormalizationActiveDefault, + this.volumeNormalizationIOSBaseGain = + _volumeNormalizationIOSBaseGainDefault, + this.volumeNormalizationMode = _volumeNormalizationModeDefault, + this.contentViewType = _contentViewType, + this.playbackSpeedVisibility = _playbackSpeedVisibility, + this.contentGridViewCrossAxisCountPortrait = + _contentGridViewCrossAxisCountPortrait, + this.contentGridViewCrossAxisCountLandscape = + _contentGridViewCrossAxisCountLandscape, + this.showTextOnGridView = _showTextOnGridView, + this.sleepTimerSeconds = _sleepTimerSeconds, + required this.downloadLocationsMap, + this.useCoverAsBackground = _useCoverAsBackground, + this.playerScreenCoverMinimumPadding = _playerScreenCoverMinimumPadding, + this.hideSongArtistsIfSameAsAlbumArtists = + _hideSongArtistsIfSameAsAlbumArtists, + this.showArtistsTopSongs = _showArtistsTopSongs, + this.bufferDurationSeconds = _bufferDurationSeconds, + required this.tabSortBy, + required this.tabSortOrder, + this.loopMode = _defaultLoopMode, + this.playbackSpeed = _defaultPlaybackSpeed, + this.tabOrder = _tabOrder, + this.autoloadLastQueueOnStartup = _autoLoadLastQueueOnStartup, + this.hasCompletedBlurhashImageMigration = true, + this.hasCompletedBlurhashImageMigrationIdFix = true, + this.hasCompleteddownloadsServiceMigration = true, + this.requireWifiForDownloads = false, + this.onlyShowFullyDownloaded = false, + this.showDownloadsWithUnknownLibrary = true, + this.maxConcurrentDownloads = 10, + this.downloadWorkers = 5, + this.resyncOnStartup = _defaultResyncOnStartup, + this.preferQuickSyncs = true, + this.hasCompletedIsarUserMigration = true, + this.downloadTranscodingCodec, + this.downloadTranscodeBitrate, + this.shouldTranscodeDownloads = _shouldTranscodeDownloadsDefault, + this.shouldRedownloadTranscodes = _shouldRedownloadTranscodesDefault, + this.swipeInsertQueueNext = _swipeInsertQueueNext, + this.useFixedSizeGridTiles = false, + this.fixedGridTileSize = _fixedGridTileSizeDefault, + this.allowSplitScreen = true, + this.splitScreenPlayerWidth = _defaultSplitScreenPlayerWidth, + this.enableVibration = _enableVibration, + this.prioritizeCoverFactor = _prioritizeCoverFactor, + this.suppressPlayerPadding = _suppressPlayerPadding, + this.hidePlayerBottomActions = _hidePlayerBottomActions, + this.reportQueueToServer = _reportQueueToServerDefault, + this.periodicPlaybackSessionUpdateFrequencySeconds = + _periodicPlaybackSessionUpdateFrequencySecondsDefault, + this.showArtistChipImage = _showArtistChipImage, + this.trackOfflineFavorites = _trackOfflineFavoritesDefault, + this.showProgressOnNowPlayingBar = _showProgressOnNowPlayingBarDefault, + this.startInstantMixForIndividualTracks = + _startInstantMixForIndividualTracksDefault, + this.showLyricsTimestamps = _showLyricsTimestampsDefault, + this.lyricsAlignment = _lyricsAlignmentDefault, + this.lyricsFontSize = _lyricsFontSizeDefault, + this.showLyricsScreenAlbumPrelude = _showLyricsScreenAlbumPreludeDefault, + this.showStopButtonOnMediaNotification = + _showStopButtonOnMediaNotificationDefault, + this.showSeekControlsOnMediaNotification = + _showSeekControlsOnMediaNotificationDefault, + this.keepScreenOnOption = _keepScreenOnOption, + this.keepScreenOnWhilePluggedIn = _keepScreenOnWhilePluggedIn}); @HiveField(0, defaultValue: _isOfflineDefault) bool isOffline; @@ -479,7 +481,6 @@ class FinampSettings { SortOrder getSortOrder(TabContentType tabType) { return tabSortOrder[tabType] ?? SortOrder.ascending; } - } enum CustomPlaybackActions { @@ -682,7 +683,6 @@ enum TabContentType { throw const FormatException("Unsupported itemType"); } } - } @HiveType(typeId: 39) @@ -2074,7 +2074,6 @@ enum MediaItemParentType { @JsonSerializable() @HiveType(typeId: 69) class MediaItemId { - MediaItemId({ required this.contentType, required this.parentType, @@ -2089,7 +2088,7 @@ class MediaItemId { MediaItemParentType parentType; @HiveField(2) - String? itemId; + String? itemId; @HiveField(3) String? parentId; @@ -2103,7 +2102,6 @@ class MediaItemId { String toString() { return jsonEncode(toJson()); } - } @HiveType(typeId: 70) @@ -2235,4 +2233,4 @@ enum KeepScreenOnOption { return AppLocalizations.of(context)!.keepScreenOnWhileLyrics; } } -} \ No newline at end of file +} diff --git a/lib/screens/customization_settings_screen.dart b/lib/screens/customization_settings_screen.dart index d8b7d7f21..d89050353 100644 --- a/lib/screens/customization_settings_screen.dart +++ b/lib/screens/customization_settings_screen.dart @@ -39,8 +39,7 @@ class _CustomizationSettingsScreenState body: ListView( children: [ const PlaybackSpeedControlVisibilityDropdownListTile(), - if (!Platform.isIOS) - const ShowStopButtonOnMediaNotificationToggle(), + if (!Platform.isIOS) const ShowStopButtonOnMediaNotificationToggle(), const ShowSeekControlsOnMediaNotificationToggle(), ], ), @@ -56,12 +55,14 @@ class ShowStopButtonOnMediaNotificationToggle extends StatelessWidget { return ValueListenableBuilder>( valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (context, box, child) { - bool? showStopButtonOnMediaNotification = box.get("FinampSettings")?.showStopButtonOnMediaNotification; + bool? showStopButtonOnMediaNotification = + box.get("FinampSettings")?.showStopButtonOnMediaNotification; return SwitchListTile.adaptive( - title: Text(AppLocalizations.of(context)!.showStopButtonOnMediaNotificationTitle), - subtitle: - Text(AppLocalizations.of(context)!.showStopButtonOnMediaNotificationSubtitle), + title: Text(AppLocalizations.of(context)! + .showStopButtonOnMediaNotificationTitle), + subtitle: Text(AppLocalizations.of(context)! + .showStopButtonOnMediaNotificationSubtitle), value: showStopButtonOnMediaNotification ?? false, onChanged: showStopButtonOnMediaNotification == null ? null @@ -85,19 +86,22 @@ class ShowSeekControlsOnMediaNotificationToggle extends StatelessWidget { return ValueListenableBuilder>( valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (context, box, child) { - bool? showSeekControlsOnMediaNotification = box.get("FinampSettings")?.showSeekControlsOnMediaNotification; + bool? showSeekControlsOnMediaNotification = + box.get("FinampSettings")?.showSeekControlsOnMediaNotification; return SwitchListTile.adaptive( - title: Text(AppLocalizations.of(context)!.showSeekControlsOnMediaNotificationTitle), - subtitle: - Text(AppLocalizations.of(context)!.showSeekControlsOnMediaNotificationSubtitle), + title: Text(AppLocalizations.of(context)! + .showSeekControlsOnMediaNotificationTitle), + subtitle: Text(AppLocalizations.of(context)! + .showSeekControlsOnMediaNotificationSubtitle), value: showSeekControlsOnMediaNotification ?? false, onChanged: showSeekControlsOnMediaNotification == null ? null : (value) { FinampSettings finampSettingsTemp = box.get("FinampSettings")!; - finampSettingsTemp.showSeekControlsOnMediaNotification = value; + finampSettingsTemp.showSeekControlsOnMediaNotification = + value; box.put("FinampSettings", finampSettingsTemp); }, ); diff --git a/lib/screens/interaction_settings_screen.dart b/lib/screens/interaction_settings_screen.dart index 11c947a29..bed1cc124 100644 --- a/lib/screens/interaction_settings_screen.dart +++ b/lib/screens/interaction_settings_screen.dart @@ -45,12 +45,14 @@ class StartInstantMixForIndividualTracksSwitch extends StatelessWidget { return ValueListenableBuilder>( valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (_, box, __) { - bool? startInstantMixForIndividualTracks = box.get("FinampSettings")?.startInstantMixForIndividualTracks; + bool? startInstantMixForIndividualTracks = + box.get("FinampSettings")?.startInstantMixForIndividualTracks; return SwitchListTile.adaptive( - title: Text(AppLocalizations.of(context)!.startInstantMixForIndividualTracksSwitchTitle), - subtitle: - Text(AppLocalizations.of(context)!.startInstantMixForIndividualTracksSwitchSubtitle), + title: Text(AppLocalizations.of(context)! + .startInstantMixForIndividualTracksSwitchTitle), + subtitle: Text(AppLocalizations.of(context)! + .startInstantMixForIndividualTracksSwitchSubtitle), value: startInstantMixForIndividualTracks ?? false, onChanged: startInstantMixForIndividualTracks == null ? null diff --git a/lib/screens/layout_settings_screen.dart b/lib/screens/layout_settings_screen.dart index 22d1bc4be..398e60eb9 100644 --- a/lib/screens/layout_settings_screen.dart +++ b/lib/screens/layout_settings_screen.dart @@ -183,12 +183,14 @@ class ShowProgressOnNowPlayingBarToggle extends StatelessWidget { return ValueListenableBuilder>( valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (context, box, child) { - bool? showProgressOnNowPlayingBar = box.get("FinampSettings")?.showProgressOnNowPlayingBar; + bool? showProgressOnNowPlayingBar = + box.get("FinampSettings")?.showProgressOnNowPlayingBar; return SwitchListTile.adaptive( - title: Text(AppLocalizations.of(context)!.showProgressOnNowPlayingBarTitle), - subtitle: - Text(AppLocalizations.of(context)!.showProgressOnNowPlayingBarSubtitle), + title: Text( + AppLocalizations.of(context)!.showProgressOnNowPlayingBarTitle), + subtitle: Text(AppLocalizations.of(context)! + .showProgressOnNowPlayingBarSubtitle), value: showProgressOnNowPlayingBar ?? false, onChanged: showProgressOnNowPlayingBar == null ? null diff --git a/lib/screens/lyrics_screen.dart b/lib/screens/lyrics_screen.dart index 9f0ac532e..609d07f4c 100644 --- a/lib/screens/lyrics_screen.dart +++ b/lib/screens/lyrics_screen.dart @@ -234,7 +234,6 @@ class _LyricsViewState extends ConsumerState @override Widget build(BuildContext context) { - final audioHandler = GetIt.instance(); final metadata = ref.watch(currentTrackMetadataProvider).unwrapPrevious(); @@ -283,7 +282,8 @@ class _LyricsViewState extends ConsumerState message: "Loading lyrics...", icon: TablerIcons.microphone_2, ); - } else if (!metadata.hasValue || metadata.value == null || + } else if (!metadata.hasValue || + metadata.value == null || metadata.value!.hasLyrics && metadata.value!.lyrics == null && !metadata.isLoading) { @@ -382,7 +382,11 @@ class _LyricsViewState extends ConsumerState child: Center( child: SizedBox( height: constraints.maxHeight * 0.55, - child: (finampSettings?.showLyricsScreenAlbumPrelude ?? true) ? const PlayerScreenAlbumImage() : null)), + child: (finampSettings + ?.showLyricsScreenAlbumPrelude ?? + true) + ? const PlayerScreenAlbumImage() + : null)), ), ), AutoScrollTag( @@ -465,17 +469,14 @@ class _LyricLine extends ConsumerWidget { onTap: isSynchronized ? onTap : null, child: Padding( padding: EdgeInsets.symmetric(vertical: isSynchronized ? 10.0 : 6.0), - child: - Text.rich( - textAlign: lyricsAlignmentToTextAlign(finampSettings?.lyricsAlignment ?? LyricsAlignment.start), + child: Text.rich( + textAlign: lyricsAlignmentToTextAlign( + finampSettings?.lyricsAlignment ?? LyricsAlignment.start), softWrap: true, - TextSpan( - children: [ - if ( - line.start != null - && (line.text?.trim().isNotEmpty ?? false) - && (finampSettings?.showLyricsTimestamps ?? true) - ) + TextSpan(children: [ + if (line.start != null && + (line.text?.trim().isNotEmpty ?? false) && + (finampSettings?.showLyricsTimestamps ?? true)) WidgetSpan( alignment: PlaceholderAlignment.bottom, child: Padding( @@ -487,27 +488,33 @@ class _LyricLine extends ConsumerWidget { ? Colors.grey : Theme.of(context).textTheme.bodyLarge!.color, fontSize: 16, - height: 1.75 * (lyricsFontSizeToSize(finampSettings?.lyricsFontSize ?? LyricsFontSize.medium) / 26), + height: 1.75 * + (lyricsFontSizeToSize( + finampSettings?.lyricsFontSize ?? + LyricsFontSize.medium) / + 26), ), ), ), ), TextSpan( - text: line.text ?? "", - style: TextStyle( - color: lowlightLine - ? Colors.grey - : Theme.of(context).textTheme.bodyLarge!.color, - fontWeight: lowlightLine || !isSynchronized - ? FontWeight.normal - : FontWeight.w500, - letterSpacing: lowlightLine || !isSynchronized - ? 0.05 - : -0.045, // keep text width consistent across the different weights - fontSize: lyricsFontSizeToSize(finampSettings?.lyricsFontSize ?? LyricsFontSize.medium) * (isSynchronized ? 1.0 : 0.75), - height: 1.25, - ), + text: line.text ?? "", + style: TextStyle( + color: lowlightLine + ? Colors.grey + : Theme.of(context).textTheme.bodyLarge!.color, + fontWeight: lowlightLine || !isSynchronized + ? FontWeight.normal + : FontWeight.w500, + letterSpacing: lowlightLine || !isSynchronized + ? 0.05 + : -0.045, // keep text width consistent across the different weights + fontSize: lyricsFontSizeToSize(finampSettings?.lyricsFontSize ?? + LyricsFontSize.medium) * + (isSynchronized ? 1.0 : 0.75), + height: 1.25, ), + ), ]), ), ), diff --git a/lib/screens/lyrics_settings_screen.dart b/lib/screens/lyrics_settings_screen.dart index e90138db8..29647d720 100644 --- a/lib/screens/lyrics_settings_screen.dart +++ b/lib/screens/lyrics_settings_screen.dart @@ -129,7 +129,6 @@ class LyricsFontSizeSelector extends StatelessWidget { } } - class ShowLyricsScreenAlbumPreludeToggle extends StatelessWidget { const ShowLyricsScreenAlbumPreludeToggle({super.key}); @@ -142,9 +141,10 @@ class ShowLyricsScreenAlbumPreludeToggle extends StatelessWidget { box.get("FinampSettings")?.showLyricsScreenAlbumPrelude; return SwitchListTile.adaptive( - title: Text(AppLocalizations.of(context)!.showLyricsScreenAlbumPreludeTitle), - subtitle: - Text(AppLocalizations.of(context)!.showLyricsScreenAlbumPreludeSubtitle), + title: Text( + AppLocalizations.of(context)!.showLyricsScreenAlbumPreludeTitle), + subtitle: Text(AppLocalizations.of(context)! + .showLyricsScreenAlbumPreludeSubtitle), value: showLyricsScreenAlbumPrelude ?? false, onChanged: showLyricsScreenAlbumPrelude == null ? null diff --git a/lib/screens/player_settings_screen.dart b/lib/screens/player_settings_screen.dart index 9fe353da2..40dce3330 100644 --- a/lib/screens/player_settings_screen.dart +++ b/lib/screens/player_settings_screen.dart @@ -71,7 +71,8 @@ class hidePlayerBottomActionsSwitch extends StatelessWidget { return SwitchListTile.adaptive( title: Text(AppLocalizations.of(context)!.hidePlayerBottomActions), - subtitle: Text(AppLocalizations.of(context)!.hidePlayerBottomActionsSubtitle), + subtitle: Text( + AppLocalizations.of(context)!.hidePlayerBottomActionsSubtitle), value: hideQueue ?? false, onChanged: hideQueue == null ? null diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e3cca288c..4947b8d9c 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -46,97 +46,100 @@ class SettingsScreen extends StatelessWidget { final applicationLegalese = AppLocalizations.of(context)!.applicationLegalese(repoLink); PackageInfo packageInfo = await PackageInfo.fromPlatform(); - + ThemeData theme = Theme.of(context); const linkStyle = TextStyle( color: Colors.blue, decoration: TextDecoration.underline, ); - + showAboutDialog( - context: context, - applicationName: packageInfo.appName, - applicationVersion: packageInfo.version, - applicationIcon: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Image.asset( - 'images/finamp_cropped.png', - width: 56, - height: 56, - ), - ), - applicationLegalese: applicationLegalese, - children: [ - const SizedBox(height: 20), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: TextStyle(color: theme.textTheme.bodyMedium!.color), - children: [ - TextSpan( - text: localizations.finampTagline, - style: const TextStyle(fontStyle: FontStyle.italic, fontWeight: FontWeight.w500), - ), - const TextSpan( - text: '\n\n', - ), - TextSpan( - text: localizations.aboutContributionPrompt, - ), - const TextSpan( - text: '\n\n', - ), - TextSpan( - text: '${localizations.aboutContributionLink}\n', - ), - TextSpan( - text: repoLink, - style: linkStyle, - recognizer: TapGestureRecognizer() - ..onTap = () async { - await launchUrl(Uri.parse(repoLink)); - }, - ), - const TextSpan( - text: '\n\n', - ), - TextSpan( - text: '${localizations.aboutTranslations}\n', - ), - TextSpan( - text: translationsLink, - style: linkStyle, - recognizer: TapGestureRecognizer() - ..onTap = () async { - await launchUrl(Uri.parse(translationsLink)); - }, - ), - const TextSpan( - text: '\n\n', - ), - TextSpan( - text: '${localizations.aboutReleaseNotes}\n', - ), - TextSpan( - text: releaseNotesLink, - style: linkStyle, - recognizer: TapGestureRecognizer() - ..onTap = () async { - await launchUrl(Uri.parse(releaseNotesLink)); - }, - ), - const TextSpan( - text: '\n\n\n', - ), - TextSpan( - text: localizations.aboutThanks, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ], + context: context, + applicationName: packageInfo.appName, + applicationVersion: packageInfo.version, + applicationIcon: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Image.asset( + 'images/finamp_cropped.png', + width: 56, + height: 56, ), ), - ] - ); + applicationLegalese: applicationLegalese, + children: [ + const SizedBox(height: 20), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle( + color: theme.textTheme.bodyMedium!.color), + children: [ + TextSpan( + text: localizations.finampTagline, + style: const TextStyle( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w500), + ), + const TextSpan( + text: '\n\n', + ), + TextSpan( + text: localizations.aboutContributionPrompt, + ), + const TextSpan( + text: '\n\n', + ), + TextSpan( + text: '${localizations.aboutContributionLink}\n', + ), + TextSpan( + text: repoLink, + style: linkStyle, + recognizer: TapGestureRecognizer() + ..onTap = () async { + await launchUrl(Uri.parse(repoLink)); + }, + ), + const TextSpan( + text: '\n\n', + ), + TextSpan( + text: '${localizations.aboutTranslations}\n', + ), + TextSpan( + text: translationsLink, + style: linkStyle, + recognizer: TapGestureRecognizer() + ..onTap = () async { + await launchUrl(Uri.parse(translationsLink)); + }, + ), + const TextSpan( + text: '\n\n', + ), + TextSpan( + text: '${localizations.aboutReleaseNotes}\n', + ), + TextSpan( + text: releaseNotesLink, + style: linkStyle, + recognizer: TapGestureRecognizer() + ..onTap = () async { + await launchUrl(Uri.parse(releaseNotesLink)); + }, + ), + const TextSpan( + text: '\n\n\n', + ), + TextSpan( + text: localizations.aboutThanks, + style: + const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + ]); }, ), ) diff --git a/lib/services/android_auto_helper.dart b/lib/services/android_auto_helper.dart index 0ace9d234..5b4aa79e8 100644 --- a/lib/services/android_auto_helper.dart +++ b/lib/services/android_auto_helper.dart @@ -20,13 +20,11 @@ class AndroidAutoSearchQuery { Map? extras; AndroidAutoSearchQuery(this.rawQuery, this.extras); - } class AndroidAutoHelper { - static final _androidAutoHelperLogger = Logger("AndroidAutoHelper"); - + final _finampUserHelper = GetIt.instance(); final _jellyfinApiHelper = GetIt.instance(); final _downloadsService = GetIt.instance(); @@ -41,7 +39,8 @@ class AndroidAutoHelper { AndroidAutoSearchQuery? get lastSearchQuery => _lastSearchQuery; Future getParentFromId(String parentId) async { - final downloadedParent = await _downloadsService.getCollectionInfo(id: parentId); + final downloadedParent = + await _downloadsService.getCollectionInfo(id: parentId); if (downloadedParent != null) { return downloadedParent.baseItem; } else if (FinampSettingsHelper.finampSettings.isOffline) { @@ -52,20 +51,24 @@ class AndroidAutoHelper { } Future> getBaseItems(MediaItemId itemId) async { - // limit amount so it doesn't crash / take forever on large libraries const onlineModeLimit = 250; const offlineModeLimit = 1000; - final sortBy = FinampSettingsHelper.finampSettings.getTabSortBy(itemId.contentType); - final sortOrder = FinampSettingsHelper.finampSettings.getSortOrder(itemId.contentType); + final sortBy = + FinampSettingsHelper.finampSettings.getTabSortBy(itemId.contentType); + final sortOrder = + FinampSettingsHelper.finampSettings.getSortOrder(itemId.contentType); // if we are in offline mode and in root parent/collection, display all matching downloaded parents - if (FinampSettingsHelper.finampSettings.isOffline && itemId.parentType == MediaItemParentType.rootCollection) { + if (FinampSettingsHelper.finampSettings.isOffline && + itemId.parentType == MediaItemParentType.rootCollection) { List baseItems = []; - for (final downloadedParent in await _downloadsService.getAllCollections()) { + for (final downloadedParent + in await _downloadsService.getAllCollections()) { if (baseItems.length >= offlineModeLimit) break; - if (downloadedParent.baseItem != null && downloadedParent.baseItemType == itemId.contentType.itemType) { + if (downloadedParent.baseItem != null && + downloadedParent.baseItemType == itemId.contentType.itemType) { baseItems.add(downloadedParent.baseItem!); } } @@ -75,48 +78,59 @@ class AndroidAutoHelper { // use downloaded parent only in offline mode // otherwise we only play downloaded songs from albums/collections, not all of them // downloaded songs will be played from device when resolving them to media items - if (FinampSettingsHelper.finampSettings.isOffline && itemId.parentType == MediaItemParentType.collection) { - + if (FinampSettingsHelper.finampSettings.isOffline && + itemId.parentType == MediaItemParentType.collection) { if (itemId.contentType == TabContentType.genres) { final genreBaseItem = await getParentFromId(itemId.itemId!); - - final List genreAlbums = (await _downloadsService.getAllCollections( - baseTypeFilter: BaseItemDtoType.album, - relatedTo: genreBaseItem)).toList() - .map((e) => e.baseItem).whereNotNull().toList(); - genreAlbums.sort((a, b) => (a.premiereDate ?? "") - .compareTo(b.premiereDate ?? "")); + + final List genreAlbums = + (await _downloadsService.getAllCollections( + baseTypeFilter: BaseItemDtoType.album, + relatedTo: genreBaseItem)) + .toList() + .map((e) => e.baseItem) + .whereNotNull() + .toList(); + genreAlbums.sort( + (a, b) => (a.premiereDate ?? "").compareTo(b.premiereDate ?? "")); return genreAlbums; } else if (itemId.contentType == TabContentType.artists) { - final artistBaseItem = await getParentFromId(itemId.itemId!); - - final List artistAlbums = (await _downloadsService.getAllCollections( - baseTypeFilter: BaseItemDtoType.album, - relatedTo: artistBaseItem)).toList() - .map((e) => e.baseItem).whereNotNull().toList(); - artistAlbums.sort((a, b) => (a.premiereDate ?? "") - .compareTo(b.premiereDate ?? "")); + + final List artistAlbums = + (await _downloadsService.getAllCollections( + baseTypeFilter: BaseItemDtoType.album, + relatedTo: artistBaseItem)) + .toList() + .map((e) => e.baseItem) + .whereNotNull() + .toList(); + artistAlbums.sort( + (a, b) => (a.premiereDate ?? "").compareTo(b.premiereDate ?? "")); final List allSongs = []; for (var album in artistAlbums) { - allSongs.addAll(await _downloadsService - .getCollectionSongs(album, playable: true)); + allSongs.addAll(await _downloadsService.getCollectionSongs(album, + playable: true)); } return allSongs; } else { - var downloadedParent = await _downloadsService.getCollectionInfo(id: itemId.itemId); + var downloadedParent = + await _downloadsService.getCollectionInfo(id: itemId.itemId); if (downloadedParent != null && downloadedParent.baseItem != null) { - final downloadedItems = await _downloadsService.getCollectionSongs(downloadedParent.baseItem!); + final downloadedItems = await _downloadsService + .getCollectionSongs(downloadedParent.baseItem!); if (downloadedItems.length >= offlineModeLimit) { - downloadedItems.removeRange(offlineModeLimit, downloadedItems.length - 1); + downloadedItems.removeRange( + offlineModeLimit, downloadedItems.length - 1); } // only sort items if we are not playing them - return _isPlayable(contentType: itemId.contentType) ? downloadedItems : sortItems(downloadedItems, sortBy, sortOrder); + return _isPlayable(contentType: itemId.contentType) + ? downloadedItems + : sortItems(downloadedItems, sortBy, sortOrder); } } - } // fetch the online version if we can't get offline version @@ -155,10 +169,18 @@ class AndroidAutoHelper { // if parent id is defined, use that to get items. // otherwise, use the current view as fallback to ensure we get the correct items. final parentItem = itemId.parentType == MediaItemParentType.collection - ? BaseItemDto(id: itemId.itemId!, type: itemId.contentType.itemType.idString) - : (itemId.contentType == TabContentType.playlists ? null : _finampUserHelper.currentUser?.currentView); - - final items = await _jellyfinApiHelper.getItems(parentItem: parentItem, sortBy: sortBy.jellyfinName(itemId.contentType), sortOrder: sortOrder.toString(), includeItemTypes: includeItemTypes, limit: onlineModeLimit); + ? BaseItemDto( + id: itemId.itemId!, type: itemId.contentType.itemType.idString) + : (itemId.contentType == TabContentType.playlists + ? null + : _finampUserHelper.currentUser?.currentView); + + final items = await _jellyfinApiHelper.getItems( + parentItem: parentItem, + sortBy: sortBy.jellyfinName(itemId.contentType), + sortOrder: sortOrder.toString(), + includeItemTypes: includeItemTypes, + limit: onlineModeLimit); return items ?? []; } @@ -170,7 +192,9 @@ class AndroidAutoHelper { final List recentMediaItems = []; for (final item in recentItems) { if (item.baseItem == null) continue; - final mediaItem = await queueService.generateMediaItem(item.baseItem!, parentType: MediaItemParentType.collection, isPlayable: _isPlayable); + final mediaItem = await queueService.generateMediaItem(item.baseItem!, + parentType: MediaItemParentType.collection, + isPlayable: _isPlayable); recentMediaItems.add(mediaItem); } return recentMediaItems; @@ -180,11 +204,11 @@ class AndroidAutoHelper { } } - Future> searchItems(AndroidAutoSearchQuery searchQuery) async { + Future> searchItems( + AndroidAutoSearchQuery searchQuery) async { final queueService = GetIt.instance(); try { - final searchFuture = Future.wait([ _searchPlaylists(searchQuery, limit: 3), _searchTracks(searchQuery, limit: 5), @@ -192,35 +216,69 @@ class AndroidAutoHelper { _searchArtists(searchQuery, limit: 3), ]); - final [playlistResults, trackResults, albumResults, artistResults] = await searchFuture; + final [playlistResults, trackResults, albumResults, artistResults] = + await searchFuture; final List allSearchResults = playlistResults - .followedBy(trackResults) - .followedBy(albumResults) - .followedBy(artistResults) - .toList(); - + .followedBy(trackResults) + .followedBy(albumResults) + .followedBy(artistResults) + .toList(); + final List mediaItems = []; for (final item in allSearchResults) { - final mediaItem = await queueService.generateMediaItem(item, parentType: MediaItemParentType.collection, parentId: item.parentId, isPlayable: _isPlayable); + final mediaItem = await queueService.generateMediaItem(item, + parentType: MediaItemParentType.collection, + parentId: item.parentId, + isPlayable: _isPlayable); // assign a group hint based on the item type, so Android Auto can group search results by type - switch(item.type) { + switch (item.type) { case "Audio": - mediaItem.extras?["android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"] = GlobalSnackbar.materialAppScaffoldKey.currentContext != null ? AppLocalizations.of(GlobalSnackbar.materialAppScaffoldKey.currentContext!)!.songs : "Tracks"; + mediaItem.extras?[ + "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"] = + GlobalSnackbar.materialAppScaffoldKey.currentContext != null + ? AppLocalizations.of(GlobalSnackbar + .materialAppScaffoldKey.currentContext!)! + .songs + : "Tracks"; break; case "MusicAlbum": - mediaItem.extras?["android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"] = GlobalSnackbar.materialAppScaffoldKey.currentContext != null ? AppLocalizations.of(GlobalSnackbar.materialAppScaffoldKey.currentContext!)!.albums : "Albums"; + mediaItem.extras?[ + "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"] = + GlobalSnackbar.materialAppScaffoldKey.currentContext != null + ? AppLocalizations.of(GlobalSnackbar + .materialAppScaffoldKey.currentContext!)! + .albums + : "Albums"; break; case "MusicArtist": - mediaItem.extras?["android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"] = GlobalSnackbar.materialAppScaffoldKey.currentContext != null ? AppLocalizations.of(GlobalSnackbar.materialAppScaffoldKey.currentContext!)!.artists : "Artists"; + mediaItem.extras?[ + "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"] = + GlobalSnackbar.materialAppScaffoldKey.currentContext != null + ? AppLocalizations.of(GlobalSnackbar + .materialAppScaffoldKey.currentContext!)! + .artists + : "Artists"; break; case "MusicGenre": - mediaItem.extras?["android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"] = GlobalSnackbar.materialAppScaffoldKey.currentContext != null ? AppLocalizations.of(GlobalSnackbar.materialAppScaffoldKey.currentContext!)!.genres : "Genres"; + mediaItem.extras?[ + "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"] = + GlobalSnackbar.materialAppScaffoldKey.currentContext != null + ? AppLocalizations.of(GlobalSnackbar + .materialAppScaffoldKey.currentContext!)! + .genres + : "Genres"; break; case "Playlist": - mediaItem.extras?["android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"] = GlobalSnackbar.materialAppScaffoldKey.currentContext != null ? AppLocalizations.of(GlobalSnackbar.materialAppScaffoldKey.currentContext!)!.playlists : "Playlists"; + mediaItem.extras?[ + "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"] = + GlobalSnackbar.materialAppScaffoldKey.currentContext != null + ? AppLocalizations.of(GlobalSnackbar + .materialAppScaffoldKey.currentContext!)! + .playlists + : "Playlists"; break; default: break; @@ -243,20 +301,25 @@ class AndroidAutoHelper { if (searchQuery.rawQuery.isEmpty) { return await shuffleAllSongs(); } - + BaseItemDtoType? itemType = TabContentType.songs.itemType; String? enhancedQuery; bool searchForPlaylists = false; - if (searchQuery.extras?["android.intent.extra.album"] != null && searchQuery.extras?["android.intent.extra.artist"] != null && searchQuery.extras?["android.intent.extra.title"] != null) { + if (searchQuery.extras?["android.intent.extra.album"] != null && + searchQuery.extras?["android.intent.extra.artist"] != null && + searchQuery.extras?["android.intent.extra.title"] != null) { // if all metadata is provided, search for song itemType = TabContentType.songs.itemType; enhancedQuery = searchQuery.extras?["android.intent.extra.title"]; - } else if (searchQuery.extras?["android.intent.extra.album"] != null && searchQuery.extras?["android.intent.extra.artist"] != null && searchQuery.extras?["android.intent.extra.title"] == null) { + } else if (searchQuery.extras?["android.intent.extra.album"] != null && + searchQuery.extras?["android.intent.extra.artist"] != null && + searchQuery.extras?["android.intent.extra.title"] == null) { // if only album is provided, search for album itemType = TabContentType.albums.itemType; enhancedQuery = searchQuery.extras?["android.intent.extra.album"]; - } else if (searchQuery.extras?["android.intent.extra.artist"] != null && searchQuery.extras?["android.intent.extra.title"] == null) { + } else if (searchQuery.extras?["android.intent.extra.artist"] != null && + searchQuery.extras?["android.intent.extra.title"] == null) { // if only artist is provided, search for artist itemType = TabContentType.artists.itemType; enhancedQuery = searchQuery.extras?["android.intent.extra.artist"]; @@ -265,27 +328,32 @@ class AndroidAutoHelper { searchForPlaylists = true; } - _androidAutoHelperLogger.info("Searching for: $itemType that matches query '${enhancedQuery ?? searchQuery.rawQuery}'${searchForPlaylists ? ", including (and preferring) playlists" : ""}"); + _androidAutoHelperLogger.info( + "Searching for: $itemType that matches query '${enhancedQuery ?? searchQuery.rawQuery}'${searchForPlaylists ? ", including (and preferring) playlists" : ""}"); - final searchTerm = searchForPlaylists ? - searchQuery.rawQuery.trim() : // always use the raw query for searching playlists - enhancedQuery?.trim() ?? searchQuery.rawQuery.trim(); + final searchTerm = searchForPlaylists + ? searchQuery.rawQuery.trim() + : // always use the raw query for searching playlists + enhancedQuery?.trim() ?? searchQuery.rawQuery.trim(); if (searchForPlaylists) { try { List? searchResult; if (FinampSettingsHelper.finampSettings.isOffline) { - List? offlineItems = await _downloadsService.getAllCollections( - nameFilter: searchTerm, - baseTypeFilter: TabContentType.playlists.itemType, - fullyDownloaded: false, - viewFilter: finampUserHelper.currentUser?.currentView?.id, - childViewFilter: null, - nullableViewFilters: FinampSettingsHelper.finampSettings.showDownloadsWithUnknownLibrary, - onlyFavorites: false); - - searchResult = offlineItems.map((e) => e.baseItem).whereNotNull().toList(); + List? offlineItems = + await _downloadsService.getAllCollections( + nameFilter: searchTerm, + baseTypeFilter: TabContentType.playlists.itemType, + fullyDownloaded: false, + viewFilter: finampUserHelper.currentUser?.currentView?.id, + childViewFilter: null, + nullableViewFilters: FinampSettingsHelper + .finampSettings.showDownloadsWithUnknownLibrary, + onlyFavorites: false); + + searchResult = + offlineItems.map((e) => e.baseItem).whereNotNull().toList(); } else { searchResult = await jellyfinApiHelper.getItems( parentItem: null, // always use global playlists @@ -297,20 +365,28 @@ class AndroidAutoHelper { } if (searchResult?.isNotEmpty ?? false) { - final playlist = searchResult![0]; List? items; - if (FinampSettingsHelper.finampSettings.isOffline) { - items = await _downloadsService.getCollectionSongs(playlist, playable: true); + if (FinampSettingsHelper.finampSettings.isOffline) { + items = await _downloadsService.getCollectionSongs(playlist, + playable: true); } else { - items = await _jellyfinApiHelper.getItems(parentItem: playlist, includeItemTypes: TabContentType.songs.itemType.idString, sortBy: "ParentIndexNumber,IndexNumber,SortName", sortOrder: "Ascending", limit: 200); + items = await _jellyfinApiHelper.getItems( + parentItem: playlist, + includeItemTypes: TabContentType.songs.itemType.idString, + sortBy: "ParentIndexNumber,IndexNumber,SortName", + sortOrder: "Ascending", + limit: 200); } - - _androidAutoHelperLogger.info("Playing playlist: ${playlist.name} (${items?.length} songs)"); - await queueService.startPlayback(items: items ?? [], source: QueueItemSource( + _androidAutoHelperLogger.info( + "Playing playlist: ${playlist.name} (${items?.length} songs)"); + + await queueService.startPlayback( + items: items ?? [], + source: QueueItemSource( type: QueueItemSourceType.playlist, name: QueueItemSourceName( type: QueueItemSourceNameType.preTranslated, @@ -318,20 +394,19 @@ class AndroidAutoHelper { id: playlist.id, item: playlist, ), - order: FinampPlaybackOrder.linear, //TODO add a setting that sets the default (because Android Auto doesn't give use the prompt as an extra), or use the current order? + order: FinampPlaybackOrder + .linear, //TODO add a setting that sets the default (because Android Auto doesn't give use the prompt as an extra), or use the current order? ); - } else { - _androidAutoHelperLogger.warning("No playlists found for query: ${enhancedQuery ?? searchQuery.rawQuery}"); + _androidAutoHelperLogger.warning( + "No playlists found for query: ${enhancedQuery ?? searchQuery.rawQuery}"); } - } catch (e) { _androidAutoHelperLogger.warning("Couldn't search for playlists: $e"); } } try { - // first try with any metadata we could get (could be corrected based on metadata or localizations, or just the raw query) List? searchResult = await _getResults( searchTerm: searchTerm, @@ -339,50 +414,75 @@ class AndroidAutoHelper { ); if (searchResult == null || searchResult.isEmpty) { - _androidAutoHelperLogger.warning("No search results found for search term: $searchTerm)"); + _androidAutoHelperLogger + .warning("No search results found for search term: $searchTerm)"); if (enhancedQuery != null) { - // if we got additional metadata, we already tried searching with it // now try searching with the raw query searchResult = await _getResults( searchTerm: searchQuery.rawQuery.trim(), itemTypes: [itemType], ); - } - + if (searchResult == null || searchResult.isEmpty) { - _androidAutoHelperLogger.warning("No search results found for search term (raw query): ${searchQuery.rawQuery}"); + _androidAutoHelperLogger.warning( + "No search results found for search term (raw query): ${searchQuery.rawQuery}"); return; } } final selectedResult = searchResult.firstWhere((element) { - if (itemType == TabContentType.songs.itemType && searchQuery.extras?["android.intent.extra.artist"] != null) { - return element.albumArtists?.any((artist) => (artist.name?.isNotEmpty ?? false) && (searchQuery.extras?["android.intent.extra.artist"]?.toString().toLowerCase().contains(artist.name?.toLowerCase() ?? "") ?? false)) ?? false; - } else if (itemType == TabContentType.songs.itemType && searchQuery.extras?["android.intent.extra.artist"] != null) { - return element.albumArtists?.any((artist) => (artist.name?.isNotEmpty ?? false) && (searchQuery.extras?["android.intent.extra.artist"]?.toString().toLowerCase().contains(artist.name?.toLowerCase() ?? "") ?? false)) ?? false; + if (itemType == TabContentType.songs.itemType && + searchQuery.extras?["android.intent.extra.artist"] != null) { + return element.albumArtists?.any((artist) => + (artist.name?.isNotEmpty ?? false) && + (searchQuery.extras?["android.intent.extra.artist"] + ?.toString() + .toLowerCase() + .contains(artist.name?.toLowerCase() ?? "") ?? + false)) ?? + false; + } else if (itemType == TabContentType.songs.itemType && + searchQuery.extras?["android.intent.extra.artist"] != null) { + return element.albumArtists?.any((artist) => + (artist.name?.isNotEmpty ?? false) && + (searchQuery.extras?["android.intent.extra.artist"] + ?.toString() + .toLowerCase() + .contains(artist.name?.toLowerCase() ?? "") ?? + false)) ?? + false; } else { return false; } - }, orElse: () => searchResult![0] - ); + }, orElse: () => searchResult![0]); + + _androidAutoHelperLogger + .info("Playing from search: ${selectedResult.name}"); - _androidAutoHelperLogger.info("Playing from search: ${selectedResult.name}"); - if (itemType == TabContentType.albums.itemType) { final album = selectedResult; List? items; - if (FinampSettingsHelper.finampSettings.isOffline) { - items = await _downloadsService.getCollectionSongs(album, playable: true); + if (FinampSettingsHelper.finampSettings.isOffline) { + items = + await _downloadsService.getCollectionSongs(album, playable: true); } else { - items = await _jellyfinApiHelper.getItems(parentItem: album, includeItemTypes: TabContentType.songs.itemType.idString, sortBy: "ParentIndexNumber,IndexNumber,SortName", sortOrder: "Ascending", limit: 200); + items = await _jellyfinApiHelper.getItems( + parentItem: album, + includeItemTypes: TabContentType.songs.itemType.idString, + sortBy: "ParentIndexNumber,IndexNumber,SortName", + sortOrder: "Ascending", + limit: 200); } - _androidAutoHelperLogger.info("Playing album: ${album.name} (${items?.length} songs)"); + _androidAutoHelperLogger + .info("Playing album: ${album.name} (${items?.length} songs)"); - await queueService.startPlayback(items: items ?? [], source: QueueItemSource( + await queueService.startPlayback( + items: items ?? [], + source: QueueItemSource( type: QueueItemSourceType.album, name: QueueItemSourceName( type: QueueItemSourceNameType.preTranslated, @@ -390,11 +490,16 @@ class AndroidAutoHelper { id: album.id, item: album, ), - order: FinampPlaybackOrder.linear, //TODO add a setting that sets the default (because Android Auto doesn't give use the prompt as an extra), or use the current order? + order: FinampPlaybackOrder + .linear, //TODO add a setting that sets the default (because Android Auto doesn't give use the prompt as an extra), or use the current order? ); } else if (itemType == TabContentType.artists.itemType) { if (FinampSettingsHelper.finampSettings.isOffline) { - final parentBaseItems = await getBaseItems(MediaItemId(contentType: TabContentType.artists, parentType: MediaItemParentType.collection, parentId: selectedResult.id, itemId: selectedResult.id)); + final parentBaseItems = await getBaseItems(MediaItemId( + contentType: TabContentType.artists, + parentType: MediaItemParentType.collection, + parentId: selectedResult.id, + itemId: selectedResult.id)); await queueService.startPlayback( items: parentBaseItems, @@ -409,7 +514,8 @@ class AndroidAutoHelper { order: FinampPlaybackOrder.linear, ); } else { - await audioServiceHelper.startInstantMixForArtists([selectedResult]).then((value) => 1); + await audioServiceHelper + .startInstantMixForArtists([selectedResult]).then((value) => 1); } } else { if (FinampSettingsHelper.finampSettings.isOffline) { @@ -418,38 +524,41 @@ class AndroidAutoHelper { offlineItems = await _downloadsService.getAllSongs( // nameFilter: widget.searchTerm, viewFilter: finampUserHelper.currentUser?.currentView?.id, - nullableViewFilters: - FinampSettingsHelper.finampSettings.showDownloadsWithUnknownLibrary); + nullableViewFilters: FinampSettingsHelper + .finampSettings.showDownloadsWithUnknownLibrary); - var items = offlineItems - .map((e) => e.baseItem) - .whereNotNull() - .toList(); + var items = + offlineItems.map((e) => e.baseItem).whereNotNull().toList(); items = sortItems( items, - FinampSettingsHelper.finampSettings.tabSortBy[TabContentType.songs]!, - FinampSettingsHelper.finampSettings.tabSortOrder[TabContentType.songs]!); + FinampSettingsHelper + .finampSettings.tabSortBy[TabContentType.songs]!, + FinampSettingsHelper + .finampSettings.tabSortOrder[TabContentType.songs]!); - final indexOfSelected = items.indexWhere((element) => element.id == selectedResult.id); + final indexOfSelected = + items.indexWhere((element) => element.id == selectedResult.id); return await queueService.startPlayback( items: items, startingIndex: indexOfSelected, source: QueueItemSource( - name: const QueueItemSourceName( - type: QueueItemSourceNameType.mix), + name: + const QueueItemSourceName(type: QueueItemSourceNameType.mix), type: QueueItemSourceType.allSongs, id: selectedResult.id, ), ); - } else { - await audioServiceHelper.startInstantMixForItem(selectedResult).then((value) => 1); + await audioServiceHelper + .startInstantMixForItem(selectedResult) + .then((value) => 1); } } } catch (err) { - _androidAutoHelperLogger.severe("Error while playing from search query: $err"); + _androidAutoHelperLogger + .severe("Error while playing from search query: $err"); } } @@ -457,7 +566,8 @@ class AndroidAutoHelper { final audioServiceHelper = GetIt.instance(); try { - await audioServiceHelper.shuffleAll(FinampSettingsHelper.finampSettings.onlyShowFavourite); + await audioServiceHelper + .shuffleAll(FinampSettingsHelper.finampSettings.onlyShowFavourite); } catch (err) { _androidAutoHelperLogger.severe("Error while shuffling all songs", err); } @@ -469,7 +579,10 @@ class AndroidAutoHelper { final List mediaItems = []; for (final item in items) { - final mediaItem = await queueService.generateMediaItem(item, parentType: MediaItemParentType.collection, parentId: item.parentId, isPlayable: _isPlayable); + final mediaItem = await queueService.generateMediaItem(item, + parentType: MediaItemParentType.collection, + parentId: item.parentId, + isPlayable: _isPlayable); mediaItems.add(mediaItem); } return mediaItems; @@ -483,7 +596,8 @@ class AndroidAutoHelper { // shouldn't happen, but just in case if (!_isPlayable(contentType: itemId.contentType)) { - _androidAutoHelperLogger.warning("Tried to play from media id with non-playable item type ${itemId.parentType.name}"); + _androidAutoHelperLogger.warning( + "Tried to play from media id with non-playable item type ${itemId.parentType.name}"); return; } @@ -494,38 +608,40 @@ class AndroidAutoHelper { offlineItems = await _downloadsService.getAllSongs( // nameFilter: widget.searchTerm, viewFilter: finampUserHelper.currentUser?.currentView?.id, - nullableViewFilters: - FinampSettingsHelper.finampSettings.showDownloadsWithUnknownLibrary); + nullableViewFilters: FinampSettingsHelper + .finampSettings.showDownloadsWithUnknownLibrary); - var items = offlineItems - .map((e) => e.baseItem) - .whereNotNull() - .toList(); + var items = offlineItems.map((e) => e.baseItem).whereNotNull().toList(); items = sortItems( items, - FinampSettingsHelper.finampSettings.tabSortBy[TabContentType.songs]!, - FinampSettingsHelper.finampSettings.tabSortOrder[TabContentType.songs]!); + FinampSettingsHelper + .finampSettings.tabSortBy[TabContentType.songs]!, + FinampSettingsHelper + .finampSettings.tabSortOrder[TabContentType.songs]!); - final indexOfSelected = items.indexWhere((element) => element.id == itemId.itemId); + final indexOfSelected = + items.indexWhere((element) => element.id == itemId.itemId); return await queueService.startPlayback( items: items, startingIndex: indexOfSelected, source: QueueItemSource( - name: const QueueItemSourceName( - type: QueueItemSourceNameType.mix), + name: const QueueItemSourceName(type: QueueItemSourceNameType.mix), type: QueueItemSourceType.allSongs, id: itemId.itemId!, ), ); } else { - return await audioServiceHelper.startInstantMixForItem(await _jellyfinApiHelper.getItemById(itemId.itemId!)); + return await audioServiceHelper.startInstantMixForItem( + await _jellyfinApiHelper.getItemById(itemId.itemId!)); } } - if (itemId.parentType != MediaItemParentType.collection || itemId.itemId == null) { - _androidAutoHelperLogger.warning("Tried to play from media id with invalid parent type '${itemId.parentType.name}' or null id"); + if (itemId.parentType != MediaItemParentType.collection || + itemId.itemId == null) { + _androidAutoHelperLogger.warning( + "Tried to play from media id with invalid parent type '${itemId.parentType.name}' or null id"); return; } // get all songs of current parent @@ -555,19 +671,22 @@ class AndroidAutoHelper { final parentBaseItems = await getBaseItems(itemId); - await queueService.startPlayback(items: parentBaseItems, source: QueueItemSource( - type: itemId.contentType == TabContentType.playlists - ? QueueItemSourceType.playlist - : QueueItemSourceType.album, - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: parentItem?.name), - id: parentItem?.id ?? itemId.parentId!, - item: parentItem, - )); + await queueService.startPlayback( + items: parentBaseItems, + source: QueueItemSource( + type: itemId.contentType == TabContentType.playlists + ? QueueItemSourceType.playlist + : QueueItemSourceType.album, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: parentItem?.name), + id: parentItem?.id ?? itemId.parentId!, + item: parentItem, + )); } - Future> _searchTracks(AndroidAutoSearchQuery searchQuery, { + Future> _searchTracks( + AndroidAutoSearchQuery searchQuery, { int limit = 20, }) async { List? searchResult; @@ -580,10 +699,13 @@ class AndroidAutoHelper { searchResultExactQuery = await _getResults( searchTerm: searchQuery.rawQuery.trim(), itemTypes: [TabContentType.songs.itemType], - limit: searchQuery.extras?["android.intent.extra.title"] != null ? (limit/2).round() : limit, + limit: searchQuery.extras?["android.intent.extra.title"] != null + ? (limit / 2).round() + : limit, ); } catch (e) { - _androidAutoHelperLogger.severe("Error while searching for exact query:", e); + _androidAutoHelperLogger.severe( + "Error while searching for exact query:", e); } if (searchQuery.extras?["android.intent.extra.title"] != null) { try { @@ -593,13 +715,16 @@ class AndroidAutoHelper { limit: limit - (searchResultExactQuery?.length ?? 0), ); } catch (e) { - _androidAutoHelperLogger.severe("Error while searching for adjusted query:", e); + _androidAutoHelperLogger.severe( + "Error while searching for adjusted query:", e); } - } - searchResult = searchResultExactQuery?.followedBy(searchResultAdjustedQuery ?? []).toList() ?? []; - + searchResult = searchResultExactQuery + ?.followedBy(searchResultAdjustedQuery ?? []) + .toList() ?? + []; + final List filteredSearchResults = []; // filter out duplicates for (final item in searchResult) { @@ -609,20 +734,29 @@ class AndroidAutoHelper { } if (searchResult.isEmpty) { - _androidAutoHelperLogger.warning("No search results found for query: ${searchQuery.rawQuery} (extras: ${searchQuery.extras})"); + _androidAutoHelperLogger.warning( + "No search results found for query: ${searchQuery.rawQuery} (extras: ${searchQuery.extras})"); } - int calculateMatchQuality(BaseItemDto item, AndroidAutoSearchQuery searchQuery) { + int calculateMatchQuality( + BaseItemDto item, AndroidAutoSearchQuery searchQuery) { final title = item.name ?? ""; - final wantedTitle = searchQuery.extras?["android.intent.extra.title"]?.toString().trim(); - final wantedArtist = searchQuery.extras?["android.intent.extra.artist"]?.toString().trim(); - - if ( - title.toLowerCase() == searchQuery.rawQuery.toLowerCase() || - wantedArtist != null && - (item.albumArtists?.any((artist) => (artist.name?.isNotEmpty ?? false) && (wantedArtist?.toString().toLowerCase().contains(artist.name?.toLowerCase() ?? "") ?? false)) ?? false) - ) { + final wantedTitle = + searchQuery.extras?["android.intent.extra.title"]?.toString().trim(); + final wantedArtist = + searchQuery.extras?["android.intent.extra.artist"]?.toString().trim(); + + if (title.toLowerCase() == searchQuery.rawQuery.toLowerCase() || + wantedArtist != null && + (item.albumArtists?.any((artist) => + (artist.name?.isNotEmpty ?? false) && + (wantedArtist + ?.toString() + .toLowerCase() + .contains(artist.name?.toLowerCase() ?? "") ?? + false)) ?? + false)) { // Title matches exactly or artist matches, highest priority return 1; } else if (title == wantedTitle) { @@ -633,7 +767,7 @@ class AndroidAutoHelper { return -1; } } - + // sort items based on match quality with extras filteredSearchResults.sort((a, b) { final aMatchQuality = calculateMatchQuality(a, searchQuery); @@ -644,12 +778,14 @@ class AndroidAutoHelper { return filteredSearchResults; } - Future> _searchAlbums(AndroidAutoSearchQuery searchQuery, { + Future> _searchAlbums( + AndroidAutoSearchQuery searchQuery, { int limit = 20, }) async { List? searchResult; - bool hasAlbumMetadata = searchQuery.extras?["android.intent.extra.album"] != null; + bool hasAlbumMetadata = + searchQuery.extras?["android.intent.extra.album"] != null; // search for exact query first, then search for adjusted query // sometimes Google's adjustment might not be what we want, but sometimes it actually helps @@ -659,10 +795,11 @@ class AndroidAutoHelper { searchResultExactQuery = await _getResults( searchTerm: searchQuery.rawQuery.trim(), itemTypes: [TabContentType.albums.itemType], - limit: hasAlbumMetadata ? (limit/2).round() : limit, + limit: hasAlbumMetadata ? (limit / 2).round() : limit, ); } catch (e) { - _androidAutoHelperLogger.severe("Error while searching for exact query:", e); + _androidAutoHelperLogger.severe( + "Error while searching for exact query:", e); } if (hasAlbumMetadata) { try { @@ -672,13 +809,16 @@ class AndroidAutoHelper { limit: limit - (searchResultExactQuery?.length ?? 0), ); } catch (e) { - _androidAutoHelperLogger.severe("Error while searching for adjusted query:", e); + _androidAutoHelperLogger.severe( + "Error while searching for adjusted query:", e); } - } - searchResult = searchResultExactQuery?.followedBy(searchResultAdjustedQuery ?? []).toList() ?? []; - + searchResult = searchResultExactQuery + ?.followedBy(searchResultAdjustedQuery ?? []) + .toList() ?? + []; + final List filteredSearchResults = []; // filter out duplicates for (final item in searchResult) { @@ -688,20 +828,29 @@ class AndroidAutoHelper { } if (searchResult.isEmpty) { - _androidAutoHelperLogger.warning("No search results found for query: ${searchQuery.rawQuery} (extras: ${searchQuery.extras})"); + _androidAutoHelperLogger.warning( + "No search results found for query: ${searchQuery.rawQuery} (extras: ${searchQuery.extras})"); } - int calculateMatchQuality(BaseItemDto item, AndroidAutoSearchQuery searchQuery) { + int calculateMatchQuality( + BaseItemDto item, AndroidAutoSearchQuery searchQuery) { final title = item.name ?? ""; - final wantedAlbum = searchQuery.extras?["android.intent.extra.album"]?.toString().trim(); - final wantedArtist = searchQuery.extras?["android.intent.extra.artist"]?.toString().trim(); - - if ( - title.toLowerCase() == searchQuery.rawQuery.toLowerCase() || - wantedArtist != null && - (item.albumArtists?.any((artist) => (artist.name?.isNotEmpty ?? false) && (wantedArtist?.toString().toLowerCase().contains(artist.name?.toLowerCase() ?? "") ?? false)) ?? false) - ) { + final wantedAlbum = + searchQuery.extras?["android.intent.extra.album"]?.toString().trim(); + final wantedArtist = + searchQuery.extras?["android.intent.extra.artist"]?.toString().trim(); + + if (title.toLowerCase() == searchQuery.rawQuery.toLowerCase() || + wantedArtist != null && + (item.albumArtists?.any((artist) => + (artist.name?.isNotEmpty ?? false) && + (wantedArtist + ?.toString() + .toLowerCase() + .contains(artist.name?.toLowerCase() ?? "") ?? + false)) ?? + false)) { // Title matches exactly or artist matches, highest priority return 1; } else if (title == wantedAlbum) { @@ -712,7 +861,7 @@ class AndroidAutoHelper { return -1; } } - + // sort items based on match quality with extras filteredSearchResults.sort((a, b) { final aMatchQuality = calculateMatchQuality(a, searchQuery); @@ -723,12 +872,14 @@ class AndroidAutoHelper { return filteredSearchResults; } - Future> _searchPlaylists(AndroidAutoSearchQuery searchQuery, { + Future> _searchPlaylists( + AndroidAutoSearchQuery searchQuery, { int limit = 20, }) async { List? searchResult; - bool hasPlaylistMetadata = searchQuery.extras?["android.intent.extra.playlist"] != null; + bool hasPlaylistMetadata = + searchQuery.extras?["android.intent.extra.playlist"] != null; // search for exact query first, then search for adjusted query // sometimes Google's adjustment might not be what we want, but sometimes it actually helps @@ -738,26 +889,31 @@ class AndroidAutoHelper { searchResultExactQuery = await _getResults( searchTerm: searchQuery.rawQuery.trim(), itemTypes: [TabContentType.playlists.itemType], - limit: hasPlaylistMetadata ? (limit/2).round() : limit, + limit: hasPlaylistMetadata ? (limit / 2).round() : limit, ); } catch (e) { - _androidAutoHelperLogger.severe("Error while searching for exact query:", e); + _androidAutoHelperLogger.severe( + "Error while searching for exact query:", e); } if (hasPlaylistMetadata) { try { searchResultAdjustedQuery = await _getResults( - searchTerm: searchQuery.extras!["android.intent.extra.playlist"].trim(), + searchTerm: + searchQuery.extras!["android.intent.extra.playlist"].trim(), itemTypes: [TabContentType.playlists.itemType], limit: limit - (searchResultExactQuery?.length ?? 0), ); } catch (e) { - _androidAutoHelperLogger.severe("Error while searching for adjusted query:", e); + _androidAutoHelperLogger.severe( + "Error while searching for adjusted query:", e); } - } - searchResult = searchResultExactQuery?.followedBy(searchResultAdjustedQuery ?? []).toList() ?? []; - + searchResult = searchResultExactQuery + ?.followedBy(searchResultAdjustedQuery ?? []) + .toList() ?? + []; + final List filteredSearchResults = []; // filter out duplicates for (final item in searchResult) { @@ -767,13 +923,18 @@ class AndroidAutoHelper { } if (searchResult.isEmpty) { - _androidAutoHelperLogger.warning("No search results found for query: ${searchQuery.rawQuery} (extras: ${searchQuery.extras})"); + _androidAutoHelperLogger.warning( + "No search results found for query: ${searchQuery.rawQuery} (extras: ${searchQuery.extras})"); } - int calculateMatchQuality(BaseItemDto item, AndroidAutoSearchQuery searchQuery) { + int calculateMatchQuality( + BaseItemDto item, AndroidAutoSearchQuery searchQuery) { final title = item.name ?? ""; - final wantedPlaylist = searchQuery.extras?["android.intent.extra.playlist"]?.toString().trim(); + final wantedPlaylist = searchQuery + .extras?["android.intent.extra.playlist"] + ?.toString() + .trim(); if (title.toLowerCase() == searchQuery.rawQuery.toLowerCase()) { // Title matches exactly, highest priority @@ -786,7 +947,7 @@ class AndroidAutoHelper { return -1; } } - + // sort items based on match quality with extras filteredSearchResults.sort((a, b) { final aMatchQuality = calculateMatchQuality(a, searchQuery); @@ -797,12 +958,14 @@ class AndroidAutoHelper { return filteredSearchResults; } - Future> _searchArtists(AndroidAutoSearchQuery searchQuery, { + Future> _searchArtists( + AndroidAutoSearchQuery searchQuery, { int limit = 20, }) async { List? searchResult; - bool hasArtistMetadata = searchQuery.extras?["android.intent.extra.artist"] != null; + bool hasArtistMetadata = + searchQuery.extras?["android.intent.extra.artist"] != null; // search for exact query first, then search for adjusted query // sometimes Google's adjustment might not be what we want, but sometimes it actually helps @@ -812,10 +975,11 @@ class AndroidAutoHelper { searchResultExactQuery = await _getResults( searchTerm: searchQuery.rawQuery.trim(), itemTypes: [TabContentType.artists.itemType], - limit: hasArtistMetadata ? (limit/2).round() : limit, + limit: hasArtistMetadata ? (limit / 2).round() : limit, ); } catch (e) { - _androidAutoHelperLogger.severe("Error while searching for exact query:", e); + _androidAutoHelperLogger.severe( + "Error while searching for exact query:", e); } if (hasArtistMetadata) { try { @@ -825,13 +989,16 @@ class AndroidAutoHelper { limit: limit - (searchResultExactQuery?.length ?? 0), ); } catch (e) { - _androidAutoHelperLogger.severe("Error while searching for adjusted query:", e); + _androidAutoHelperLogger.severe( + "Error while searching for adjusted query:", e); } - } - searchResult = searchResultExactQuery?.followedBy(searchResultAdjustedQuery ?? []).toList() ?? []; - + searchResult = searchResultExactQuery + ?.followedBy(searchResultAdjustedQuery ?? []) + .toList() ?? + []; + final List filteredSearchResults = []; // filter out duplicates for (final item in searchResult) { @@ -841,19 +1008,21 @@ class AndroidAutoHelper { } if (searchResult.isEmpty) { - _androidAutoHelperLogger.warning("No search results found for query: ${searchQuery.rawQuery} (extras: ${searchQuery.extras})"); + _androidAutoHelperLogger.warning( + "No search results found for query: ${searchQuery.rawQuery} (extras: ${searchQuery.extras})"); } - int calculateMatchQuality(BaseItemDto item, AndroidAutoSearchQuery searchQuery) { + int calculateMatchQuality( + BaseItemDto item, AndroidAutoSearchQuery searchQuery) { final title = item.name ?? ""; - final wantedArtist = searchQuery.extras?["android.intent.extra.artist"]?.toString().trim(); + final wantedArtist = + searchQuery.extras?["android.intent.extra.artist"]?.toString().trim(); if (title.toLowerCase() == searchQuery.rawQuery.toLowerCase()) { // Title matches exactly, highest priority return 1; - } else - if (title == wantedArtist) { + } else if (title == wantedArtist) { // Title matches, normal priority return 0; } else { @@ -861,7 +1030,7 @@ class AndroidAutoHelper { return -1; } } - + // sort items based on match quality with extras filteredSearchResults.sort((a, b) { final aMatchQuality = calculateMatchQuality(a, searchQuery); @@ -880,9 +1049,8 @@ class AndroidAutoHelper { final jellyfinApiHelper = GetIt.instance(); final finampUserHelper = GetIt.instance(); List? searchResult; - - if (FinampSettingsHelper.finampSettings.isOffline) { + if (FinampSettingsHelper.finampSettings.isOffline) { List offlineItems; if (itemTypes.first == TabContentType.songs.itemType) { @@ -891,7 +1059,8 @@ class AndroidAutoHelper { offlineItems = await _downloadsService.getAllSongs( nameFilter: searchTerm, viewFilter: finampUserHelper.currentUser?.currentView?.id, - nullableViewFilters: FinampSettingsHelper.finampSettings.showDownloadsWithUnknownLibrary, + nullableViewFilters: FinampSettingsHelper + .finampSettings.showDownloadsWithUnknownLibrary, onlyFavorites: false); } else { offlineItems = await _downloadsService.getAllCollections( @@ -901,16 +1070,19 @@ class AndroidAutoHelper { viewFilter: itemTypes.first == TabContentType.albums.itemType ? finampUserHelper.currentUser?.currentView?.id : null, - childViewFilter: (itemTypes.first != TabContentType.albums.itemType && - itemTypes.first != TabContentType.playlists.itemType) - ? finampUserHelper.currentUser?.currentView?.id - : null, - nullableViewFilters: itemTypes.first == TabContentType.albums.itemType && - FinampSettingsHelper.finampSettings.showDownloadsWithUnknownLibrary, + childViewFilter: + (itemTypes.first != TabContentType.albums.itemType && + itemTypes.first != TabContentType.playlists.itemType) + ? finampUserHelper.currentUser?.currentView?.id + : null, + nullableViewFilters: + itemTypes.first == TabContentType.albums.itemType && + FinampSettingsHelper + .finampSettings.showDownloadsWithUnknownLibrary, onlyFavorites: false); } - searchResult = offlineItems.map((e) => e.baseItem).whereNotNull().toList(); - + searchResult = + offlineItems.map((e) => e.baseItem).whereNotNull().toList(); } else { if (itemTypes.first == BaseItemDtoType.artist) { searchResult = await jellyfinApiHelper.getArtists( @@ -921,11 +1093,14 @@ class AndroidAutoHelper { ); } else { searchResult = await jellyfinApiHelper.getItems( - parentItem: itemTypes.contains(BaseItemDtoType.playlist) ? null : finampUserHelper.currentUser?.currentView, + parentItem: itemTypes.contains(BaseItemDtoType.playlist) + ? null + : finampUserHelper.currentUser?.currentView, includeItemTypes: itemTypes.map((type) => type.idString).join(","), searchTerm: searchTerm, startIndex: 0, - limit: limit, // get more than the first result so we can filter using additional metadata + limit: + limit, // get more than the first result so we can filter using additional metadata ); } } @@ -940,9 +1115,11 @@ class AndroidAutoHelper { BaseItemDto? item, TabContentType? contentType, }) { - final tabContentType = TabContentType.fromItemType(item?.type ?? contentType?.itemType.idString ?? "Audio"); - return tabContentType == TabContentType.albums || tabContentType == TabContentType.playlists - || tabContentType == TabContentType.artists || tabContentType == TabContentType.songs; + final tabContentType = TabContentType.fromItemType( + item?.type ?? contentType?.itemType.idString ?? "Audio"); + return tabContentType == TabContentType.albums || + tabContentType == TabContentType.playlists || + tabContentType == TabContentType.artists || + tabContentType == TabContentType.songs; } - } diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index e212912c9..d00b50c9d 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -28,11 +28,11 @@ class AudioServiceHelper { // shuffle them before making a sublist, but I couldn't think of a better // way. items = (await _isarDownloader.getAllSongs( - viewFilter: _finampUserHelper.currentUser?.currentView?.id, - nullableViewFilters: - FinampSettingsHelper.finampSettings.showDownloadsWithUnknownLibrary - )).map((e) => e.baseItem!) - .toList(); + viewFilter: _finampUserHelper.currentUser?.currentView?.id, + nullableViewFilters: FinampSettingsHelper + .finampSettings.showDownloadsWithUnknownLibrary)) + .map((e) => e.baseItem!) + .toList(); items.shuffle(); if (items.length - 1 > FinampSettingsHelper.finampSettings.songShuffleItemCount) { diff --git a/lib/services/finamp_settings_helper.dart b/lib/services/finamp_settings_helper.dart index 7a3f5fdf2..4d1d6e440 100644 --- a/lib/services/finamp_settings_helper.dart +++ b/lib/services/finamp_settings_helper.dart @@ -325,7 +325,8 @@ class FinampSettingsHelper { static void resetCustomizationSettings() { FinampSettings finampSettingsTemp = finampSettings; //TODO refactor this so default settings are available here - finampSettingsTemp.playbackSpeedVisibility = PlaybackSpeedVisibility.automatic; + finampSettingsTemp.playbackSpeedVisibility = + PlaybackSpeedVisibility.automatic; finampSettingsTemp.showStopButtonOnMediaNotification = false; finampSettingsTemp.showSeekControlsOnMediaNotification = true; Hive.box("FinampSettings") @@ -370,7 +371,7 @@ class FinampSettingsHelper { } static void setKeepScreenOnWhileCharging(bool keepScreenOnWhileCharging) { - FinampSettings finampSettingsTemp = finampSettings; + FinampSettings finampSettingsTemp = finampSettings; finampSettingsTemp.keepScreenOnWhilePluggedIn = keepScreenOnWhileCharging; Hive.box("FinampSettings") .put("FinampSettings", finampSettingsTemp); diff --git a/lib/services/jellyfin_api_helper.dart b/lib/services/jellyfin_api_helper.dart index d92648a94..ef5d03b8f 100644 --- a/lib/services/jellyfin_api_helper.dart +++ b/lib/services/jellyfin_api_helper.dart @@ -237,6 +237,7 @@ class JellyfinApiHelper { String? searchTerm, String? filters, String? fields, + /// The record index to start at. All items with a lower index will be /// dropped from the results. int? startIndex, @@ -258,7 +259,8 @@ class JellyfinApiHelper { defaultFields; // explicitly set the default fields, if we pass `null` to [JellyfinAPI.getItems] it will **not** apply the default fields, since the argument *is* provided. if (parentItem != null) { - _jellyfinApiHelperLogger.fine("Getting artists which are children of ${parentItem.name}"); + _jellyfinApiHelperLogger + .fine("Getting artists which are children of ${parentItem.name}"); } else { _jellyfinApiHelperLogger.fine("Getting artists."); } @@ -566,14 +568,15 @@ class JellyfinApiHelper { entryIds: entryIds?.join(","), ); if (response.statusCode == 403) { - _jellyfinApiHelperLogger.warning("Failed to remove items from playlist due to insufficient permissions. Status code: ${response.statusCode}"); + _jellyfinApiHelperLogger.warning( + "Failed to remove items from playlist due to insufficient permissions. Status code: ${response.statusCode}"); throw "You do not have permission to remove items from this playlist. Status code: ${response.statusCode}"; } else if (response.error != null) { if (response.error == "") { throw "An unknown error occurred while removing items from the playlist. Status code: ${response.statusCode}"; } throw "${response.error}. Status code: ${response.statusCode}"; - } + } } /// Updates an item. diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 30c108def..e8ad884fa 100644 --- a/lib/services/playback_history_service.dart +++ b/lib/services/playback_history_service.dart @@ -78,7 +78,6 @@ class PlaybackHistoryService { final currentItem = _queueService.getCurrentTrack(); if (currentIndex != null && currentItem != null) { - // differences in queue index or item id are considered track changes if (currentItem.id != prevItem?.id) { if (currentState.playing != prevState?.playing) { @@ -462,7 +461,8 @@ class PlaybackHistoryService { Future _reportPlaybackStopped() async { if (FinampSettingsHelper.finampSettings.isOffline) { if (_currentTrack != null) { - await _offlineListenLogHelper.logOfflineListen(_currentTrack!.item.item); + await _offlineListenLogHelper + .logOfflineListen(_currentTrack!.item.item); } return; } @@ -476,7 +476,8 @@ class PlaybackHistoryService { } } catch (e) { _playbackHistoryServiceLogger.warning(e); - await _offlineListenLogHelper.logOfflineListen(_currentTrack!.item.item); + await _offlineListenLogHelper + .logOfflineListen(_currentTrack!.item.item); } } } diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 80b8488f0..98a3b65f5 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -27,10 +27,9 @@ import 'music_player_background_task.dart'; /// A track queueing service for Finamp. class QueueService { - /// Used to build content:// URIs that are handled by Finamp's built-in content provider. static final contentProviderPackageName = "com.unicornsonlsd.finamp"; - + final _jellyfinApiHelper = GetIt.instance(); final _audioHandler = GetIt.instance(); final _finampUserHelper = GetIt.instance(); @@ -488,8 +487,8 @@ class QueueService { for (int i = 0; i < itemList.length; i++) { jellyfin_models.BaseItemDto item = itemList[i]; try { - MediaItem mediaItem = - await generateMediaItem(item, contextNormalizationGain: source.contextNormalizationGain); + MediaItem mediaItem = await generateMediaItem(item, + contextNormalizationGain: source.contextNormalizationGain); newItems.add(FinampQueueItem( item: mediaItem, source: source, @@ -583,21 +582,22 @@ class QueueService { required List items, QueueItemSource? source, }) async { - if (_queueAudioSource.length == 0) { return _replaceWholeQueue( itemList: items, - source: source ?? QueueItemSource( - type: QueueItemSourceType.queue, - name: const QueueItemSourceName(type: QueueItemSourceNameType.queue), - id: "queue", - item: null, - ), + source: source ?? + QueueItemSource( + type: QueueItemSourceType.queue, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.queue), + id: "queue", + item: null, + ), initialIndex: 0, beginPlaying: false, ); } - + try { if (_savedQueueState == SavedQueueState.pendingSave) { _savedQueueState = SavedQueueState.saving; @@ -605,8 +605,8 @@ class QueueService { List queueItems = []; for (final item in items) { queueItems.add(FinampQueueItem( - item: - await generateMediaItem(item, contextNormalizationGain: source?.contextNormalizationGain), + item: await generateMediaItem(item, + contextNormalizationGain: source?.contextNormalizationGain), source: source ?? _order.originalSource, type: QueueItemQueueType.queue, )); @@ -631,16 +631,17 @@ class QueueService { required List items, QueueItemSource? source, }) async { - if (_queueAudioSource.length == 0) { return _replaceWholeQueue( itemList: items, - source: source ?? QueueItemSource( - type: QueueItemSourceType.queue, - name: const QueueItemSourceName(type: QueueItemSourceNameType.queue), - id: "queue", - item: null, - ), + source: source ?? + QueueItemSource( + type: QueueItemSourceType.queue, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.queue), + id: "queue", + item: null, + ), initialIndex: 0, beginPlaying: false, ); @@ -653,8 +654,8 @@ class QueueService { List queueItems = []; for (final item in items) { queueItems.add(FinampQueueItem( - item: - await generateMediaItem(item, contextNormalizationGain: source?.contextNormalizationGain), + item: await generateMediaItem(item, + contextNormalizationGain: source?.contextNormalizationGain), source: source ?? QueueItemSource( id: "next-up", @@ -687,21 +688,22 @@ class QueueService { required List items, QueueItemSource? source, }) async { - if (_queueAudioSource.length == 0) { return _replaceWholeQueue( itemList: items, - source: source ?? QueueItemSource( - type: QueueItemSourceType.queue, - name: const QueueItemSourceName(type: QueueItemSourceNameType.queue), - id: "queue", - item: null, - ), + source: source ?? + QueueItemSource( + type: QueueItemSourceType.queue, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.queue), + id: "queue", + item: null, + ), initialIndex: 0, beginPlaying: false, ); } - + try { if (_savedQueueState == SavedQueueState.pendingSave) { _savedQueueState = SavedQueueState.saving; @@ -709,8 +711,8 @@ class QueueService { List queueItems = []; for (final item in items) { queueItems.add(FinampQueueItem( - item: - await generateMediaItem(item, contextNormalizationGain: source?.contextNormalizationGain), + item: await generateMediaItem(item, + contextNormalizationGain: source?.contextNormalizationGain), source: source ?? QueueItemSource( id: "next-up", @@ -979,7 +981,9 @@ class QueueService { double? contextNormalizationGain, MediaItemParentType? parentType, String? parentId, - bool Function({ jellyfin_models.BaseItemDto? item, TabContentType? contentType })? isPlayable, + bool Function( + {jellyfin_models.BaseItemDto? item, TabContentType? contentType})? + isPlayable, }) async { const uuid = Uuid(); @@ -1005,9 +1009,11 @@ class QueueService { downloadedSong = downloadsService.getSongDownload(item: item); isDownloaded = downloadedSong != null; } else { - downloadedCollection = await downloadsService.getCollectionInfo(item: item); + downloadedCollection = + await downloadsService.getCollectionInfo(item: item); if (downloadedCollection != null) { - final downloadStatus = downloadsService.getStatus(downloadedCollection, null); + final downloadStatus = + downloadsService.getStatus(downloadedCollection, null); isDownloaded = downloadStatus != DownloadItemStatus.notNeeded; } } @@ -1015,7 +1021,8 @@ class QueueService { try { downloadedImage = downloadsService.getImageDownload(item: item); } catch (e) { - _queueServiceLogger.warning("Couldn't get the offline image for track '${item.name}' because it's not downloaded or missing a blurhash"); + _queueServiceLogger.warning( + "Couldn't get the offline image for track '${item.name}' because it's not downloaded or missing a blurhash"); } Uri? artUri; @@ -1028,12 +1035,15 @@ class QueueService { // try to get image file (Android Automotive needs this) if (artUri != null) { try { - final fileInfo = await AudioService.cacheManager.getFileFromCache(item.id); + final fileInfo = + await AudioService.cacheManager.getFileFromCache(item.id); if (fileInfo != null) { artUri = fileInfo.file.uri; } } catch (e) { - _queueServiceLogger.severe("Error setting new media artwork uri for item: ${item.id} name: ${item.name}", e); + _queueServiceLogger.severe( + "Error setting new media artwork uri for item: ${item.id} name: ${item.name}", + e); } } } @@ -1042,17 +1052,29 @@ class QueueService { if (Platform.isAndroid) { // replace with placeholder art if (artUri == null) { - final applicationSupportDirectory = await getApplicationSupportDirectory(); - artUri = Uri(scheme: "content", host: contentProviderPackageName, path: path_helper.join(applicationSupportDirectory.absolute.path, Assets.images.albumWhite.path)); + final applicationSupportDirectory = + await getApplicationSupportDirectory(); + artUri = Uri( + scheme: "content", + host: contentProviderPackageName, + path: path_helper.join(applicationSupportDirectory.absolute.path, + Assets.images.albumWhite.path)); } else { // store the origin in fragment since it should be unused - artUri = Uri(scheme: "content", host: contentProviderPackageName, path: artUri.path, fragment: ["http", "https"].contains(artUri.scheme) ? artUri.origin : null); + artUri = Uri( + scheme: "content", + host: contentProviderPackageName, + path: artUri.path, + fragment: ["http", "https"].contains(artUri.scheme) + ? artUri.origin + : null); } } return MediaItem( id: itemId?.toString() ?? uuid.v4(), - playable: isItemPlayable, // this dictates whether clicking on an item will try to play it or browse it in media browsers like Android Auto + playable: + isItemPlayable, // this dictates whether clicking on an item will try to play it or browse it in media browsers like Android Auto album: item.album, artist: item.artists?.join(", ") ?? item.albumArtist, artUri: artUri, From b952aa748859391e32eb5701a70ca10b0fa6ed7a Mon Sep 17 00:00:00 2001 From: Maxr1998 Date: Thu, 26 Sep 2024 16:46:43 +0200 Subject: [PATCH 14/21] Sort albums by PremiereDate on artist screen --- lib/components/ArtistScreen/artist_screen_content.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/components/ArtistScreen/artist_screen_content.dart b/lib/components/ArtistScreen/artist_screen_content.dart index 878714f78..d9dfbd23e 100644 --- a/lib/components/ArtistScreen/artist_screen_content.dart +++ b/lib/components/ArtistScreen/artist_screen_content.dart @@ -91,11 +91,11 @@ class _ArtistScreenContentState extends State { ) else Future.value(null), - // Get Albums sorted by Production Year + // Get Albums sorted by Production Year and Premiere Date jellyfinApiHelper.getItems( parentItem: widget.parent, filters: "Artist=${widget.parent.name}", - sortBy: "ProductionYear", + sortBy: "ProductionYear,PremiereDate", includeItemTypes: "MusicAlbum", ), ]); @@ -182,4 +182,4 @@ class _ArtistScreenContentState extends State { ]); }); } -} +} \ No newline at end of file From cbc6f044377297356190339e1c544eb356e0a482 Mon Sep 17 00:00:00 2001 From: Elias Date: Thu, 26 Sep 2024 20:10:43 +0200 Subject: [PATCH 15/21] fix: login side padding Fixes the cramped feeling on the page change. --- lib/components/LoginScreen/login_authentication_page.dart | 1 + lib/components/LoginScreen/login_server_selection_page.dart | 1 + lib/components/LoginScreen/login_splash_page.dart | 1 + lib/components/LoginScreen/login_user_selection_page.dart | 1 + lib/screens/login_screen.dart | 5 +---- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/components/LoginScreen/login_authentication_page.dart b/lib/components/LoginScreen/login_authentication_page.dart index 8f1f71b7e..7f12cb579 100644 --- a/lib/components/LoginScreen/login_authentication_page.dart +++ b/lib/components/LoginScreen/login_authentication_page.dart @@ -54,6 +54,7 @@ class _LoginAuthenticationPageState extends State { Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Center( child: Column( children: [ diff --git a/lib/components/LoginScreen/login_server_selection_page.dart b/lib/components/LoginScreen/login_server_selection_page.dart index b17b921c7..51c8059be 100644 --- a/lib/components/LoginScreen/login_server_selection_page.dart +++ b/lib/components/LoginScreen/login_server_selection_page.dart @@ -73,6 +73,7 @@ class _LoginServerSelectionPageState extends State { Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Center( child: Column( children: [ diff --git a/lib/components/LoginScreen/login_splash_page.dart b/lib/components/LoginScreen/login_splash_page.dart index 5ae0491b4..a300c4a07 100644 --- a/lib/components/LoginScreen/login_splash_page.dart +++ b/lib/components/LoginScreen/login_splash_page.dart @@ -17,6 +17,7 @@ class LoginSplashPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Center( child: Column( children: [ diff --git a/lib/components/LoginScreen/login_user_selection_page.dart b/lib/components/LoginScreen/login_user_selection_page.dart index b74be569f..711cfa2b2 100644 --- a/lib/components/LoginScreen/login_user_selection_page.dart +++ b/lib/components/LoginScreen/login_user_selection_page.dart @@ -59,6 +59,7 @@ class _LoginUserSelectionPageState extends State { return Scaffold( body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 67ce4e7a7..98af7233a 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -21,10 +21,7 @@ class LoginScreen extends StatelessWidget { ), child: const Scaffold( body: SafeArea( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 32.0), - child: LoginFlow(), - ), + child: LoginFlow(), ), bottomNavigationBar: _LoginAuxillaryOptions(), ), From 9ceab66314889badd99489a46d9e920e7c4fb3de Mon Sep 17 00:00:00 2001 From: Elias Date: Thu, 26 Sep 2024 20:26:23 +0200 Subject: [PATCH 16/21] fix: increase the logo image resolution --- images/finamp_cropped.png | Bin 13492 -> 10370 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/images/finamp_cropped.png b/images/finamp_cropped.png index 948f8c4395de6e18f36fc58d10c5906de6117e8c..648735656dc23053940c4db5c5bf05bfd5a21ee1 100644 GIT binary patch literal 10370 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
    AGa2&b9@Me-`v;|I(XMI8JqU2-F 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 Date: Thu, 26 Sep 2024 23:28:16 +0200 Subject: [PATCH 17/21] Remove unnecessary ProductionYear sortBy PremiereDate already automatically falls back to the ProductionYear on the server-side if missing, so there's no need to specify it explicitly. --- lib/components/ArtistScreen/artist_screen_content.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/ArtistScreen/artist_screen_content.dart b/lib/components/ArtistScreen/artist_screen_content.dart index d9dfbd23e..45b97f0bf 100644 --- a/lib/components/ArtistScreen/artist_screen_content.dart +++ b/lib/components/ArtistScreen/artist_screen_content.dart @@ -91,11 +91,11 @@ class _ArtistScreenContentState extends State { ) else Future.value(null), - // Get Albums sorted by Production Year and Premiere Date + // Get Albums sorted by Premiere Date jellyfinApiHelper.getItems( parentItem: widget.parent, filters: "Artist=${widget.parent.name}", - sortBy: "ProductionYear,PremiereDate", + sortBy: "PremiereDate", includeItemTypes: "MusicAlbum", ), ]); From 69f085134cf6db8df514e4213ea06e510b79aed2 Mon Sep 17 00:00:00 2001 From: Maxr1998 Date: Thu, 26 Sep 2024 23:28:58 +0200 Subject: [PATCH 18/21] Fall back to sort artist albums by name If albums don't have a PremiereDate set, they should be sorted by name to have at least some type of order. --- lib/components/ArtistScreen/artist_screen_content.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/ArtistScreen/artist_screen_content.dart b/lib/components/ArtistScreen/artist_screen_content.dart index 45b97f0bf..d1c28e397 100644 --- a/lib/components/ArtistScreen/artist_screen_content.dart +++ b/lib/components/ArtistScreen/artist_screen_content.dart @@ -95,7 +95,7 @@ class _ArtistScreenContentState extends State { jellyfinApiHelper.getItems( parentItem: widget.parent, filters: "Artist=${widget.parent.name}", - sortBy: "PremiereDate", + sortBy: "PremiereDate,SortName", includeItemTypes: "MusicAlbum", ), ]); From f904c17c97f150b34392fc0d141255244c314677 Mon Sep 17 00:00:00 2001 From: Elias Date: Fri, 27 Sep 2024 12:53:40 +0200 Subject: [PATCH 19/21] feat: switch login screen to use svg logo --- images/finamp_cropped.svg | 16 ++++++++++++++++ .../LoginScreen/login_authentication_page.dart | 5 +++-- .../LoginScreen/login_server_selection_page.dart | 5 +++-- .../LoginScreen/login_splash_page.dart | 5 +++-- .../LoginScreen/login_user_selection_page.dart | 5 +++-- pubspec.lock | 16 ++++++++++++++++ pubspec.yaml | 2 ++ 7 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 images/finamp_cropped.svg diff --git a/images/finamp_cropped.svg b/images/finamp_cropped.svg new file mode 100644 index 000000000..57fc195b6 --- /dev/null +++ b/images/finamp_cropped.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/lib/components/LoginScreen/login_authentication_page.dart b/lib/components/LoginScreen/login_authentication_page.dart index 8f1f71b7e..40e34da62 100644 --- a/lib/components/LoginScreen/login_authentication_page.dart +++ b/lib/components/LoginScreen/login_authentication_page.dart @@ -6,6 +6,7 @@ import 'package:finamp/models/jellyfin_models.dart'; import 'package:finamp/services/jellyfin_api_helper.dart'; import 'package:flutter/material.dart' hide ConnectionState; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; @@ -59,8 +60,8 @@ class _LoginAuthenticationPageState extends State { children: [ Padding( padding: const EdgeInsets.only(top: 32.0, bottom: 20.0), - child: Image.asset( - 'images/finamp_cropped.png', + child: SvgPicture.asset( + 'images/finamp_cropped.svg', width: 75, height: 75, ), diff --git a/lib/components/LoginScreen/login_server_selection_page.dart b/lib/components/LoginScreen/login_server_selection_page.dart index b17b921c7..b670d7939 100644 --- a/lib/components/LoginScreen/login_server_selection_page.dart +++ b/lib/components/LoginScreen/login_server_selection_page.dart @@ -3,6 +3,7 @@ import 'package:finamp/models/jellyfin_models.dart'; import 'package:finamp/services/jellyfin_api_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; @@ -80,8 +81,8 @@ class _LoginServerSelectionPageState extends State { padding: const EdgeInsets.only(top: 32.0, bottom: 20.0), child: Hero( tag: "finamp_logo", - child: Image.asset( - 'images/finamp_cropped.png', + child: SvgPicture.asset( + 'images/finamp_cropped.svg', width: 75, height: 75, ), diff --git a/lib/components/LoginScreen/login_splash_page.dart b/lib/components/LoginScreen/login_splash_page.dart index 5ae0491b4..3f781ef99 100644 --- a/lib/components/LoginScreen/login_splash_page.dart +++ b/lib/components/LoginScreen/login_splash_page.dart @@ -1,5 +1,6 @@ import 'package:finamp/components/Buttons/cta_large.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -24,8 +25,8 @@ class LoginSplashPage extends StatelessWidget { padding: const EdgeInsets.only(top: 80.0, bottom: 40.0), child: Hero( tag: "finamp_logo", - child: Image.asset( - 'images/finamp_cropped.png', + child: SvgPicture.asset( + 'images/finamp_cropped.svg', width: 150, height: 150, ), diff --git a/lib/components/LoginScreen/login_user_selection_page.dart b/lib/components/LoginScreen/login_user_selection_page.dart index b74be569f..7041416ce 100644 --- a/lib/components/LoginScreen/login_user_selection_page.dart +++ b/lib/components/LoginScreen/login_user_selection_page.dart @@ -3,6 +3,7 @@ import 'package:finamp/components/LoginScreen/login_server_selection_page.dart'; import 'package:finamp/models/jellyfin_models.dart'; import 'package:finamp/services/jellyfin_api_helper.dart'; import 'package:flutter/material.dart' hide ConnectionState; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; @@ -65,8 +66,8 @@ class _LoginUserSelectionPageState extends State { children: [ Padding( padding: const EdgeInsets.only(top: 32.0, bottom: 20.0), - child: Image.asset( - 'images/finamp_cropped.png', + child: SvgPicture.asset( + 'images/finamp_cropped.svg', width: 75, height: 75, ), diff --git a/pubspec.lock b/pubspec.lock index 220a43769..9d0b32ebf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -564,6 +564,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.5" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + url: "https://pub.dev" + source: hosted + version: "2.0.10+1" flutter_tabler_icons: dependency: "direct main" description: @@ -1599,6 +1607,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + url: "https://pub.dev" + source: hosted + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5c26f4395..a4381d184 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,6 +105,7 @@ dependencies: wakelock_plus: ^1.2.8 battery_plus: ^6.0.2 focus_on_it: ^2.0.1 + flutter_svg: ^2.0.10+1 dev_dependencies: flutter_test: @@ -143,6 +144,7 @@ flutter: - images/finamp.png - images/album_white.png - images/finamp_cropped.png + - images/finamp_cropped.svg - images/jellyfin-icon-transparent.png # - images/a_dot_ham.jpeg From 88d8dc0642d187f8d0e2900d76f6a17f9730c92b Mon Sep 17 00:00:00 2001 From: Elias Date: Fri, 27 Sep 2024 23:55:31 +0200 Subject: [PATCH 20/21] feat: replace finamp_cropped.png in the rest of the screens --- lib/components/MusicScreen/music_screen_drawer.dart | 5 +++-- lib/screens/settings_screen.dart | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/components/MusicScreen/music_screen_drawer.dart b/lib/components/MusicScreen/music_screen_drawer.dart index b64a476cb..e7396988b 100644 --- a/lib/components/MusicScreen/music_screen_drawer.dart +++ b/lib/components/MusicScreen/music_screen_drawer.dart @@ -2,6 +2,7 @@ import 'package:finamp/screens/playback_history_screen.dart'; import 'package:finamp/screens/queue_restore_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get_it/get_it.dart'; @@ -37,8 +38,8 @@ class MusicScreenDrawer extends StatelessWidget { alignment: Alignment.topCenter, child: Padding( padding: const EdgeInsets.all(16.0), - child: Image.asset( - 'images/finamp_cropped.png', + child: SvgPicture.asset( + 'images/finamp_cropped.svg', width: 56, height: 56, ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 4947b8d9c..2e503b2b7 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:locale_names/locale_names.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -59,8 +60,8 @@ class SettingsScreen extends StatelessWidget { applicationVersion: packageInfo.version, applicationIcon: Padding( padding: const EdgeInsets.only(top: 8.0), - child: Image.asset( - 'images/finamp_cropped.png', + child: SvgPicture.asset( + 'images/finamp_cropped.svg', width: 56, height: 56, ), From 9257e9a414aa5954ea5ca7bed31c782aa9773c7b Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 3 Oct 2024 16:10:33 +0200 Subject: [PATCH 21/21] upgrade dependencies --- pubspec.lock | 109 ++++++++++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 48 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 9d0b32ebf..d640da1ca 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.7.0" analyzer_plugin: dependency: transitive description: @@ -109,10 +114,10 @@ packages: dependency: "direct main" description: name: background_downloader - sha256: "6a945db1a1c7727a4bc9c1d7c882cfb1a819f873b77e01d5e5dd6a3fb231cb28" + sha256: "6b73fa5d20c47e855f6ef3ed6fb3e0d164141d8ae7d43ca0a42c78f90eaa15e7" url: "https://pub.dev" source: hosted - version: "8.5.5" + version: "8.5.6" balanced_text: dependency: "direct main" description: @@ -190,18 +195,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.1" + version: "7.3.2" built_collection: dependency: transitive description: @@ -238,18 +243,18 @@ packages: dependency: "direct main" description: name: chopper - sha256: "6b2f5681f2bdca65a1fe2372922e797303fa058b6ead765afa88e40e0fd61071" + sha256: "40899b729fb6d8969d967264b189efaf2452bc3ccf6ed0782d00f1d8a6161c31" url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "8.0.3" chopper_generator: dependency: "direct dev" description: name: chopper_generator - sha256: "7d25ad17062a9b671020f96082ed5f8ee85e18137beb74aca4620137ae6ea523" + sha256: de438569cba1e2a2888e8d91e3c2ac60106574eea7f36823ed0334e96146328a url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "8.0.3" ci: dependency: transitive description: @@ -366,10 +371,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" dartx: dependency: transitive description: @@ -576,10 +581,10 @@ packages: dependency: "direct main" description: name: flutter_tabler_icons - sha256: "08581b2d87e41c86e3acb7cf48482f1a1775e4ed37febc02ca1b99a221836580" + sha256: "657c2201e12fa9121a12ddb4edb74d69290f803868eb6526f04102e6d49ec882" url: "https://pub.dev" source: hosted - version: "1.40.0" + version: "1.43.0" flutter_test: dependency: "direct dev" description: flutter @@ -835,10 +840,10 @@ packages: dependency: transitive description: name: just_audio_web - sha256: b163878529d9b028c53a6972fcd58cae2405bcd11cbfcea620b6fb9f151429d6 + sha256: "9a98035b8b24b40749507687520ec5ab404e291d2b0937823ff45d92cb18d448" url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.13" leak_tracker: dependency: transitive description: @@ -887,6 +892,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" marquee: dependency: "direct main" description: @@ -1189,18 +1202,18 @@ packages: dependency: transitive description: name: puppeteer - sha256: "871140cbcc1bcbc6d8e4c2c6ca8fdeed5fae66dfef1efc4c271160a96e0823f9" + sha256: fc33b2a12731e0b9e16c40cd91ea2b6886bcc24037a435fceb59b786d4074f2b url: "https://pub.dev" source: hosted - version: "3.14.0" + version: "3.15.0" qs_dart: dependency: transitive description: name: qs_dart - sha256: "8dddeaf1d32fe407e253840b2c25c9ab5bf347d2761d82cb4ce010096565c9ff" + sha256: be73d060d29c0716ded88380ba32e87ce8105f0ba234edb3edefa0d74d47d64b url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.2.4" recursive_regex: dependency: transitive description: @@ -1221,10 +1234,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" + sha256: ee72770090078e6841d51355292335f1bc254907c6694283389dcb8156d99a4d url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.3" riverpod_annotation: dependency: "direct main" description: @@ -1237,18 +1250,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22 + sha256: "1ad626afbd8b01d168870b13c0b036f8a5bdb57c14cd426dc5b4595466bd6e2f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" + sha256: b95a8cdc6102397f7d51037131c25ce7e51be900be021af4bf0c2d6f1b8f7aa7 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.3.12" rxdart: dependency: "direct main" description: @@ -1387,18 +1400,18 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788 url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.3.3+2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.4+4" stack_trace: dependency: transitive description: @@ -1443,10 +1456,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -1539,10 +1552,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.3.9" + version: "6.3.10" url_launcher_ios: dependency: transitive description: @@ -1563,10 +1576,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: @@ -1707,18 +1720,18 @@ packages: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.5.5" win32_registry: dependency: transitive description: name: win32_registry - sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.1.5" window_manager: dependency: "direct main" description: @@ -1747,10 +1760,10 @@ packages: dependency: transitive description: name: xxh3 - sha256: a92b30944a9aeb4e3d4f3c3d4ddb3c7816ca73475cd603682c4f8149690f56d7 + sha256: cbeb0e1d10f4c6bf67b650f395eac0cc689425b5efc2ba0cc3d3e069a0beaeec url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" yaml: dependency: transitive description: @@ -1760,5 +1773,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0"