Skip to content

Commit

Permalink
Use currentAlbumImageProvider for all uses of current track album image.
Browse files Browse the repository at this point in the history
Perform now-playing queue album precaching in currentAlbumImageProvider instead of AlbumImage.
Directly handle image loading error in playerScreenThemeProvider instead of waiting for timeout.
  • Loading branch information
Komodo5197 committed Dec 15, 2023
1 parent 6433dd3 commit 7cb55c4
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 133 deletions.
19 changes: 2 additions & 17 deletions lib/components/PlayerScreen/queue_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -681,12 +682,6 @@ class _CurrentTrackState extends State<CurrentTrack> {
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;

Expand Down Expand Up @@ -727,18 +722,8 @@ class _CurrentTrackState extends State<CurrentTrack> {
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<String, dynamic>)
: null;
return item!;
}).toList(),
imageListenable: currentAlbumImageProvider,
),
Container(
width: albumImageSize,
Expand Down
21 changes: 2 additions & 19 deletions lib/components/PlayerScreen/song_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -119,13 +120,6 @@ class _PlayerScreenAlbumImage extends StatelessWidget {

@override
Widget build(BuildContext context) {
final queueService = GetIt.instance<QueueService>();

final item = queueItem.item.extras?["itemJson"] != null
? jellyfin_models.BaseItemDto.fromJson(
queueItem.item.extras!["itemJson"] as Map<String, dynamic>)
: null;

return Container(
decoration: BoxDecoration(
boxShadow: [
Expand All @@ -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<String, dynamic>)
: null;
return item!;
}).toList(),
imageListenable: currentAlbumImageProvider,
),
),
);
Expand Down
49 changes: 8 additions & 41 deletions lib/components/album_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class AlbumImage extends ConsumerWidget {
Key? key,
this.item,
this.imageListenable,
this.itemsToPrecache,
this.borderRadius,
this.placeholderBuilder,
}) : super(key: key);
Expand All @@ -26,9 +25,6 @@ class AlbumImage extends ConsumerWidget {

final ProviderListenable<AsyncValue<ImageProvider?>>? imageListenable;

/// A list of items to precache
final List<BaseItemDto>? itemsToPrecache;

final BorderRadius? borderRadius;

final WidgetBuilder? placeholderBuilder;
Expand Down Expand Up @@ -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,
);
Expand All @@ -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<AsyncValue<ImageProvider?>>? imageListenable;
final int? maxWidth;
final int? maxHeight;
final ProviderListenable<AsyncValue<ImageProvider?>> imageListenable;
final WidgetBuilder placeholderBuilder;
final OctoErrorBuilder errorBuilder;

/// A list of items to precache
final List<BaseItemDto>? itemsToPrecache;

static Widget defaultPlaceholderBuilder(BuildContext context) {
return Container(color: Theme.of(context).cardColor);
}
Expand All @@ -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<ImageProvider?> image = ref.watch(imageListenable ??
albumImageProvider(AlbumImageRequest(
item: item!,
maxWidth: maxWidth,
maxHeight: maxHeight,
)));
AsyncValue<ImageProvider?> image = ref.watch(imageListenable);

if (image.hasValue && image.value != null) {
return OctoImage(
Expand Down
15 changes: 2 additions & 13 deletions lib/components/now_playing_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -106,7 +107,6 @@ class NowPlayingBar extends ConsumerWidget {
const albumImageSize = 70.0;

final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
final queueService = GetIt.instance<QueueService>();

Duration? playbackPosition;

Expand Down Expand Up @@ -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<String, dynamic>)
: null;
return item!;
}).toList(),
),
Container(
width: albumImageSize,
Expand Down
6 changes: 6 additions & 0 deletions lib/models/finamp_models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic>)
: null;
}
}

@HiveType(typeId: 58)
Expand Down
49 changes: 31 additions & 18 deletions lib/services/current_album_image_provider.dart
Original file line number Diff line number Diff line change
@@ -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<ImageProvider?>((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<String, dynamic>)
: null;
if (currentTrackBaseItem != null) {
final request = AlbumImageRequest(
item: currentTrackBaseItem,
maxHeight: 300,
maxWidth: 300,
);
return ref.read(albumImageProvider(request).future);
final currentAlbumImageProvider = FutureProvider<ImageProvider?>((ref) async {
final List<FinampQueueItem> precacheItems =
GetIt.instance<QueueService>().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;
});

Expand Down
66 changes: 41 additions & 25 deletions lib/services/player_screen_theme_provider.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -34,35 +36,49 @@ final AutoDisposeFutureProviderFamily<ColorScheme?, Brightness>
if (image == null) {
return null;
}

Logger("colorProvider").fine("Re-theming based on image $image");
Completer<ColorScheme?> 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;
});

0 comments on commit 7cb55c4

Please sign in to comment.