Bulk tagging + channel-name search
Bulk tagging - Desktop bulk-mode toolbar grows three new buttons next to ✓ Watched and ○ Unwatched: ★ Favourite, 🔖 Bookmark, ⏳ Waiting. Each applies the corresponding flag to every video in the current bulk selection. - New `App::bulk_set_flag` mirrors the existing `bulk_mark_watched`: iterates selection, persists each flag via `Database::set_video_flag`, mirrors into the in-memory `flags` bundle, busts the cards-cache so the smart-folder counts refresh on the next render. - Web bulk-actions row gets the same three buttons. New `bulkFlag(flag)` fires the POSTs in parallel via Promise.all and reloads the library once they complete. - Tagging in bulk only ever sets a flag — no toggle. Removing flags in bulk is rarely what users want; the per-card buttons handle un-flag. Channel-name search - Both UIs already filter by video title + id; extend to match the channel name too. Searching "Linus" now surfaces every video from a channel called "Linus Tech Tips" without typing the title. - Description matching deliberately skipped — would require reading the .description sidecar per-video per-keystroke. Mentioned in the code comment as a future "load descriptions into the search index on rescan" pass if real users want it. - New JS helper `matchesSearch(v, chName, q)` centralises the four call sites in `currentVideos()` (continue / recent / smart-folder / default). 55 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9d414919cd
commit
c86e9b90a5
2 changed files with 78 additions and 7 deletions
47
src/app.rs
47
src/app.rs
|
|
@ -451,9 +451,14 @@ impl App {
|
|||
let mut cards = Vec::new();
|
||||
|
||||
let add_video = |cards: &mut Vec<Card>, ch_name: &str, v: &library::Video| {
|
||||
// Search matches title / id / channel name. Description matching
|
||||
// would require reading the .description sidecar per-video on every
|
||||
// keystroke — punted to a future "load descriptions into the
|
||||
// search index on rescan" pass if users ask for it.
|
||||
if !query.is_empty()
|
||||
&& !v.title.to_lowercase().contains(&query)
|
||||
&& !v.id.to_lowercase().contains(&query)
|
||||
&& !ch_name.to_lowercase().contains(&query)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -762,6 +767,32 @@ impl App {
|
|||
);
|
||||
}
|
||||
|
||||
/// Bulk-apply a single per-video flag (`"bookmark"` / `"favourite"` /
|
||||
/// `"waiting"` / `"archive"`) across the current selection. Same shape
|
||||
/// as [`bulk_mark_watched`], extended for the smart-folder flag set.
|
||||
fn bulk_set_flag(&mut self, flag: &'static str, value: bool) {
|
||||
let ids: Vec<String> = self.bulk_selected.iter().cloned().collect();
|
||||
let count = ids.len();
|
||||
for id in &ids {
|
||||
if self.db.set_video_flag(id, flag, value).is_err() { continue; }
|
||||
let set = match flag {
|
||||
"bookmark" => &mut self.flags.bookmark,
|
||||
"favourite" => &mut self.flags.favourite,
|
||||
"waiting" => &mut self.flags.waiting,
|
||||
"archive" => &mut self.flags.archive,
|
||||
_ => return,
|
||||
};
|
||||
if value { set.insert(id.clone()); } else { set.remove(id); }
|
||||
}
|
||||
self.bulk_selected.clear();
|
||||
self.cards_cache_key = None;
|
||||
self.status = format!(
|
||||
"{count} {} {}{flag}",
|
||||
if count == 1 { "video" } else { "videos" },
|
||||
if value { "→ " } else { "un" },
|
||||
);
|
||||
}
|
||||
|
||||
fn run_scheduled_check(&mut self) {
|
||||
// Snapshot each channel's URL + options first so we don't hold an
|
||||
// immutable borrow of `self.library` while calling
|
||||
|
|
@ -2623,14 +2654,26 @@ impl App {
|
|||
ui.separator();
|
||||
let n = self.bulk_selected.len();
|
||||
ui.label(format!("{n} selected"));
|
||||
if ui.button("✓ Mark watched").clicked() {
|
||||
if ui.button("✓ Watched").clicked() {
|
||||
self.bulk_mark_watched(true);
|
||||
self.bulk_mode = false;
|
||||
}
|
||||
if ui.button("○ Mark unwatched").clicked() {
|
||||
if ui.button("○ Unwatched").clicked() {
|
||||
self.bulk_mark_watched(false);
|
||||
self.bulk_mode = false;
|
||||
}
|
||||
if ui.button("★ Favourite").on_hover_text("Mark every selected video as favourite").clicked() {
|
||||
self.bulk_set_flag("favourite", true);
|
||||
self.bulk_mode = false;
|
||||
}
|
||||
if ui.button("🔖 Bookmark").on_hover_text("Mark every selected video as bookmarked").clicked() {
|
||||
self.bulk_set_flag("bookmark", true);
|
||||
self.bulk_mode = false;
|
||||
}
|
||||
if ui.button("⏳ Waiting").on_hover_text("Add every selected video to the waiting list").clicked() {
|
||||
self.bulk_set_flag("waiting", true);
|
||||
self.bulk_mode = false;
|
||||
}
|
||||
}
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue