feat: open combined collections directly from any video view
This commit is contained in:
@@ -71,12 +71,14 @@ These are the default bindings. Overrides in `[keys]` replace an action's bindin
|
|||||||
| `s` | Edit the selected channel's tags |
|
| `s` | Edit the selected channel's tags |
|
||||||
| `S` | Edit tags for all saved channels in the current category |
|
| `S` | Edit tags for all saved channels in the current category |
|
||||||
| `w` | Save the selected video to watch later (also works in previews) |
|
| `w` | Save the selected video to watch later (also works in previews) |
|
||||||
| `W` | Browse watch later for the current category, or the combined collection elsewhere |
|
| `W` | Open all-categories watch later from any video view or preview |
|
||||||
|
| `Alt-w` | Open the current category's watch later |
|
||||||
| `f` | In watch later, cycle All → Unwatched → Watched |
|
| `f` | In watch later, cycle All → Unwatched → Watched |
|
||||||
| `x` | In watch-later or saved-video lists, remove the selected video |
|
| `x` | In watch-later or saved-video lists, remove the selected video |
|
||||||
| `X` (Shift-X) | In either collection list, confirm bulk removal of the current category/filter subset |
|
| `X` (Shift-X) | In either collection list, confirm bulk removal of the current category/filter subset |
|
||||||
| `v` | Save the selected video to saved videos (also works in previews) |
|
| `v` | Save the selected video to saved videos (also works in previews) |
|
||||||
| `V` | Browse saved videos for the current category, or the combined collection elsewhere |
|
| `V` | Open all-categories saved videos from any video view or preview |
|
||||||
|
| `Alt-v` | Open the current category's saved videos |
|
||||||
| Tab / Shift-Tab | Switch between visible views |
|
| Tab / Shift-Tab | Switch between visible views |
|
||||||
| `r` | Reload configuration and refresh the current view |
|
| `r` | Reload configuration and refresh the current view |
|
||||||
| `?` | Open / close the scrollable keybinding reference |
|
| `?` | Open / close the scrollable keybinding reference |
|
||||||
@@ -175,7 +177,7 @@ Bulk editing refuses to clear all tags when a target has no `[channels]` registr
|
|||||||
|
|
||||||
Watch later is **one collection**, deduplicated by YouTube video ID. Category lists are derived from the saved videos' channel identities and the **current** subscription categories: adding or removing a channel from a category changes its watch-later list after saving tags or reloading config with `r`. No category memberships are stored on individual videos.
|
Watch later is **one collection**, deduplicated by YouTube video ID. Category lists are derived from the saved videos' channel identities and the **current** subscription categories: adding or removing a channel from a category changes its watch-later list after saving tags or reloading config with `r`. No category memberships are stored on individual videos.
|
||||||
|
|
||||||
Browse a category with `c`, then use `W` for its watch-later list. From Search, Feed, or a channel view, `W` opens the combined collection. Watch-later views use saved title, creator, and publication metadata without fetching video lists, so they also work offline. Their filters are independent and retained during the session; each pane's border title shows the active All, Unwatched, or Watched filter.
|
`W` always opens the all-categories watch-later list, even from a category view or preview. To browse one category, choose it with `c` and press `Alt-w`. Moving between watch-later scopes carries the current All/Unwatched/Watched filter to the destination, including cached lists; back navigation restores the originating category list. Repeating the shortcut for the current list does not add history. The border title shows the active filter. Lists use saved title, creator, and publication metadata without fetching video lists, so they also work offline.
|
||||||
|
|
||||||
Watch-later-only titles are **magenta** in all video tables; categorized channel names remain cyan. A `✓` marks watched videos, including outside watch-later views. The selected row retains its usual highlight.
|
Watch-later-only titles are **magenta** in all video tables; categorized channel names remain cyan. A `✓` marks watched videos, including outside watch-later views. The selected row retains its usual highlight.
|
||||||
|
|
||||||
@@ -200,13 +202,13 @@ Progress sessions survive Tubular exit so later player reports can be recovered
|
|||||||
|
|
||||||
Saved videos is a separate collection from watch later, deduplicated by YouTube video ID. It uses the same offline title, creator, and publication metadata and follows **current channel category memberships**, not categories stored on individual videos.
|
Saved videos is a separate collection from watch later, deduplicated by YouTube video ID. It uses the same offline title, creator, and publication metadata and follows **current channel category memberships**, not categories stored on individual videos.
|
||||||
|
|
||||||
Browse a category with `c`, then use `V` for its saved-video list. From Search, Feed, or a channel view, `V` opens the combined collection. Switching between a category's saved videos and watch later with `V` / `W` retains that category. Both lists share the existing temporary-tab and back/forward history behavior.
|
`V` always opens the all-categories saved-video list from any video view or preview. Choose a category with `c`, then press `Alt-v` for its saved-video list. `Alt-w` / `Alt-v` retain the explicit category when switching between category-scoped collection lists; without a current category, they leave the view unchanged. All four shortcuts work in normal mode and previews, not text-entry fields or modal selectors. Both collections share the existing temporary-tab and back/forward history behavior.
|
||||||
|
|
||||||
Saved lists have no watched filter. Individual and bulk removal use the shared collection-list controls below.
|
Saved lists have no watched filter. Individual and bulk removal use the shared collection-list controls below.
|
||||||
|
|
||||||
Saved titles are **green**, taking precedence over watch-later magenta when a video belongs to both collections. Removing it from saved videos restores magenta if it remains in watch later. Channel styling, selected-row highlighting, and watched markers are unchanged.
|
Saved titles are **green**, taking precedence over watch-later magenta when a video belongs to both collections. Removing it from saved videos restores magenta if it remains in watch later. Channel styling, selected-row highlighting, and watched markers are unchanged.
|
||||||
|
|
||||||
The collection is persisted independently in `saved-videos.toml` beside `config.toml`, using the same atomic, owner-only writes and validation as watch later. Saving repeatedly does not add duplicates. Configure saving and browsing with `video.saved.save` and `view.saved.browse`; the live hints and `?` reference reflect overrides.
|
The collection is persisted independently in `saved-videos.toml` beside `config.toml`, using the same atomic, owner-only writes and validation as watch later. Saving repeatedly does not add duplicates. Configure saving and combined browsing with `video.saved.save` and `view.saved.browse`; category browsing uses `view.saved.category`. The corresponding watch-later navigation actions are `view.watch_later.browse` and `view.watch_later.category`. The live hints and `?` reference reflect overrides. Alt shortcuts require the terminal to transmit Alt/Meta; if macOS Option inserts symbols, enable Option-as-Meta or remap the actions.
|
||||||
|
|
||||||
## Collection removal
|
## Collection removal
|
||||||
|
|
||||||
|
|||||||
+59
-7
@@ -127,12 +127,14 @@ actions! {
|
|||||||
ChannelTags, "channel.tags", "Edit the selected channel's category tags", VIEW, ["s"];
|
ChannelTags, "channel.tags", "Edit the selected channel's category tags", VIEW, ["s"];
|
||||||
CategoryTags, "category.tags", "Edit category tags for all channels in this category", VIEW, ["S"];
|
CategoryTags, "category.tags", "Edit category tags for all channels in this category", VIEW, ["S"];
|
||||||
WatchLaterSave, "video.watch_later.save", "Save the selected video to watch later", VIEW, ["w"];
|
WatchLaterSave, "video.watch_later.save", "Save the selected video to watch later", VIEW, ["w"];
|
||||||
WatchLaterBrowse, "view.watch_later.browse", "Browse this category's watch later, or the combined collection elsewhere", &[Context::Normal, Context::CollectionNormal], ["W"];
|
WatchLaterBrowse, "view.watch_later.browse", "Browse watch later across all categories", VIEW, ["W"];
|
||||||
|
WatchLaterBrowseCategory, "view.watch_later.category", "Browse the current category's watch later", VIEW, ["alt+w"];
|
||||||
WatchLaterFilter, "view.watch_later.filter", "Cycle watch-later filter: all, unwatched, watched", &[Context::Normal, Context::CollectionNormal], ["f"];
|
WatchLaterFilter, "view.watch_later.filter", "Cycle watch-later filter: all, unwatched, watched", &[Context::Normal, Context::CollectionNormal], ["f"];
|
||||||
CollectionRemoveSelected, "view.collection.remove.selected", "Remove the selected video from the current collection (also in its previews)", COLLECTION, ["x"];
|
CollectionRemoveSelected, "view.collection.remove.selected", "Remove the selected video from the current collection (also in its previews)", COLLECTION, ["x"];
|
||||||
CollectionRemoveBulk, "view.collection.remove.bulk", "Confirm removal of the current collection subset", COLLECTION, ["X"];
|
CollectionRemoveBulk, "view.collection.remove.bulk", "Confirm removal of the current collection subset", COLLECTION, ["X"];
|
||||||
SavedVideoSave, "video.saved.save", "Save the selected video (also in previews)", VIEW, ["v"];
|
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, Context::CollectionNormal], ["V"];
|
SavedVideoBrowse, "view.saved.browse", "Browse saved videos across all categories", VIEW, ["V"];
|
||||||
|
SavedVideoBrowseCategory, "view.saved.category", "Browse the current category's saved videos", VIEW, ["alt+v"];
|
||||||
SelectorNext, "selector.select.next", "Select the next category or channel", &[Context::SelectorNormal], ["j", "down"];
|
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"];
|
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"];
|
SelectorInputNext, "selector.input.next", "Select the next match while editing the filter", &[Context::SelectorInput], ["down", "ctrl+j"];
|
||||||
@@ -492,7 +494,11 @@ impl Bindings {
|
|||||||
(&[WatchLaterSave], "Save the selected video"),
|
(&[WatchLaterSave], "Save the selected video"),
|
||||||
(
|
(
|
||||||
&[WatchLaterBrowse],
|
&[WatchLaterBrowse],
|
||||||
"Browse category watch later; combined elsewhere",
|
"Browse watch later across all categories",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[WatchLaterBrowseCategory],
|
||||||
|
"Browse current category's watch later",
|
||||||
),
|
),
|
||||||
(&[WatchLaterFilter], "Cycle all / unwatched / watched"),
|
(&[WatchLaterFilter], "Cycle all / unwatched / watched"),
|
||||||
],
|
],
|
||||||
@@ -506,7 +512,11 @@ impl Bindings {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
&[SavedVideoBrowse],
|
&[SavedVideoBrowse],
|
||||||
"Browse category saved videos; combined elsewhere",
|
"Browse saved videos across all categories",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[SavedVideoBrowseCategory],
|
||||||
|
"Browse current category's saved videos",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1147,10 +1157,52 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collection_browse_shortcuts_are_distinct_and_view_scoped() {
|
||||||
|
let defaults = Bindings::default();
|
||||||
|
let overrides = configured(
|
||||||
|
"[keys.view.watch_later]\ncategory = ['OpTiOn+b', 'alt+b']\n[keys]\n'view.saved.category' = []\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
for &context in ALL {
|
||||||
|
for (key, modifiers, action) in [
|
||||||
|
('W', KeyModifiers::NONE, Action::WatchLaterBrowse),
|
||||||
|
('w', KeyModifiers::SHIFT, Action::WatchLaterBrowse),
|
||||||
|
('V', KeyModifiers::NONE, Action::SavedVideoBrowse),
|
||||||
|
('v', KeyModifiers::SHIFT, Action::SavedVideoBrowse),
|
||||||
|
('w', KeyModifiers::ALT, Action::WatchLaterBrowseCategory),
|
||||||
|
('v', KeyModifiers::ALT, Action::SavedVideoBrowseCategory),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
press(&defaults, context, KeyCode::Char(key), modifiers),
|
||||||
|
VIEW.contains(&context).then_some(action),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
press(&overrides, context, KeyCode::Char('b'), KeyModifiers::ALT),
|
||||||
|
VIEW.contains(&context)
|
||||||
|
.then_some(Action::WatchLaterBrowseCategory),
|
||||||
|
);
|
||||||
|
for key in ['w', 'v'] {
|
||||||
|
assert_eq!(
|
||||||
|
press(&overrides, context, KeyCode::Char(key), KeyModifiers::ALT),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for key in ['W', 'V'] {
|
||||||
|
assert_eq!(
|
||||||
|
press(&defaults, context, KeyCode::Char(key), KeyModifiers::ALT),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(overrides.label(Action::WatchLaterBrowseCategory), "Alt+b");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn collection_browsing_and_save_overrides_retain_contexts() {
|
fn collection_browsing_and_save_overrides_retain_contexts() {
|
||||||
let bindings = configured(
|
let bindings = configured(
|
||||||
"[keys.video.watch_later]\nsave = 'alt+w'\n[keys.video.saved]\nsave = 'alt+v'\n[keys.view.watch_later]\nbrowse = 'shift+x'\nfilter = 'ctrl+f'\n[keys.view.saved]\nbrowse = 'alt+b'\n[keys.view.collection.remove]\nbulk = []\n",
|
"[keys.video.watch_later]\nsave = 'alt+w'\n[keys.video.saved]\nsave = 'alt+v'\n[keys.view.watch_later]\nbrowse = 'shift+x'\ncategory = []\nfilter = 'ctrl+f'\n[keys.view.saved]\nbrowse = 'alt+b'\ncategory = []\n[keys.view.collection.remove]\nbulk = []\n",
|
||||||
).unwrap();
|
).unwrap();
|
||||||
for &context in ALL {
|
for &context in ALL {
|
||||||
for (key, modifiers, action, enabled) in [
|
for (key, modifiers, action, enabled) in [
|
||||||
@@ -1170,7 +1222,7 @@ mod tests {
|
|||||||
'x',
|
'x',
|
||||||
KeyModifiers::SHIFT,
|
KeyModifiers::SHIFT,
|
||||||
Action::WatchLaterBrowse,
|
Action::WatchLaterBrowse,
|
||||||
matches!(context, Context::Normal | Context::CollectionNormal),
|
VIEW.contains(&context),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'f',
|
'f',
|
||||||
@@ -1182,7 +1234,7 @@ mod tests {
|
|||||||
'b',
|
'b',
|
||||||
KeyModifiers::ALT,
|
KeyModifiers::ALT,
|
||||||
Action::SavedVideoBrowse,
|
Action::SavedVideoBrowse,
|
||||||
matches!(context, Context::Normal | Context::CollectionNormal),
|
VIEW.contains(&context),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+287
-16
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user