feat: add saved-video collections and category views

This commit is contained in:
2026-10-11 20:47:24 +02:00
parent 6eed1cdd9e
commit b65834e9d4
6 changed files with 680 additions and 138 deletions
+63 -2
View File
@@ -109,6 +109,9 @@ actions! {
WatchLaterBrowse, "view.watch_later.browse", "Browse this category's watch later, or the combined collection elsewhere", &[Context::Normal], ["W"];
WatchLaterFilter, "view.watch_later.filter", "Cycle watch-later filter: all, unwatched, watched", &[Context::Normal], ["f"];
WatchLaterRemove, "view.watch_later.remove", "Confirm removal of the current filtered watch-later subset", &[Context::Normal], ["D"];
SavedVideoSave, "video.saved.save", "Save the selected video (also in previews)", VIEW, ["v"];
SavedVideoBrowse, "view.saved.browse", "Browse this category's saved videos, or the combined collection elsewhere", &[Context::Normal], ["V"];
SavedVideoRemove, "video.saved.remove", "Remove the selected saved video (also in previews)", VIEW, ["x"];
SelectorNext, "selector.select.next", "Select the next category or channel", &[Context::SelectorNormal], ["j", "down"];
SelectorPrevious, "selector.select.previous", "Select the previous category or channel", &[Context::SelectorNormal], ["k", "up"];
SelectorInputNext, "selector.input.next", "Select the next match while editing the filter", &[Context::SelectorInput], ["down", "ctrl+j"];
@@ -477,6 +480,23 @@ impl Bindings {
),
],
),
(
"SAVED VIDEOS",
&[
(
&[SavedVideoSave],
"Save the selected video (also in previews)",
),
(
&[SavedVideoBrowse],
"Browse category saved videos; combined elsewhere",
),
(
&[SavedVideoRemove],
"Remove the selected video (also in previews)",
),
],
),
(
"FULL DESCRIPTION",
&[
@@ -926,8 +946,8 @@ mod tests {
#[test]
fn aliases_replace_defaults_and_can_disable_actions() {
let bindings = configured("[keys.view]\nrefresh = ['x', 'CTRL+y']\n[keys.app]\nquit = []\n[keys.input]\nsubmit = 'RETURN'\ncancel = 'EsC'\n").unwrap();
let quoted = configured("[keys]\n'view.refresh' = ['x', 'CTRL+y']\n'app.quit' = []\n'input.submit' = 'RETURN'\n'input.cancel' = 'EsC'\n").unwrap();
let bindings = configured("[keys.video.saved]\nremove = []\n[keys.view]\nrefresh = ['x', 'CTRL+y']\n[keys.app]\nquit = []\n[keys.input]\nsubmit = 'RETURN'\ncancel = 'EsC'\n").unwrap();
let quoted = configured("[keys]\n'video.saved.remove' = []\n'view.refresh' = ['x', 'CTRL+y']\n'app.quit' = []\n'input.submit' = 'RETURN'\n'input.cancel' = 'EsC'\n").unwrap();
assert_eq!(quoted, bindings);
assert_eq!(
press(
@@ -1047,6 +1067,47 @@ mod tests {
);
}
#[test]
fn saved_video_overrides_disable_and_respect_contexts() {
let defaults = Bindings::default();
for context in ALL {
for (key, action, enabled) in [
('v', Action::SavedVideoSave, VIEW.contains(context)),
('V', Action::SavedVideoBrowse, *context == Context::Normal),
('x', Action::SavedVideoRemove, VIEW.contains(context)),
] {
assert_eq!(
press(&defaults, *context, KeyCode::Char(key), KeyModifiers::NONE),
enabled.then_some(action),
);
}
}
let bindings = configured(
"[keys.video.saved]\nsave = 'alt+v'\nremove = []\n[keys.view.saved]\nbrowse = 'shift+x'\n",
)
.unwrap();
for context in ALL {
assert_eq!(
press(&bindings, *context, KeyCode::Char('v'), KeyModifiers::ALT),
VIEW.contains(context).then_some(Action::SavedVideoSave),
);
assert_eq!(
press(&bindings, *context, KeyCode::Char('x'), KeyModifiers::SHIFT),
(*context == Context::Normal).then_some(Action::SavedVideoBrowse),
);
for key in ['v', 'V', 'x'] {
assert_eq!(
press(&bindings, *context, KeyCode::Char(key), KeyModifiers::NONE),
None,
);
}
}
let collision = configured("[keys]\nview.saved.browse = 'v'")
.unwrap_err()
.to_string();
assert!(collision.contains("view.saved.browse") && collision.contains("video.saved.save"));
}
#[test]
fn invalid_entries_and_collisions_have_actionable_errors() {
for (source, path) in [