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

+10 -2
View File
@@ -149,6 +149,9 @@ class _FakeJellyfin {
res.statusCode = 204;
return res.close();
}
if (path.startsWith('/UserPlayedItems/') || path.startsWith('/UserItems/')) {
return json({'Played': path.startsWith('/UserPlayedItems/'), 'PlaybackPositionTicks': 0});
}
if (RegExp(r'^/Videos/\w+/stream$').hasMatch(path)) return _stream(req);
res.statusCode = 404;
await res.close();
@@ -378,8 +381,13 @@ void main() {
expect(ep1.last, 'Stopped');
expect(ep1.where((k) => k == 'Stopped'), hasLength(1));
final stopped1 = aCalls.lastWhere((s) => s.body?['ItemId'] == _ep1);
expect(stopped1.body!['PositionTicks'], greaterThan(19 * jellyfinTicksPerSecond),
reason: 'played to the end');
// Part of it watched, then a seek to the end: not watched (see
// WatchSession). A resume point capped at 85 % of the 20 s, so
// Jellyfin cannot mark it played on its own.
expect(stopped1.body!['PositionTicks'], lessThan(18 * 10000000),
reason: 'part way: resume point under 90 %, so not marked played');
expect(jelly.calls.any((c) => c.method == 'POST' && c.path == '/UserPlayedItems/$_ep1'), isFalse,
reason: 'seeking to the end is not watching');
expect(stopped1.body!['PlaySessionId'], 'ps-ua-$_ep1');
expect(ep2.first, 'Playing');
expect(ep2.last, 'Stopped', reason: 'leaving reports Stopped');