feat: filter channel category tagging

This commit is contained in:
2026-10-11 19:37:52 +02:00
parent 766fef625c
commit 6afe229406
3 changed files with 394 additions and 102 deletions
+8 -5
View File
@@ -85,11 +85,11 @@ The full-description view scrolls with ↓ / ↑, Ctrl-J / Ctrl-K, or PageDown /
### Text input
These shortcuts work in search, category/channel-filter, and new-tag fields:
These shortcuts work in search, category/channel/tag-filter, and new-tag fields:
| Key | Action |
| --- | --- |
| Enter | Submit the search, browse the selected category/channel, or stage the new tag |
| Enter | Submit the search, browse the selected category/channel, leave tag-filter mode, or stage the new tag |
| Esc | Leave insert mode or abandon the new-tag name |
| Backspace | Delete one character |
| Option/Alt-Backspace or Ctrl-W | Delete the previous whitespace-delimited word |
@@ -115,9 +115,12 @@ Press **`U` (Shift-U)** on a video to browse its creator directly, including cha
### Channel tags (`s`, or `S` for bulk editing)
- `j` / `k` or arrow keys navigate; Space toggles a tag.
- `n` prompts for a new tag name. Enter stages it checked and returns to the list.
- Enter **in the main tag list** saves the staged memberships. Esc cancels without writing.
- **Normal:** `j` / `k` or arrow keys navigate; Space toggles a tag; `i` edits the filter.
- **Insert:** type a case-insensitive substring to filter live; arrow keys or Ctrl-J / Ctrl-K navigate matches. Enter or Esc returns to normal mode without saving or clearing the filter.
- Filtering only hides rows: checked and unchecked staged memberships outside the filter are retained. With no matches, navigation and toggling do nothing. Reopening the dialog clears its filter.
- `n` prompts for a new tag name. Enter stages it checked, clears the filter, and selects the new tag; Esc abandons the name.
- Enter **in the normal-mode tag list** saves all staged memberships, including hidden tags. Esc cancels without writing. These controls apply to both single-channel and bulk tagging.
- Filter actions are configurable as `tags.edit`, `tags.input.next`, and `tags.input.previous`; text editing uses the shared `input.*` actions.
Bulk editing applies the exact checked tag set to **every saved channel in the current category**, not only channels with loaded videos. Tags shared by every target start checked; mixed memberships start unchecked. Review the checked set before saving: unchecked tags are removed from all targets. Other channels, saved registrations, empty categories, pins, and comments are preserved.
+18 -4
View File
@@ -17,6 +17,7 @@ pub enum Context {
SelectorNormal,
SelectorInput,
TagList,
TagFilter,
TagInput,
Help,
}
@@ -29,6 +30,7 @@ const ALL: &[Context] = &[
Context::SelectorNormal,
Context::SelectorInput,
Context::TagList,
Context::TagFilter,
Context::TagInput,
Context::Help,
];
@@ -36,6 +38,7 @@ const VIEW: &[Context] = &[Context::Normal, Context::Info, Context::Description]
const TEXT: &[Context] = &[
Context::SearchInput,
Context::SelectorInput,
Context::TagFilter,
Context::TagInput,
];
const NON_TEXT: &[Context] = &[
@@ -78,7 +81,7 @@ actions! {
Interrupt, "app.interrupt", "Exit immediately", ALL, ["ctrl+c"];
Quit, "app.quit", "Quit the application", &[Context::Normal, Context::Info, Context::Description, Context::Help], ["q"];
Help, "help.toggle", "Open or close keyboard help", NON_TEXT, ["?"];
InputSubmit, "input.submit", "Submit search, selector filter or new tag", TEXT, ["enter"];
InputSubmit, "input.submit", "Submit search, browse selector, leave tag filter or stage new tag", TEXT, ["enter"];
InputCancel, "input.cancel", "Leave text input without submitting", TEXT, ["escape"];
DeleteCharacter, "input.delete.character", "Delete the previous character", TEXT, ["backspace"];
DeleteWord, "input.delete.word", "Delete the previous word", TEXT, ["alt+backspace", "ctrl+w"];
@@ -112,6 +115,9 @@ actions! {
SelectorClose, "selector.close", "Close the category or channel picker", &[Context::SelectorNormal], ["escape"];
TagNext, "tags.select.next", "Select the next category tag", &[Context::TagList], ["j", "down"];
TagPrevious, "tags.select.previous", "Select the previous category tag", &[Context::TagList], ["k", "up"];
TagEdit, "tags.edit", "Edit the category tag filter", &[Context::TagList], ["i"];
TagInputNext, "tags.input.next", "Select the next tag while editing the filter", &[Context::TagFilter], ["down", "ctrl+j"];
TagInputPrevious, "tags.input.previous", "Select the previous tag while editing the filter", &[Context::TagFilter], ["up", "ctrl+k"];
TagToggle, "tags.toggle", "Toggle the selected category tag", &[Context::TagList], ["space"];
TagNew, "tags.new", "Enter a new category tag name", &[Context::TagList], ["n"];
TagSave, "tags.save", "Save category tag changes", &[Context::TagList], ["enter"];
@@ -262,7 +268,7 @@ impl Key {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Bindings {
labels: Vec<Vec<String>>,
contexts: [Vec<(Key, Action)>; 9],
contexts: [Vec<(Key, Action)>; ALL.len()],
}
impl Default for Bindings {
@@ -378,7 +384,7 @@ impl Bindings {
}
fn build(keys: Vec<Vec<Key>>) -> io::Result<Self> {
let mut contexts: [Vec<(Key, Action)>; 9] = std::array::from_fn(|_| Vec::new());
let mut contexts: [Vec<(Key, Action)>; ALL.len()] = std::array::from_fn(|_| Vec::new());
for definition in DEFINITIONS {
for &context in definition.contexts {
let entries = &mut contexts[context as usize];
@@ -483,6 +489,12 @@ impl Bindings {
"CHANNEL TAGS",
&[
(&[TagNext, TagPrevious], "Move between tags"),
(&[TagEdit], "Enter filter INSERT mode"),
(&[TagInputNext, TagInputPrevious], "Move while filtering"),
(
&[InputSubmit, InputCancel],
"Leave tag filter without saving",
),
(&[TagToggle], "Toggle a tag"),
(&[TagNew], "Enter a new tag name"),
(&[TagSave], "Save tags"),
@@ -565,7 +577,9 @@ impl Bindings {
}
if matches!(
actions,
[DescriptionNext, DescriptionPrevious] | [SelectorInputNext, SelectorInputPrevious]
[DescriptionNext, DescriptionPrevious]
| [SelectorInputNext, SelectorInputPrevious]
| [TagInputNext, TagInputPrevious]
) && matches(actions[0], &["↓", "Ctrl+j"])
&& matches(actions[1], &["↑", "Ctrl+k"])
{
+368 -93
View File
File diff suppressed because one or more lines are too long