freezer/lib/ui/queue_screen.dart
Pato05 4b5d0bd09c
improve player screen with blurred album art
ui improvements in lyrics screen
animated bars when track is playing
fix back button when player screen is open
instantly pop when track is changed in queue list
2024-04-29 16:23:22 +02:00

189 lines
6.6 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player/audio_handler.dart';
import 'package:freezer/api/player/player_helper.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/ui/tiles.dart';
class QueueScreen extends StatelessWidget {
final VoidCallback closePlayer;
const QueueScreen({super.key, required this.closePlayer});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Queue'.i18n),
// actions: <Widget>[
// IconButton(
// icon: Icon(
// Icons.shuffle,
// semanticLabel: "Shuffle".i18n,
// ),
// onPressed: () async {
// await playerHelper.toggleShuffle();
// setState(() {});
// },
// )
// ],
),
body: SafeArea(
child: QueueListWidget(
shouldPopOnTap: true, closePlayer: closePlayer)));
}
}
class QueueListWidget extends StatefulWidget {
final VoidCallback closePlayer;
final bool shouldPopOnTap;
final bool isInsidePlayer;
const QueueListWidget(
{super.key,
this.shouldPopOnTap = false,
this.isInsidePlayer = false,
required this.closePlayer});
@override
State<QueueListWidget> createState() => _QueueListWidgetState();
}
class _QueueListWidgetState extends State<QueueListWidget> {
static const itemExtent = 68.0; // height of each TrackTile
final _scrollController = ScrollController();
late StreamSubscription _queueSub;
static const _dismissibleBackground = DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Icon(Icons.delete))));
static const _dismissibleSecondaryBackground = DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Icon(Icons.delete))));
bool _isReordering = false;
/// Basically a simple list that keeps itself synchronized with [AudioHandler.queue],
/// so that the [ReorderableListView] is updated instanly (as it should be)
List<MediaItem> _queueCache = [];
@override
void initState() {
_queueCache = List.from(audioHandler.queue.value); // avoid shadow-copying
_queueSub = audioHandler.queue.listen((newQueue) {
print('got new queue!');
// if (listEquals(_queueCache, newQueue)) {
// print('avoiding rebuilding queue since they are the same');
// return;
// }
_queueCache = List.from(newQueue);
setState(() {});
});
WidgetsBinding.instance.addPostFrameCallback((_) {
// calculate position of current item
double position = min(playerHelper.queueIndex * itemExtent,
_scrollController.position.maxScrollExtent);
_scrollController.jumpTo(position);
});
super.initState();
}
@override
void dispose() {
_queueSub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final menuSheet = MenuSheet(context, navigateCallback: () {
if (!widget.isInsidePlayer) {
Navigator.pop(context);
}
widget.closePlayer.call();
});
return ReorderableListView.builder(
buildDefaultDragHandles: false,
scrollController: _scrollController,
// specify the itemExtent normally and remove it when reordering because of an issue with [SliverFixedExtentList]
// https://github.com/flutter/flutter/issues/84901
itemExtent: _isReordering ? null : itemExtent,
onReorderStart: (_) => setState(() => _isReordering = true),
onReorderEnd: (_) => setState(() => _isReordering = true),
onReorder: (oldIndex, newIndex) {
setState(() => _queueCache.reorder(oldIndex, newIndex));
if (oldIndex == playerHelper.queueIndex) {
audioHandler.customAction('setIndex', {'index': newIndex});
}
playerHelper.reorder(oldIndex, newIndex);
},
itemCount: _queueCache.length,
itemBuilder: (BuildContext context, int index) {
final mediaItem = _queueCache[index];
final int itemId = mediaItem.extras!['id'] ?? 0;
return Dismissible(
key: ValueKey(mediaItem.id.hashCode ^ itemId),
background: _dismissibleBackground,
secondaryBackground: _dismissibleSecondaryBackground,
onDismissed: (_) {
audioHandler.removeQueueItemAt(index).then((value) {
if (index == playerHelper.queueIndex) {
audioHandler.skipToNext();
}
});
setState(() => _queueCache.removeAt(index));
},
confirmDismiss: (_) {
final completer = Completer<bool>();
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Song deleted from queue'.i18n),
action: SnackBarAction(
label: 'UNDO'.i18n,
onPressed: () => completer.complete(false))))
.closed
.then((value) {
if (value == SnackBarClosedReason.action) return;
completer.complete(true);
});
return completer.future;
},
child: SizedBox(
height: itemExtent,
child: TrackTile.fromMediaItem(
mediaItem,
trailing: ReorderableDragStartListener(
index: index, child: const Icon(Icons.drag_handle)),
onTap: () {
if (widget.shouldPopOnTap) {
Navigator.pop(context);
}
audioHandler.skipToQueueItem(index);
},
onSecondary: (details) => menuSheet.defaultTrackMenu(
Track.fromMediaItem(mediaItem),
details: details),
checkTrackOffline: false,
),
),
);
},
);
}
}