Pato05
f126ffef46
use get_url api by default, and fall back to old generation if get_url failed start to write a better cachemanager to implement in all systems write in more appropriate directories on windows and linux improve check for Connectivity by adding a fallback (needed for example on linux systems without NetworkManager) allow to dynamically change track quality without rebuilding the object
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/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 {
|
|
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,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|