mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
feat(core,app): a history page
Every played episode, newest first, under Today, Yesterday, a weekday or a date. A row shows the still (which plays it), the title (which opens it), the episode, and where it stopped or that it was finished. Removing a row resets that file to never played, with an Undo. History sits second in the top bar, so Ctrl+1 to Ctrl+5 now run Library, History, Shares, Downloads, Settings.
This commit is contained in:
1 parent
39866bee8d
commit
e7eeaa518f
9 files changed
+372
-28
No files matched your search
+66
-24
@@ -46,6 +46,7 @@ impl FrameTimer {
|
||||
started: std::time::Instant::now(),
|
||||
page: match page {
|
||||
Page::Library => "library",
|
||||
Page::History => "history",
|
||||
Page::Title(_) => "title",
|
||||
Page::Shares => "shares",
|
||||
Page::Downloads => "downloads",
|
||||
@@ -80,6 +81,8 @@ const UP_NEXT_SECONDS: f64 = 10.0;
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Page {
|
||||
Library,
|
||||
/// Everything played, newest first.
|
||||
History,
|
||||
/// One title, by [`pstr_core::library::Title::key`].
|
||||
Title(String),
|
||||
Shares,
|
||||
@@ -91,6 +94,15 @@ pub enum Page {
|
||||
Player,
|
||||
}
|
||||
|
||||
/// The pages in the top bar, in order. Ctrl+1 to Ctrl+5 go by position here.
|
||||
const NAV: [Page; 5] = [
|
||||
Page::Library,
|
||||
Page::History,
|
||||
Page::Shares,
|
||||
Page::Downloads,
|
||||
Page::Settings,
|
||||
];
|
||||
|
||||
/// Something a click asked for, applied once the frame is drawn.
|
||||
pub enum Action {
|
||||
Goto(Page),
|
||||
@@ -177,6 +189,12 @@ pub enum Action {
|
||||
share_id: String,
|
||||
link_id: String,
|
||||
},
|
||||
/// Take one file off the history page: forget its position and its
|
||||
/// watched mark, with an Undo.
|
||||
RemoveFromHistory {
|
||||
share_id: String,
|
||||
link_id: String,
|
||||
},
|
||||
/// Put watch states back as they were — what Undo does.
|
||||
RestoreWatch(Vec<(String, String, pstr_core::catalog::WatchState)>),
|
||||
ClearStreamCache,
|
||||
@@ -293,6 +311,8 @@ pub struct LibraryView {
|
||||
built_with: (Filter, Sort),
|
||||
/// Indices into [`Library::titles`].
|
||||
pub matches: Vec<usize>,
|
||||
/// Every played episode, newest first, for the history page.
|
||||
pub history: Vec<ui::history::Entry>,
|
||||
/// The "Continue watching" shelf, most recent first, as indices.
|
||||
pub resumable: Vec<usize>,
|
||||
/// The library changed under the lists.
|
||||
@@ -366,6 +386,7 @@ impl LibraryView {
|
||||
self.matches = matches.into_iter().filter_map(index_of).collect();
|
||||
self.built_with = (self.filter, self.sort);
|
||||
if self.stale {
|
||||
self.history = ui::history::entries(library);
|
||||
self.resumable = library
|
||||
.continue_watching()
|
||||
.into_iter()
|
||||
@@ -1053,6 +1074,39 @@ impl App {
|
||||
self.engine.set_playback_prefs(prefs, commit);
|
||||
}
|
||||
|
||||
/// Reset one file to never played, with an Undo. What leaving Continue
|
||||
/// watching and leaving the history both come down to: the two are read
|
||||
/// off the same watch state.
|
||||
fn forget_watch(
|
||||
&mut self,
|
||||
ctx: &egui::Context,
|
||||
share_id: String,
|
||||
link_id: String,
|
||||
place: &str,
|
||||
) {
|
||||
let found = self.library.titles.iter().find_map(|title| {
|
||||
let episode = title.episodes().find(|episode| {
|
||||
episode.node.share_id == share_id && episode.node.link_id == link_id
|
||||
})?;
|
||||
Some((title.name.clone(), episode.watch))
|
||||
});
|
||||
let Some((name, Some(before))) = found else {
|
||||
return;
|
||||
};
|
||||
self.engine.save_watch_state(
|
||||
share_id.clone(),
|
||||
link_id.clone(),
|
||||
watch_state(0.0, before.duration_secs, false),
|
||||
);
|
||||
let now = ctx.input(|input| input.time);
|
||||
self.toasts.push_with(
|
||||
now,
|
||||
format!("removed {name} from {place}"),
|
||||
"Undo",
|
||||
Action::RestoreWatch(vec![(share_id, link_id, before)]),
|
||||
);
|
||||
}
|
||||
|
||||
fn apply(&mut self, ctx: &egui::Context, action: Action) {
|
||||
match action {
|
||||
Action::Goto(page) => self.page = page,
|
||||
@@ -1318,27 +1372,10 @@ impl App {
|
||||
);
|
||||
}
|
||||
Action::ForgetPosition { share_id, link_id } => {
|
||||
let found = self.library.titles.iter().find_map(|title| {
|
||||
let episode = title.episodes().find(|episode| {
|
||||
episode.node.share_id == share_id && episode.node.link_id == link_id
|
||||
})?;
|
||||
Some((title.name.clone(), episode.watch))
|
||||
});
|
||||
let Some((name, Some(before))) = found else {
|
||||
return;
|
||||
};
|
||||
self.engine.save_watch_state(
|
||||
share_id.clone(),
|
||||
link_id.clone(),
|
||||
watch_state(0.0, before.duration_secs, false),
|
||||
);
|
||||
let now = ctx.input(|input| input.time);
|
||||
self.toasts.push_with(
|
||||
now,
|
||||
format!("removed {name} from Continue watching"),
|
||||
"Undo",
|
||||
Action::RestoreWatch(vec![(share_id, link_id, before)]),
|
||||
);
|
||||
self.forget_watch(ctx, share_id, link_id, "Continue watching");
|
||||
}
|
||||
Action::RemoveFromHistory { share_id, link_id } => {
|
||||
self.forget_watch(ctx, share_id, link_id, "history");
|
||||
}
|
||||
Action::RestoreWatch(states) => {
|
||||
for (share_id, link_id, state) in states {
|
||||
@@ -1459,12 +1496,13 @@ impl App {
|
||||
} else {
|
||||
format!("Downloads ({active})")
|
||||
};
|
||||
let pages = [Page::Library, Page::Shares, Page::Downloads, Page::Settings];
|
||||
let pages = NAV;
|
||||
let clicked = ui::tabs(
|
||||
ui,
|
||||
ui.id().with("nav"),
|
||||
&[
|
||||
("Library", on_library),
|
||||
("History", self.page == Page::History),
|
||||
("Shares", self.page == Page::Shares),
|
||||
(&downloads, self.page == Page::Downloads),
|
||||
("Settings", self.page == Page::Settings),
|
||||
@@ -1776,6 +1814,9 @@ impl eframe::App for App {
|
||||
ui::settings::show(ui, settings, prefs, api_key, &mut actions)
|
||||
}
|
||||
Page::Downloads => ui::downloads::show(ui, downloads, &mut actions),
|
||||
Page::History => {
|
||||
ui::history::show(ui, &mut art, library, &view.history, &mut actions)
|
||||
}
|
||||
// Drawn above, without any of these panels.
|
||||
Page::Player => {}
|
||||
}
|
||||
@@ -2008,13 +2049,14 @@ fn global_shortcuts(ctx: &egui::Context, page: &Page, actions: &mut Vec<Action>)
|
||||
(Key::Num2, 1),
|
||||
(Key::Num3, 2),
|
||||
(Key::Num4, 3),
|
||||
(Key::Num5, 4),
|
||||
] {
|
||||
if input.consume_key(Modifiers::COMMAND, key) {
|
||||
tab = Some(index);
|
||||
}
|
||||
}
|
||||
if input.consume_key(Modifiers::COMMAND, Key::Comma) {
|
||||
tab = Some(3);
|
||||
tab = Some(4);
|
||||
}
|
||||
// A share link pasted anywhere outside a text field is someone adding
|
||||
// a share, wherever they happen to be.
|
||||
@@ -2029,7 +2071,7 @@ fn global_shortcuts(ctx: &egui::Context, page: &Page, actions: &mut Vec<Action>)
|
||||
}
|
||||
});
|
||||
if let Some(index) = tab {
|
||||
let page = [Page::Library, Page::Shares, Page::Downloads, Page::Settings][index].clone();
|
||||
let page = NAV[index].clone();
|
||||
actions.push(Action::Goto(page));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user