Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Redesign] Recover Now-Playing Queue on Startup #547

Merged
merged 13 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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
6 changes: 3 additions & 3 deletions lib/components/PlayerScreen/queue_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ 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 +283,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 @@ -729,7 +729,7 @@ class _CurrentTrackState extends State<CurrentTrack> {
item: baseItem,
borderRadius: BorderRadius.zero,
itemsToPrecache:
_queueService.getNextXTracksInQueue(3).map((e) {
_queueService.getNextXTracksInQueue(3,reverse: 1).map((e) {
final item = e.item.extras?["itemJson"] != null
? jellyfin_models.BaseItemDto.fromJson(
e.item.extras!["itemJson"]
Expand Down
56 changes: 3 additions & 53 deletions lib/components/PlayerScreen/song_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,14 @@ 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 +113,7 @@ class _SongInfoState extends State<SongInfo> {
}
}

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

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

final item = queueItem.item.extras?["itemJson"] != null
Expand Down Expand Up @@ -159,58 +154,13 @@ class _PlayerScreenAlbumImage extends ConsumerWidget {
// 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) {
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(),
// 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,
);
}
}),
),
),
);
Expand Down
79 changes: 79 additions & 0 deletions lib/components/QueueRestoreScreen/queue_restore_tile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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
Loading