diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 9fa9f5b..d5e334e 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use chrono::Local; use serde::{Deserialize, Serialize}; -use crate::recorder_manager::ClipRangeParams; +use crate::{danmu2ass::Danmu2AssOptions, recorder_manager::ClipRangeParams}; #[derive(Deserialize, Serialize, Clone)] pub struct Config { @@ -37,6 +37,8 @@ pub struct Config { pub whisper_language: String, #[serde(default = "default_webhook_url")] pub webhook_url: String, + #[serde(default = "default_danmu_ass_options")] + pub danmu_ass_options: Danmu2AssOptions, } #[derive(Deserialize, Serialize, Clone)] @@ -45,6 +47,10 @@ pub struct AutoGenerateConfig { pub encode_danmu: bool, } +fn default_danmu_ass_options() -> Danmu2AssOptions { + Danmu2AssOptions::default() +} + fn default_auto_subtitle() -> bool { false } @@ -130,6 +136,7 @@ impl Config { config_path: config_path.to_str().unwrap().into(), whisper_language: default_whisper_language(), webhook_url: default_webhook_url(), + danmu_ass_options: default_danmu_ass_options(), }; config.save(); @@ -162,6 +169,12 @@ impl Config { self.save(); } + #[allow(dead_code)] + pub fn set_danmu_ass_options(&mut self, options: Danmu2AssOptions) { + self.danmu_ass_options = options; + self.save(); + } + pub fn generate_clip_name(&self, params: &ClipRangeParams) -> PathBuf { // get format config // filter special characters from title to make sure file name is valid diff --git a/src-tauri/src/danmu2ass.rs b/src-tauri/src/danmu2ass.rs index 23ed2f6..ec83220 100644 --- a/src-tauri/src/danmu2ass.rs +++ b/src-tauri/src/danmu2ass.rs @@ -1,4 +1,5 @@ use recorder::danmu::DanmuEntry; +use serde::{Deserialize, Serialize}; use std::collections::VecDeque; // code reference: https://github.com/tiansh/us-danmaku/blob/master/bilibili/bilibili_ASS_Danmaku_Downloader.user.js @@ -30,9 +31,32 @@ const BOTTOM_RESERVED: f64 = 50.0; const R2L_TIME: f64 = 8.0; const MAX_DELAY: f64 = 6.0; -pub fn danmu_to_ass(danmus: Vec) -> String { +#[derive(Deserialize, Serialize, Clone)] +pub struct Danmu2AssOptions { + pub font_size: f64, + pub opacity: f64, // 透明度,范围 0.0-1.0,0.0为完全透明,1.0为完全不透明 +} + +impl Default for Danmu2AssOptions { + fn default() -> Self { + Self { + font_size: 36.0, + opacity: 0.8, // 默认80%透明度 + } + } +} + +pub fn danmu_to_ass(danmus: Vec, options: Danmu2AssOptions) -> String { + let font_size = options.font_size; // Default font size + let opacity = options.opacity; // 透明度参数 + + // 将透明度转换为十六进制Alpha值 (0.0-1.0 -> 0x00-0xFF) + let alpha = ((1.0 - opacity) * 255.0) as u8; + let alpha_hex = format!("{:02X}", alpha); + // ASS header - let header = r"[Script Info] + let header = format!( + r"[Script Info] Title: Bilibili Danmaku ScriptType: v4.00+ Collisions: Normal @@ -42,14 +66,15 @@ Timer: 10.0000 [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding -Style: Default,微软雅黑,36,&H7fFFFFFF,&H7fFFFFFF,&H7f000000,&H7f000000,0,0,0,0,100,100,0,0,1,1,0,2,20,20,2,0 +Style: Default,微软雅黑,{},&H{}FFFFFF,&H{}FFFFFF,&H{}000000,&H{}000000,0,0,0,0,100,100,0,0,1,1,0,2,20,20,2,0 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text -"; +", + font_size, alpha_hex, alpha_hex, alpha_hex, alpha_hex + ); let mut normal = normal_danmaku(); - let font_size = 36.0; // Default font size // Convert danmus to ASS events let events = danmus diff --git a/src-tauri/src/handlers/config.rs b/src-tauri/src/handlers/config.rs index 2814043..5245fb9 100644 --- a/src-tauri/src/handlers/config.rs +++ b/src-tauri/src/handlers/config.rs @@ -1,4 +1,5 @@ use crate::config::Config; +use crate::danmu2ass::Danmu2AssOptions; use crate::state::State; use crate::state_type; @@ -280,3 +281,18 @@ pub async fn update_webhook_url(state: state_type!(), webhook_url: String) -> Re state.config.write().await.save(); Ok(()) } + +#[cfg_attr(feature = "gui", tauri::command)] +pub async fn update_danmu_ass_options( + state: state_type!(), + font_size: f64, + opacity: f64, +) -> Result<(), ()> { + log::info!("Updating danmu ass options"); + state + .config + .write() + .await + .set_danmu_ass_options(Danmu2AssOptions { font_size, opacity }); + Ok(()) +} diff --git a/src-tauri/src/handlers/recorder.rs b/src-tauri/src/handlers/recorder.rs index 8e7aeb2..127cb97 100644 --- a/src-tauri/src/handlers/recorder.rs +++ b/src-tauri/src/handlers/recorder.rs @@ -333,7 +333,10 @@ pub async fn export_danmu( } if options.ass { - Ok(danmu2ass::danmu_to_ass(danmus)) + Ok(danmu2ass::danmu_to_ass( + danmus, + danmu2ass::Danmu2AssOptions::default(), + )) } else { // map and join entries Ok(danmus diff --git a/src-tauri/src/http_server/api_server.rs b/src-tauri/src/http_server/api_server.rs index 13f2a72..6466342 100644 --- a/src-tauri/src/http_server/api_server.rs +++ b/src-tauri/src/http_server/api_server.rs @@ -14,10 +14,11 @@ use crate::{ add_account, get_account_count, get_accounts, get_qr, get_qr_status, remove_account, }, config::{ - get_config, update_auto_generate, update_clip_name_format, update_notify, - update_openai_api_endpoint, update_openai_api_key, update_status_check_interval, - update_subtitle_generator_type, update_subtitle_setting, update_webhook_url, - update_whisper_language, update_whisper_model, update_whisper_prompt, + get_config, update_auto_generate, update_clip_name_format, update_danmu_ass_options, + update_notify, update_openai_api_endpoint, update_openai_api_key, + update_status_check_interval, update_subtitle_generator_type, update_subtitle_setting, + update_webhook_url, update_whisper_language, update_whisper_model, + update_whisper_prompt, }, message::{delete_message, get_messages, read_message}, recorder::{ @@ -1050,6 +1051,21 @@ async fn handler_get_archives_by_parent_id( Ok(Json(ApiResponse::success(archives))) } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct UpdateDanmuAssOptionsRequest { + font_size: f64, + opacity: f64, +} + +async fn handler_update_danmu_ass_options( + state: axum::extract::State, + Json(param): Json, +) -> Result>, ApiError> { + update_danmu_ass_options(state.0, param.font_size, param.opacity).await; + Ok(Json(ApiResponse::success(()))) +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct BatchImportExternalVideosRequest { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 59a9c32..0591023 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -430,6 +430,7 @@ fn setup_invoke_handlers(builder: tauri::Builder) -> tauri::Builder< crate::handlers::config::update_status_check_interval, crate::handlers::config::update_whisper_language, crate::handlers::config::update_webhook_url, + crate::handlers::config::update_danmu_ass_options, crate::handlers::message::get_messages, crate::handlers::message::read_message, crate::handlers::message::delete_message, diff --git a/src-tauri/src/recorder_manager.rs b/src-tauri/src/recorder_manager.rs index fd26aa6..8c4be56 100644 --- a/src-tauri/src/recorder_manager.rs +++ b/src-tauri/src/recorder_manager.rs @@ -888,7 +888,8 @@ impl RecorderManager { return Ok(clip_file); } - let ass_content = danmu2ass::danmu_to_ass(danmus); + let ass_content = + danmu2ass::danmu_to_ass(danmus, self.config.read().await.danmu_ass_options.clone()); // dump ass_content into a temp file let ass_file_path = clip_file.with_extension("ass"); if let Err(e) = write(&ass_file_path, ass_content).await { @@ -928,7 +929,8 @@ impl RecorderManager { for d in &mut danmus { d.ts -= first_segment_timestamp_milis; } - let ass_content = danmu2ass::danmu_to_ass(danmus); + let ass_content = + danmu2ass::danmu_to_ass(danmus, self.config.read().await.danmu_ass_options.clone()); let work_dir = CachePath::new( self.config.read().await.cache.clone().into(), platform, diff --git a/src/lib/interface.ts b/src/lib/interface.ts index f8d0c9a..85def61 100644 --- a/src/lib/interface.ts +++ b/src/lib/interface.ts @@ -150,6 +150,12 @@ export interface Config { status_check_interval: number; whisper_language: string; webhook_url: string; + danmu_ass_options: Danmu2AssOptions; +} + +export interface Danmu2AssOptions { + font_size: number; + opacity: number; } export interface AutoGenerateConfig { diff --git a/src/page/Setting.svelte b/src/page/Setting.svelte index 1dc6c70..3af0421 100644 --- a/src/page/Setting.svelte +++ b/src/page/Setting.svelte @@ -38,6 +38,10 @@ status_check_interval: 30, // 默认30秒 whisper_language: "", webhook_url: "", + danmu_ass_options: { + font_size: 36, + opacity: 0.8, + }, }; let showModal = false; @@ -148,6 +152,13 @@ }); } + async function update_danmu_ass_options() { + await invoke("update_danmu_ass_options", { + fontSize: setting_model.danmu_ass_options.font_size, + opacity: setting_model.danmu_ass_options.opacity, + }); + } + onMount(async () => { await get_config(); }); @@ -720,6 +731,73 @@ + +
+

+ + 弹幕压制样式 +

+
+ +
+
+
+

+ 字体大小 +

+

+ 设置弹幕字体大小 +

+
+
+ +
+
+
+ +
+
+
+

+ 不透明度 +

+

+ 设置弹幕不透明度,范围 + 0.0-1.0,0.0为完全透明,1.0为完全不透明 +

+
+
+ +
+
+
+
+
+