From 7cb55c4f6c67f09f0b05cb2fac87640f818d5313 Mon Sep 17 00:00:00 2001 From: Komodo <45665554+Komodo5197@users.noreply.github.com> Date: Thu, 14 Dec 2023 23:59:51 -0500 Subject: [PATCH] Use currentAlbumImageProvider for all uses of current track album image. Perform now-playing queue album precaching in currentAlbumImageProvider instead of AlbumImage. Directly handle image loading error in playerScreenThemeProvider instead of waiting for timeout. --- lib/components/PlayerScreen/queue_list.dart | 19 +----- lib/components/PlayerScreen/song_info.dart | 21 +----- lib/components/album_image.dart | 49 +++----------- lib/components/now_playing_bar.dart | 15 +---- lib/models/finamp_models.dart | 6 ++ .../current_album_image_provider.dart | 49 +++++++++----- .../player_screen_theme_provider.dart | 66 ++++++++++++------- 7 files changed, 92 insertions(+), 133 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index d810f7711..ede234ae1 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -18,6 +18,7 @@ import 'package:get_it/get_it.dart'; import 'package:rxdart/rxdart.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; +import '../../services/current_album_image_provider.dart'; import '../album_image.dart'; import '../../models/jellyfin_models.dart' as jellyfin_models; import '../../services/process_artist.dart'; @@ -681,12 +682,6 @@ class _CurrentTrackState extends State { currentTrack = snapshot.data!.queueInfo?.currentTrack; mediaState = snapshot.data!.mediaState; - jellyfin_models.BaseItemDto? baseItem = - currentTrack!.item.extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson( - currentTrack!.item.extras?["itemJson"]); - const horizontalPadding = 8.0; const albumImageSize = 70.0; @@ -727,18 +722,8 @@ class _CurrentTrackState extends State { alignment: Alignment.center, children: [ AlbumImage( - item: baseItem, borderRadius: BorderRadius.zero, - itemsToPrecache: _queueService - .getNextXTracksInQueue(3, reverse: 1) - .map((e) { - final item = e.item.extras?["itemJson"] != null - ? jellyfin_models.BaseItemDto.fromJson( - e.item.extras!["itemJson"] - as Map) - : null; - return item!; - }).toList(), + imageListenable: currentAlbumImageProvider, ), Container( width: albumImageSize, diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index ead3b7e5b..ba0be2bcb 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -7,6 +7,7 @@ import 'package:get_it/get_it.dart'; import '../../models/jellyfin_models.dart' as jellyfin_models; import '../../screens/artist_screen.dart'; +import '../../services/current_album_image_provider.dart'; import '../../services/finamp_settings_helper.dart'; import '../../services/jellyfin_api_helper.dart'; import '../../services/music_player_background_task.dart'; @@ -119,13 +120,6 @@ class _PlayerScreenAlbumImage extends StatelessWidget { @override Widget build(BuildContext context) { - final queueService = GetIt.instance(); - - final item = queueItem.item.extras?["itemJson"] != null - ? jellyfin_models.BaseItemDto.fromJson( - queueItem.item.extras!["itemJson"] as Map) - : null; - return Container( decoration: BoxDecoration( boxShadow: [ @@ -146,18 +140,7 @@ class _PlayerScreenAlbumImage extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 40), child: AlbumImage( - item: item, - // Here we get the next 3 queue items so that we - // can precache them (so that the image is already loaded - // when the next song comes on). - itemsToPrecache: - queueService.getNextXTracksInQueue(3, reverse: 1).map((e) { - final item = e.item.extras?["itemJson"] != null - ? jellyfin_models.BaseItemDto.fromJson( - e.item.extras!["itemJson"] as Map) - : null; - return item!; - }).toList(), + imageListenable: currentAlbumImageProvider, ), ), ); diff --git a/lib/components/album_image.dart b/lib/components/album_image.dart index ee4ffc0d7..a6d78f32e 100644 --- a/lib/components/album_image.dart +++ b/lib/components/album_image.dart @@ -16,7 +16,6 @@ class AlbumImage extends ConsumerWidget { Key? key, this.item, this.imageListenable, - this.itemsToPrecache, this.borderRadius, this.placeholderBuilder, }) : super(key: key); @@ -26,9 +25,6 @@ class AlbumImage extends ConsumerWidget { final ProviderListenable>? imageListenable; - /// A list of items to precache - final List? itemsToPrecache; - final BorderRadius? borderRadius; final WidgetBuilder? placeholderBuilder; @@ -67,11 +63,11 @@ class AlbumImage extends ConsumerWidget { (constraints.maxHeight * mediaQuery.devicePixelRatio).toInt(); return BareAlbumImage( - item: item, - imageListenable: imageListenable, - maxWidth: physicalWidth, - maxHeight: physicalHeight, - itemsToPrecache: itemsToPrecache, + imageListenable: imageListenable ?? albumImageProvider(AlbumImageRequest( + item: item!, + maxWidth: physicalWidth, + maxHeight: physicalHeight, + )), placeholderBuilder: placeholderBuilder ?? BareAlbumImage.defaultPlaceholderBuilder, ); @@ -85,25 +81,15 @@ class AlbumImage extends ConsumerWidget { class BareAlbumImage extends ConsumerWidget { const BareAlbumImage({ Key? key, - this.item, - this.imageListenable, - this.maxWidth, - this.maxHeight, + required this.imageListenable, this.errorBuilder = defaultErrorBuilder, this.placeholderBuilder = defaultPlaceholderBuilder, - this.itemsToPrecache, }) : super(key: key); - final BaseItemDto? item; - final ProviderListenable>? imageListenable; - final int? maxWidth; - final int? maxHeight; + final ProviderListenable> imageListenable; final WidgetBuilder placeholderBuilder; final OctoErrorBuilder errorBuilder; - /// A list of items to precache - final List? itemsToPrecache; - static Widget defaultPlaceholderBuilder(BuildContext context) { return Container(color: Theme.of(context).cardColor); } @@ -114,26 +100,7 @@ class BareAlbumImage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - assert(item != null || imageListenable != null); - for (final itemToPrecache in itemsToPrecache ?? []) { - ref.listen( - albumImageProvider(AlbumImageRequest( - item: itemToPrecache, - maxWidth: maxWidth, - maxHeight: maxHeight)), (previous, next) { - if ((previous == null || previous.valueOrNull == null) && - next.valueOrNull != null) { - precacheImage(next.value!, context); - } - }); - } - - AsyncValue image = ref.watch(imageListenable ?? - albumImageProvider(AlbumImageRequest( - item: item!, - maxWidth: maxWidth, - maxHeight: maxHeight, - ))); + AsyncValue image = ref.watch(imageListenable); if (image.hasValue && image.value != null) { return OctoImage( diff --git a/lib/components/now_playing_bar.dart b/lib/components/now_playing_bar.dart index fb282382b..2b43d4429 100644 --- a/lib/components/now_playing_bar.dart +++ b/lib/components/now_playing_bar.dart @@ -11,6 +11,7 @@ import 'package:get_it/get_it.dart'; import 'package:simple_gesture_detector/simple_gesture_detector.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; +import '../services/current_album_image_provider.dart'; import '../services/finamp_settings_helper.dart'; import '../services/media_state_stream.dart'; import 'album_image.dart'; @@ -106,7 +107,6 @@ class NowPlayingBar extends ConsumerWidget { const albumImageSize = 70.0; final audioHandler = GetIt.instance(); - final queueService = GetIt.instance(); Duration? playbackPosition; @@ -191,19 +191,8 @@ class NowPlayingBar extends ConsumerWidget { AlbumImage( placeholderBuilder: (_) => const SizedBox.shrink(), - item: currentTrackBaseItem, + imageListenable: currentAlbumImageProvider, borderRadius: BorderRadius.zero, - itemsToPrecache: queueService - .getNextXTracksInQueue(3, reverse: 1) - .map((e) { - final item = e.item.extras?["itemJson"] != - null - ? jellyfin_models.BaseItemDto.fromJson( - e.item.extras!["itemJson"] - as Map) - : null; - return item!; - }).toList(), ), Container( width: albumImageSize, diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 1c957ded1..2fe80701f 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -733,6 +733,12 @@ class FinampQueueItem { @HiveField(3) QueueItemQueueType type; + + BaseItemDto? get baseItem { + return (item.extras?["itemJson"] != null) + ? BaseItemDto.fromJson(item.extras!["itemJson"] as Map) + : null; + } } @HiveType(typeId: 58) diff --git a/lib/services/current_album_image_provider.dart b/lib/services/current_album_image_provider.dart index 0499380cf..691c21419 100644 --- a/lib/services/current_album_image_provider.dart +++ b/lib/services/current_album_image_provider.dart @@ -1,31 +1,44 @@ +import 'dart:async'; + +import 'package:finamp/models/finamp_models.dart'; +import 'package:finamp/models/jellyfin_models.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; - -import '../models/jellyfin_models.dart'; import 'album_image_provider.dart'; /// Provider to handle syncing up the current playing item's image provider. /// Used on the player screen to sync up loading the blurred background. -final currentAlbumImageProvider = - FutureProvider((ref) async { - final currentTrack = - ref.watch(currentSongProvider.select((data) => data.value?.item)); - if (currentTrack != null) { - final currentTrackBaseItem = currentTrack.extras?["itemJson"] != null - ? BaseItemDto.fromJson( - currentTrack.extras!["itemJson"] as Map) - : null; - if (currentTrackBaseItem != null) { - final request = AlbumImageRequest( - item: currentTrackBaseItem, - maxHeight: 300, - maxWidth: 300, - ); - return ref.read(albumImageProvider(request).future); +final currentAlbumImageProvider = FutureProvider((ref) async { + final List precacheItems = + GetIt.instance().getNextXTracksInQueue(3, reverse: 1); + for (final itemToPrecache in precacheItems) { + BaseItemDto? base = itemToPrecache.baseItem; + if (base != null) { + final request = AlbumImageRequest(item: base); + unawaited(ref.read(albumImageProvider(request).future).then((value) { + if (value != null) { + // Cache the returned image + var stream = + value.resolve(const ImageConfiguration(devicePixelRatio: 1.0)); + var listener = ImageStreamListener((image, synchronousCall) {}); + ref.onDispose(() { + stream.removeListener(listener); + }); + stream.addListener(listener); + } + })); } } + + final currentTrack = ref.watch(currentSongProvider).value?.baseItem; + if (currentTrack != null) { + final request = AlbumImageRequest( + item: currentTrack, + ); + return ref.read(albumImageProvider(request).future); + } return null; }); diff --git a/lib/services/player_screen_theme_provider.dart b/lib/services/player_screen_theme_provider.dart index 3a89e9358..055bcccea 100644 --- a/lib/services/player_screen_theme_provider.dart +++ b/lib/services/player_screen_theme_provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:finamp/at_contrast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -34,35 +36,49 @@ final AutoDisposeFutureProviderFamily if (image == null) { return null; } + Logger("colorProvider").fine("Re-theming based on image $image"); + Completer completer = Completer(); + ImageStream stream = + image.resolve(const ImageConfiguration(devicePixelRatio: 1.0)); + ImageStreamListener? listener; - final PaletteGenerator? palette = await PaletteGenerator.fromImageProvider( - image, - timeout: const Duration(seconds: 5), - ).then((value) => value, onError: (_) => null); - if (palette == null) { - return ColorScheme.fromSeed( - seedColor: const Color.fromARGB(255, 0, 164, 220), - brightness: brightness); - } - // Color accent = palette.dominantColor!.color; - Color accent = palette.vibrantColor?.color ?? - palette.dominantColor?.color ?? - const Color.fromARGB(255, 0, 164, 220); + listener = ImageStreamListener((image, synchronousCall) async { + stream.removeListener(listener!); + // Use fromImage instead of fromImageProvider to better handle error case + final PaletteGenerator palette = + await PaletteGenerator.fromImage(image.image); + + Color accent = palette.vibrantColor?.color ?? + palette.dominantColor?.color ?? + const Color.fromARGB(255, 0, 164, 220); + + final lighter = brightness == Brightness.dark; - final lighter = brightness == Brightness.dark; + final background = Color.alphaBlend( + lighter + ? Colors.black.withOpacity(0.675) + : Colors.white.withOpacity(0.675), + accent); - final background = Color.alphaBlend( - lighter - ? Colors.black.withOpacity(0.675) - : Colors.white.withOpacity(0.675), - accent); + accent = accent.atContrast(4.5, background, lighter); + + completer.complete(ColorScheme.fromSwatch( + primarySwatch: generateMaterialColor(accent), + accentColor: accent, + brightness: brightness, + )); + }, onError: (_, __) { + stream.removeListener(listener!); + completer.complete(ColorScheme.fromSeed( + seedColor: const Color.fromARGB(255, 0, 164, 220), + brightness: brightness)); + }); - accent = accent.atContrast(4.5, background, lighter); + ref.onDispose(() { + stream.removeListener(listener!); + }); - return ColorScheme.fromSwatch( - primarySwatch: generateMaterialColor(accent), - accentColor: accent, - brightness: brightness, - ); + stream.addListener(listener); + return completer.future; });