Player overlay as one piece, Up next always warm and on every source, Jellyfin never unmarks a watched episode
- Overlay: one layout for both designs (top bar with Up next as one of its buttons; one action band above the controls for the status pill, skips, next episode and end cards), one glass recipe (AppGlass) and one pill (FloatingPill); hovering any of them, or the open queue, holds the controls up. - Up next: the first item (or the first continuation episode) is kept resolved on the app that puts it up, refreshed before it expires; Schedule and drop/paste can queue too. - Jellyfin: WatchSession (core) counts real watched coverage; an episode is watched at 80 % or at its ending after half; a played one is re-marked after Playing (10.11 unmarks on open) and never gets a position; a peek leaves the old resume point; part-way resume points stay under 85 %. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6b10cae4e6
commit
771b961b32
27 files changed
+1396
-558
No files matched your search
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:signals/signals_flutter.dart';
|
||||
|
||||
import '../../state/settings.dart';
|
||||
import '../controls_probe.dart' show ControlsHold;
|
||||
|
||||
// The mpv-style (uosc) controls frame: a full-width timeline at the
|
||||
// bottom with a row of flat buttons above it, the title top left and a
|
||||
@@ -28,6 +29,8 @@ class UoscControls extends StatefulWidget {
|
||||
final Duration hideAfter;
|
||||
/// Follows whether the controls are up (for overlays outside them).
|
||||
final ValueNotifier<bool>? shown;
|
||||
/// Keeps them up while held (the mouse on an overlay, an open popover).
|
||||
final ControlsHold? holdBy;
|
||||
const UoscControls({
|
||||
super.key,
|
||||
required this.timeline,
|
||||
@@ -38,9 +41,12 @@ class UoscControls extends StatefulWidget {
|
||||
this.onDoubleTap,
|
||||
this.hideAfter = const Duration(seconds: 3),
|
||||
this.shown,
|
||||
this.holdBy,
|
||||
});
|
||||
|
||||
static const buttonRowHeight = 40.0;
|
||||
/// Under the button row, above the timeline.
|
||||
static const rowBottomPadding = 4.0;
|
||||
|
||||
/// The controls around [context], for widgets that need them kept up
|
||||
/// (a timeline drag, an open menu).
|
||||
@@ -54,6 +60,7 @@ class UoscControls extends StatefulWidget {
|
||||
class UoscControlsState extends State<UoscControls> {
|
||||
bool _visible = true;
|
||||
int _holds = 0;
|
||||
bool _heldBy = false;
|
||||
Timer? _timer;
|
||||
|
||||
bool get visible => _visible;
|
||||
@@ -61,6 +68,7 @@ class UoscControlsState extends State<UoscControls> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.holdBy?.addListener(_holdByChanged);
|
||||
// Not during this build: an overlay listening to [shown] may be built.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) widget.shown?.value = true;
|
||||
@@ -68,12 +76,30 @@ class UoscControlsState extends State<UoscControls> {
|
||||
_hideLater();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(UoscControls oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.holdBy != widget.holdBy) {
|
||||
oldWidget.holdBy?.removeListener(_holdByChanged);
|
||||
widget.holdBy?.addListener(_holdByChanged);
|
||||
_holdByChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.holdBy?.removeListener(_holdByChanged);
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _holdByChanged() {
|
||||
final on = widget.holdBy?.held ?? false;
|
||||
if (on == _heldBy || !mounted) return;
|
||||
_heldBy = on;
|
||||
hold(on);
|
||||
}
|
||||
|
||||
/// Show the controls and restart the hide countdown.
|
||||
void wake() {
|
||||
if (!_visible && mounted) {
|
||||
@@ -185,8 +211,9 @@ class UoscControlsState extends State<UoscControls> {
|
||||
child: AnimatedOpacity(
|
||||
duration: fade,
|
||||
opacity: visible ? 1 : 0,
|
||||
// The buttons centred on the title, like the standard
|
||||
// design's top bar.
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
@@ -196,7 +223,8 @@ class UoscControlsState extends State<UoscControls> {
|
||||
: Align(alignment: AlignmentDirectional.centerStart, child: widget.topLeft),
|
||||
),
|
||||
),
|
||||
?widget.top,
|
||||
if (widget.top != null)
|
||||
Padding(padding: const EdgeInsets.fromLTRB(0, 12, 6, 0), child: widget.top),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -217,7 +245,7 @@ class UoscControlsState extends State<UoscControls> {
|
||||
duration: fade,
|
||||
opacity: visible ? 1 : 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 0, 6, 4),
|
||||
padding: const EdgeInsets.fromLTRB(6, 0, 6, UoscControls.rowBottomPadding),
|
||||
child: SizedBox(
|
||||
height: UoscControls.buttonRowHeight,
|
||||
child: Row(children: [...widget.left, const Spacer(), ...widget.right]),
|
||||
|
||||
Reference in new issue
Block a user