forked from Foxyz/sync_replay_project
Restructure step 6: what's next and watch tracking
- What's next is the server's: state.advance counts down (10 s) when the media finishes with an Up next item first, holds while its performer or warm source is gone, and sets queuePlay at its end; cancel is shared, a seek or swap ends it; the server skips items nobody can put up. Shared rules in core whats_next.dart; the app's janitor, per-screen countdown and end-of-episode card are gone. - The continuation (next Jellyfin episode, next file of a local show with its gaps) is an auto item under "Continues with", prepared like Up next's first item. - Watch tracking by time In (D3): only time played while this viewer is in counts; away, paused, held, jumps and stalls never do (P7, P9). WatchTracker + device WatchHistory (files, links); Jellyfin progress reads the same watch; records are the AniList seam. - A server restart no longer loses what played: apps hold their media and one offers it back (swap.restore, first offer wins), Up next is re-added, the source re-feeds, and the watch carries on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a87ee98faa
commit
741b258ac1
55 files changed
+2724
-964
No files matched your search
@@ -278,7 +278,7 @@ Future<void> _messageMenu(BuildContext context, String text, String? link) async
|
||||
case 'seek':
|
||||
return (Icons.fast_forward, scheme.primary, 'seeked');
|
||||
case 'swap':
|
||||
return (Icons.swap_horiz, scheme.primary, 'changed the media');
|
||||
return (Icons.swap_horiz, scheme.primary, f.reason == 'restore' ? 'put back what was playing' : 'changed the media');
|
||||
case 'gone':
|
||||
return (Icons.cloud_off, Colors.orange, 'lost signal');
|
||||
case 'return':
|
||||
@@ -296,6 +296,9 @@ Future<void> _messageMenu(BuildContext context, String text, String? link) async
|
||||
case 'queue-skip':
|
||||
final why = f.reason.isEmpty ? '' : ' · ${f.reason}';
|
||||
return (Icons.redo_rounded, Colors.orange, 'skipped ${f.target ?? ''}$why');
|
||||
case 'advance-cancel':
|
||||
final what = f.target == null || f.target!.isEmpty ? '' : ' (${f.target} is off Up next)';
|
||||
return (Icons.cancel_outlined, Colors.grey, 'cancelled what plays next$what');
|
||||
case 'fetch-fail':
|
||||
return (Icons.cloud_off, Colors.red, "couldn't fetch ${f.target ?? 'the video'}");
|
||||
default:
|
||||
@@ -304,9 +307,16 @@ Future<void> _messageMenu(BuildContext context, String text, String? link) async
|
||||
}
|
||||
}
|
||||
|
||||
/// The server's own lines (pause / resume).
|
||||
/// The server's own lines (pause / resume, the end, an Up next item
|
||||
/// nobody here can put up).
|
||||
(IconData, Color, String) serverFeedStyle(FeedMessage f) => switch (f.action) {
|
||||
'resume' => (Icons.play_arrow, Colors.green, 'Playback resumed'),
|
||||
'finished' => (Icons.stop_circle_outlined, Colors.grey, 'Finished'),
|
||||
'queue-skip' => (
|
||||
Icons.redo_rounded,
|
||||
Colors.orange,
|
||||
'Skipped ${f.target ?? ''}${f.reason.isEmpty ? '' : ' · ${f.reason}'}'
|
||||
),
|
||||
_ => (Icons.pause, Colors.orange, 'Playback paused · waiting for readiness'),
|
||||
};
|
||||
|
||||
|
||||
@@ -419,16 +419,14 @@ class _SessionPlayerState extends State<SessionPlayer> {
|
||||
});
|
||||
}
|
||||
|
||||
/// The action area's right end: the end-of-media card (Up next, else the
|
||||
/// next episode; it stays up), else the pills, shown only with the
|
||||
/// The action area's right end: the end-of-media card (Up next, else
|
||||
/// the continuation; it stays up), else the pills, shown only with the
|
||||
/// controls ([PillGate]).
|
||||
Widget _actionSide() {
|
||||
final coord = _coord;
|
||||
return SignalBuilder(
|
||||
builder: (context) {
|
||||
final next = coord.nextEpisode.value;
|
||||
final countdown = coord.autoAdvance.value;
|
||||
// Up next goes before the next episode.
|
||||
final upNext = coord.queue.countdown.value;
|
||||
final finished = coord.serverState.value?.transport == 'finished';
|
||||
final pill = coord.nextPromptVisible.value;
|
||||
@@ -437,18 +435,6 @@ class _SessionPlayerState extends State<SessionPlayer> {
|
||||
if (upNext != null && finished) {
|
||||
return ControlsHoldRegion(hold: _hold, child: UpNextCard(queue: coord.queue, countdown: upNext));
|
||||
}
|
||||
// "Clear" in Up next hides the next episode for this media.
|
||||
if (next != null && (finished || countdown != null) && coord.queue.shownContinuation.value.isNotEmpty) {
|
||||
return ControlsHoldRegion(
|
||||
hold: _hold,
|
||||
child: EndOfEpisodeCard(
|
||||
next: next,
|
||||
countdown: countdown,
|
||||
onPlayNow: coord.playNextEpisode,
|
||||
onCancel: coord.cancelAutoAdvance,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!showNext && skip == null) return const SizedBox.shrink();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -152,7 +152,7 @@ class SettingsTab extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
const SettingsPanel(title: 'Jellyfin playback', child: AutoAdvanceSetting()),
|
||||
const SettingsPanel(title: 'Episodes', child: AutoAdvanceSetting()),
|
||||
const SettingsPanel(title: 'Jellyfin library', child: HideReReleasesSetting()),
|
||||
];
|
||||
case SettingsSection.notifications:
|
||||
|
||||
Reference in new issue
Block a user