Swap shows only the new media, no guessed endings, quieter screen

- A swap stops the previous media at once; buffer counts only for the
  media the player holds (client) and the current mediaId (server), so
  the old video can no longer start the new one.
- The automatic frozen-picture reload is gone (false alarms on still
  scenes); Reload video stays.
- The "Next episode" pill shows only from a known ending.
- A web link's thumbnail is its cover, under the video's own name.
- While playing, the unread chip stays off the picture; chat pop-ups
  can be All / Only when paused / Off.
- Server fetch failures say when the server has no cookies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-11 04:31:14 +03:00
1 parent ab133dd82f
commit 27b4ec2c96
25 files changed
+274 -485

No files matched your search

@@ -1,65 +0,0 @@
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import '../playback/frame_watchdog.dart';
/// Fingerprints what the compositor really shows of the video. Place
/// [anchor] under a `RepaintBoundary` around the video (the session player
/// does); [sample] captures that boundary around the newest mounted anchor
/// at ~200 px wide — the texture included — and hashes its centre. This is the only per-presented-frame signal: mpv's own
/// counters keep ticking when the Flutter texture is stuck.
class ShownFrameProbe {
final _anchors = <_AnchorState>[];
Widget anchor() => _Anchor(probe: this);
Future<int?> sample() async {
for (final a in _anchors.reversed) {
if (!a.mounted) continue;
final boundary = a.context.findAncestorRenderObjectOfType<RenderRepaintBoundary>();
if (boundary == null || !boundary.attached || !boundary.hasSize) return null;
final width = boundary.size.width;
if (width <= 0) return null;
ui.Image? image;
try {
image = await boundary.toImage(pixelRatio: (200 / width).clamp(0.02, 1.0));
final data = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
if (data == null) return null;
return hashCenterPixels(data, image.width, image.height);
} catch (_) {
// Route not painted (covered) or mid-teardown: no reading.
return null;
} finally {
image?.dispose();
}
}
return null;
}
}
class _Anchor extends StatefulWidget {
final ShownFrameProbe probe;
const _Anchor({required this.probe});
@override
State<_Anchor> createState() => _AnchorState();
}
class _AnchorState extends State<_Anchor> {
@override
void initState() {
super.initState();
widget.probe._anchors.add(this);
}
@override
void dispose() {
widget.probe._anchors.remove(this);
super.dispose();
}
@override
Widget build(BuildContext context) => const SizedBox.shrink();
}