feat(core,app,android,ios): keep a show in Continue watching after an episode ends

Continue watching listed only part-watched episodes, so finishing one -
the most ordinary moment to want the next - dropped the show from the
shelf. Title::continue_with now reads the episode touched last: a
part-watched one is resumed, a finished one offers the episode after it
("Next: S01E04"), and one reset to the start (what leaving the shelf
does) keeps the title off. A few seconds of an episode are passed over.

The phones computed their own shelf from episode records. The bridge now
exports the engine's choice as TitleRecord.continue_index, and Play on a
title lands there on every client.

Undo on the phones re-saved a watch state with a fresh time, which both
reordered the shelf and could not express "never played". A new
restore_watch_state puts the state back as it was read.
This commit is contained in:
nvrl committed 2026-10-12 22:05:05 +02:00
1 parent c6b0457c4d
commit a306d02cf1
15 files changed
+225 -68

No files matched your search

+10 -1
View File
@@ -1312,9 +1312,18 @@ impl App {
})?;
Some((title.name.clone(), episode.watch))
});
let Some((name, Some(before))) = found else {
let Some((name, before)) = found else {
return;
};
// An episode Continue watching offers next has never been played.
// Its undo is a reset as old as time, which the finished episode
// before it outranks.
let before = before.unwrap_or(pstr_core::catalog::WatchState {
position_secs: 0.0,
duration_secs: None,
watched: false,
updated_at: 0,
});
self.engine.save_watch_state(
share_id.clone(),
link_id.clone(),
+11 -15
View File
@@ -382,7 +382,7 @@ fn tile_shape(tiles: Tiles) -> (f32, f32) {
}
}
/// The top shelf: one card per part-watched title, scrolling sideways.
/// The top shelf: one card per title in progress, scrolling sideways.
fn continue_row(
ui: &mut egui::Ui,
art: &mut Art<'_>,
@@ -395,21 +395,17 @@ fn continue_row(
.show(ui, |ui| {
ui.horizontal(|ui| {
for title in titles {
let Some(episode) = title.resume() else {
let Some(episode) = title.continue_with() else {
continue;
};
let remaining = episode
.watch
.and_then(|watch| watch.duration_secs)
.map(|duration| {
format!(
"{} left",
ui::format_time(
duration - episode.watch.map_or(0.0, |w| w.position_secs)
)
)
})
.unwrap_or_else(|| episode.label());
let remaining = match episode.resume_at() {
Some(at) => episode
.watch
.and_then(|watch| watch.duration_secs)
.map(|duration| format!("{} left", ui::format_time(duration - at)))
.unwrap_or_else(|| episode.label()),
None => format!("Next: {}", episode.label()),
};
let picture = art.of(title);
let response = ui::card(
@@ -550,7 +546,7 @@ fn tile_menu(
});
}
if continuing
&& let Some(episode) = title.resume()
&& let Some(episode) = title.continue_with()
&& ui
.button("Remove from Continue watching")
.on_hover_text("Forget where you stopped in it")