Enhance comment viewer + make SponsorBlock configurable (3.6 + parity)
Comment viewer (web UI): the flat tree gains search (highlight + keep ancestor context), sort (threaded / top / newest / oldest), per-thread collapse/expand with collapse-all/expand-all, an OP badge for the uploader, and a "new since last visit" highlight + count (localStorage per-video timestamp). API now returns each comment's `timestamp` and `author_is_uploader`. SponsorBlock: was a hardcoded `--sponsorblock-mark all`. Now a real setting (off / mark / remove) with a per-channel override, threaded through the full five touchpoints — config `[backup].sponsorblock_mode` (default "mark", preserving prior behavior), DownloadOptions override, a downloader resolver (apply_sponsorblock), and both UIs (egui Settings + channel dialog; web Settings modal + channel dialog + SettingsPayload). Integration tests extended: settings round-trip asserts the sponsorblock default + persistence; channel-options round-trip asserts the override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6f174f703b
commit
3031b5b0e5
7 changed files with 219 additions and 20 deletions
64
src/app.rs
64
src/app.rs
|
|
@ -240,6 +240,9 @@ struct ChannelOptionsForm {
|
|||
// Per-channel convert override: 0=Default(global), 1=Off, 2=remux-mp4,
|
||||
// 3=h264-mp4, 4=audio. Maps to Option<String> on save.
|
||||
convert_idx: usize,
|
||||
// Per-channel SponsorBlock override: 0=Default(global), 1=Off, 2=mark,
|
||||
// 3=remove. Maps to Option<String> on save.
|
||||
sponsorblock_idx: usize,
|
||||
extra_args: String, // one per line
|
||||
}
|
||||
|
||||
|
|
@ -273,6 +276,25 @@ fn idx_to_convert_mode(i: usize) -> Option<String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Per-channel SponsorBlock override ⇄ combo index.
|
||||
/// 0=Default (None, defer to global), 1=Off, 2=mark, 3=remove.
|
||||
fn sponsorblock_to_idx(v: &Option<String>) -> usize {
|
||||
match v.as_deref() {
|
||||
Some("off") => 1,
|
||||
Some("mark") => 2,
|
||||
Some("remove") => 3,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
fn idx_to_sponsorblock(i: usize) -> Option<String> {
|
||||
match i {
|
||||
1 => Some("off".into()),
|
||||
2 => Some("mark".into()),
|
||||
3 => Some("remove".into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(cc: &eframe::CreationContext<'_>, tray: Option<crate::tray::TrayHandle>) -> Self {
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
|
|
@ -348,6 +370,7 @@ impl App {
|
|||
);
|
||||
downloader.subtitle_defaults = config.subtitles.clone();
|
||||
downloader.youtube_player_clients = config.backup.youtube_player_clients.clone();
|
||||
downloader.sponsorblock_mode = config.backup.sponsorblock_mode.clone();
|
||||
downloader.convert_defaults = config.convert.clone();
|
||||
let config_bind = config.web.bind.clone();
|
||||
let password_set = db.get_setting("password_hash").ok().flatten().is_some();
|
||||
|
|
@ -512,6 +535,7 @@ impl App {
|
|||
subtitle_format: opts.subtitle_format.clone().unwrap_or_default(),
|
||||
youtube_player_clients: opts.youtube_player_clients.clone().unwrap_or_default(),
|
||||
convert_idx: convert_mode_to_idx(&opts.convert_mode),
|
||||
sponsorblock_idx: sponsorblock_to_idx(&opts.sponsorblock_mode),
|
||||
extra_args: opts.extra_args.join("\n"),
|
||||
}
|
||||
}
|
||||
|
|
@ -550,6 +574,7 @@ impl App {
|
|||
subtitle_format: trim_opt(&f.subtitle_format),
|
||||
youtube_player_clients: trim_opt(&f.youtube_player_clients),
|
||||
convert_mode: idx_to_convert_mode(f.convert_idx),
|
||||
sponsorblock_mode: idx_to_sponsorblock(f.sponsorblock_idx),
|
||||
extra_args: f.extra_args.lines()
|
||||
.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(),
|
||||
}
|
||||
|
|
@ -2092,6 +2117,25 @@ impl App {
|
|||
global Format-conversion setting. CRF / preset / audio-format come \
|
||||
from the global config.");
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("SponsorBlock");
|
||||
let label = match form.sponsorblock_idx {
|
||||
1 => "Off", 2 => "Mark chapters", 3 => "Remove segments",
|
||||
_ => "Default (global)",
|
||||
};
|
||||
egui::ComboBox::from_id_salt("ch_sponsorblock")
|
||||
.selected_text(label)
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut form.sponsorblock_idx, 0, "Default (global)");
|
||||
ui.selectable_value(&mut form.sponsorblock_idx, 1, "Off");
|
||||
ui.selectable_value(&mut form.sponsorblock_idx, 2, "Mark chapters");
|
||||
ui.selectable_value(&mut form.sponsorblock_idx, 3, "Remove segments");
|
||||
});
|
||||
}).response.on_hover_text(
|
||||
"SponsorBlock handling for this channel. Default = use the global \
|
||||
setting. Mark = chapter-mark segments; Remove = cut them from the file.");
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.label("Extra yt-dlp args (one per line):");
|
||||
ui.add(
|
||||
|
|
@ -2579,6 +2623,25 @@ impl App {
|
|||
Per-channel overrides live in each channel's options.");
|
||||
ui.end_row();
|
||||
|
||||
ui.label("SponsorBlock:");
|
||||
egui::ComboBox::from_id_salt("global_sponsorblock")
|
||||
.selected_text(match self.config.backup.sponsorblock_mode.as_str() {
|
||||
"off" => "Off",
|
||||
"remove" => "Remove segments",
|
||||
_ => "Mark chapters",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.config.backup.sponsorblock_mode, "off".to_string(), "Off");
|
||||
ui.selectable_value(&mut self.config.backup.sponsorblock_mode, "mark".to_string(), "Mark chapters");
|
||||
ui.selectable_value(&mut self.config.backup.sponsorblock_mode, "remove".to_string(), "Remove segments");
|
||||
})
|
||||
.response.on_hover_text(
|
||||
"SponsorBlock uses the community database for sponsor / intro / \
|
||||
self-promo segments. Mark = add skippable chapter markers; \
|
||||
Remove = cut the segments from the saved file. Per-channel \
|
||||
overrides live in each channel's options.");
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Web UI port:");
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.config.web.port)
|
||||
|
|
@ -2987,6 +3050,7 @@ impl App {
|
|||
self.downloader.use_pot_provider = self.config.backup.use_pot_provider;
|
||||
self.downloader.subtitle_defaults = self.config.subtitles.clone();
|
||||
self.downloader.youtube_player_clients = self.config.backup.youtube_player_clients.clone();
|
||||
self.downloader.sponsorblock_mode = self.config.backup.sponsorblock_mode.clone();
|
||||
self.downloader.convert_defaults = self.config.convert.clone();
|
||||
if dir_changed {
|
||||
self.channels_root = new_dir.clone();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue