Pato05
2862c9ec05
restore translations functionality make scrollViews handle mouse pointers like touch, so that pull to refresh functionality is available exit app if opening cache or settings fails (another instance running) remove draggable_scrollbar and use builtin widget instead fix email login better way to manage lyrics (less updates and lookups in the lyrics List) fix player_screen on mobile (too big -> just average :)) right click: use TapUp events instead desktop: show context menu on triple dots button also avoid showing connection error if the homepage is cached and available offline i'm probably forgetting something idk
198 lines
7.1 KiB
Dart
198 lines
7.1 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),
|
|
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: 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,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|