diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a86976c6b..5269544f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,9 @@ on: pull_request: push: +# Due to a flutter bug, flutter build is not generating localizations +# workaround by always running flutter pub get immediately before building, +# and performing build for msix:create manually to allow implementing this workaround jobs: build-android: name: Build for Android @@ -53,7 +56,6 @@ jobs: channel: 'stable' - run: flutter doctor - run: flutter pub get - - run: flutter gen-l10n # - run: flutter test - run: flutter build linux --release --no-pub - run: | @@ -87,13 +89,12 @@ jobs: channel: 'stable' - run: flutter doctor - run: flutter pub get - - run: flutter gen-l10n # - run: flutter test - run: flutter build windows # TODO pack in redistributables? - uses: actions/upload-artifact@v4 with: - name: finamp-windows-zip + name: finamp-windows path: build/windows/x64/runner/Release/ - run: dart run msix:create --install-certificate false #TODO would be nice to have an old-school installer here that can take the .exe + libraries and install them to the device + create a shortcut diff --git a/lib/components/AddToPlaylistScreen/playlist_actions_menu.dart b/lib/components/AddToPlaylistScreen/playlist_actions_menu.dart index 43bd6f3e7..e0401837a 100644 --- a/lib/components/AddToPlaylistScreen/playlist_actions_menu.dart +++ b/lib/components/AddToPlaylistScreen/playlist_actions_menu.dart @@ -115,7 +115,7 @@ Future showPlaylistActionsMenu({ ), ), sliver: MenuMask( - height: 45.0, + height: 36.0, child: SliverList( delegate: SliverChildListDelegate.fixed( menuEntries, diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 8af9e493c..320b0e520 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:audio_service/audio_service.dart'; -import 'package:collection/collection.dart'; import 'package:Finamp/components/AlbumScreen/song_menu.dart'; import 'package:Finamp/components/MusicScreen/music_screen_tab_view.dart'; import 'package:Finamp/components/global_snackbar.dart'; @@ -9,6 +7,8 @@ import 'package:Finamp/models/finamp_models.dart'; import 'package:Finamp/models/jellyfin_models.dart' as jellyfin_models; import 'package:Finamp/services/finamp_user_helper.dart'; import 'package:Finamp/services/queue_service.dart'; +import 'package:audio_service/audio_service.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -223,6 +223,85 @@ class _SongListTileState extends ConsumerState ), ], ), + // This must be in ListTile instead of parent GestureDetecter to + // enable hover color changes + onTap: () async { + if (!playable) return; + var children = await widget.children; + if (children != null) { + // start linear playback of album from the given index + await _queueService.startPlayback( + items: children, + startingIndex: await widget.index, + order: FinampPlaybackOrder.linear, + source: QueueItemSource( + type: widget.isInPlaylist + ? QueueItemSourceType.playlist + : widget.isOnArtistScreen + ? QueueItemSourceType.artist + : QueueItemSourceType.album, + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: ((widget.isInPlaylist || + widget.isOnArtistScreen) + ? widget.parentItem?.name + : widget.item.album) ?? + AppLocalizations.of(context)!.placeholderSource), + id: widget.parentItem?.id ?? "", + item: widget.parentItem, + // we're playing from an album, so we should use the album's normalization gain. + contextNormalizationGain: + (widget.isInPlaylist || widget.isOnArtistScreen) + ? null + : widget.parentItem?.normalizationGain, + ), + ); + } else { + // TODO put in a real offline songs implementation + if (FinampSettingsHelper.finampSettings.isOffline) { + final settings = FinampSettingsHelper.finampSettings; + final downloadService = GetIt.instance(); + final finampUserHelper = GetIt.instance(); + + // get all downloaded songs in order + List offlineItems; + // If we're on the songs tab, just get all of the downloaded items + offlineItems = await downloadService.getAllSongs( + // nameFilter: widget.searchTerm, + viewFilter: finampUserHelper.currentUser?.currentView?.id, + nullableViewFilters: + settings.showDownloadsWithUnknownLibrary); + + var items = offlineItems + .map((e) => e.baseItem) + .whereNotNull() + .toList(); + + items = sortItems( + items, + settings.tabSortBy[TabContentType.songs], + settings.tabSortOrder[TabContentType.songs]); + + await _queueService.startPlayback( + items: items, + startingIndex: widget.isShownInSearch + ? items.indexWhere( + (element) => element.id == widget.item.id) + : await widget.index, + source: QueueItemSource( + name: QueueItemSourceName( + type: QueueItemSourceNameType.preTranslated, + pretranslatedName: + AppLocalizations.of(context)!.placeholderSource), + type: QueueItemSourceType.allSongs, + id: widget.item.id, + ), + ); + } else { + await _audioServiceHelper.startInstantMixForItem(widget.item); + } + } + }, ); }); @@ -247,77 +326,6 @@ class _SongListTileState extends ConsumerState }, onLongPressStart: (details) => menuCallback(), onSecondaryTapDown: (details) => menuCallback(), - onTap: () async { - if (!playable) return; - var children = await widget.children; - if (children != null) { - // start linear playback of album from the given index - await _queueService.startPlayback( - items: children, - startingIndex: await widget.index, - order: FinampPlaybackOrder.linear, - source: QueueItemSource( - type: widget.isInPlaylist - ? QueueItemSourceType.playlist - : widget.isOnArtistScreen - ? QueueItemSourceType.artist - : QueueItemSourceType.album, - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: - ((widget.isInPlaylist || widget.isOnArtistScreen) - ? widget.parentItem?.name - : widget.item.album) ?? - AppLocalizations.of(context)!.placeholderSource), - id: widget.parentItem?.id ?? "", - item: widget.parentItem, - // we're playing from an album, so we should use the album's normalization gain. - contextNormalizationGain: - (widget.isInPlaylist || widget.isOnArtistScreen) - ? null - : widget.parentItem?.normalizationGain, - ), - ); - } else { - // TODO put in a real offline songs implementation - if (FinampSettingsHelper.finampSettings.isOffline) { - final settings = FinampSettingsHelper.finampSettings; - final downloadService = GetIt.instance(); - final finampUserHelper = GetIt.instance(); - - // get all downloaded songs in order - List offlineItems; - // If we're on the songs tab, just get all of the downloaded items - offlineItems = await downloadService.getAllSongs( - // nameFilter: widget.searchTerm, - viewFilter: finampUserHelper.currentUser?.currentView?.id, - nullableViewFilters: settings.showDownloadsWithUnknownLibrary); - - var items = - offlineItems.map((e) => e.baseItem).whereNotNull().toList(); - - items = sortItems(items, settings.tabSortBy[TabContentType.songs], - settings.tabSortOrder[TabContentType.songs]); - - await _queueService.startPlayback( - items: items, - startingIndex: widget.isShownInSearch - ? items.indexWhere((element) => element.id == widget.item.id) - : await widget.index, - source: QueueItemSource( - name: QueueItemSourceName( - type: QueueItemSourceNameType.preTranslated, - pretranslatedName: - AppLocalizations.of(context)!.placeholderSource), - type: QueueItemSourceType.allSongs, - id: widget.item.id, - ), - ); - } else { - await _audioServiceHelper.startInstantMixForItem(widget.item); - } - } - }, child: (widget.isSong || !playable) ? listTile : Dismissible( diff --git a/lib/components/album_image.dart b/lib/components/album_image.dart index 48b717bc3..8acb47082 100644 --- a/lib/components/album_image.dart +++ b/lib/components/album_image.dart @@ -105,8 +105,8 @@ class AlbumImage extends ConsumerWidget { )).select((value) => (value, item?.blurHash)), imageProviderCallback: themeCallback == null ? null - : (image) => - FinampTheme.fromImageDeferred(image, item?.blurHash), + : (image) => themeCallback!( + FinampTheme.fromImageDeferred(image, item?.blurHash)), placeholderBuilder: placeholderBuilder); return disabled ? Opacity( diff --git a/lib/components/themed_bottom_sheet.dart b/lib/components/themed_bottom_sheet.dart index 480464647..252c802c0 100644 --- a/lib/components/themed_bottom_sheet.dart +++ b/lib/components/themed_bottom_sheet.dart @@ -28,7 +28,7 @@ Future showThemedBottomSheet({ WrapperBuilder? buildWrapper, bool usePlayerTheme = false, required FinampTheme? themeProvider, - double minDraggableHeight = 0.4, + double minDraggableHeight = 0.6, bool showDragHandle = true, }) async { if (usePlayerTheme) { diff --git a/lib/main.dart b/lib/main.dart index 31f35661f..03d595fa5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:audio_service/audio_service.dart'; -import 'package:audio_session/audio_session.dart'; -import 'package:background_downloader/background_downloader.dart'; import 'package:Finamp/color_schemes.g.dart'; import 'package:Finamp/screens/downloads_settings_screen.dart'; import 'package:Finamp/screens/interaction_settings_screen.dart'; @@ -20,6 +17,9 @@ import 'package:Finamp/services/offline_listen_helper.dart'; import 'package:Finamp/services/playback_history_service.dart'; import 'package:Finamp/services/queue_service.dart'; import 'package:Finamp/services/theme_provider.dart'; +import 'package:audio_service/audio_service.dart'; +import 'package:audio_session/audio_session.dart'; +import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -544,7 +544,11 @@ class _FinampState extends ConsumerState with WindowListener { return; } - //!!! destroying the window manager instance doesn't seem to work on Windows release builds, the app just freezes instead + // Destroy player on platforms using mediaKit. + if (Platform.isWindows || Platform.isLinux) { + await GetIt.instance().dispose(); + windowManagerLogger.info("Player disposed."); + } } } diff --git a/lib/screens/blurred_player_screen_background.dart b/lib/screens/blurred_player_screen_background.dart index 2b61bd480..a9e51fe2b 100644 --- a/lib/screens/blurred_player_screen_background.dart +++ b/lib/screens/blurred_player_screen_background.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -61,16 +62,23 @@ class BlurredPlayerScreenBackground extends ConsumerWidget { filterQuality: FilterQuality.none, errorBuilder: (x, _, __) => placeholderBuilder(x), placeholderBuilder: placeholderBuilder, - imageBuilder: (context, child) => CachePaint( - imageKey: imageProvider.toString(), - child: ImageFiltered( - imageFilter: ui.ImageFilter.blur( - sigmaX: 85, - sigmaY: 85, - tileMode: TileMode.mirror, - ), - child: SizedBox.expand(child: child), - ))))); + imageBuilder: (context, child) { + var image = ImageFiltered( + imageFilter: ui.ImageFilter.blur( + sigmaX: 85, + sigmaY: 85, + tileMode: TileMode.mirror, + ), + child: SizedBox.expand(child: child), + ); + // There seems to be some sort of issue with how Linux handles ui.Image that breaks + // cachePaint. This shouldn't be too important outside mobile, though. + if (Platform.isLinux) { + return image; + } + return CachePaint( + imageKey: imageProvider.toString(), child: image); + }))); } } @@ -79,37 +87,21 @@ class CachePaint extends SingleChildRenderObjectWidget { final String imageKey; - @override - void updateRenderObject(BuildContext context, RenderCachePaint renderObject) { - renderObject.screenSize = MediaQuery.sizeOf(context); - } - @override RenderCachePaint createRenderObject(BuildContext context) { - return RenderCachePaint( - imageKey, MediaQuery.sizeOf(context), Theme.of(context).brightness); + return RenderCachePaint(imageKey, Theme.of(context).brightness); } } class RenderCachePaint extends RenderProxyBox { - RenderCachePaint(this._imageKey, this._screenSize, this._brightness); + RenderCachePaint(this._imageKey, this._brightness); final String _imageKey; - String get _cacheKey => - _imageKey + _screenSize.toString() + _brightness.toString(); - - Size _screenSize; + String get _cacheKey => _imageKey + size.toString() + _brightness.toString(); final Brightness _brightness; - set screenSize(Size value) { - if (value != _screenSize) { - _disposeCache(); - } - _screenSize = value; - } - static final Map, ui.Image?)> _cache = {}; @override @@ -137,10 +129,8 @@ class RenderCachePaint extends RenderProxyBox { // Save image of child to cache final OffsetLayer offsetLayer = layer! as OffsetLayer; Future.sync(() async { - _cache[_cacheKey] = ( - _cache[_cacheKey]!.$1, - await offsetLayer.toImage(offset & _screenSize) - ); + _cache[_cacheKey] = + (_cache[_cacheKey]!.$1, await offsetLayer.toImage(offset & size)); // Schedule repaint next frame because the image is lighter than the full // child during compositing, which is more frequent than paints. for (var element in _cache[_cacheKey]!.$1) { @@ -150,18 +140,32 @@ class RenderCachePaint extends RenderProxyBox { } } - void _disposeCache() { - _cache[_cacheKey]?.$1.remove(this); - if (_cache[_cacheKey]?.$1.isEmpty ?? false) { + @override + + /// Dispose of outdated render cache whenever widget size changes + set size(Size newSize) { + String? oldKey; + if (hasSize) { + oldKey = _cacheKey; + } + super.size = newSize; + if (_cacheKey != oldKey && oldKey != null) { + _disposeCache(oldKey); + } + } + + void _disposeCache(String key) { + _cache[key]?.$1.remove(this); + if (_cache[key]?.$1.isEmpty ?? false) { // If we are last user of image, dispose - _cache[_cacheKey]?.$2?.dispose(); - _cache.remove(_cacheKey); + _cache[key]?.$2?.dispose(); + _cache.remove(key); } } @override void dispose() { - _disposeCache(); + _disposeCache(_cacheKey); super.dispose(); } } diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index fbc60819f..23ab6d935 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; -import 'package:audio_service/audio_service.dart'; import 'package:Finamp/models/finamp_models.dart'; import 'package:Finamp/models/jellyfin_models.dart' as jellyfin_models; import 'package:Finamp/services/queue_service.dart'; +import 'package:audio_service/audio_service.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:just_audio/just_audio.dart'; @@ -189,6 +189,9 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { } } + /// Fully dispose the player instance. Should only be called during app shutdown. + Future dispose() => _player.dispose(); + @override Future play() { return _player.play(); diff --git a/lib/services/theme_provider.dart b/lib/services/theme_provider.dart index ad405fd6c..aa54c9849 100644 --- a/lib/services/theme_provider.dart +++ b/lib/services/theme_provider.dart @@ -114,11 +114,12 @@ class FinampTheme { listener = ImageStreamListener((image, synchronousCall) { stream.removeListener(listener!); _results[brightness]!._completer.complete(getColorSchemeForImage( - image.image, brightness, + image, brightness, useIsolate: useIsolate)); - }, onError: (_, __) { + }, onError: (e, stack) { stream.removeListener(listener!); _results[brightness]!._completer.complete(getDefaultTheme(brightness)); + themeProviderLogger.severe(e,e,stack); }); _dispose = () { @@ -164,11 +165,15 @@ class _ThemeProviderResults { ColorScheme? colorScheme; } -Future getColorSchemeForImage(Image image, Brightness brightness, +Future getColorSchemeForImage(ImageInfo image, Brightness brightness, {bool useIsolate = true}) async { // Use fromImage instead of fromImageProvider to better handle error case - final PaletteGenerator palette = - await PaletteGenerator.fromImage(image, useIsolate: useIsolate); + final PaletteGenerator palette; + try{ + palette = await PaletteGenerator.fromImage(image.image, useIsolate: useIsolate); + } finally { + image.dispose(); + } Color accent = palette.vibrantColor?.color ?? palette.dominantColor?.color ?? diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index dc0fec40d..8791d8651 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = finamp +PRODUCT_NAME = Finamp // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.unicornsonlsd.finamp diff --git a/pubspec.lock b/pubspec.lock index 4da57017b..0c0c3fcc4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -850,10 +850,11 @@ packages: media_kit_libs_windows_audio: dependency: "direct main" description: - name: media_kit_libs_windows_audio - sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53 - url: "https://pub.dev" - source: hosted + path: "libs/windows/media_kit_libs_windows_audio" + ref: "475a08cc97b94702f774bc906e1472b5bddc932b" + resolved-ref: "475a08cc97b94702f774bc906e1472b5bddc932b" + url: "https://github.com/Komodo5197/media-kit.git" + source: git version: "1.0.9" meta: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 47ea9dad5..8b106478e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,14 @@ dependencies: just_audio: ^0.9.37 just_audio_media_kit: ^2.0.4 media_kit_libs_linux: ^1.1.3 - media_kit_libs_windows_audio: ^1.0.9 + + # Transcoding does not work on windows with current media-kit release. This fork uses the most recent + # build of MPV from https://github.com/zhongfly/mpv-winbuild, which works correctly while transcoding. + media_kit_libs_windows_audio: #^1.0.9 + git: + url: https://github.com/Komodo5197/media-kit.git + ref: 475a08cc97b94702f774bc906e1472b5bddc932b + path: libs/windows/media_kit_libs_windows_audio smtc_windows: ^0.1.2 audio_service: ^0.18.13 audio_service_mpris: ^0.1.3