feat: filter channel category tagging
This commit is contained in:
@@ -85,11 +85,11 @@ The full-description view scrolls with ↓ / ↑, Ctrl-J / Ctrl-K, or PageDown /
|
|||||||
|
|
||||||
### Text input
|
### 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 |
|
| 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 |
|
| Esc | Leave insert mode or abandon the new-tag name |
|
||||||
| Backspace | Delete one character |
|
| Backspace | Delete one character |
|
||||||
| Option/Alt-Backspace or Ctrl-W | Delete the previous whitespace-delimited word |
|
| 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)
|
### Channel tags (`s`, or `S` for bulk editing)
|
||||||
|
|
||||||
- `j` / `k` or arrow keys navigate; Space toggles a tag.
|
- **Normal:** `j` / `k` or arrow keys navigate; Space toggles a tag; `i` edits the filter.
|
||||||
- `n` prompts for a new tag name. Enter stages it checked and returns to the list.
|
- **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.
|
||||||
- Enter **in the main tag list** saves the staged memberships. Esc cancels without writing.
|
- 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.
|
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
@@ -17,6 +17,7 @@ pub enum Context {
|
|||||||
SelectorNormal,
|
SelectorNormal,
|
||||||
SelectorInput,
|
SelectorInput,
|
||||||
TagList,
|
TagList,
|
||||||
|
TagFilter,
|
||||||
TagInput,
|
TagInput,
|
||||||
Help,
|
Help,
|
||||||
}
|
}
|
||||||
@@ -29,6 +30,7 @@ const ALL: &[Context] = &[
|
|||||||
Context::SelectorNormal,
|
Context::SelectorNormal,
|
||||||
Context::SelectorInput,
|
Context::SelectorInput,
|
||||||
Context::TagList,
|
Context::TagList,
|
||||||
|
Context::TagFilter,
|
||||||
Context::TagInput,
|
Context::TagInput,
|
||||||
Context::Help,
|
Context::Help,
|
||||||
];
|
];
|
||||||
@@ -36,6 +38,7 @@ const VIEW: &[Context] = &[Context::Normal, Context::Info, Context::Description]
|
|||||||
const TEXT: &[Context] = &[
|
const TEXT: &[Context] = &[
|
||||||
Context::SearchInput,
|
Context::SearchInput,
|
||||||
Context::SelectorInput,
|
Context::SelectorInput,
|
||||||
|
Context::TagFilter,
|
||||||
Context::TagInput,
|
Context::TagInput,
|
||||||
];
|
];
|
||||||
const NON_TEXT: &[Context] = &[
|
const NON_TEXT: &[Context] = &[
|
||||||
@@ -78,7 +81,7 @@ actions! {
|
|||||||
Interrupt, "app.interrupt", "Exit immediately", ALL, ["ctrl+c"];
|
Interrupt, "app.interrupt", "Exit immediately", ALL, ["ctrl+c"];
|
||||||
Quit, "app.quit", "Quit the application", &[Context::Normal, Context::Info, Context::Description, Context::Help], ["q"];
|
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, ["?"];
|
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"];
|
InputCancel, "input.cancel", "Leave text input without submitting", TEXT, ["escape"];
|
||||||
DeleteCharacter, "input.delete.character", "Delete the previous character", TEXT, ["backspace"];
|
DeleteCharacter, "input.delete.character", "Delete the previous character", TEXT, ["backspace"];
|
||||||
DeleteWord, "input.delete.word", "Delete the previous word", TEXT, ["alt+backspace", "ctrl+w"];
|
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"];
|
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"];
|
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"];
|
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"];
|
TagToggle, "tags.toggle", "Toggle the selected category tag", &[Context::TagList], ["space"];
|
||||||
TagNew, "tags.new", "Enter a new category tag name", &[Context::TagList], ["n"];
|
TagNew, "tags.new", "Enter a new category tag name", &[Context::TagList], ["n"];
|
||||||
TagSave, "tags.save", "Save category tag changes", &[Context::TagList], ["enter"];
|
TagSave, "tags.save", "Save category tag changes", &[Context::TagList], ["enter"];
|
||||||
@@ -262,7 +268,7 @@ impl Key {
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Bindings {
|
pub struct Bindings {
|
||||||
labels: Vec<Vec<String>>,
|
labels: Vec<Vec<String>>,
|
||||||
contexts: [Vec<(Key, Action)>; 9],
|
contexts: [Vec<(Key, Action)>; ALL.len()],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Bindings {
|
impl Default for Bindings {
|
||||||
@@ -378,7 +384,7 @@ impl Bindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build(keys: Vec<Vec<Key>>) -> io::Result<Self> {
|
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 definition in DEFINITIONS {
|
||||||
for &context in definition.contexts {
|
for &context in definition.contexts {
|
||||||
let entries = &mut contexts[context as usize];
|
let entries = &mut contexts[context as usize];
|
||||||
@@ -483,6 +489,12 @@ impl Bindings {
|
|||||||
"CHANNEL TAGS",
|
"CHANNEL TAGS",
|
||||||
&[
|
&[
|
||||||
(&[TagNext, TagPrevious], "Move between 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"),
|
(&[TagToggle], "Toggle a tag"),
|
||||||
(&[TagNew], "Enter a new tag name"),
|
(&[TagNew], "Enter a new tag name"),
|
||||||
(&[TagSave], "Save tags"),
|
(&[TagSave], "Save tags"),
|
||||||
@@ -565,7 +577,9 @@ impl Bindings {
|
|||||||
}
|
}
|
||||||
if matches!(
|
if matches!(
|
||||||
actions,
|
actions,
|
||||||
[DescriptionNext, DescriptionPrevious] | [SelectorInputNext, SelectorInputPrevious]
|
[DescriptionNext, DescriptionPrevious]
|
||||||
|
| [SelectorInputNext, SelectorInputPrevious]
|
||||||
|
| [TagInputNext, TagInputPrevious]
|
||||||
) && matches(actions[0], &["↓", "Ctrl+j"])
|
) && matches(actions[0], &["↓", "Ctrl+j"])
|
||||||
&& matches(actions[1], &["↑", "Ctrl+k"])
|
&& matches(actions[1], &["↑", "Ctrl+k"])
|
||||||
{
|
{
|
||||||
|
|||||||
+368
-93
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user