Skip to content

Commit

Permalink
improved BaseItemDto favorite state handling
Browse files Browse the repository at this point in the history
- try to update the actual QueueItem wherever possible to be robust against rebuilds
- added a callback to `FavouriteButton` to support updating e.g. the QueueItems in response to a toggle
- the favorite buttons on the player screen and in the queue list are now synced
  • Loading branch information
Chaphasilor committed Sep 21, 2023
1 parent 04773fb commit 6ef5f00
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 66 deletions.
71 changes: 43 additions & 28 deletions lib/components/PlayerScreen/queue_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,13 @@ class _CurrentTrackState extends State<CurrentTrack> {
topLeft: Radius.circular(8),
bottomLeft: Radius.circular(8),
),
itemsToPrecache: _queueService.getNextXTracksInQueue(3).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: 70,
Expand Down Expand Up @@ -836,28 +843,31 @@ class _CurrentTrackState extends State<CurrentTrack> {
// ),
],
),
IconButton(
iconSize: 16,
visualDensity: const VisualDensity(horizontal: -4),
icon: jellyfin_models.BaseItemDto.fromJson(currentTrack!.item.extras?["itemJson"]).userData!.isFavorite ? const Icon(
TablerIcons.heart,
size: 28,
color: Colors.white,
fill: 1.0,
weight:
1.5, //TODO weight not working, stroke is too thick for most icons
) : const Icon(
TablerIcons.heart,
size: 28,
color: Colors.white,
weight:
1.5, //TODO weight not working, stroke is too thick for most icons
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: IconButton(
iconSize: 16,
visualDensity: const VisualDensity(horizontal: -4),
icon: jellyfin_models.BaseItemDto.fromJson(currentTrack!.item.extras?["itemJson"]).userData!.isFavorite ? Icon(
Icons.favorite,
size: 28,
color: IconTheme.of(context).color!,
fill: 1.0,
weight:
1.5, //TODO weight not working, stroke is too thick for most icons
) : const Icon(
Icons.favorite_outline,
size: 28,
color: Colors.white,
weight:
1.5, //TODO weight not working, stroke is too thick for most icons
),
onPressed: () => {
setState(() {
setFavourite(currentTrack!);
})
},
),
onPressed: () => {
setState(() {
setFavourite(jellyfin_models.BaseItemDto.fromJson(currentTrack!.item.extras?["itemJson"]));
})
},
),
IconButton(
iconSize: 28,
Expand Down Expand Up @@ -1052,18 +1062,20 @@ class _CurrentTrackState extends State<CurrentTrack> {
break;
case SongListTileMenuItems.addFavourite:
case SongListTileMenuItems.removeFavourite:
await setFavourite(item);
await setFavourite(currentTrack);
break;
case null:
break;
}
}

Future<void> setFavourite(jellyfin_models.BaseItemDto item) async {
Future<void> setFavourite(QueueItem track) async {
try {
// We switch the widget state before actually doing the request to
// make the app feel faster (without, there is a delay from the
// user adding the favourite and the icon showing)
jellyfin_models.BaseItemDto item = jellyfin_models.BaseItemDto.fromJson(track.item.extras!["itemJson"]);

setState(() {
item.userData!.isFavorite = !item.userData!.isFavorite;
});
Expand All @@ -1074,15 +1086,18 @@ class _CurrentTrackState extends State<CurrentTrack> {
? await _jellyfinApiHelper.addFavourite(item.id)
: await _jellyfinApiHelper.removeFavourite(item.id);

if (!mounted) return;

item.userData = newUserData;

if (!mounted) return;
setState(() {
item.userData = newUserData;
//!!! update the QueueItem with the new BaseItemDto, then trigger a rebuild of the widget with the current snapshot (**which includes the modified QueueItem**)
track.item.extras!["itemJson"] = item.toJson();
});

_queueService.refreshQueueStream();

} catch (e) {
setState(() {
item.userData!.isFavorite = !item.userData!.isFavorite;
});
errorSnackbar(e, context);
}
}
Expand Down
53 changes: 28 additions & 25 deletions lib/components/PlayerScreen/song_info.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:math';

import 'package:audio_service/audio_service.dart';
import 'package:finamp/models/finamp_models.dart';
import 'package:finamp/services/queue_service.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
Expand All @@ -9,7 +11,7 @@ import 'package:get_it/get_it.dart';
import 'package:palette_generator/palette_generator.dart';

import '../../generate_material_color.dart';
import '../../models/jellyfin_models.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';
Expand All @@ -32,22 +34,18 @@ class SongInfo extends StatefulWidget {
class _SongInfoState extends State<SongInfo> {
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
final queueService = GetIt.instance<QueueService>();

@override
Widget build(BuildContext context) {
return StreamBuilder<MediaItem?>(
stream: audioHandler.mediaItem,
initialData: MediaItem(
id: "",
title: AppLocalizations.of(context)!.noItem,
album: AppLocalizations.of(context)!.noAlbum,
artist: AppLocalizations.of(context)!.noArtist,
),
return StreamBuilder<QueueInfo>(
stream: queueService.getQueueStream(),
builder: (context, snapshot) {
final mediaItem = snapshot.data!;
final currentTrack = snapshot.data!.currentTrack!;
final mediaItem = currentTrack.item;
final songBaseItemDto =
(mediaItem.extras?.containsKey("itemJson") ?? false)
? BaseItemDto.fromJson(mediaItem.extras!["itemJson"])
? jellyfin_models.BaseItemDto.fromJson(mediaItem.extras!["itemJson"])
: null;

List<TextSpan> separatedArtistTextSpans = [];
Expand Down Expand Up @@ -98,11 +96,10 @@ class _SongInfoState extends State<SongInfo> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_PlayerScreenAlbumImage(item: songBaseItemDto),
_PlayerScreenAlbumImage(queueItem: currentTrack!),
const Padding(padding: EdgeInsets.symmetric(vertical: 6)),
SongNameContent(
songBaseItemDto: songBaseItemDto,
mediaItem: mediaItem,
currentTrack: currentTrack,
separatedArtistTextSpans: separatedArtistTextSpans,
secondaryTextColour: secondaryTextColour,
)
Expand All @@ -114,16 +111,22 @@ class _SongInfoState extends State<SongInfo> {
}

class _PlayerScreenAlbumImage extends ConsumerWidget {
const _PlayerScreenAlbumImage({
_PlayerScreenAlbumImage({
Key? key,
required this.item,
required this.queueItem,
}) : super(key: key);

final BaseItemDto? item;
final QueueItem queueItem;

@override
Widget build(BuildContext context, WidgetRef ref) {
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
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(
Expand All @@ -146,16 +149,16 @@ class _PlayerScreenAlbumImage extends ConsumerWidget {
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 40),
child: AlbumImage(
item: item,
// Here we awkwardly get the next 3 queue items so that we
// 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: audioHandler.queue.value
.sublist(min(
(audioHandler.playbackState.value.queueIndex ?? 0) + 1,
audioHandler.queue.value.length))
.take(3)
.map((e) => BaseItemDto.fromJson(e.extras!["itemJson"]))
.toList(),
itemsToPrecache: queueService.getNextXTracksInQueue(3).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(),
// We need a post frame callback because otherwise this
// widget rebuilds on the same frame
imageProviderCallback: (imageProvider) =>
Expand Down
23 changes: 16 additions & 7 deletions lib/components/PlayerScreen/song_name_content.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:audio_service/audio_service.dart';
import 'package:finamp/components/PlayerScreen/player_buttons_more.dart';
import 'package:finamp/models/jellyfin_models.dart';
import 'package:finamp/models/finamp_models.dart';
import 'package:finamp/models/jellyfin_models.dart' as jellyfin_models;
import 'package:flutter/material.dart';

import '../favourite_button.dart';
Expand All @@ -10,26 +11,28 @@ import 'artist_chip.dart';
class SongNameContent extends StatelessWidget {
const SongNameContent(
{Key? key,
required this.songBaseItemDto,
required this.mediaItem,
required this.currentTrack,
required this.separatedArtistTextSpans,
required this.secondaryTextColour})
: super(key: key);
final BaseItemDto? songBaseItemDto;
final MediaItem mediaItem;
final QueueItem currentTrack;
final List<TextSpan> separatedArtistTextSpans;
final Color? secondaryTextColour;

@override
Widget build(BuildContext context) {

final jellyfin_models.BaseItemDto? songBaseItemDto = currentTrack.item.extras!["itemJson"] != null
? jellyfin_models.BaseItemDto.fromJson(currentTrack.item.extras!["itemJson"]) : null;

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 8),
child: Text(
mediaItem.title,
currentTrack.item.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 20,
Expand Down Expand Up @@ -75,7 +78,13 @@ class SongNameContent extends StatelessWidget {
),
],
),
FavoriteButton(item: songBaseItemDto),
FavoriteButton(
item: songBaseItemDto,
onToggle: (isFavorite) {
songBaseItemDto!.userData!.isFavorite = isFavorite;
currentTrack.item.extras!["itemJson"] = songBaseItemDto.toJson();
},
),
],
),
),
Expand Down
19 changes: 13 additions & 6 deletions lib/components/favourite_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';

class FavoriteButton extends StatefulWidget {
const FavoriteButton(
{Key? key,
required this.item,
this.onlyIfFav = false,
this.inPlayer = false})
: super(key: key);
const FavoriteButton({
Key? key,
required this.item,
this.onToggle,
this.onlyIfFav = false,
this.inPlayer = false,
}) : super(key: key);

final BaseItemDto? item;
final void Function(bool isFavorite)? onToggle;
final bool onlyIfFav;
final bool inPlayer;

Expand Down Expand Up @@ -64,6 +66,11 @@ class _FavoriteButtonState extends State<FavoriteButton> {
widget.item!.toJson();
}
});

if (widget.onToggle != null) {
widget.onToggle!(widget.item!.userData!.isFavorite);
}

} catch (e) {
errorSnackbar(e, context);
}
Expand Down
17 changes: 17 additions & 0 deletions lib/services/queue_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,23 @@ class QueueService {
return _queueStream;
}

void refreshQueueStream() {
_queueStream.add(getQueue());
}

/// returns the next [amount] QueueItems from Next Up and the regular queue
List<QueueItem> getNextXTracksInQueue(int amount) {
List<QueueItem> nextTracks = [];
if (_queueNextUp.isNotEmpty) {
nextTracks.addAll(_queueNextUp.sublist(0, min(amount, _queueNextUp.length)));
amount -= _queueNextUp.length;
}
if (_queue.isNotEmpty && amount > 0) {
nextTracks.addAll(_queue.sublist(0, min(amount, _queue.length)));
}
return nextTracks;
}

BehaviorSubject<PlaybackOrder> getPlaybackOrderStream() {
return _playbackOrderStream;
}
Expand Down

0 comments on commit 6ef5f00

Please sign in to comment.