diff --git a/.github/workflows/upload-assets.yml b/.github/workflows/upload-assets.yml index 3242f22cd..02c0d78c2 100644 --- a/.github/workflows/upload-assets.yml +++ b/.github/workflows/upload-assets.yml @@ -59,21 +59,21 @@ jobs: && cp assets/com.unicornsonlsd.finamp.metainfo.xml build/linux/x64/release/ # archive bundle and generate checksum - 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-${{ 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-${{ github.ref }}-linux-release.tar.gz - path: finamp-${{ github.ref }}-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-${{ github.ref }}-linux-release.tar.gz.sha256sum - path: finamp-${{ github.ref }}-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-${{ github.ref }}-linux-release.tar.gz", "./finamp-${{ github.ref }}-linux-release.tar.gz.sha256sum"]' + asset_paths: '["./finamp-${{ github.ref_name }}-linux-release.tar.gz", "./finamp-${{ github.ref_name }}-linux-release.tar.gz.sha256sum"]' diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png deleted file mode 100644 index e13b1d7e3..000000000 Binary files a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 451f9b174..000000000 Binary files a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..edd7619ef --- /dev/null +++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 4dab6c7ca..000000000 Binary files a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index c68294672..000000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 7bc6bcc27..000000000 Binary files a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index af6803219..b78e732b5 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index bb453396e..50c2cd5d9 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 44f3a4897..dcda1b70c 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index dc3464a9a..034c5a091 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 5b0216b48..9b73ebaf2 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ 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/assets/com.unicornsonlsd.finamp.metainfo.xml b/assets/com.unicornsonlsd.finamp.metainfo.xml index 99d997032..30e706d87 100644 --- a/assets/com.unicornsonlsd.finamp.metainfo.xml +++ b/assets/com.unicornsonlsd.finamp.metainfo.xml @@ -39,6 +39,45 @@ 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 + + + New Features + + + Added setting to keep the screen on + Accessibility improvements + More lyrics screen customizations + + + Bug Fixes + + + Improved handling of non-square album covers + Improve fast scroller + Fixed fully black offline mode toggle + + + Thank you for using Finamp! + + + https://github.com/jmshrv/finamp/releases/tag/0.9.9-beta diff --git a/images/finamp_cropped.png b/images/finamp_cropped.png index 948f8c439..648735656 100644 Binary files a/images/finamp_cropped.png and b/images/finamp_cropped.png differ 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/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/components/AddToPlaylistScreen/add_to_playlist_button.dart b/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart index 8c25165b8..e50178ab0 100644 --- a/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart +++ b/lib/components/AddToPlaylistScreen/add_to_playlist_button.dart @@ -6,6 +6,7 @@ import 'package:finamp/services/favorite_provider.dart'; import 'package:finamp/services/feedback_helper.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; @@ -41,37 +42,47 @@ class _AddToPlaylistButtonState extends ConsumerState { } bool isFav = ref.watch(isFavoriteProvider(FavoriteRequest(widget.item))); - return GestureDetector( - onLongPress: () async { - FeedbackHelper.feedback(FeedbackType.selection); - ref - .read(isFavoriteProvider(FavoriteRequest(widget.item)).notifier) - .updateFavorite(!isFav); - }, - child: IconButton( - icon: Icon( - isFav ? Icons.favorite : Icons.favorite_outline, - size: widget.size ?? 24.0, - ), - color: widget.color ?? IconTheme.of(context).color, - disabledColor: - (widget.color ?? IconTheme.of(context).color)!.withOpacity(0.3), - visualDensity: widget.visualDensity ?? VisualDensity.compact, - // tooltip: AppLocalizations.of(context)!.addToPlaylistTooltip, - onPressed: () async { - if (FinampSettingsHelper.finampSettings.isOffline) { - return GlobalSnackbar.message((context) => - AppLocalizations.of(context)!.notAvailableInOfflineMode); - } + return Semantics.fromProperties( + properties: SemanticsProperties( + label: AppLocalizations.of(context)!.addToPlaylistTooltip, + hint: AppLocalizations.of(context)!.playlistActionsMenuButtonTooltip, + button: true, + ), + excludeSemantics: true, + container: true, + child: GestureDetector( + onLongPress: () async { + FeedbackHelper.feedback(FeedbackType.selection); + ref + .read(isFavoriteProvider(FavoriteRequest(widget.item)).notifier) + .updateFavorite(!isFav); + }, + child: IconButton( + icon: Icon( + isFav ? Icons.favorite : Icons.favorite_outline, + size: widget.size ?? 24.0, + ), + color: widget.color ?? IconTheme.of(context).color, + disabledColor: + (widget.color ?? IconTheme.of(context).color)!.withOpacity(0.3), + visualDensity: widget.visualDensity ?? VisualDensity.compact, + // tooltip: AppLocalizations.of(context)!.addToPlaylistTooltip, + onPressed: () async { + if (FinampSettingsHelper.finampSettings.isOffline) { + 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, - usePlayerTheme: true, - ); - }), + bool inPlaylist = queueItemInPlaylist(widget.queueItem); + await showPlaylistActionsMenu( + context: context, + item: widget.item!, + 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 ab6fb68c9..558b64fde 100644 --- a/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart +++ b/lib/components/AlbumScreen/album_screen_content_flexible_space_bar.dart @@ -1,8 +1,8 @@ import 'package:finamp/components/Buttons/cta_medium.dart'; import 'package:finamp/components/global_snackbar.dart'; import 'package:finamp/models/finamp_models.dart'; -import 'package:finamp/services/queue_service.dart'; import 'package:finamp/services/feedback_helper.dart'; +import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; @@ -260,7 +260,8 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { children: [ CTAMedium( text: AppLocalizations.of(context)! - .playButtonLabel, + .playButtonLabel + .toUpperCase(), icon: TablerIcons.player_play, onPressed: () => playAlbum(), // set the minimum width as 25% of the screen width, @@ -345,7 +346,8 @@ class AlbumScreenContentFlexibleSpaceBar extends StatelessWidget { children: [ CTAMedium( text: AppLocalizations.of(context)! - .shuffleButtonLabel, + .shuffleButtonLabel + .toUpperCase(), icon: TablerIcons.arrows_shuffle, onPressed: () => shuffleAlbum(), // set the minimum width as 25% of the screen width, diff --git a/lib/components/AlbumScreen/download_button.dart b/lib/components/AlbumScreen/download_button.dart index d31117f58..f03a54ac1 100644 --- a/lib/components/AlbumScreen/download_button.dart +++ b/lib/components/AlbumScreen/download_button.dart @@ -1,6 +1,7 @@ import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:finamp/services/finamp_user_helper.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; @@ -79,6 +80,7 @@ class DownloadButton extends ConsumerWidget { ); 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. @@ -110,6 +112,7 @@ class DownloadButton extends ConsumerWidget { ); var syncButton = IconButton( icon: const Icon(Icons.sync), + tooltip: AppLocalizations.of(context)!.syncDownloads, onPressed: () { downloadsService.resync(item, viewId); }, @@ -125,7 +128,7 @@ class DownloadButton extends ConsumerWidget { var coreButton = status.isRequired ? deleteButton : downloadButton; // Only show sync on album/song if there we know we are outdated due to failed downloads or the like. // On playlists/artists/genres, always show if downloaded. - List buttons; + List buttons; if (status == DownloadItemStatus.notNeeded || ((item.baseItemType == BaseItemDtoType.album || item.baseItemType == BaseItemDtoType.song) && diff --git a/lib/components/AlbumScreen/download_dialog.dart b/lib/components/AlbumScreen/download_dialog.dart index 023849807..5648f0a62 100644 --- a/lib/components/AlbumScreen/download_dialog.dart +++ b/lib/components/AlbumScreen/download_dialog.dart @@ -121,31 +121,34 @@ class _DownloadDialogState extends State { DownloadProfile(transcodeCodec: FinampTranscodingCodec.original); if (widget.children != null) { - final originalFileSize = widget.children! - .map((e) => e.mediaSources?.first.size ?? 0) - .fold(0, (a, b) => a + b); - final transcodedFileSize = widget.children! .map((e) => e.mediaSources?.first.transcodedSize(FinampSettingsHelper .finampSettings.downloadTranscodingProfile.bitrateChannels)) .fold(0, (a, b) => a + (b ?? 0)); + transcodeDescription = FileSize.getSize( + transcodedFileSize, + precision: PrecisionValue.None, + ); + + final originalFileSize = widget.children! + .map((e) => e.mediaSources?.first.size ?? 0) + .fold(0, (a, b) => a + b); + final originalFileSizeFormatted = FileSize.getSize( originalFileSize, precision: PrecisionValue.None, ); + originalDescription = originalFileSizeFormatted; + final formats = widget.children! .map((e) => e.mediaSources?.first.mediaStreams.first.codec) .toSet(); - transcodeDescription = FileSize.getSize( - transcodedFileSize, - precision: PrecisionValue.None, - ); - - originalDescription = - "$originalFileSizeFormatted ${formats.length == 1 ? formats.first!.toUpperCase() : "null"}"; + if (formats.length == 1) { + originalDescription += " ${formats.first!.toUpperCase()}"; + } } return AlertDialog( diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 9f5c225b8..7cb348993 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -178,12 +178,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 @@ -203,8 +203,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, ), diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index faa2c43fe..614fd2929 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -625,7 +625,7 @@ class _SongMenuState extends ConsumerState { final loopModeTooltips = { FinampLoopMode.none: AppLocalizations.of(context)?.loopModeNoneButtonLabel ?? - "Looping off", + "Not looping", FinampLoopMode.one: AppLocalizations.of(context)?.loopModeOneButtonLabel ?? "Looping this song", diff --git a/lib/components/ArtistScreen/artist_screen_content.dart b/lib/components/ArtistScreen/artist_screen_content.dart index 878714f78..d1c28e397 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 Premiere Date jellyfinApiHelper.getItems( parentItem: widget.parent, filters: "Artist=${widget.parent.name}", - sortBy: "ProductionYear", + sortBy: "PremiereDate,SortName", includeItemTypes: "MusicAlbum", ), ]); @@ -182,4 +182,4 @@ class _ArtistScreenContentState extends State { ]); }); } -} +} \ No newline at end of file diff --git a/lib/components/InteractionSettingsScreen/keep_screen_on_dropdown_list_tile.dart b/lib/components/InteractionSettingsScreen/keep_screen_on_dropdown_list_tile.dart new file mode 100644 index 000000000..0f4883c9c --- /dev/null +++ b/lib/components/InteractionSettingsScreen/keep_screen_on_dropdown_list_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:hive/hive.dart'; + +import '../../models/finamp_models.dart'; +import '../../services/finamp_settings_helper.dart'; + +class KeepScreenOnDropdownListTile extends StatelessWidget { + const KeepScreenOnDropdownListTile({super.key}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: FinampSettingsHelper.finampSettingsListener, + builder: (_, box, __) { + return ListTile( + title: Text(AppLocalizations.of(context)!.keepScreenOn), + subtitle: Text(AppLocalizations.of(context)!.keepScreenOnSubtitle), + trailing: DropdownButton( + value: box.get("FinampSettings")?.keepScreenOnOption, + items: KeepScreenOnOption.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.toLocalisedString(context)), + )) + .toList(), + onChanged: (value) { + if (value != null) { + FinampSettingsHelper.setKeepScreenOnOption(value); + } + }, + ), + ); + }, + ); + } +} diff --git a/lib/components/InteractionSettingsScreen/keep_screen_on_while_charging_selector.dart b/lib/components/InteractionSettingsScreen/keep_screen_on_while_charging_selector.dart new file mode 100644 index 000000000..a2713ce38 --- /dev/null +++ b/lib/components/InteractionSettingsScreen/keep_screen_on_while_charging_selector.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:hive/hive.dart'; + +import '../../models/finamp_models.dart'; +import '../../services/finamp_settings_helper.dart'; + +class KeepScreenOnWhilePluggedInSelector extends StatelessWidget { + const KeepScreenOnWhilePluggedInSelector({super.key}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: FinampSettingsHelper.finampSettingsListener, + builder: (_, box, __) { + return SwitchListTile.adaptive( + title: Text(AppLocalizations.of(context)!.keepScreenOnWhilePluggedIn), + subtitle: Text( + AppLocalizations.of(context)!.keepScreenOnWhilePluggedInSubtitle), + value: FinampSettingsHelper.finampSettings.keepScreenOnWhilePluggedIn, + onChanged: (value) { + FinampSettingsHelper.setKeepScreenOnWhileCharging(value); + }, + ); + }, + ); + } +} diff --git a/lib/components/LoginScreen/login_authentication_page.dart b/lib/components/LoginScreen/login_authentication_page.dart index 8f1f71b7e..8835971e8 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'; @@ -54,13 +55,14 @@ class _LoginAuthenticationPageState extends State { Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Center( child: Column( 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..3906391ca 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'; @@ -73,6 +74,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: [ @@ -80,8 +82,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..e68f22a0f 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'; @@ -17,6 +18,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: [ @@ -24,8 +26,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..efd2336bb 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'; @@ -59,14 +60,15 @@ class _LoginUserSelectionPageState extends State { return Scaffold( body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, 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/MusicScreen/alphabet_item_list.dart b/lib/components/MusicScreen/alphabet_item_list.dart index 2946b2908..660ec9e50 100644 --- a/lib/components/MusicScreen/alphabet_item_list.dart +++ b/lib/components/MusicScreen/alphabet_item_list.dart @@ -7,6 +7,7 @@ import 'package:finamp/services/feedback_helper.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:focus_on_it/focus_on_it.dart'; enum Drag { start, @@ -84,21 +85,24 @@ class _AlphabetListState extends State { onPointerMove: (x) => updateSelected(x.localPosition, Drag.update), onPointerUp: (x) => updateSelected(x.localPosition, Drag.end), behavior: HitTestBehavior.opaque, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - alphabet.length, - (x) => Container( - padding: - const EdgeInsets.only(right: 1.0), - height: _letterHeight, - child: FittedBox( - child: Text( - alphabet[x].toUpperCase(), + child: Semantics( + excludeSemantics: + true, // replace child semantics with custom semantics + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + alphabet.length, + (x) => Container( + padding: const EdgeInsets.only(right: 1.0), + height: _letterHeight, + child: FittedBox( + child: Text( + alphabet[x].toUpperCase(), + ), ), ), - ), - )), + )), + ), ); }), ); @@ -129,15 +133,31 @@ class _AlphabetListState extends State { Positioned( left: 20, top: 20, - child: Container( - width: MediaQuery.sizeOf(context).width / 3, - height: MediaQuery.sizeOf(context).width / 3, - decoration: BoxDecoration( - color: Theme.of(context).cardColor.withOpacity(0.85), - borderRadius: BorderRadius.circular(20)), - child: FittedBox( - child: Text(_currentSelected!, - style: const TextStyle(fontSize: 120))), + child: FocusOnIt( + onUnfocus: () { + setState(() { + _currentSelected = null; + _displayPreview = false; + }); + }, + child: GestureDetector( + onTap: () { + setState(() { + _currentSelected = null; + _displayPreview = false; + }); + }, + child: Container( + width: MediaQuery.sizeOf(context).width / 3, + height: MediaQuery.sizeOf(context).width / 3, + decoration: BoxDecoration( + color: Theme.of(context).cardColor.withOpacity(0.85), + borderRadius: BorderRadius.circular(20)), + child: FittedBox( + child: Text(_currentSelected!, + style: const TextStyle(fontSize: 120))), + ), + ), ), ), Directionality.of(context) == TextDirection.rtl @@ -186,6 +206,9 @@ class _AlphabetListState extends State { _displayPreview = false; _currentSelected = null; }); + } else { + _displayPreview = false; + _currentSelected = null; } }); } else if (_currentSelected != _toBeSelected) { 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/components/MusicScreen/offline_mode_switch_list_tile.dart b/lib/components/MusicScreen/offline_mode_switch_list_tile.dart index d332e9df5..1130e2bde 100644 --- a/lib/components/MusicScreen/offline_mode_switch_list_tile.dart +++ b/lib/components/MusicScreen/offline_mode_switch_list_tile.dart @@ -21,12 +21,7 @@ class OfflineModeSwitchListTile extends StatelessWidget { padding: EdgeInsets.only(right: 16), child: Icon(Icons.cloud_off), ), - //trackColor: MaterialStateProperty.all(Colors.black12), - activeTrackColor: Theme.of(context).brightness == Brightness.light - ? Theme.of(context).primaryColor.withOpacity(0.5) - : null, - trackOutlineColor: MaterialStateProperty.all(Colors.black26), - thumbColor: MaterialStateProperty.all(Theme.of(context).primaryColor), + inactiveTrackColor: Colors.transparent, value: box.get("FinampSettings")?.isOffline ?? false, onChanged: (value) { FinampSettingsHelper.setIsOffline(value); diff --git a/lib/components/MusicScreen/view_list_tile.dart b/lib/components/MusicScreen/view_list_tile.dart index 29b423b88..759f770cb 100644 --- a/lib/components/MusicScreen/view_list_tile.dart +++ b/lib/components/MusicScreen/view_list_tile.dart @@ -1,5 +1,6 @@ import 'package:finamp/components/AlbumScreen/download_button.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; @@ -20,29 +21,37 @@ class ViewListTile extends ConsumerWidget { var currentViewId = ref.watch(FinampUserHelper.finampCurrentUserProvider .select((value) => value.valueOrNull?.currentViewId)); - return ListTile( - leading: Padding( - padding: const EdgeInsets.only(right: 16), - child: ViewIcon( - collectionType: view.collectionType, - color: currentViewId == view.id - ? Theme.of(context).colorScheme.primary - : null, - ), + return Semantics.fromProperties( + properties: SemanticsProperties( + label: view.name, + selected: currentViewId == view.id, ), - title: Text( - view.name ?? "Unknown Name", - style: TextStyle( - color: currentViewId == view.id - ? Theme.of(context).colorScheme.primary - : null, + container: true, + child: ListTile( + leading: Padding( + padding: const EdgeInsets.only(right: 16), + child: ViewIcon( + collectionType: view.collectionType, + color: currentViewId == view.id + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + title: Text( + view.name ?? "Unknown Name", + semanticsLabel: "", // covered by SemanticsProperties + style: TextStyle( + color: currentViewId == view.id + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + onTap: () => finampUserHelper.setCurrentUserCurrentViewId(view.id), + trailing: DownloadButton( + isLibrary: true, + item: DownloadStub.fromItem( + item: view, type: DownloadItemType.collection), ), - ), - onTap: () => finampUserHelper.setCurrentUserCurrentViewId(view.id), - trailing: DownloadButton( - isLibrary: true, - item: DownloadStub.fromItem( - item: view, type: DownloadItemType.collection), ), ); } diff --git a/lib/components/PlayerScreen/album_chip.dart b/lib/components/PlayerScreen/album_chip.dart index 234726e1f..77a2a8762 100644 --- a/lib/components/PlayerScreen/album_chip.dart +++ b/lib/components/PlayerScreen/album_chip.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; @@ -68,29 +69,39 @@ class _AlbumChipContent extends StatelessWidget { final jellyfinApiHelper = GetIt.instance(); final _isarDownloader = GetIt.instance(); - return Material( - color: backgroundColor ?? Colors.white.withOpacity(0.1), - borderRadius: _borderRadius, - child: InkWell( + final albumName = item.album ?? AppLocalizations.of(context)!.noAlbum; + + return Semantics.fromProperties( + properties: SemanticsProperties( + label: "$albumName (${AppLocalizations.of(context)!.album})", + button: true, + ), + excludeSemantics: true, + container: true, + child: Material( + color: backgroundColor ?? Colors.white.withOpacity(0.1), borderRadius: _borderRadius, - onTap: FinampSettingsHelper.finampSettings.isOffline - ? () => _isarDownloader.getCollectionInfo(id: item.albumId!).then( - (album) => Navigator.of(context).pushNamed( - AlbumScreen.routeName, - arguments: album!.baseItem!)) - : () => jellyfinApiHelper.getItemById(item.albumId!).then((album) => - Navigator.of(context) - .pushNamed(AlbumScreen.routeName, arguments: album)), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), - child: Text( - item.album ?? AppLocalizations.of(context)!.noAlbum, - overflow: TextOverflow.ellipsis, - softWrap: false, - style: TextStyle( - color: color ?? - Theme.of(context).textTheme.bodySmall!.color ?? - Colors.white, + child: InkWell( + borderRadius: _borderRadius, + onTap: FinampSettingsHelper.finampSettings.isOffline + ? () => _isarDownloader.getCollectionInfo(id: item.albumId!).then( + (album) => Navigator.of(context).pushNamed( + AlbumScreen.routeName, + arguments: album!.baseItem!)) + : () => jellyfinApiHelper.getItemById(item.albumId!).then( + (album) => Navigator.of(context) + .pushNamed(AlbumScreen.routeName, arguments: album)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + child: Text( + albumName, + overflow: TextOverflow.ellipsis, + softWrap: false, + style: TextStyle( + color: color ?? + Theme.of(context).textTheme.bodySmall!.color ?? + Colors.white, + ), ), ), ), diff --git a/lib/components/PlayerScreen/artist_chip.dart b/lib/components/PlayerScreen/artist_chip.dart index 5e9bb6206..244c483f6 100644 --- a/lib/components/PlayerScreen/artist_chip.dart +++ b/lib/components/PlayerScreen/artist_chip.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; @@ -105,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; } @@ -138,45 +140,52 @@ class _ArtistChipContent extends StatelessWidget { ? item?.name : (item?.artists?.firstOrNull ?? item?.albumArtist); - return SizedBox( - height: _height, - child: Material( - color: backgroundColor, - borderRadius: _borderRadius, - child: InkWell( - // We shouldn't click through to artists if not passed one - onTap: !isArtist - ? null - : () => Navigator.of(context) - .pushNamed(ArtistScreen.routeName, arguments: item), + return Semantics.fromProperties( + properties: SemanticsProperties( + label: "$name (${AppLocalizations.of(context)!.artist})", + button: true, + ), + container: true, + child: SizedBox( + height: _height, + child: Material( + color: backgroundColor, borderRadius: _borderRadius, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isArtist && item?.imageId != null) - AlbumImage( - item: item, - borderRadius: const BorderRadius.only( - topLeft: _radius, - bottomLeft: _radius, + child: InkWell( + // We shouldn't click through to artists if not passed one + onTap: !isArtist + ? null + : () => Navigator.of(context) + .pushNamed(ArtistScreen.routeName, arguments: item), + borderRadius: _borderRadius, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isArtist && item?.imageId != null) + AlbumImage( + item: item, + borderRadius: const BorderRadius.only( + topLeft: _radius, + bottomLeft: _radius, + ), ), - ), - Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 220), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Text( - name ?? AppLocalizations.of(context)!.unknownArtist, - style: TextStyle( - color: color, overflow: TextOverflow.ellipsis), - softWrap: false, - overflow: TextOverflow.ellipsis, + Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 220), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + name ?? AppLocalizations.of(context)!.unknownArtist, + style: TextStyle( + color: color, overflow: TextOverflow.ellipsis), + softWrap: false, + overflow: TextOverflow.ellipsis, + ), ), ), - ), - ) - ], + ) + ], + ), ), ), ), diff --git a/lib/components/PlayerScreen/player_buttons.dart b/lib/components/PlayerScreen/player_buttons.dart index 3f252bec4..40ba65de8 100644 --- a/lib/components/PlayerScreen/player_buttons.dart +++ b/lib/components/PlayerScreen/player_buttons.dart @@ -3,9 +3,11 @@ import 'package:finamp/components/PlayerScreen/player_buttons_shuffle.dart'; import 'package:finamp/screens/player_screen.dart'; import 'package:finamp/services/feedback_helper.dart'; import 'package:flutter/material.dart'; +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'; @@ -32,50 +34,77 @@ class PlayerButtons extends StatelessWidget { children: [ if (controller.shouldShow(PlayerHideable.loopShuffleButtons)) PlayerButtonsRepeating(), - IconButton( - icon: const Icon(TablerIcons.player_skip_back), - onPressed: playbackState != null - ? () async { - FeedbackHelper.feedback(FeedbackType.light); - await audioHandler.skipToPrevious(); - } - : null, + Semantics.fromProperties( + properties: SemanticsProperties( + label: AppLocalizations.of(context)! + .skipToPreviousTrackButtonTooltip, + button: true, + ), + container: true, + excludeSemantics: true, + child: IconButton( + icon: const Icon(TablerIcons.player_skip_back), + onPressed: playbackState != null + ? () async { + FeedbackHelper.feedback(FeedbackType.light); + await audioHandler.skipToPrevious(); + } + : null, + ), ), - _RoundedIconButton( - width: controller.shouldShow(PlayerHideable.bigPlayButton) - ? 62 - : 48, - height: controller.shouldShow(PlayerHideable.bigPlayButton) - ? 62 - : 48, - borderRadius: BorderRadius.circular( - controller.shouldShow(PlayerHideable.bigPlayButton) - ? 16 - : 12), - onTap: playbackState != null - ? () async { - FeedbackHelper.feedback(FeedbackType.light); - if (playbackState.playing) { - await audioHandler.pause(); - } else { - await audioHandler.play(); + Semantics.fromProperties( + properties: SemanticsProperties( + label: + AppLocalizations.of(context)!.togglePlaybackButtonTooltip, + button: true, + ), + container: true, + excludeSemantics: true, + child: _RoundedIconButton( + width: controller.shouldShow(PlayerHideable.bigPlayButton) + ? 62 + : 48, + height: controller.shouldShow(PlayerHideable.bigPlayButton) + ? 62 + : 48, + borderRadius: BorderRadius.circular( + controller.shouldShow(PlayerHideable.bigPlayButton) + ? 16 + : 12), + onTap: playbackState != null + ? () async { + FeedbackHelper.feedback(FeedbackType.light); + if (playbackState.playing) { + await audioHandler.pause(); + } else { + await audioHandler.play(); + } } - } - : null, - icon: Icon( - playbackState == null || playbackState.playing - ? TablerIcons.player_pause - : TablerIcons.player_play, - size: 28), + : null, + icon: Icon( + playbackState == null || playbackState.playing + ? TablerIcons.player_pause + : TablerIcons.player_play, + size: 28), + ), ), - IconButton( - icon: const Icon(TablerIcons.player_skip_forward), - onPressed: playbackState != null - ? () async { - FeedbackHelper.feedback(FeedbackType.light); - await audioHandler.skipToNext(); - } - : null, + Semantics.fromProperties( + properties: SemanticsProperties( + label: AppLocalizations.of(context)! + .skipToNextTrackButtonTooltip, + button: true, + ), + container: true, + excludeSemantics: true, + child: IconButton( + icon: const Icon(TablerIcons.player_skip_forward), + onPressed: playbackState != null + ? () async { + FeedbackHelper.feedback(FeedbackType.light); + await audioHandler.skipToNext(); + } + : null, + ), ), if (controller.shouldShow(PlayerHideable.loopShuffleButtons)) PlayerButtonsShuffle() diff --git a/lib/components/PlayerScreen/player_buttons_more.dart b/lib/components/PlayerScreen/player_buttons_more.dart index 6ffb1ae20..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'; @@ -20,28 +21,33 @@ class PlayerButtonsMore extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return IconTheme( - data: IconThemeData( - color: IconTheme.of(context).color, - size: 24, - ), - child: IconButton( - icon: const Icon( - TablerIcons.menu_2, + return Semantics( + label: AppLocalizations.of(context)!.trackMenuButtonTooltip, + excludeSemantics: true, // replace child semantics with custom semantics + container: true, + child: IconTheme( + data: IconThemeData( + color: IconTheme.of(context).color, + size: 24, + ), + child: IconButton( + icon: const Icon( + TablerIcons.menu_2, + ), + visualDensity: VisualDensity.compact, + onPressed: () async { + if (item == null) return; + var inPlaylist = queueItemInPlaylist(queueItem); + await showModalSongMenu( + context: context, + item: item!, + usePlayerTheme: true, + showPlaybackControls: true, // show controls on player screen + parentItem: inPlaylist ? queueItem!.source.item : null, + isInPlaylist: inPlaylist, + ); + }, ), - visualDensity: VisualDensity.compact, - onPressed: () async { - if (item == null) return; - var inPlaylist = queueItemInPlaylist(queueItem); - await showModalSongMenu( - context: context, - item: item!, - usePlayerTheme: true, - showPlaybackControls: true, // show controls on player screen - parentItem: inPlaylist ? queueItem!.source.item : null, - isInPlaylist: inPlaylist, - ); - }, ), ); } diff --git a/lib/components/PlayerScreen/player_buttons_repeating.dart b/lib/components/PlayerScreen/player_buttons_repeating.dart index 0f5d2c70d..72a0f890f 100644 --- a/lib/components/PlayerScreen/player_buttons_repeating.dart +++ b/lib/components/PlayerScreen/player_buttons_repeating.dart @@ -4,9 +4,11 @@ import 'package:finamp/services/media_state_stream.dart'; import 'package:finamp/services/music_player_background_task.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; +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 PlayerButtonsRepeating extends StatelessWidget { final audioHandler = GetIt.instance(); @@ -22,6 +24,8 @@ class PlayerButtonsRepeating extends StatelessWidget { stream: mediaStateStream, builder: (BuildContext context, AsyncSnapshot snapshot) { return IconButton( + tooltip: + "${getLocalizedLoopMode(context, queueService.loopMode)}. ${AppLocalizations.of(context)!.genericToggleButtonTooltip}", onPressed: () async { FeedbackHelper.feedback(FeedbackType.light); queueService.toggleLoopMode(); @@ -42,4 +46,15 @@ class PlayerButtonsRepeating extends StatelessWidget { return const Icon(TablerIcons.repeat_off); } } + + String getLocalizedLoopMode(BuildContext context, FinampLoopMode loopMode) { + switch (loopMode) { + case FinampLoopMode.all: + return AppLocalizations.of(context)!.loopModeAllButtonLabel; + case FinampLoopMode.one: + return AppLocalizations.of(context)!.loopModeOneButtonLabel; + case FinampLoopMode.none: + return AppLocalizations.of(context)!.loopModeNoneButtonLabel; + } + } } diff --git a/lib/components/PlayerScreen/player_buttons_shuffle.dart b/lib/components/PlayerScreen/player_buttons_shuffle.dart index 56b429e19..71dcfa654 100644 --- a/lib/components/PlayerScreen/player_buttons_shuffle.dart +++ b/lib/components/PlayerScreen/player_buttons_shuffle.dart @@ -5,9 +5,11 @@ import 'package:finamp/services/music_player_background_task.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:finamp/services/feedback_helper.dart'; import 'package:flutter/material.dart'; +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,6 +23,8 @@ class PlayerButtonsShuffle extends StatelessWidget { stream: mediaStateStream, builder: (BuildContext context, AsyncSnapshot snapshot) { return IconButton( + tooltip: + getLocalizedPlaybackOrder(context, _queueService.playbackOrder), onPressed: () async { FeedbackHelper.feedback(FeedbackType.light); _queueService.togglePlaybackOrder(); @@ -34,4 +38,14 @@ class PlayerButtonsShuffle extends StatelessWidget { }, ); } + + 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 9c6f42fa3..ed5ba7956 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'; @@ -32,90 +33,97 @@ class PlayerScreenAlbumImage extends ConsumerWidget { child: CircularProgressIndicator(), ); } + final currentTrack = snapshot.data!.currentTrack; - return GestureDetector( - onSecondaryTapDown: (_) async { - var queueItem = snapshot.data!.currentTrack; - if (queueItem?.baseItem != null) { - var inPlaylist = queueItemInPlaylist(queueItem); - await showModalSongMenu( - context: context, - item: queueItem!.baseItem!, - usePlayerTheme: true, - showPlaybackControls: true, - // show controls on player screen - parentItem: inPlaylist ? queueItem.source.item : null, - isInPlaylist: inPlaylist, - ); - } - }, - child: SimpleGestureDetector( - //TODO replace with PageView, this is just a placeholder - onTap: () { - final audioService = GetIt.instance(); - audioService.togglePlayback(); - FeedbackHelper.feedback(FeedbackType.selection); - }, - onDoubleTap: () { - final currentTrack = queueService.getCurrentTrack(); - if (currentTrack?.baseItem != null && - !FinampSettingsHelper.finampSettings.isOffline) { - ref - .read(isFavoriteProvider( - FavoriteRequest(currentTrack!.baseItem)) - .notifier) - .toggleFavorite(); + return 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 { + var queueItem = snapshot.data!.currentTrack; + if (queueItem?.baseItem != null) { + var inPlaylist = queueItemInPlaylist(queueItem); + await showModalSongMenu( + context: context, + item: queueItem!.baseItem!, + usePlayerTheme: true, + showPlaybackControls: true, + // show controls on player screen + parentItem: inPlaylist ? queueItem.source.item : null, + isInPlaylist: inPlaylist, + ); } }, - onHorizontalSwipe: (direction) { - final queueService = GetIt.instance(); - if (direction == SwipeDirection.left) { - if (!FinampSettingsHelper.finampSettings.disableGesture) { - queueService.skipByOffset(1); - FeedbackHelper.feedback(FeedbackType.selection); + child: SimpleGestureDetector( + //TODO replace with PageView, this is just a placeholder + onTap: () { + final audioService = + GetIt.instance(); + audioService.togglePlayback(); + FeedbackHelper.feedback(FeedbackType.selection); + }, + onDoubleTap: () { + final currentTrack = queueService.getCurrentTrack(); + if (currentTrack?.baseItem != null && + !FinampSettingsHelper.finampSettings.isOffline) { + ref + .read(isFavoriteProvider( + FavoriteRequest(currentTrack!.baseItem)) + .notifier) + .toggleFavorite(); } - } else if (direction == SwipeDirection.right) { - if (!FinampSettingsHelper.finampSettings.disableGesture) { - queueService.skipByOffset(-1); - FeedbackHelper.feedback(FeedbackType.selection); + }, + onHorizontalSwipe: (direction) { + final queueService = GetIt.instance(); + if (direction == SwipeDirection.left) { + if (!FinampSettingsHelper.finampSettings.disableGesture) { + queueService.skipByOffset(1); + FeedbackHelper.feedback(FeedbackType.selection); + } + } else if (direction == SwipeDirection.right) { + if (!FinampSettingsHelper.finampSettings.disableGesture) { + queueService.skipByOffset(-1); + FeedbackHelper.feedback(FeedbackType.selection); + } } - } - }, - child: AspectRatio( - aspectRatio: 1.0, - //aspectRatio: 0.5, - child: Align( - alignment: Alignment.center, - child: LayoutBuilder(builder: (context, constraints) { - //print( - // "control height is ${MediaQuery.sizeOf(context).height - 53.0 - constraints.maxHeight - 24}"); - final horizontalPadding = constraints.maxWidth * - (FinampSettingsHelper - .finampSettings.playerScreenCoverMinimumPadding / - 100.0); - return Padding( - padding: EdgeInsets.only( - left: horizontalPadding, - right: horizontalPadding, - ), - child: AlbumImage( - imageListenable: currentAlbumImageProvider, - borderRadius: BorderRadius.circular(8.0), - // Load player cover at max size to allow more seamless scaling - autoScale: false, - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - blurRadius: 24, - offset: const Offset(0, 4), - color: Colors.black.withOpacity(0.3), - ) - ], - ), + }, + child: LayoutBuilder(builder: (context, constraints) { + //print( + // "control height is ${MediaQuery.sizeOf(context).height - 53.0 - constraints.maxHeight - 24}"); + final horizontalPadding = constraints.maxWidth * + (FinampSettingsHelper + .finampSettings.playerScreenCoverMinimumPadding / + 100.0); + final verticalPadding = constraints.maxHeight * + (FinampSettingsHelper + .finampSettings.playerScreenCoverMinimumPadding / + 100.0); + return Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + vertical: verticalPadding, + ), + child: AlbumImage( + imageListenable: currentAlbumImageProvider, + borderRadius: BorderRadius.circular(8.0), + // Load player cover at max size to allow more seamless scaling + autoScale: false, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: 24, + offset: const Offset(0, 4), + color: Colors.black.withOpacity(0.3), + ) + ], ), - ); - }), - ), + ), + ); + }), ), ), ); diff --git a/lib/components/PlayerScreen/player_split_screen_scaffold.dart b/lib/components/PlayerScreen/player_split_screen_scaffold.dart index 24851d87d..8476f4837 100644 --- a/lib/components/PlayerScreen/player_split_screen_scaffold.dart +++ b/lib/components/PlayerScreen/player_split_screen_scaffold.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:finamp/components/global_snackbar.dart'; import 'package:finamp/screens/lyrics_screen.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; +import 'package:finamp/services/keep_screen_on_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; @@ -132,7 +133,8 @@ Widget buildPlayerSplitScreenScaffold(BuildContext context, Widget? widget) { .pushNamed(x.name!, arguments: x.arguments); return EmptyRoute(); - }), + }, + observers: [KeepScreenOnObserver()]), )), ) ]); diff --git a/lib/components/PlayerScreen/progress_slider.dart b/lib/components/PlayerScreen/progress_slider.dart index 55875f758..1f697dec4 100644 --- a/lib/components/PlayerScreen/progress_slider.dart +++ b/lib/components/PlayerScreen/progress_slider.dart @@ -1,9 +1,13 @@ +import 'dart:io'; + import 'package:audio_service/audio_service.dart'; import 'package:finamp/components/print_duration.dart'; import 'package:finamp/services/progress_state_stream.dart'; 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'; @@ -131,20 +135,29 @@ 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()); return Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - printDuration( - Duration(microseconds: position.inMicroseconds), - ), + printDuration(currentPosition), style: Theme.of(context).textTheme.bodySmall?.copyWith( height: 0.5, // reduce line height ), ), Text( - printDuration(itemDuration), + printDuration( + // display remaining time if on iOS or macOS + showRemaining + ? (roundedDuration - currentPosition) + : roundedDuration, + isRemaining: showRemaining, + ), style: Theme.of(context).textTheme.bodySmall?.copyWith( height: 0.5, // reduce line height ), @@ -217,6 +230,25 @@ class __PlaybackProgressSliderState value: (_dragValue ?? widget.position.inMicroseconds) .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 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); + }, secondaryTrackValue: widget.mediaItem?.extras?["downloadedSongPath"] == null ? widget.playbackState.bufferedPosition.inMicroseconds diff --git a/lib/components/PlayerScreen/queue_button.dart b/lib/components/PlayerScreen/queue_button.dart index 743d8339f..67432f878 100644 --- a/lib/components/PlayerScreen/queue_button.dart +++ b/lib/components/PlayerScreen/queue_button.dart @@ -1,3 +1,4 @@ +import 'package:finamp/components/Buttons/simple_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; @@ -11,10 +12,9 @@ class QueueButton extends StatelessWidget { @override Widget build(BuildContext context) { - return IconButton( - icon: const Icon(TablerIcons.playlist), - visualDensity: const VisualDensity(horizontal: -4, vertical: -4), - tooltip: AppLocalizations.of(context)!.queue, + return SimpleButton( + 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 5b97139e8..1234d6485 100644 --- a/lib/components/PlayerScreen/song_name_content.dart +++ b/lib/components/PlayerScreen/song_name_content.dart @@ -5,7 +5,9 @@ import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models; 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'; @@ -55,24 +57,32 @@ class SongNameContent extends StatelessWidget { : 24, maxWidth: 280, ), - child: BalancedText( - currentTrack.item.title, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - height: 26 / 20, - fontWeight: - Theme.of(context).brightness == Brightness.light - ? FontWeight.w500 - : FontWeight.w600, - overflow: TextOverflow.visible, + child: Semantics.fromProperties( + properties: SemanticsProperties( + label: + "${currentTrack.item.title} (${AppLocalizations.of(context)!.title})", + ), + excludeSemantics: true, + container: true, + child: BalancedText( + currentTrack.item.title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + height: 26 / 20, + fontWeight: + Theme.of(context).brightness == Brightness.light + ? FontWeight.w500 + : FontWeight.w600, + overflow: TextOverflow.visible, + ), + softWrap: true, + overflow: TextOverflow.ellipsis, + maxLines: + controller.shouldShow(PlayerHideable.twoLineTitle) + ? 2 + : 1, ), - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: - controller.shouldShow(PlayerHideable.twoLineTitle) - ? 2 - : 1, ), ), ), 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 06e72bf9e..076c6409b 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'; @@ -74,61 +75,72 @@ class AlbumImage extends ConsumerWidget { ); } - return ClipRRect( - borderRadius: borderRadius, + return Semantics( + // 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, - child: LayoutBuilder(builder: (context, constraints) { - int? physicalWidth; - int? physicalHeight; - if (autoScale) { - // LayoutBuilder (and other pixel-related stuff in Flutter) returns logical pixels instead of physical pixels. - // While this is great for doing layout stuff, we want to get images that are the right size in pixels. - // Logical pixels aren't the same as the physical pixels on the device, they're quite a bit bigger. - // If we use logical pixels for the image request, we'll get a smaller image than we want. - // 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(); - physicalHeight = - (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.contentViewType == - ContentViewType.grid) { - physicalWidth = exp((log(physicalWidth) * 3).ceil() / 3).toInt(); - physicalHeight = - exp((log(physicalHeight) * 3).ceil() / 3).toInt(); - } - } - - var image = Container( - decoration: decoration, - child: BareAlbumImage( - imageListenable: imageListenable ?? - albumImageProvider(AlbumImageRequest( - item: item!, - maxWidth: physicalWidth, - maxHeight: physicalHeight, - )).select((value) => (value, item?.blurHash)), - imageProviderCallback: themeCallback == null - ? null - : (image) => themeCallback!( - FinampTheme.fromImageDeferred(image, item?.blurHash)), - placeholderBuilder: placeholderBuilder), - ); - return disabled - ? Opacity( - opacity: 0.75, - child: ColorFiltered( - colorFilter: - const ColorFilter.mode(Colors.black, BlendMode.color), - child: image)) - : image; - }), + aspectRatio: 1.0, + child: Align( + child: ClipRRect( + borderRadius: borderRadius, + child: LayoutBuilder(builder: (context, constraints) { + int? physicalWidth; + int? physicalHeight; + if (autoScale) { + // LayoutBuilder (and other pixel-related stuff in Flutter) returns logical pixels instead of physical pixels. + // While this is great for doing layout stuff, we want to get images that are the right size in pixels. + // Logical pixels aren't the same as the physical pixels on the device, they're quite a bit bigger. + // If we use logical pixels for the image request, we'll get a smaller image than we want. + // 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(); + physicalHeight = + (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.contentViewType == + ContentViewType.grid) { + physicalWidth = + exp((log(physicalWidth) * 3).ceil() / 3).toInt(); + physicalHeight = + exp((log(physicalHeight) * 3).ceil() / 3).toInt(); + } + } + + var image = Container( + decoration: decoration, + child: BareAlbumImage( + imageListenable: imageListenable ?? + albumImageProvider(AlbumImageRequest( + item: item!, + maxWidth: physicalWidth, + maxHeight: physicalHeight, + )).select((value) => (value, item?.blurHash)), + imageProviderCallback: themeCallback == null + ? null + : (image) => themeCallback!( + FinampTheme.fromImageDeferred( + image, item?.blurHash)), + placeholderBuilder: placeholderBuilder), + ); + return disabled + ? Opacity( + opacity: 0.75, + child: ColorFiltered( + colorFilter: const ColorFilter.mode( + Colors.black, BlendMode.color), + child: image)) + : image; + }), + ), + ), ), ); } @@ -163,9 +175,11 @@ class BareAlbumImage extends ConsumerWidget { var localPlaceholder = placeholderBuilder; if (blurHash != null) { localPlaceholder ??= (_) => Image( - fit: BoxFit.cover, + fit: BoxFit.contain, image: BlurHashImage( blurHash, + // Allow scaling blurhashes up to 3200 pixels wide by setting scale + scale: 0.01, ), ); } diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index 41572caa7..bb4172afd 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -3,12 +3,14 @@ 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'; import 'package:finamp/services/queue_service.dart'; import 'package:finamp/services/theme_provider.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; @@ -46,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) { @@ -138,7 +135,6 @@ class NowPlayingBar extends ConsumerWidget { Widget buildNowPlayingBar( BuildContext context, FinampQueueItem currentTrack) { - final audioHandler = GetIt.instance(); Duration? playbackPosition; @@ -149,352 +145,401 @@ 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), - 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( - 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), - ), - ), - ), - ); - } 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), + 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, ), - 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), + 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), + ), ), ), - Row( - children: [ - StreamBuilder< - Duration>( - stream: - AudioService - .position, - initialData: - audioHandler - .playbackState - .value - .position, - builder: (context, - snapshot) { - final TextStyle - style = - TextStyle( - color: Colors - .white - .withOpacity( - 0.8), - fontSize: 14, - fontWeight: - FontWeight - .w400, - ); - if (snapshot - .hasData) { - playbackPosition = - snapshot - .data; - return 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')}", - style: - style, - ); - } else { - return Text( - "0:00", - style: - style, - ); - } - }), - const SizedBox( - width: 2), - Text( - '/', + ); + } 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.8), - fontSize: 14, - fontWeight: - FontWeight - .w400, - ), + color: Colors + .white + .withOpacity( + 0.85), + fontSize: 13, + fontWeight: + FontWeight + .w300, + overflow: + TextOverflow + .ellipsis), ), - const SizedBox( - width: 2), - Text( - // '3:44', - (mediaState.mediaItem?.duration + ), + 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.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, - ), - ), - ], - ) - ], - ), - ], + 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 + .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(); + } + }) + ], + ), + ], + ), ), ), - ), - 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/components/print_duration.dart b/lib/components/print_duration.dart index 7140c5cfc..eb9a934af 100644 --- a/lib/components/print_duration.dart +++ b/lib/components/print_duration.dart @@ -1,17 +1,32 @@ /// 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, +}) { 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); - return "$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds"; + String twoDigitHours = leadingZeroes + ? twoDigits(duration.inHours) + : duration.inHours.toString(); + durationString = "$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds"; + } else { + durationString = "$twoDigitMinutes:$twoDigitSeconds"; } - return "$twoDigitMinutes:$twoDigitSeconds"; + if (isRemaining) { + durationString = "-$durationString"; + } + + return durationString; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8628b2a58..7638511fe 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -17,6 +17,10 @@ } } }, + "about": "About Finamp", + "@about": { + "description": "Label for the about/info button on the settings screen" + }, "aboutContributionPrompt": "Made by awesome people in their free time.\nYou could be one of them!", "@aboutContributionPrompt": { "description": "Text shown in the about screen, explaining that the app is made by volunteers and that the user could be one of them." @@ -215,6 +219,8 @@ "@sortOrder": {}, "sortBy": "Sort by", "@sortBy": {}, + "title": "Title", + "@title": {}, "album": "Album", "@album": {}, "albumArtist": "Album Artist", @@ -355,9 +361,9 @@ } } }, - "playButtonLabel": "PLAY", + "playButtonLabel": "Play", "@playButtonLabel": {}, - "shuffleButtonLabel": "SHUFFLE", + "shuffleButtonLabel": "Shuffle", "@shuffleButtonLabel": {}, "songCount": "{count,plural,=1{{count} Track} other{{count} Tracks}}", "@songCount": { @@ -392,7 +398,7 @@ "@editPlaylistNameTitle": {}, "required": "Required", "@required": {}, - "updateButtonLabel": "UPDATE", + "updateButtonLabel": "Update", "@updateButtonLabel": {}, "playlistNameUpdated": "Playlist name updated.", "@playlistNameUpdated": {}, @@ -410,7 +416,7 @@ }, "downloadsQueued": "Download prepared, downloading files", "@downloadsQueued": {}, - "addButtonLabel": "ADD", + "addButtonLabel": "Add", "@addButtonLabel": {}, "shareLogs": "Share logs", "@shareLogs": {}, @@ -613,14 +619,22 @@ }, "cancelSleepTimer": "Cancel Sleep Timer?", "@cancelSleepTimer": {}, - "yesButtonLabel": "YES", + "yesButtonLabel": "Yes", "@yesButtonLabel": {}, - "noButtonLabel": "NO", + "noButtonLabel": "No", "@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", @@ -672,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", @@ -792,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)." @@ -914,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.", @@ -948,7 +986,7 @@ "@playbackSpeedIncreaseLabel": { "description": "Label for the button in the speed menu that increases the playback speed." }, - "loopModeNoneButtonLabel": "Looping off", + "loopModeNoneButtonLabel": "Not looping", "@loopModeNoneButtonLabel": { "description": "Label for the button that toggles the loop mode between off, loop one, and loop all, while the queue is in loop off mode" }, @@ -1077,7 +1115,7 @@ "description": "Title for message that shows on the views screen when no music libraries could be found." }, "noMusicLibrariesBody": "Finamp could not find any music libraries. Please ensure that your Jellyfin server contains at least one library with the content type set to \"Music\".", - "refresh": "REFRESH", + "refresh": "Refresh", "moreInfo": "More Info", "volumeNormalizationSettingsTitle": "Volume Normalization", "@volumeNormalizationSettingsTitle": { @@ -1163,7 +1201,7 @@ "@syncComplete": { "description": "Message displayed after download sync completes." }, - "syncDownloads": "Sync and download all items.", + "syncDownloads": "Sync and download missing items.", "@syncDownloads": { "description": "Tooltip for downloads sync button." }, @@ -1420,7 +1458,7 @@ }, "description": "Collection info line in downloads screen lists" }, - "dontTranscode": "Download original{description, select, null{} other { - {description}}}", + "dontTranscode": "Download original{description, select, null{} other{ - {description}}}", "@dontTranscode": { "placeholders": { "description": { @@ -1528,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." @@ -1633,5 +1675,46 @@ "showLyricsScreenAlbumPreludeSubtitle": "Controls if the album cover is shown above the lyrics before being scrolled away.", "@showLyricsScreenAlbumPreludeSubtitle": { "description": "Subtitle for the setting that controls if the album cover is shown before the lyrics in the lyrics view" + }, + "keepScreenOn": "Keep Screen On", + "@keepScreenOn": { + "description": "Option to keep the screen on while using the app" + }, + "keepScreenOnSubtitle": "When to keep the screen on", + "keepScreenOnDisabled": "Disabled", + "keepScreenOnAlwaysOn": "Always On", + "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", + "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" } } diff --git a/lib/main.dart b/lib/main.dart index 46bad0f53..298d1e92f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'package:finamp/services/downloads_service.dart'; import 'package:finamp/services/downloads_service_backend.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:finamp/services/finamp_user_helper.dart'; +import 'package:finamp/services/keep_screen_on_helper.dart'; import 'package:finamp/services/offline_listen_helper.dart'; import 'package:finamp/services/playback_history_service.dart'; import 'package:finamp/services/queue_service.dart'; @@ -86,6 +87,7 @@ void main() async { await _setupDownloadsHelper(); await _setupOSIntegration(); await _setupPlaybackServices(); + await _setupKeepScreenOnHelper(); } catch (error, trace) { hasFailed = true; Logger("ErrorApp").severe(error, null, trace); @@ -151,6 +153,10 @@ Future _setupDownloadsHelper() async { await downloadsService.startQueues(); } +Future _setupKeepScreenOnHelper() async { + GetIt.instance.registerSingleton(KeepScreenOnHelper()); +} + Future setupHive() async { await Hive.initFlutter(); Hive.registerAdapter(BaseItemDtoAdapter()); @@ -214,6 +220,7 @@ Future setupHive() async { Hive.registerAdapter(LyricDtoAdapter()); Hive.registerAdapter(LyricsAlignmentAdapter()); Hive.registerAdapter(LyricsFontSizeAdapter()); + Hive.registerAdapter(KeepScreenOnOptionAdapter()); final dir = (Platform.isAndroid || Platform.isIOS) ? await getApplicationDocumentsDirectory() @@ -397,7 +404,7 @@ Future _setupFinampUserHelper() async { } class Finamp extends ConsumerStatefulWidget { - const Finamp({Key? key}) : super(key: key); + const Finamp({super.key}); @override ConsumerState createState() => _FinampState(); @@ -510,7 +517,10 @@ class _FinampState extends ConsumerState with WindowListener { const LanguageSelectionScreen(), }, initialRoute: SplashScreen.routeName, - navigatorObservers: [SplitScreenNavigatorObserver()], + navigatorObservers: [ + SplitScreenNavigatorObserver(), + KeepScreenOnObserver() + ], builder: buildPlayerSplitScreenScaffold, theme: ThemeData( brightness: Brightness.light, diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 6262f1b55..41d13e3f7 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -111,86 +111,92 @@ const _lyricsFontSizeDefault = LyricsFontSize.medium; const _showLyricsScreenAlbumPreludeDefault = true; const _showStopButtonOnMediaNotificationDefault = false; const _showSeekControlsOnMediaNotificationDefault = true; +const _keepScreenOnOption = KeepScreenOnOption.whileLyrics; +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, - }); + 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; @@ -421,6 +427,12 @@ class FinampSettings { @HiveField(71, defaultValue: _showLyricsScreenAlbumPreludeDefault) bool showLyricsScreenAlbumPrelude; + @HiveField(72, defaultValue: _keepScreenOnOption) + KeepScreenOnOption keepScreenOnOption; + + @HiveField(73, defaultValue: _keepScreenOnWhilePluggedIn) + bool keepScreenOnWhilePluggedIn; + static Future create() async { final downloadLocation = await DownloadLocation.create( name: "Internal Storage", @@ -469,7 +481,6 @@ class FinampSettings { SortOrder getSortOrder(TabContentType tabType) { return tabSortOrder[tabType] ?? SortOrder.ascending; } - } enum CustomPlaybackActions { @@ -672,7 +683,6 @@ enum TabContentType { throw const FormatException("Unsupported itemType"); } } - } @HiveType(typeId: 39) @@ -2064,7 +2074,6 @@ enum MediaItemParentType { @JsonSerializable() @HiveType(typeId: 69) class MediaItemId { - MediaItemId({ required this.contentType, required this.parentType, @@ -2079,7 +2088,7 @@ class MediaItemId { MediaItemParentType parentType; @HiveField(2) - String? itemId; + String? itemId; @HiveField(3) String? parentId; @@ -2093,7 +2102,6 @@ class MediaItemId { String toString() { return jsonEncode(toJson()); } - } @HiveType(typeId: 70) @@ -2177,3 +2185,52 @@ enum LyricsFontSize { } } } + +@HiveType(typeId: 72) +enum KeepScreenOnOption { + @HiveField(0) + disabled, + @HiveField(1) + alwaysOn, + @HiveField(2) + whilePlaying, + @HiveField(3) + whileLyrics; + + /// Human-readable version of this enum. I've written longer descriptions on + /// enums like [TabContentType], and I can't be bothered to copy and paste it + /// again. + @override + @Deprecated("Use toLocalisedString when possible") + String toString() => _humanReadableName(this); + + String toLocalisedString(BuildContext context) => + _humanReadableLocalisedName(this, context); + + String _humanReadableName(KeepScreenOnOption keepScreenOnOption) { + switch (keepScreenOnOption) { + case KeepScreenOnOption.disabled: + return "Disabled"; + case KeepScreenOnOption.alwaysOn: + return "Always On"; + case KeepScreenOnOption.whilePlaying: + return "While Playing Music"; + case KeepScreenOnOption.whileLyrics: + return "While Showing Lyrics"; + } + } + + String _humanReadableLocalisedName( + KeepScreenOnOption keepScreenOnOption, BuildContext context) { + switch (keepScreenOnOption) { + case KeepScreenOnOption.disabled: + return AppLocalizations.of(context)!.keepScreenOnDisabled; + case KeepScreenOnOption.alwaysOn: + return AppLocalizations.of(context)!.keepScreenOnAlwaysOn; + case KeepScreenOnOption.whilePlaying: + return AppLocalizations.of(context)!.keepScreenOnWhilePlaying; + case KeepScreenOnOption.whileLyrics: + return AppLocalizations.of(context)!.keepScreenOnWhileLyrics; + } + } +} diff --git a/lib/models/finamp_models.g.dart b/lib/models/finamp_models.g.dart index 4cff2a9dd..4c9612b83 100644 --- a/lib/models/finamp_models.g.dart +++ b/lib/models/finamp_models.g.dart @@ -180,6 +180,11 @@ class FinampSettingsAdapter extends TypeAdapter { fields[68] == null ? false : fields[68] as bool, showSeekControlsOnMediaNotification: fields[69] == null ? true : fields[69] as bool, + keepScreenOnOption: fields[72] == null + ? KeepScreenOnOption.whileLyrics + : fields[72] as KeepScreenOnOption, + keepScreenOnWhilePluggedIn: + fields[73] == null ? true : fields[73] as bool, ) ..disableGesture = fields[19] == null ? false : fields[19] as bool ..showFastScroller = fields[25] == null ? true : fields[25] as bool @@ -189,7 +194,7 @@ class FinampSettingsAdapter extends TypeAdapter { @override void write(BinaryWriter writer, FinampSettings obj) { writer - ..writeByte(70) + ..writeByte(72) ..writeByte(0) ..write(obj.isOffline) ..writeByte(1) @@ -329,7 +334,11 @@ class FinampSettingsAdapter extends TypeAdapter { ..writeByte(70) ..write(obj.lyricsFontSize) ..writeByte(71) - ..write(obj.showLyricsScreenAlbumPrelude); + ..write(obj.showLyricsScreenAlbumPrelude) + ..writeByte(72) + ..write(obj.keepScreenOnOption) + ..writeByte(73) + ..write(obj.keepScreenOnWhilePluggedIn); } @override @@ -1820,6 +1829,55 @@ class LyricsFontSizeAdapter extends TypeAdapter { typeId == other.typeId; } +class KeepScreenOnOptionAdapter extends TypeAdapter { + @override + final int typeId = 72; + + @override + KeepScreenOnOption read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return KeepScreenOnOption.disabled; + case 1: + return KeepScreenOnOption.alwaysOn; + case 2: + return KeepScreenOnOption.whilePlaying; + case 3: + return KeepScreenOnOption.whileLyrics; + default: + return KeepScreenOnOption.disabled; + } + } + + @override + void write(BinaryWriter writer, KeepScreenOnOption obj) { + switch (obj) { + case KeepScreenOnOption.disabled: + writer.writeByte(0); + break; + case KeepScreenOnOption.alwaysOn: + writer.writeByte(1); + break; + case KeepScreenOnOption.whilePlaying: + writer.writeByte(2); + break; + case KeepScreenOnOption.whileLyrics: + writer.writeByte(3); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is KeepScreenOnOptionAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + // ************************************************************************** // IsarCollectionGenerator // ************************************************************************** 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 b11bd7ca5..bed1cc124 100644 --- a/lib/screens/interaction_settings_screen.dart +++ b/lib/screens/interaction_settings_screen.dart @@ -1,3 +1,5 @@ +import 'package:finamp/components/InteractionSettingsScreen/keep_screen_on_dropdown_list_tile.dart'; +import 'package:finamp/components/InteractionSettingsScreen/keep_screen_on_while_charging_selector.dart'; import 'package:finamp/models/finamp_models.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:flutter/material.dart'; @@ -27,6 +29,8 @@ class InteractionSettingsScreen extends StatelessWidget { FastScrollSelector(), DisableGestureSelector(), DisableVibrationSelector(), + KeepScreenOnDropdownListTile(), + KeepScreenOnWhilePluggedInSelector() ], ), ); @@ -41,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/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(), ), 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/music_screen.dart b/lib/screens/music_screen.dart index cb1e84a86..b97bb6e27 100644 --- a/lib/screens/music_screen.dart +++ b/lib/screens/music_screen.dart @@ -317,24 +317,128 @@ class _MusicScreenState extends ConsumerState : 8.0), child: getFloatingActionButton(sortedTabs.toList()), ), - body: 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(), - ), + body: Builder(builder: (context) { + 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; + }), ), ); }, ); } } + +// This class causes a horizontal swipe to be processed even when another widget +// wins the GestureArena. +class _TransparentSwipeRecognizer extends HorizontalDragGestureRecognizer { + _TransparentSwipeRecognizer({ + super.debugOwner, + super.supportedDevices, + }); + + @override + void rejectGesture(int pointer) { + acceptGesture(pointer); + } +} + +// This class is a cut-down version of SimplifiedGestureDetector/GestureDetector, +// but using _TransparentSwipeRecognizer instead of HorizontalDragGestureRecognizer +// to allow both it and the TabBarView to process the same gestures. +class TransparentRightSwipeDetector extends StatefulWidget { + const TransparentRightSwipeDetector( + {super.key, this.child, required this.action}); + + final Widget? child; + + final void Function() action; + + @override + State createState() => + _TransparentRightSwipeDetectorState(); +} + +class _TransparentRightSwipeDetectorState + extends State { + @override + Widget build(BuildContext context) { + /// Device types that scrollables should accept drag gestures from by default. + const Set supportedDevices = { + PointerDeviceKind.touch, + PointerDeviceKind.stylus, + PointerDeviceKind.invertedStylus, + PointerDeviceKind.trackpad, + // The VoiceAccess sends pointer events with unknown type when scrolling + // scrollables. + PointerDeviceKind.unknown, + }; + final Map gestures = + {}; + gestures[_TransparentSwipeRecognizer] = + GestureRecognizerFactoryWithHandlers<_TransparentSwipeRecognizer>( + () => _TransparentSwipeRecognizer( + debugOwner: this, supportedDevices: supportedDevices), + (_TransparentSwipeRecognizer instance) { + instance + ..onStart = _onHorizontalDragStart + ..onUpdate = _onHorizontalDragUpdate + ..onEnd = _onHorizontalDragEnd + ..supportedDevices = supportedDevices; + }, + ); + + return RawGestureDetector( + gestures: gestures, + child: widget.child, + ); + } + + Offset? _initialSwipeOffset; + + void _onHorizontalDragStart(DragStartDetails details) { + _initialSwipeOffset = details.globalPosition; + } + + void _onHorizontalDragUpdate(DragUpdateDetails details) { + final finalOffset = details.globalPosition; + final initialOffset = _initialSwipeOffset; + if (initialOffset != null) { + final offsetDifference = initialOffset.dx - finalOffset.dx; + if (offsetDifference < -100.0) { + _initialSwipeOffset = null; + widget.action(); + } + } + } + + void _onHorizontalDragEnd(DragEndDetails details) { + _initialSwipeOffset = null; + } +} diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index 3b85cce9c..1f786163b 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -205,7 +205,8 @@ class _PlayerScreenContent extends ConsumerWidget { }, onHorizontalSwipe: (direction) { if (direction == SwipeDirection.left && isLyricsAvailable) { - if (!FinampSettingsHelper.finampSettings.disableGesture || !controller.shouldShow(PlayerHideable.bottomActions)) { + if (!FinampSettingsHelper.finampSettings.disableGesture || + !controller.shouldShow(PlayerHideable.bottomActions)) { Navigator.of(context).push(_buildSlideRouteTransition( playerScreen, const LyricsScreen(), routeSettings: @@ -302,7 +303,7 @@ class _PlayerScreenContent extends ConsumerWidget { ); } else { return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( @@ -509,9 +510,12 @@ class PlayerHideableController { size.height - maxAlbumSize * (1 - (minAlbumPadding / 100.0) * 2); var paddedControlsHeight = max(_getSize().height, desiredHeight); _target = Size(targetWidth, _controlsInternalHeight(paddedControlsHeight)); + // 1/3 of padding goes under the controls and is added by the Column, the other + // 2/3 should be included in the album cover region + var controlsBottomPadding = (paddedControlsHeight - _target!.height) / 3.0; // Do not let album size go negative, use full width _album = Size(size.width, - (size.height - paddedControlsHeight).clamp(1.0, size.width)); + max(1.0, size.height - _target!.height - controlsBottomPadding)); } /// Update player screen hidden elements based on usable area in landscape mode. 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 271104f1f..2e503b2b7 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,7 +1,9 @@ import 'package:finamp/screens/interaction_settings_screen.dart'; 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'; @@ -31,105 +33,116 @@ class SettingsScreen extends StatelessWidget { appBar: AppBar( title: Text(AppLocalizations.of(context)!.settings), actions: [ - IconButton( - icon: const Icon(Icons.info), - onPressed: () async { - final localizations = AppLocalizations.of(context)!; - final applicationLegalese = - AppLocalizations.of(context)!.applicationLegalese(repoLink); - PackageInfo packageInfo = await PackageInfo.fromPlatform(); + Semantics.fromProperties( + properties: SemanticsProperties( + label: AppLocalizations.of(context)!.about, + button: true, + ), + excludeSemantics: true, + container: true, + child: IconButton( + icon: const Icon(Icons.info), + onPressed: () async { + final localizations = AppLocalizations.of(context)!; + 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, - ); + 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), - ), - ], + showAboutDialog( + context: context, + applicationName: packageInfo.appName, + applicationVersion: packageInfo.version, + applicationIcon: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: SvgPicture.asset( + 'images/finamp_cropped.svg', + 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/album_image_provider.dart b/lib/services/album_image_provider.dart index a0979c791..8eb474772 100644 --- a/lib/services/album_image_provider.dart +++ b/lib/services/album_image_provider.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:finamp/models/finamp_models.dart'; import 'package:flutter/cupertino.dart'; @@ -74,18 +75,24 @@ final AutoDisposeProviderFamily request.maxWidth.toString() + request.maxHeight.toString(); } - return CachedImage(NetworkImage(imageUrl.toString()), key); + // Allow drawing albums up to 4X intrinsic size by setting scale + return CachedImage(NetworkImage(imageUrl.toString(), scale: 0.25), key); } // downloads are already de-dupped by blurHash and do not need CachedImage - ImageProvider out = FileImage(downloadedImage!.file!); + // Allow drawing albums up to 4X intrinsic size by setting scale + ImageProvider out = FileImage(downloadedImage!.file!, scale: 0.25); if (request.maxWidth != null && request.maxHeight != null) { // Limit memory cached image size to twice displayed size // This helps keep cache usage by fileImages in check // Caching smaller at 2X size results in blurriness comparable to // NetworkImages fetched with display size - out = ResizeImage(out, - width: request.maxWidth! * 2, height: request.maxHeight! * 2); + out = ResizeImage( + out, + width: request.maxWidth! * 2, + height: request.maxHeight! * 2, + policy: ResizeImagePolicy.fit, + ); } return out; }); 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 e883125a6..d00b50c9d 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -27,7 +27,10 @@ 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()) + items = (await _isarDownloader.getAllSongs( + viewFilter: _finampUserHelper.currentUser?.currentView?.id, + nullableViewFilters: FinampSettingsHelper + .finampSettings.showDownloadsWithUnknownLibrary)) .map((e) => e.baseItem!) .toList(); items.shuffle(); diff --git a/lib/services/finamp_settings_helper.dart b/lib/services/finamp_settings_helper.dart index a27da8b85..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") @@ -361,4 +362,18 @@ class FinampSettingsHelper { Hive.box("FinampSettings") .put("FinampSettings", finampSettingsTemp); } + + static void setKeepScreenOnOption(KeepScreenOnOption keepScreenOnOption) { + FinampSettings finampSettingsTemp = finampSettings; + finampSettingsTemp.keepScreenOnOption = keepScreenOnOption; + Hive.box("FinampSettings") + .put("FinampSettings", finampSettingsTemp); + } + + static void setKeepScreenOnWhileCharging(bool keepScreenOnWhileCharging) { + FinampSettings finampSettingsTemp = finampSettings; + finampSettingsTemp.keepScreenOnWhilePluggedIn = keepScreenOnWhileCharging; + Hive.box("FinampSettings") + .put("FinampSettings", finampSettingsTemp); + } } diff --git a/lib/services/jellyfin_api.dart b/lib/services/jellyfin_api.dart index 93240f9dd..28425d964 100644 --- a/lib/services/jellyfin_api.dart +++ b/lib/services/jellyfin_api.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io' show HttpClient, Platform; +import 'package:app_set_id/app_set_id.dart'; import 'package:chopper/chopper.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:finamp/services/http_aggregate_logging_interceptor.dart'; @@ -331,7 +332,6 @@ abstract class JellyfinApi extends ChopperService { ) @Get(path: "/Artists") Future getArtists({ - /// Specify this to localize the search to a specific item or folder. Omit /// to use the root. @Query("ParentId") String? parentId, @@ -382,7 +382,6 @@ abstract class JellyfinApi extends ChopperService { /// Optional. If enabled, only favorite artists will be returned. @Query("IsFavorite") bool? isFavorite, - }); @FactoryConverter( @@ -521,7 +520,8 @@ abstract class JellyfinApi extends ChopperService { final client = ChopperClient( client: http.IOClient(HttpClient() ..connectionTimeout = const Duration( - seconds: 10) // if we don't get a response by then, it's probably not worth it to wait any longer. this prevents the server connection test from taking too long + seconds: + 10) // if we don't get a response by then, it's probably not worth it to wait any longer. this prevents the server connection test from taking too long ), // The first part of the URL is now here services: [ @@ -604,13 +604,14 @@ Future getAuthHeader() async { DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (Platform.isAndroid) { AndroidDeviceInfo androidDeviceInfo = await deviceInfo.androidInfo; + final appSetId = await AppSetId().getIdentifier(); authHeader = '${authHeader}Device="${androidDeviceInfo.model}", '; - authHeader = '${authHeader}DeviceId="${androidDeviceInfo.id}", '; + authHeader = '${authHeader}DeviceId="$appSetId", '; } else if (Platform.isIOS) { IosDeviceInfo iosDeviceInfo = await deviceInfo.iosInfo; + final appSetId = await AppSetId().getIdentifier(); authHeader = '${authHeader}Device="${iosDeviceInfo.name}", '; - authHeader = - '${authHeader}DeviceId="${iosDeviceInfo.identifierForVendor}", '; + authHeader = '${authHeader}DeviceId="$appSetId", '; } else if (Platform.isWindows) { WindowsDeviceInfo windowsDeviceInfo = await deviceInfo.windowsInfo; authHeader = '${authHeader}Device="${windowsDeviceInfo.computerName}", '; 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/keep_screen_on_helper.dart b/lib/services/keep_screen_on_helper.dart new file mode 100644 index 000000000..470e877dd --- /dev/null +++ b/lib/services/keep_screen_on_helper.dart @@ -0,0 +1,145 @@ +import 'package:battery_plus/battery_plus.dart'; +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/screens/lyrics_screen.dart'; +import 'package:finamp/services/finamp_settings_helper.dart'; +import 'package:finamp/services/music_player_background_task.dart'; +import 'package:flutter/widgets.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +/// Implements ability to keep screen on according to various conditions +class KeepScreenOnHelper { + bool _keepingScreenOn = false; + + bool _isPlaying = false; + bool _isLyricsShowing = false; + bool _isPluggedIn = false; + + final _keepScreenOnLogger = Logger("KeepScreenOnHelper"); + + KeepScreenOnHelper() { + _attachEvents(); + } + + void _attachEvents() { + // Subscribe to audio playback events + final audioHandler = GetIt.instance(); + audioHandler.playbackState.listen((event) async { + if (_isPlaying != event.playing) { + setCondition(isPlaying: event.playing); + } + }); + + // Subscribe to battery state change events + var battery = Battery(); + BatteryState prevBattState = BatteryState.unknown; + battery.onBatteryStateChanged.listen((BatteryState state) { + if (prevBattState != state) { + prevBattState = state; + setCondition(batteryState: state); + } + }); + + FinampSettingsHelper.finampSettingsListener.addListener(() { + // When a settings change occurs, check keepScreenOnState. + setKeepScreenOn(); + }); + } + + void setKeepScreenOn() { + if (FinampSettingsHelper.finampSettings.keepScreenOnWhilePluggedIn && + !_isPluggedIn) { + _turnOff(); + } else { + switch (FinampSettingsHelper.finampSettings.keepScreenOnOption) { + case KeepScreenOnOption.disabled: + if (_keepingScreenOn) _turnOff(); + break; + case KeepScreenOnOption.alwaysOn: + _turnOn(); + break; + case KeepScreenOnOption.whilePlaying: + if (_isPlaying) { + _turnOn(); + } else { + _turnOff(); + } + break; + case KeepScreenOnOption.whileLyrics: + if (_isPlaying && _isLyricsShowing) { + _turnOn(); + } else { + _turnOff(); + } + break; + } + } + + _keepScreenOnLogger.fine( + "keepingScreenOn: $_keepingScreenOn | mainSetting: ${FinampSettingsHelper.finampSettings.keepScreenOnOption} | whilePluggedInSetting: ${FinampSettingsHelper.finampSettings.keepScreenOnWhilePluggedIn} | isPlaying: $_isPlaying | lyricsShowing: $_isLyricsShowing | isPluggedIn: $_isPluggedIn"); + } + + void setCondition( + {bool? isPlaying, bool? isLyricsShowing, BatteryState? batteryState}) { + if (isPlaying != null) _isPlaying = isPlaying; + if (isLyricsShowing != null) _isLyricsShowing = isLyricsShowing; + if (batteryState != null) { + _keepScreenOnLogger.fine("reported battery state: $batteryState"); + switch (batteryState) { + case BatteryState.charging: + case BatteryState.connectedNotCharging: + case BatteryState.full: + _isPluggedIn = true; + break; + case BatteryState.discharging: + case BatteryState.unknown: + _isPluggedIn = false; + break; + default: + // Do nothing + break; + } + } + + setKeepScreenOn(); + } + + void _turnOn() { + if (!_keepingScreenOn) { + _keepingScreenOn = true; + WakelockPlus.enable(); + } + } + + void _turnOff() { + if (_keepingScreenOn) { + _keepingScreenOn = false; + WakelockPlus.disable(); + } + } +} + +class KeepScreenOnObserver extends NavigatorObserver { + final KeepScreenOnHelper keepScreenOnHelper = + GetIt.instance(); + + static final _lyricsCheck = ModalRoute.withName(LyricsScreen.routeName); + @override + void didPush(Route route, Route? previousRoute) { + // Just pushed to lyrics? + if (_lyricsCheck(route)) { + keepScreenOnHelper.setCondition(isLyricsShowing: true); + } + super.didPush(route, previousRoute); + } + + @override + void didPop(Route route, Route? previousRoute) { + // Just popped lyrics? + if (_lyricsCheck(route)) { + keepScreenOnHelper.setCondition(isLyricsShowing: false); + } + super.didPop(route, previousRoute); + } +} diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 84a9fe524..553c3c161 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -26,7 +26,6 @@ import 'android_auto_helper.dart'; /// This provider handles the currently playing music so that multiple widgets /// can control music. class MusicPlayerBackgroundTask extends BaseAudioHandler { - final _androidAutoHelper = GetIt.instance(); AppLocalizations? _appLocalizations; @@ -173,6 +172,14 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { _audioServiceBackgroundTaskLogger.info( "Loop mode changed to ${event.repeatMode} (${_player.loopMode})."); }); + + // This listener basically just kicks the playback state into updating + // whenever a song changes, since some stuff. Done to fix the favorite state + // not updating between songs (https://github.com/jmshrv/finamp/issues/844) + mediaItem.listen((_) { + final event = _transformEvent(_player.playbackEvent); + playbackState.add(event); + }); } /// this could be useful for updating queue state from this player class, but isn't used right now due to limitations with just_audio @@ -447,25 +454,39 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { List _getRootMenu() { return [ MediaItem( - id: MediaItemId(contentType: TabContentType.albums, parentType: MediaItemParentType.rootCollection).toString(), - title: _appLocalizations?.albums ?? TabContentType.albums.toString(), - playable: false, + id: MediaItemId( + contentType: TabContentType.albums, + parentType: MediaItemParentType.rootCollection) + .toString(), + title: _appLocalizations?.albums ?? TabContentType.albums.toString(), + playable: false, ), MediaItem( - id: MediaItemId(contentType: TabContentType.artists, parentType: MediaItemParentType.rootCollection).toString(), - title: _appLocalizations?.artists ?? TabContentType.artists.toString(), - playable: false, + id: MediaItemId( + contentType: TabContentType.artists, + parentType: MediaItemParentType.rootCollection) + .toString(), + title: _appLocalizations?.artists ?? TabContentType.artists.toString(), + playable: false, ), MediaItem( - id: MediaItemId(contentType: TabContentType.playlists, parentType: MediaItemParentType.rootCollection).toString(), - title: _appLocalizations?.playlists ?? TabContentType.playlists.toString(), - playable: false, + id: MediaItemId( + contentType: TabContentType.playlists, + parentType: MediaItemParentType.rootCollection) + .toString(), + title: + _appLocalizations?.playlists ?? TabContentType.playlists.toString(), + playable: false, ), MediaItem( - id: MediaItemId(contentType: TabContentType.genres, parentType: MediaItemParentType.rootCollection).toString(), - title: _appLocalizations?.genres ?? TabContentType.genres.toString(), - playable: false, - )]; + id: MediaItemId( + contentType: TabContentType.genres, + parentType: MediaItemParentType.rootCollection) + .toString(), + title: _appLocalizations?.genres ?? TabContentType.genres.toString(), + playable: false, + ) + ]; } /// Implements a media browser, like used in Android Auto. @@ -475,39 +496,38 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { /// - [AudioService.browsableRootId] is passed when the client requests the root menu (the list of top-level categories) /// - [AudioService.recentRootId] is passed when the client requests the recent items (e.g. in the "For you" section of Android Auto). @override - Future> getChildren(String parentMediaId, [Map? options]) async { - + Future> getChildren(String parentMediaId, + [Map? options]) async { // display root category/parent if (parentMediaId == AudioService.browsableRootId) { - _appLocalizations ??= await AppLocalizations.delegate.load( - LocaleHelper.locale ?? const Locale("en", "US")); + _appLocalizations ??= await AppLocalizations.delegate + .load(LocaleHelper.locale ?? const Locale("en", "US")); return _getRootMenu(); - } - else if (parentMediaId == AudioService.recentRootId) { + } else if (parentMediaId == AudioService.recentRootId) { // return await _androidAutoHelper.getRecentItems(); // return playlists for now - return await _androidAutoHelper.getMediaItems(MediaItemId(contentType: TabContentType.playlists, parentType: MediaItemParentType.rootCollection)); + return await _androidAutoHelper.getMediaItems(MediaItemId( + contentType: TabContentType.playlists, + parentType: MediaItemParentType.rootCollection)); } else { try { final itemId = MediaItemId.fromJson(jsonDecode(parentMediaId)); return await _androidAutoHelper.getMediaItems(itemId); - } catch (e) { _audioServiceBackgroundTaskLogger.severe(e); return super.getChildren(parentMediaId); } } - } /// Called when a media item is requested to be played. /// We jerry-rig the [mediaId] to be a JSON string that can be parsed into a [MediaItemId] object, otherwise we don't have a way to tell which item the mediaId refers to. @override - Future playFromMediaId(String mediaId, [Map? extras]) async { + Future playFromMediaId(String mediaId, + [Map? extras]) async { try { - final mediaItemId = MediaItemId.fromJson(jsonDecode(mediaId)); return await _androidAutoHelper.playFromMediaId(mediaItemId); @@ -518,15 +538,17 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } /// Called when a media browser performs a search, e.g. using a search bar or to correct a voice search. - /// Currently, the [extras] parameter isn't passed correctly by AudioService, so some of the metadata available during a voice search isn't available here, that's why we store the [lastSearchQuery] to use it here. + /// Currently, the [extras] parameter isn't passed correctly by AudioService, so some of the metadata available during a voice search isn't available here, that's why we store the [lastSearchQuery] to use it here. @override - Future> search(String query, [Map? extras]) async { + Future> search(String query, + [Map? extras]) async { _audioServiceBackgroundTaskLogger.info("search: $query ; extras: $extras"); - - final previousItemTitle = _androidAutoHelper.lastSearchQuery?.extras?["android.intent.extra.title"]; - + + final previousItemTitle = _androidAutoHelper + .lastSearchQuery?.extras?["android.intent.extra.title"]; + final currentSearchQuery = AndroidAutoSearchQuery(query, extras); - + if (previousItemTitle != null) { // when voice searching for a song with title + artist, Android Auto / Google Assistant combines the title and artist into a single query, with no way to differentiate them // so we try to instead use the title provided in the extras right after the voice search, and just search for that @@ -542,24 +564,27 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { final results = await _androidAutoHelper.searchItems(currentSearchQuery); return results; - } /// Called when the user asks for an item to be played based on a query. /// In this case, the search needs to be performed and the "best" result should be played immediately. - /// [extras] can contain additional information about the search, like the original query, a title, artist, or album (all optional and filled in by e.g. the Voice Assistant for popular items. Provided fields can indicate which type of item was requested). + /// [extras] can contain additional information about the search, like the original query, a title, artist, or album (all optional and filled in by e.g. the Voice Assistant for popular items. Provided fields can indicate which type of item was requested). @override - Future playFromSearch(String query, [Map? extras]) async { - _audioServiceBackgroundTaskLogger.info("playFromSearch: $query ; extras: $extras"); + Future playFromSearch(String query, + [Map? extras]) async { + _audioServiceBackgroundTaskLogger + .info("playFromSearch: $query ; extras: $extras"); final searchQuery = AndroidAutoSearchQuery(query, extras); _androidAutoHelper.setLastSearchQuery(searchQuery); await _androidAutoHelper.playFromSearch(searchQuery); } @override - Future customAction(String name, [Map? extras]) async { + Future customAction(String name, + [Map? extras]) async { try { - final action = CustomPlaybackActions.values.firstWhere((element) => element.name == name); + final action = CustomPlaybackActions.values + .firstWhere((element) => element.name == name); switch (action) { case CustomPlaybackActions.shuffle: final queueService = GetIt.instance(); @@ -568,7 +593,8 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { jellyfin_models.BaseItemDto? currentItem; if (mediaItem.valueOrNull?.extras?["itemJson"] != null) { - currentItem = jellyfin_models.BaseItemDto.fromJson(mediaItem.valueOrNull?.extras!["itemJson"] as Map); + currentItem = jellyfin_models.BaseItemDto.fromJson(mediaItem + .valueOrNull?.extras!["itemJson"] as Map); } else { return; } @@ -576,12 +602,16 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { bool isFavorite = currentItem.userData?.isFavorite ?? false; if (GlobalSnackbar.materialAppScaffoldKey.currentContext != null) { // get current favorite status from the provider - isFavorite = ProviderScope.containerOf(GlobalSnackbar.materialAppScaffoldKey.currentContext!, listen: false) - .read(isFavoriteProvider(FavoriteRequest(currentItem))); + isFavorite = ProviderScope.containerOf( + GlobalSnackbar.materialAppScaffoldKey.currentContext!, + listen: false) + .read(isFavoriteProvider(FavoriteRequest(currentItem))); // update favorite status with the value returned by the provider - isFavorite = ProviderScope.containerOf(GlobalSnackbar.materialAppScaffoldKey.currentContext!, listen: false) - .read(isFavoriteProvider(FavoriteRequest(currentItem)).notifier) - .updateFavorite(!isFavorite); + isFavorite = ProviderScope.containerOf( + GlobalSnackbar.materialAppScaffoldKey.currentContext!, + listen: false) + .read(isFavoriteProvider(FavoriteRequest(currentItem)).notifier) + .updateFavorite(!isFavorite); } else { // fallback if we can't find the context final jellyfinApiHelper = GetIt.instance(); @@ -607,12 +637,13 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { final event = _transformEvent(_player.playbackEvent); return playbackState.add(event); default: - // NOP, handled below + // NOP, handled below } } catch (e) { - _audioServiceBackgroundTaskLogger.severe("Custom action '$name' not found.", e); + _audioServiceBackgroundTaskLogger.severe( + "Custom action '$name' not found.", e); } - + // only called if no custom action was found return await super.customAction(name, extras); } @@ -700,34 +731,43 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { /// just_audio player will be transformed into an audio_service state so that /// it can be broadcast to audio_service clients. PlaybackState _transformEvent(PlaybackEvent event) { - jellyfin_models.BaseItemDto? currentItem; bool isFavorite = false; if (mediaItem.valueOrNull?.extras?["itemJson"] != null) { - currentItem = jellyfin_models.BaseItemDto.fromJson(mediaItem.valueOrNull?.extras!["itemJson"] as Map); + currentItem = jellyfin_models.BaseItemDto.fromJson( + mediaItem.valueOrNull?.extras!["itemJson"] as Map); if (GlobalSnackbar.materialAppScaffoldKey.currentContext != null) { - isFavorite = ProviderScope.containerOf(GlobalSnackbar.materialAppScaffoldKey.currentContext!, listen: false) - .read(isFavoriteProvider(FavoriteRequest(currentItem))); + isFavorite = ProviderScope.containerOf( + GlobalSnackbar.materialAppScaffoldKey.currentContext!, + listen: false) + .read(isFavoriteProvider(FavoriteRequest(currentItem))); } else { isFavorite = currentItem.userData?.isFavorite ?? false; } } - return PlaybackState( controls: [ MediaControl.skipToPrevious, if (_player.playing) MediaControl.pause else MediaControl.play, MediaControl.skipToNext, MediaControl.custom( - name: CustomPlaybackActions.toggleFavorite.name, - androidIcon: isFavorite - ? "drawable/baseline_heart_filled_24" - : "drawable/baseline_heart_24", - label: isFavorite ? - (GlobalSnackbar.materialAppScaffoldKey.currentContext != null ? AppLocalizations.of(GlobalSnackbar.materialAppScaffoldKey.currentContext!)!.removeFavourite : "Remove favorite") : - (GlobalSnackbar.materialAppScaffoldKey.currentContext != null ? AppLocalizations.of(GlobalSnackbar.materialAppScaffoldKey.currentContext!)!.addFavourite : "Add favorite"), + name: CustomPlaybackActions.toggleFavorite.name, + androidIcon: isFavorite + ? "drawable/baseline_heart_filled_24" + : "drawable/baseline_heart_24", + label: isFavorite + ? (GlobalSnackbar.materialAppScaffoldKey.currentContext != null + ? AppLocalizations.of(GlobalSnackbar + .materialAppScaffoldKey.currentContext!)! + .removeFavourite + : "Remove favorite") + : (GlobalSnackbar.materialAppScaffoldKey.currentContext != null + ? AppLocalizations.of(GlobalSnackbar + .materialAppScaffoldKey.currentContext!)! + .addFavourite + : "Add favorite"), ), //!!! Android Auto adds a shuffle toggle button automatically, adding it here would result in a duplicate button // MediaControl.custom( @@ -739,16 +779,19 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { // (GlobalSnackbar.materialAppScaffoldKey.currentContext != null ? AppLocalizations.of(GlobalSnackbar.materialAppScaffoldKey.currentContext!)!.playbackOrderShuffledButtonLabel : "Shuffle enabled") : // (GlobalSnackbar.materialAppScaffoldKey.currentContext != null ? AppLocalizations.of(GlobalSnackbar.materialAppScaffoldKey.currentContext!)!.playbackOrderLinearButtonLabel : "Shuffle disabled"), // ), - if (FinampSettingsHelper.finampSettings.showStopButtonOnMediaNotification) - MediaControl.stop.copyWith( - androidIcon: "drawable/baseline_stop_24"), - // MediaControl.stop, + if (FinampSettingsHelper + .finampSettings.showStopButtonOnMediaNotification) + MediaControl.stop.copyWith(androidIcon: "drawable/baseline_stop_24"), + // MediaControl.stop, ], - systemActions: FinampSettingsHelper.finampSettings.showSeekControlsOnMediaNotification ? const { - MediaAction.seek, - MediaAction.seekForward, - MediaAction.seekBackward, - } : {}, + systemActions: FinampSettingsHelper + .finampSettings.showSeekControlsOnMediaNotification + ? const { + MediaAction.seek, + MediaAction.seekForward, + MediaAction.seekBackward, + } + : {}, androidCompactActionIndices: const [0, 1, 2], processingState: const { ProcessingState.idle: AudioProcessingState.idle, diff --git a/lib/services/offline_listen_helper.dart b/lib/services/offline_listen_helper.dart index a9556b9f1..95ec2debd 100644 --- a/lib/services/offline_listen_helper.dart +++ b/lib/services/offline_listen_helper.dart @@ -48,7 +48,7 @@ class OfflineListenLogHelper { trackMbid: itemJson["ProviderIds"]?["MusicBrainzTrack"], ); - await _logOfflineListen(offlineListen); + return _logOfflineListen(offlineListen); } /// Logs a listen to a file. @@ -56,10 +56,11 @@ class OfflineListenLogHelper { /// This is used when the user is offline or submitting live playback events fails. /// The [timestamp] provided to this function should be in seconds /// and marks the time the track was stopped. - Future _logOfflineListen(OfflineListen listen) async { - Hive.box("OfflineListens").add(listen); - - _exportOfflineListenToFile(listen); + Future _logOfflineListen(OfflineListen listen) { + return Future.wait([ + Hive.box("OfflineListens").add(listen), + _exportOfflineListenToFile(listen) + ]); } Future _exportOfflineListenToFile(OfflineListen listen) async { @@ -76,7 +77,7 @@ class OfflineListenLogHelper { final file = await _logFile; try { - file.writeAsString(content, mode: FileMode.append, flush: true); + await file.writeAsString(content, mode: FileMode.append, flush: true); } catch (e) { _logger.warning("Failed to write listen to file: $content"); } diff --git a/lib/services/playback_history_service.dart b/lib/services/playback_history_service.dart index 05b67af7f..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) { @@ -341,7 +340,7 @@ class PlaybackHistoryService { ) async { if (FinampSettingsHelper.finampSettings.isOffline) { if (previousItem != null) { - _offlineListenLogHelper.logOfflineListen(previousItem.item); + await _offlineListenLogHelper.logOfflineListen(previousItem.item); } return; } @@ -382,7 +381,7 @@ class PlaybackHistoryService { } catch (e) { _playbackHistoryServiceLogger.warning(e); if (previousItem != null) { - _offlineListenLogHelper.logOfflineListen(previousItem.item); + await _offlineListenLogHelper.logOfflineListen(previousItem.item); } } } @@ -396,7 +395,7 @@ class PlaybackHistoryService { await _jellyfinApiHelper.reportPlaybackStart(newTrackplaybackData); } catch (e) { _playbackHistoryServiceLogger.warning(e); - //!!! don't catch with offline listen log helper, as only stop events are logged + // don't log start event to offline listen log helper, as only stop events are logged } } } @@ -433,7 +432,7 @@ class PlaybackHistoryService { } } catch (e) { _playbackHistoryServiceLogger.warning(e); - _offlineListenLogHelper.logOfflineListen(currentItem.item); + await _offlineListenLogHelper.logOfflineListen(currentItem.item); } } } @@ -462,7 +461,8 @@ class PlaybackHistoryService { Future _reportPlaybackStopped() async { if (FinampSettingsHelper.finampSettings.isOffline) { if (_currentTrack != null) { - _offlineListenLogHelper.logOfflineListen(_currentTrack!.item.item); + await _offlineListenLogHelper + .logOfflineListen(_currentTrack!.item.item); } return; } @@ -476,7 +476,8 @@ class PlaybackHistoryService { } } catch (e) { _playbackHistoryServiceLogger.warning(e); - _offlineListenLogHelper.logOfflineListen(_currentTrack!.item.item); + await _offlineListenLogHelper + .logOfflineListen(_currentTrack!.item.item); } } } @@ -499,7 +500,6 @@ class PlaybackHistoryService { } } catch (e) { _playbackHistoryServiceLogger.warning(e); - _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, 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 diff --git a/pubspec.lock b/pubspec.lock index 493878bbf..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: @@ -25,14 +30,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.3" + app_set_id: + dependency: "direct main" + description: + name: app_set_id + sha256: "455b04bc03fd9b1b66a50bbca74278d973ac4e6f2c03d5dd4707bb071613a7e5" + url: "https://pub.dev" + source: hosted + version: "1.2.0" archive: dependency: transitive description: name: archive - sha256: "6bd38d335f0954f5fad9c79e614604fbf03a0e5b975923dd001b6ea965ef5b4b" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.6.1" args: dependency: transitive description: @@ -53,18 +66,18 @@ packages: dependency: "direct main" description: name: audio_service - sha256: "4547c312a94f9cb2c48b60823fb190767cbd63454a83c73049384d5d3cba4650" + sha256: "9dd5ba7e77567b290c35908b1950d61485b4dfdd3a0ac398e98cfeec04651b75" url: "https://pub.dev" source: hosted - version: "0.18.13" + version: "0.18.15" audio_service_mpris: dependency: "direct main" description: name: audio_service_mpris - sha256: a8d1583f9143d17b2facc994a99bd1ea257cec43adcb8d7349458555c62b570f + sha256: b16db3584a4b2464c0bfd575c1a21765723d257931222f8adfcb0511f940d352 url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.5" audio_service_platform_interface: dependency: "direct main" description: @@ -77,18 +90,18 @@ packages: dependency: transitive description: name: audio_service_web - sha256: "9d7d5ae5f98a5727f2580fad73062f2484f400eef6cef42919413268e62a363e" + sha256: "4cdc2127cd4562b957fb49227dc58e3303fafb09bde2573bc8241b938cf759d9" url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" audio_session: dependency: "direct main" description: name: audio_session - sha256: a49af9981eec5d7cd73b37bacb6ee73f8143a6a9f9bd5b6021e6c346b9b6cf4e + sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261" url: "https://pub.dev" source: hosted - version: "0.1.19" + version: "0.1.21" auto_size_text: dependency: "direct main" description: @@ -101,10 +114,10 @@ packages: dependency: "direct main" description: name: background_downloader - sha256: "9504093db43da6095c44dd14fc816f3ee8961633ace12340f5d3c4fbfd346e2d" + sha256: "6b73fa5d20c47e855f6ef3ed6fb3e0d164141d8ae7d43ca0a42c78f90eaa15e7" url: "https://pub.dev" source: hosted - version: "8.5.2" + version: "8.5.6" balanced_text: dependency: "direct main" description: @@ -114,6 +127,22 @@ packages: url: "https://github.com/Komodo5197/flutter-balanced-text.git" source: git version: "0.0.3" + battery_plus: + dependency: "direct main" + description: + name: battery_plus + sha256: ccc1322fee1153a0f89e663e0eac2f64d659da506454cf24dcad75eb08ae138b + url: "https://pub.dev" + source: hosted + version: "6.0.2" + battery_plus_platform_interface: + dependency: transitive + description: + name: battery_plus_platform_interface + sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910 + url: "https://pub.dev" + source: hosted + version: "2.0.1" boolean_selector: dependency: transitive description: @@ -166,18 +195,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -214,18 +243,18 @@ packages: dependency: "direct main" description: name: chopper - sha256: "779a7bc5c7af2e45bd35c49698f2b6fabc23ac053b622294369bbb079eeb8920" + sha256: "40899b729fb6d8969d967264b189efaf2452bc3ccf6ed0782d00f1d8a6161c31" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.0.3" chopper_generator: dependency: "direct dev" description: name: chopper_generator - sha256: f7f4913d14bbec24b5cc3c5270f47a3a218bd1c764d7ed3eb0bf4574913208f3 + sha256: de438569cba1e2a2888e8d91e3c2ac60106574eea7f36823ed0334e96146328a url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.0.3" ci: dependency: transitive description: @@ -302,18 +331,18 @@ packages: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" custom_lint: dependency: "direct dev" description: @@ -342,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: @@ -366,18 +395,18 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" equatable: dependency: transitive description: @@ -406,10 +435,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -422,10 +451,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" + sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "8.0.7" file_sizes: dependency: "direct main" description: @@ -467,18 +496,18 @@ packages: dependency: transitive description: name: flutter_gen_core - sha256: b9894396b2a790cc2d6eb3ed86e5e113aaed993765b21d4b981c9da4476e0f52 + sha256: "638d518897f1aefc55a24278968027591d50223a6943b6ae9aa576fe1494d99d" url: "https://pub.dev" source: hosted - version: "5.5.0+1" + version: "5.7.0" flutter_gen_runner: dependency: "direct dev" description: name: flutter_gen_runner - sha256: b4c4c54e4dd89022f5e405fe96f16781be2dfbeabe8a70ccdf73b7af1302c655 + sha256: "7f2f02d95e3ec96cf70a1c515700c0dd3ea905af003303a55d6fb081240e6b8a" url: "https://pub.dev" source: hosted - version: "5.5.0+1" + version: "5.7.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -504,10 +533,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.22" flutter_riverpod: dependency: "direct main" description: @@ -540,14 +569,22 @@ 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: name: flutter_tabler_icons - sha256: "5c27597ed7e3a7f8ae5a3d6aa2c27223fdd3da165c08a4879083c394acfa3678" + sha256: "657c2201e12fa9121a12ddb4edb74d69290f803868eb6526f04102e6d49ec882" url: "https://pub.dev" source: hosted - version: "1.29.0" + version: "1.43.0" flutter_test: dependency: "direct dev" description: flutter @@ -557,10 +594,10 @@ packages: dependency: "direct main" description: name: flutter_to_airplay - sha256: "9ed02327954d2bdb28d529198bedb1b6ab993be08125fdff46af2c5e5f245983" + sha256: "702408986b652dfaef5ad68c6f3c3008941ae8d8ef5db526792239c8d490a16d" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.5" flutter_vibrate: dependency: "direct main" description: @@ -574,14 +611,22 @@ packages: description: flutter source: sdk version: "0.0.0" + focus_on_it: + dependency: "direct main" + description: + name: focus_on_it + sha256: bd75489d7cb3cbde483126316a753e1a442a35c01a2278ae9bf54a4d86b89f02 + url: "https://pub.dev" + source: hosted + version: "2.0.1" freezed_annotation: dependency: transitive description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -610,10 +655,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" hashcodes: dependency: transitive description: @@ -658,10 +703,10 @@ packages: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -771,18 +816,18 @@ packages: dependency: "direct main" description: name: just_audio - sha256: "5abfab1d199e01ab5beffa61b3e782350df5dad036cb8c83b79fa45fc656614e" + sha256: d8e8aaf417d33e345299c17f6457f72bd4ba0c549dc34607abb5183a354edc4d url: "https://pub.dev" source: hosted - version: "0.9.38" + version: "0.9.40" just_audio_media_kit: dependency: "direct main" description: name: just_audio_media_kit - sha256: bbecbd43959c230d9f9610df0e0165855e711b4c960ce730c08f31107cc3bd26 + sha256: "7f57d317fafa04cb3e70b924e8f632ffb7eca7a97a369e1e44738ed89fbd5da1" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.5" just_audio_platform_interface: dependency: transitive description: @@ -795,26 +840,26 @@ packages: dependency: transitive description: name: just_audio_web - sha256: "0edb481ad4aa1ff38f8c40f1a3576013c3420bf6669b686fe661627d49bc606c" + sha256: "9a98035b8b24b40749507687520ec5ab404e291d2b0937823ff45d92cb18d448" url: "https://pub.dev" source: hosted - version: "0.4.11" + version: "0.4.13" leak_tracker: 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: @@ -847,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: @@ -867,18 +920,18 @@ 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: name: media_kit - sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a" + sha256: "1f1deee148533d75129a6f38251ff8388e33ee05fc2d20a6a80e57d6051b7b62" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11" media_kit_libs_linux: dependency: "direct main" description: @@ -900,18 +953,18 @@ 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: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mini_music_visualizer: dependency: "direct main" description: @@ -924,10 +977,10 @@ packages: dependency: "direct dev" description: name: msix - sha256: "519b183d15dc9f9c594f247e2d2339d855cf0eaacc30e19b128e14f3ecc62047" + sha256: c50d6bd1aafe0d071a3c1e5a5ccb056404502935cb0a549e3178c4aae16caf33 url: "https://pub.dev" source: hosted - version: "3.16.7" + version: "3.16.8" nested: dependency: transitive description: @@ -940,10 +993,10 @@ packages: dependency: "direct main" description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" package_config: dependency: transitive description: @@ -956,18 +1009,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.0.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" palette_generator: dependency: "direct main" description: @@ -997,18 +1050,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -1037,10 +1090,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" permission_handler: dependency: "direct main" description: @@ -1053,34 +1106,34 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5" + sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa" url: "https://pub.dev" source: hosted - version: "12.0.6" + version: "12.0.12" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 + sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 url: "https://pub.dev" source: hosted - version: "9.4.4" + version: "9.4.5" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.3+2" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.3" permission_handler_windows: dependency: transitive description: @@ -1101,10 +1154,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -1141,26 +1194,26 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" puppeteer: dependency: transitive description: name: puppeteer - sha256: c45c51b4ad8d70acdffeb1cfb9d16b60a7eaab7bfef314dd5b02c3607269b556 + sha256: fc33b2a12731e0b9e16c40cd91ea2b6886bcc24037a435fceb59b786d4074f2b url: "https://pub.dev" source: hosted - version: "3.11.0" + version: "3.15.0" qs_dart: dependency: transitive description: name: qs_dart - sha256: "5f1827ccdfa061582c121e7a8fe4a83319fa455bcd1fd6e46ff5b17b57aed680" + sha256: be73d060d29c0716ded88380ba32e87ce8105f0ba234edb3edefa0d74d47d64b url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.4" recursive_regex: dependency: transitive description: @@ -1181,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: @@ -1197,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: @@ -1269,10 +1322,10 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -1347,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: @@ -1403,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: @@ -1419,10 +1472,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: @@ -1455,6 +1508,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" universal_platform: dependency: transitive description: @@ -1463,6 +1524,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + upower: + dependency: transitive + description: + name: upower + sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf + url: "https://pub.dev" + source: hosted + version: "0.7.0" uri_parser: dependency: transitive description: @@ -1475,42 +1544,42 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.6" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.10" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: 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: @@ -1523,18 +1592,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: "direct main" description: @@ -1551,6 +1620,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: @@ -1575,14 +1652,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" vm_service: 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: + name: wakelock_plus + sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 + url: "https://pub.dev" + source: hosted + version: "1.2.8" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" + url: "https://pub.dev" + source: hosted + version: "1.2.1" watcher: dependency: transitive description: @@ -1619,18 +1720,18 @@ packages: dependency: transitive description: name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "5.5.5" win32_registry: dependency: transitive description: name: win32_registry - sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.5" window_manager: dependency: "direct main" description: @@ -1659,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: @@ -1672,5 +1773,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 495becbd1..a4381d184 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.9+109 +version: 0.9.11+111 environment: sdk: ">=3.3.0 <4.0.0" @@ -62,6 +62,7 @@ dependencies: infinite_scroll_pagination: ^4.0.0 flutter_sticky_header: ^0.6.5 device_info_plus: ^10.0.1 + app_set_id: ^1.2.0 package_info_plus: ^8.0.0 octo_image: ^2.0.0 # Main split view package does not seem actively maintained. Use fork with ability to specify @@ -101,6 +102,10 @@ dependencies: scroll_to_index: ^3.0.1 window_manager: ^0.3.8 url_launcher: ^6.2.6 + 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: @@ -139,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 @@ -185,7 +191,7 @@ msix_config: display_name: Finamp publisher_display_name: jmshrv identity_name: com.unicornsonlsd.finamp - msix_version: 0.9.8.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 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d9dcef48d..8c09f956d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -15,6 +16,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + BatteryPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c8ae9a5a8..41be227b4 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + battery_plus isar_flutter_libs media_kit_libs_windows_audio permission_handler_windows
This is a hotfix for a bug introduced with 0.9.10
+ Bug Fixes +
+ Thank you for using Finamp! +
+ New Features +