freezer/lib/ui/queue_screen.dart

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/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: () {
audioHandler.skipToQueueItem(index).then((value) {
if (widget.shouldPopOnTap) {
Navigator.of(context).pop();
}
});
},
onSecondary: (details) => menuSheet.defaultTrackMenu(
Track.fromMediaItem(mediaItem),
details: details),
checkTrackOffline: false,
),
),
);
},
);
}
}