182 lines
6.7 KiB
Dart
182 lines
6.7 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.dart';
|
|
import 'package:freezer/translations.i18n.dart';
|
|
import 'package:freezer/ui/menu.dart';
|
|
import 'package:freezer/ui/tiles.dart';
|
|
|
|
class QueueScreen extends StatelessWidget {
|
|
const QueueScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text('Queue'.i18n),
|
|
systemOverlayStyle: SystemUiOverlayStyle(
|
|
statusBarColor: Colors.transparent,
|
|
statusBarIconBrightness: Brightness.light,
|
|
statusBarBrightness: Brightness.light,
|
|
systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor,
|
|
systemNavigationBarDividerColor: Color(
|
|
Theme.of(context).scaffoldBackgroundColor.value - 0x00111111),
|
|
systemNavigationBarIconBrightness: Brightness.light,
|
|
),
|
|
// actions: <Widget>[
|
|
// IconButton(
|
|
// icon: Icon(
|
|
// Icons.shuffle,
|
|
// semanticLabel: "Shuffle".i18n,
|
|
// ),
|
|
// onPressed: () async {
|
|
// await playerHelper.toggleShuffle();
|
|
// setState(() {});
|
|
// },
|
|
// )
|
|
// ],
|
|
),
|
|
body: const SafeArea(child: QueueListWidget(shouldPopOnTap: true)));
|
|
}
|
|
}
|
|
|
|
class QueueListWidget extends StatefulWidget {
|
|
final bool shouldPopOnTap;
|
|
const QueueListWidget({super.key, this.shouldPopOnTap = false});
|
|
|
|
@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) {
|
|
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(context).defaultTrackMenu(
|
|
Track.fromMediaItem(mediaItem),
|
|
details: details),
|
|
checkTrackOffline: false,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|