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:
foxyzandClaude Opus 5.5 committed 2026-10-11 07:53:42 +03:00
1 parent 6b10cae4e6
commit 771b961b32
27 files changed
+1396 -558

No files matched your search

+3 -19
View File
@@ -2,9 +2,9 @@ import 'package:sync_relay_core/core.dart';
import '../jellyfin/session_media.dart' show compareMemberIds;
// Who puts an Up next item up, and when to get the first one ready. Pure:
// only what every client sees in `state` decides, so all clients agree
// without a wire message (like the Jellyfin auto-advance swapper).
// Who puts an Up next item up. Pure: only what every client sees in
// `state` decides, so all clients agree without a wire message (like the
// Jellyfin auto-advance swapper).
/// Who puts [item] up: [id] a member id; null with [why] when nobody here
/// can (the item is skipped), null without [why] when nobody is around
@@ -66,22 +66,6 @@ String queueHowLabel(QueueItem item, {required String selfId}) {
};
}
/// Seconds before the end at which the first Up next item is got ready.
const queueWarmLeadSecs = 90.0;
/// Whether to get the next item ready now: the current media is in its
/// last [queueWarmLeadSecs], is buffered to its end, or has finished.
bool queueWarmDue({
required String transport,
required double position,
required double duration,
required double bufferedTo,
}) {
if (transport == 'finished') return true;
if (duration <= 0) return false;
return duration - position <= queueWarmLeadSecs || bufferedTo >= duration - 1;
}
/// "1:23:45" / "23:45" for [secs]; empty when unknown.
String queueClock(double secs) {
if (secs <= 0) return '';