Skip to content

Commit

Permalink
[Redesign] Recover Now-Playing Queue on Startup (#547)
Browse files Browse the repository at this point in the history
* Save queue item ids on change and attempt to reload queue on next startup.

* Added saving of playback position in current track.

* Initial version of queue history and restore page.

* Improved queue history loading, added loading indication.

* Improved loading indicater and Now Playing bar transitions.  Added setting to diable queue autoload.

* Improved various transitions between various screens and states.

* Prevented saving of empty queues.
Reversed queue restore screen.
Added saving queue on startPlayback().
Added saving global queue source.
Added ability to easily retry queue load if all items fail.
Delayed overwriting latest until user initiates playback or modifies queue after load.

* Moved to batched queries in queue item lookup.
Improved song count display on queue restore screen.

* Removed function getTrackFromId.
Added direct dependancy on collections package.
Made loadSavedQueue cleaner.

* Moved theme generation logic out of UI.
Increased PaletteGenerator timeout from 2 to 5 seconds.

* Return blue theme on failure loading theme image.
Ran code through dart format.

* 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.

---------

Co-authored-by: Chaphasilor <[email protected]>
  • Loading branch information
Komodo5197 and Chaphasilor authored Dec 15, 2023
1 parent 6af1d72 commit 82628e7
Show file tree
Hide file tree
Showing 26 changed files with 1,437 additions and 774 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:hive/hive.dart';

import '../../services/finamp_settings_helper.dart';
import '../../models/finamp_models.dart';

class LoadQueueOnStartupSelector extends StatelessWidget {
const LoadQueueOnStartupSelector({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Box<FinampSettings>>(
valueListenable: FinampSettingsHelper.finampSettingsListener,
builder: (_, box, __) {
return SwitchListTile.adaptive(
title: Text(AppLocalizations.of(context)!.autoloadLastQueueOnStartup),
subtitle: Text(
AppLocalizations.of(context)!.autoloadLastQueueOnStartupSubtitle),
value: FinampSettingsHelper.finampSettings.autoloadLastQueueOnStartup,
onChanged: (value) =>
FinampSettingsHelper.setAutoloadLastQueueOnStartup(value),
);
},
);
}
}
7 changes: 7 additions & 0 deletions lib/components/MusicScreen/music_screen_drawer.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:finamp/screens/queue_restore_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';
Expand Down Expand Up @@ -80,6 +81,12 @@ class MusicScreenDrawer extends StatelessWidget {
onTap: () => Navigator.of(context)
.pushNamed(LogsScreen.routeName),
),
ListTile(
leading: const Icon(Icons.auto_delete),
title: Text(AppLocalizations.of(context)!.queuesScreen),
onTap: () => Navigator.of(context)
.pushNamed(QueueRestoreScreen.routeName),
),
ListTile(
leading: const Icon(Icons.settings),
title: Text(AppLocalizations.of(context)!.settings),
Expand Down
31 changes: 11 additions & 20 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 @@ -274,7 +275,8 @@ Future<dynamic> showQueueBottomSheet(BuildContext context) {
builder: (context) {
return Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
final imageTheme = ref.watch(playerScreenThemeProvider);
final imageTheme =
ref.watch(playerScreenThemeProvider(Theme.of(context).brightness));

return AnimatedTheme(
duration: const Duration(milliseconds: 500),
Expand All @@ -283,7 +285,7 @@ Future<dynamic> showQueueBottomSheet(BuildContext context) {
colorScheme: imageTheme,
brightness: Theme.of(context).brightness,
iconTheme: Theme.of(context).iconTheme.copyWith(
color: imageTheme?.primary,
color: imageTheme.primary,
),
),
child: DraggableScrollableSheet(
Expand Down Expand Up @@ -680,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 @@ -726,17 +722,8 @@ class _CurrentTrackState extends State<CurrentTrack> {
alignment: Alignment.center,
children: [
AlbumImage(
item: baseItem,
borderRadius: BorderRadius.zero,
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(),
imageListenable: currentAlbumImageProvider,
),
Container(
width: albumImageSize,
Expand Down Expand Up @@ -1434,13 +1421,17 @@ class PreviousTracksSectionHeader extends SliverPersistentHeaderDelegate {
return Icon(
TablerIcons.chevron_up,
size: 28.0,
color: Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white,
color: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white,
);
} else {
return Icon(
TablerIcons.chevron_down,
size: 28.0,
color: Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white,
color: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white,
);
}
}),
Expand Down
76 changes: 3 additions & 73 deletions lib/components/PlayerScreen/song_info.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
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';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get_it/get_it.dart';
import 'package:palette_generator/palette_generator.dart';

import '../../generate_material_color.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';
import '../../services/player_screen_theme_provider.dart';
import 'song_name_content.dart';
import '../album_image.dart';
import '../../at_contrast.dart';

/// Album image and song name/album etc. We do this in one widget to share a
/// StreamBuilder and to make alignment easier.
Expand Down Expand Up @@ -118,7 +110,7 @@ class _SongInfoState extends State<SongInfo> {
}
}

class _PlayerScreenAlbumImage extends ConsumerWidget {
class _PlayerScreenAlbumImage extends StatelessWidget {
_PlayerScreenAlbumImage({
Key? key,
required this.queueItem,
Expand All @@ -127,14 +119,7 @@ class _PlayerScreenAlbumImage extends ConsumerWidget {
final FinampQueueItem queueItem;

@override
Widget build(BuildContext context, WidgetRef ref) {
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;

Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
boxShadow: [
Expand All @@ -155,62 +140,7 @@ class _PlayerScreenAlbumImage extends ConsumerWidget {
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).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) =>
WidgetsBinding.instance.addPostFrameCallback((_) async {
// Don't do anything if the image from the callback is the same as
// the current provider's image. This is probably needed because of
// addPostFrameCallback shenanigans
if (imageProvider != null &&
ref.read(currentAlbumImageProvider.notifier).state ==
imageProvider) {
return;
}

ref.read(currentAlbumImageProvider.notifier).state = imageProvider;

if (imageProvider != null) {
final theme = Theme.of(context);

final palette =
await PaletteGenerator.fromImageProvider(
imageProvider,
timeout: const Duration(milliseconds: 2000),
);

// Color accent = palette.dominantColor!.color;
Color accent = palette.vibrantColor?.color ?? palette.dominantColor?.color ?? const Color.fromARGB(255, 0, 164, 220);

final lighter = theme.brightness == Brightness.dark;

final background = Color.alphaBlend(
lighter
? Colors.black.withOpacity(0.675)
: Colors.white.withOpacity(0.675),
accent);

accent = accent.atContrast(4.5, background, lighter);

ref.read(playerScreenThemeProvider.notifier).state =
ColorScheme.fromSwatch(
primarySwatch: generateMaterialColor(accent),
accentColor: accent,
brightness: theme.brightness,
);
}
}),
imageListenable: currentAlbumImageProvider,
),
),
);
Expand Down
82 changes: 82 additions & 0 deletions lib/components/QueueRestoreScreen/queue_restore_tile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:finamp/models/jellyfin_models.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

import '../../models/finamp_models.dart';
import '../../services/downloads_helper.dart';
import '../../services/finamp_settings_helper.dart';
import '../../services/jellyfin_api_helper.dart';
import '../../services/queue_service.dart';
import '../album_image.dart';
import '../error_snackbar.dart';

class QueueRestoreTile extends StatelessWidget {
const QueueRestoreTile({Key? key, required this.info}) : super(key: key);

final FinampStorableQueueInfo info;

@override
Widget build(BuildContext context) {
final queuesBox = Hive.box<FinampStorableQueueInfo>("Queues");
final downloadsHelper = GetIt.instance<DownloadsHelper>();
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
final queueService = GetIt.instance<QueueService>();
int remainingSongs = info.songCount - info.previousTracks.length;
Future<BaseItemDto?> track;
if (info.currentTrack == null) {
track = Future.value(null);
} else if (FinampSettingsHelper.finampSettings.isOffline) {
track = Future.value(
downloadsHelper.getDownloadedSong(info.currentTrack!)?.song);
} else {
track = jellyfinApiHelper
.getItemById(info.currentTrack!)
.then((x) => x, onError: (_) => null);
}

return ListTile(
title: Text(AppLocalizations.of(context)!.queueRestoreTitle(
DateTime.fromMillisecondsSinceEpoch(info.creation))),
leading: FutureBuilder<BaseItemDto?>(
future: track,
builder: (context, snapshot) => AlbumImage(item: snapshot.data)),
isThreeLine: true,
//dense: true,
subtitle: FutureBuilder<BaseItemDto?>(
future: track,
initialData: null,
builder: (context, snapshot) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: ((snapshot.data?.name == null)
? <Text>[]
: [
// exclude subtitle line 1 if song name is null
Text(
AppLocalizations.of(context)!
.queueRestoreSubtitle1(snapshot.data!.name!),
overflow: TextOverflow.ellipsis)
]) +
[
Text(AppLocalizations.of(context)!
.queueRestoreSubtitle2(info.songCount, remainingSongs))
])),
trailing: IconButton(
icon: const Icon(Icons.arrow_circle_right_outlined),
onPressed: () {
var latest = queuesBox.get("latest");
if (latest != null && latest.songCount != 0) {
queuesBox.put(latest.creation.toString(), latest);
}
BuildContext parentContext = Navigator.of(context).context;
queueService
.loadSavedQueue(info)
.catchError((x) => errorSnackbar(x, parentContext));
Navigator.of(context).popUntil(
(route) => route.isFirst && !route.willHandlePopInternally);
}),
);
}
}
Loading

0 comments on commit 82628e7

Please sign in to comment.