diff --git a/broadcast-client/src-tauri/Cargo.lock b/broadcast-client/src-tauri/Cargo.lock index 09b0773..ddcc8a7 100644 --- a/broadcast-client/src-tauri/Cargo.lock +++ b/broadcast-client/src-tauri/Cargo.lock @@ -290,6 +290,7 @@ dependencies = [ name = "broadcast-client" version = "0.1.0" dependencies = [ + "chrono", "serde", "serde_json", "tauri", @@ -463,8 +464,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] diff --git a/broadcast-client/src-tauri/Cargo.toml b/broadcast-client/src-tauri/Cargo.toml index 924aa60..7c7fb9f 100644 --- a/broadcast-client/src-tauri/Cargo.toml +++ b/broadcast-client/src-tauri/Cargo.toml @@ -18,3 +18,4 @@ tauri-plugin-opener = "2" tauri-plugin-store = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +chrono = { version = "0.4", features = ["clock"] } diff --git a/broadcast-client/src-tauri/icons/128x128.png b/broadcast-client/src-tauri/icons/128x128.png index d3f4590..bca6043 100644 Binary files a/broadcast-client/src-tauri/icons/128x128.png and b/broadcast-client/src-tauri/icons/128x128.png differ diff --git a/broadcast-client/src-tauri/icons/128x128@2x.png b/broadcast-client/src-tauri/icons/128x128@2x.png index 2c18d1b..885e15c 100644 Binary files a/broadcast-client/src-tauri/icons/128x128@2x.png and b/broadcast-client/src-tauri/icons/128x128@2x.png differ diff --git a/broadcast-client/src-tauri/icons/32x32.png b/broadcast-client/src-tauri/icons/32x32.png index d282b62..f1680a5 100644 Binary files a/broadcast-client/src-tauri/icons/32x32.png and b/broadcast-client/src-tauri/icons/32x32.png differ diff --git a/broadcast-client/src-tauri/icons/64x64.png b/broadcast-client/src-tauri/icons/64x64.png index 2f63f14..09a0f26 100644 Binary files a/broadcast-client/src-tauri/icons/64x64.png and b/broadcast-client/src-tauri/icons/64x64.png differ diff --git a/broadcast-client/src-tauri/icons/Square107x107Logo.png b/broadcast-client/src-tauri/icons/Square107x107Logo.png index ab121b5..fa672cd 100644 Binary files a/broadcast-client/src-tauri/icons/Square107x107Logo.png and b/broadcast-client/src-tauri/icons/Square107x107Logo.png differ diff --git a/broadcast-client/src-tauri/icons/Square142x142Logo.png b/broadcast-client/src-tauri/icons/Square142x142Logo.png index 60573c1..dc60106 100644 Binary files a/broadcast-client/src-tauri/icons/Square142x142Logo.png and b/broadcast-client/src-tauri/icons/Square142x142Logo.png differ diff --git a/broadcast-client/src-tauri/icons/Square150x150Logo.png b/broadcast-client/src-tauri/icons/Square150x150Logo.png index fac629c..f105b94 100644 Binary files a/broadcast-client/src-tauri/icons/Square150x150Logo.png and b/broadcast-client/src-tauri/icons/Square150x150Logo.png differ diff --git a/broadcast-client/src-tauri/icons/Square284x284Logo.png b/broadcast-client/src-tauri/icons/Square284x284Logo.png index 5b5c7b4..acb19fb 100644 Binary files a/broadcast-client/src-tauri/icons/Square284x284Logo.png and b/broadcast-client/src-tauri/icons/Square284x284Logo.png differ diff --git a/broadcast-client/src-tauri/icons/Square30x30Logo.png b/broadcast-client/src-tauri/icons/Square30x30Logo.png index 055a148..b8e2077 100644 Binary files a/broadcast-client/src-tauri/icons/Square30x30Logo.png and b/broadcast-client/src-tauri/icons/Square30x30Logo.png differ diff --git a/broadcast-client/src-tauri/icons/Square310x310Logo.png b/broadcast-client/src-tauri/icons/Square310x310Logo.png index 98ad237..c0ab349 100644 Binary files a/broadcast-client/src-tauri/icons/Square310x310Logo.png and b/broadcast-client/src-tauri/icons/Square310x310Logo.png differ diff --git a/broadcast-client/src-tauri/icons/Square44x44Logo.png b/broadcast-client/src-tauri/icons/Square44x44Logo.png index b859795..735d939 100644 Binary files a/broadcast-client/src-tauri/icons/Square44x44Logo.png and b/broadcast-client/src-tauri/icons/Square44x44Logo.png differ diff --git a/broadcast-client/src-tauri/icons/Square71x71Logo.png b/broadcast-client/src-tauri/icons/Square71x71Logo.png index e3fecd6..792bbc5 100644 Binary files a/broadcast-client/src-tauri/icons/Square71x71Logo.png and b/broadcast-client/src-tauri/icons/Square71x71Logo.png differ diff --git a/broadcast-client/src-tauri/icons/Square89x89Logo.png b/broadcast-client/src-tauri/icons/Square89x89Logo.png index 0a5fc9d..01610fb 100644 Binary files a/broadcast-client/src-tauri/icons/Square89x89Logo.png and b/broadcast-client/src-tauri/icons/Square89x89Logo.png differ diff --git a/broadcast-client/src-tauri/icons/StoreLogo.png b/broadcast-client/src-tauri/icons/StoreLogo.png index 86dd29f..295207c 100644 Binary files a/broadcast-client/src-tauri/icons/StoreLogo.png and b/broadcast-client/src-tauri/icons/StoreLogo.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png index 330bc79..a37af08 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png and b/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png index 836b019..2924daa 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png and b/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png index d06a0ca..008adb8 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png and b/broadcast-client/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png index ccab000..677cf96 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png and b/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png index 11d3c68..9601429 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png and b/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png index 1fc18d4..160773c 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png and b/broadcast-client/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png index 53a959b..2c2db9b 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png and b/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png index acd443a..eafc01a 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png and b/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png index c18292d..13a30f1 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png and b/broadcast-client/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png index 3d7a10e..d943bbd 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png and b/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png index c02b3b9..e5a5768 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png and b/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png index b8e4789..0438088 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png and b/broadcast-client/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png index 989a410..a6d4c3c 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png and b/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png index e6422f2..4845de6 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png and b/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png index 3c8e174..440465f 100644 Binary files a/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png and b/broadcast-client/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/broadcast-client/src-tauri/icons/bc_icon.png b/broadcast-client/src-tauri/icons/bc_icon.png new file mode 100644 index 0000000..8c98fcd Binary files /dev/null and b/broadcast-client/src-tauri/icons/bc_icon.png differ diff --git a/broadcast-client/src-tauri/icons/icon.icns b/broadcast-client/src-tauri/icons/icon.icns index a491647..27028f1 100644 Binary files a/broadcast-client/src-tauri/icons/icon.icns and b/broadcast-client/src-tauri/icons/icon.icns differ diff --git a/broadcast-client/src-tauri/icons/icon.ico b/broadcast-client/src-tauri/icons/icon.ico index 77a8b03..f999d5a 100644 Binary files a/broadcast-client/src-tauri/icons/icon.ico and b/broadcast-client/src-tauri/icons/icon.ico differ diff --git a/broadcast-client/src-tauri/icons/icon.png b/broadcast-client/src-tauri/icons/icon.png index c84d9fd..c075d42 100644 Binary files a/broadcast-client/src-tauri/icons/icon.png and b/broadcast-client/src-tauri/icons/icon.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@1x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@1x.png index e476617..38fa847 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@1x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@2x-1.png index 2d1834e..2d05dd1 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@2x-1.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@2x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@2x.png index 2d1834e..2d05dd1 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@2x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@3x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@3x.png index 195fc9a..7269222 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@3x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@1x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@1x.png index 6b854c2..98a26d5 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@1x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@2x-1.png index 7ae1af0..ffbab0d 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@2x-1.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@2x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@2x.png index 7ae1af0..ffbab0d 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@2x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@3x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@3x.png index c2bee10..75a9abd 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@3x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@1x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@1x.png index 2d1834e..2d05dd1 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@1x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@2x-1.png index 33f2fa1..6ddc0d2 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@2x-1.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@2x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@2x.png index 33f2fa1..6ddc0d2 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@2x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@3x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@3x.png index 8d18124..a7daec1 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@3x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-512@2x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-512@2x.png index f6fe6d4..8e3a15b 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-512@2x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-60x60@2x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-60x60@2x.png index 8d18124..a7daec1 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-60x60@2x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-60x60@3x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-60x60@3x.png index dfb986d..83f565a 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-60x60@3x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-76x76@1x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-76x76@1x.png index 48b18f3..3bf74c8 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-76x76@1x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-76x76@2x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-76x76@2x.png index 23d90e8..982bdf1 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-76x76@2x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/broadcast-client/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/broadcast-client/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png index f3b48e5..d8f1fee 100644 Binary files a/broadcast-client/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png and b/broadcast-client/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/broadcast-client/src-tauri/src/lib.rs b/broadcast-client/src-tauri/src/lib.rs index 2cc0049..17cdb35 100644 --- a/broadcast-client/src-tauri/src/lib.rs +++ b/broadcast-client/src-tauri/src/lib.rs @@ -1,8 +1,8 @@ use std::{ - fs::{create_dir_all, OpenOptions}, + fs::{create_dir_all, read_dir, remove_file, OpenOptions}, io::Read, - net::TcpListener, io::Write, + net::TcpListener, path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, @@ -12,17 +12,22 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; +use chrono::Local; use serde::{Deserialize, Serialize}; use tauri::{Emitter, Manager}; const SOCKET_PORT: u16 = 9501; const SOCKET_STATUS_EVENT: &str = "socket-status"; const SOCKET_CALL_EVENT: &str = "socket-call-message"; -const LOG_FILE_NAME: &str = "socket-service.log"; +const LOG_FILE_PREFIX: &str = "socket-service-"; +const LOG_FILE_EXT: &str = ".log"; +const LOG_FILE_MAX_BYTES: u64 = 5 * 1024 * 1024; +const LOG_RETENTION_DAYS: u64 = 7; #[derive(Default)] struct SocketServiceState { runtime: Mutex>, + log_file: Mutex>, } struct SocketServiceRuntime { @@ -86,6 +91,11 @@ pub fn run() { "open_sync_config" => { let _ = ensure_config_window(app); } + "minimize_main" => { + if let Some(main_window) = app.get_webview_window("main") { + let _ = main_window.minimize(); + } + } "quit_app" => app.exit(0), _ => {} }) @@ -120,8 +130,16 @@ fn show_context_menu(window: tauri::Window) -> Result<(), String> { None::<&str>, ) .map_err(|error| format!("创建菜单项失败: {error}"))?; + let minimize_item = MenuItem::with_id( + window.app_handle(), + "minimize_main", + "最小化", + true, + None::<&str>, + ) + .map_err(|error| format!("创建菜单项失败: {error}"))?; - let menu = Menu::with_items(window.app_handle(), &[&config_item, &quit_item]) + let menu = Menu::with_items(window.app_handle(), &[&config_item, &minimize_item, &quit_item]) .map_err(|error| format!("创建菜单失败: {error}"))?; window @@ -362,13 +380,22 @@ fn try_dispatch_one_message(app: &tauri::AppHandle, text: &str) -> bool { } fn append_socket_log>(app: &tauri::AppHandle, level: &str, message: S) { - let Ok(log_file_path) = resolve_log_file_path(app) else { + let Ok(log_dir) = app.path().app_config_dir() else { return; }; - if let Some(parent) = log_file_path.parent() { - let _ = create_dir_all(parent); - } + let _ = create_dir_all(&log_dir); + cleanup_expired_logs(&log_dir); + + let now_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis()) + .unwrap_or(0); + + let line = format!("[{}][{}] {}\n", now_ms, level, message.as_ref()); + let Ok(log_file_path) = resolve_log_file_path(app, line.len() as u64, &log_dir) else { + return; + }; let Ok(mut file) = OpenOptions::new() .create(true) @@ -378,18 +405,85 @@ fn append_socket_log>(app: &tauri::AppHandle, level: &str, message return; }; - let now_ms = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis()) - .unwrap_or(0); - - let line = format!("[{}][{}] {}\n", now_ms, level, message.as_ref()); let _ = file.write_all(line.as_bytes()); } -fn resolve_log_file_path(app: &tauri::AppHandle) -> tauri::Result { - let log_dir = app.path().app_log_dir()?; - Ok(log_dir.join(LOG_FILE_NAME)) +fn resolve_log_file_path( + app: &tauri::AppHandle, + incoming_bytes: u64, + log_dir: &PathBuf, +) -> tauri::Result { + let state = app.state::(); + let mut guard = state + .log_file + .lock() + .map_err(|_| tauri::Error::FailedToReceiveMessage)?; + + if let Some(current) = guard.as_ref() { + let keep_current = std::fs::metadata(current) + .map(|meta| meta.len().saturating_add(incoming_bytes) <= LOG_FILE_MAX_BYTES) + .unwrap_or(false); + if keep_current { + return Ok(current.clone()); + } + } + + let next = create_next_log_file_path(log_dir); + *guard = Some(next.clone()); + Ok(next) +} + +fn create_next_log_file_path(log_dir: &PathBuf) -> PathBuf { + let base = format!( + "{}{}{}", + LOG_FILE_PREFIX, + Local::now().format("%Y%m%d-%H%M%S-%3f"), + LOG_FILE_EXT + ); + let mut path = log_dir.join(&base); + let mut index = 1; + + while path.exists() { + let candidate = format!( + "{}{}-{}{}", + LOG_FILE_PREFIX, + Local::now().format("%Y%m%d-%H%M%S-%3f"), + index, + LOG_FILE_EXT + ); + path = log_dir.join(candidate); + index += 1; + } + + path +} + +fn cleanup_expired_logs(log_dir: &PathBuf) { + let Ok(entries) = read_dir(log_dir) else { + return; + }; + let expire_before = SystemTime::now() + .checked_sub(Duration::from_secs(LOG_RETENTION_DAYS * 24 * 60 * 60)) + .unwrap_or(SystemTime::UNIX_EPOCH); + + for entry in entries.flatten() { + let path = entry.path(); + let Some(file_name) = path.file_name().and_then(|s| s.to_str()) else { + continue; + }; + if !file_name.starts_with(LOG_FILE_PREFIX) || !file_name.ends_with(LOG_FILE_EXT) { + continue; + } + + let should_delete = entry + .metadata() + .and_then(|meta| meta.modified()) + .map(|modified| modified < expire_before) + .unwrap_or(false); + if should_delete { + let _ = remove_file(path); + } + } } fn truncate_for_log(source: &str, max_chars: usize) -> String { diff --git a/broadcast-client/src/components/RulerSegment.vue b/broadcast-client/src/components/RulerSegment.vue index 2d3390c..8f59b7d 100644 --- a/broadcast-client/src/components/RulerSegment.vue +++ b/broadcast-client/src/components/RulerSegment.vue @@ -24,14 +24,17 @@ /> + diff --git a/broadcast-client/src/components/SubtitleAreasLayer.vue b/broadcast-client/src/components/SubtitleAreasLayer.vue new file mode 100644 index 0000000..1830ad7 --- /dev/null +++ b/broadcast-client/src/components/SubtitleAreasLayer.vue @@ -0,0 +1,50 @@ + + + diff --git a/broadcast-client/src/components/WindowAreasLayer.vue b/broadcast-client/src/components/WindowAreasLayer.vue index b38ee90..1f57fd5 100644 --- a/broadcast-client/src/components/WindowAreasLayer.vue +++ b/broadcast-client/src/components/WindowAreasLayer.vue @@ -19,50 +19,115 @@ transform: `translateX(${-slice.clipOffset}px)`, }" > -
+
- {{ slice.area.windowNumber }} - -
-
-
- {{ slice.area.staticText }} -
-
- {{ slice.area.dynamicText }} -
+ {{ nowTime }} +
+
diff --git a/broadcast-client/src/models/config.ts b/broadcast-client/src/models/config.ts index b9999f4..c3024f1 100644 --- a/broadcast-client/src/models/config.ts +++ b/broadcast-client/src/models/config.ts @@ -14,16 +14,24 @@ export interface TextStyleConfig { fontWeight: number; } +export interface CircleStyleConfig { + size: number; + borderWidth: number; + borderRadius: number; +} + // 子div(窗口区域)配置模型。 export interface ChildWindowAreaConfig { id: string; windowId: number; + isClockWindow: boolean; width: number; height: number; x: number; y: number; windowNumber: string; windowNumberCircle: boolean; + windowNumberCircleStyle: CircleStyleConfig; windowNumberStyle: TextStyleConfig; staticText: string; staticTextStyle: TextStyleConfig; @@ -31,6 +39,18 @@ export interface ChildWindowAreaConfig { dynamicTextStyle: TextStyleConfig; } +// 滚动字幕区域配置。 +export interface SubtitleAreaConfig { + id: string; + width: number; + height: number; + x: number; + y: number; + text: string; + textStyle: TextStyleConfig; + speed: number; +} + // 广播渲染配置模型。 export interface BroadcastConfig { totalWidth: number; @@ -38,6 +58,7 @@ export interface BroadcastConfig { showRuler: boolean; segments: SegmentConfigItem[]; windowAreas: ChildWindowAreaConfig[]; + subtitleAreas: SubtitleAreaConfig[]; } // 应用启动时使用的默认配置。 @@ -47,4 +68,5 @@ export const DEFAULT_BROADCAST_CONFIG: BroadcastConfig = { showRuler: true, segments: [], windowAreas: [], + subtitleAreas: [], }; diff --git a/broadcast-client/src/models/ruler.ts b/broadcast-client/src/models/ruler.ts index 7facd02..a9b50c9 100644 --- a/broadcast-client/src/models/ruler.ts +++ b/broadcast-client/src/models/ruler.ts @@ -4,10 +4,12 @@ export const DEFAULT_TOTAL_WIDTH = 800; export const DEFAULT_SEGMENT_HEIGHT = 64; // 小刻度间隔(单位 px)。 export const MINOR_TICK_GAP = 10; +// 中刻度间隔(单位 px)。 +export const MID_TICK_GAP = 50; // 大刻度间隔(单位 px)。 export const MAJOR_TICK_GAP = 100; -export type TickType = "minor" | "major"; +export type TickType = "minor" | "mid" | "major"; export interface Tick { x: number; diff --git a/broadcast-client/src/services/configStore.ts b/broadcast-client/src/services/configStore.ts index 59eb389..85699c8 100644 --- a/broadcast-client/src/services/configStore.ts +++ b/broadcast-client/src/services/configStore.ts @@ -1,6 +1,12 @@ import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event"; import { load, type Store } from "@tauri-apps/plugin-store"; -import type { BroadcastConfig, ChildWindowAreaConfig, TextStyleConfig } from "../models/config"; +import type { + BroadcastConfig, + ChildWindowAreaConfig, + CircleStyleConfig, + SubtitleAreaConfig, + TextStyleConfig, +} from "../models/config"; import { DEFAULT_BROADCAST_CONFIG } from "../models/config"; import { normalizeSegmentConfigItem, @@ -41,6 +47,28 @@ function normalizeTextStyle(raw: unknown, fallback: TextStyleConfig): TextStyleC }; } +function normalizeCircleStyle(raw: unknown, fallback: CircleStyleConfig): CircleStyleConfig { + const source = (raw ?? {}) as Partial; + return { + size: + typeof source.size === "number" && Number.isFinite(source.size) && source.size > 0 + ? Math.floor(source.size) + : fallback.size, + borderWidth: + typeof source.borderWidth === "number" && + Number.isFinite(source.borderWidth) && + source.borderWidth > 0 + ? Math.floor(source.borderWidth) + : fallback.borderWidth, + borderRadius: + typeof source.borderRadius === "number" && + Number.isFinite(source.borderRadius) && + source.borderRadius >= 0 + ? Math.floor(source.borderRadius) + : fallback.borderRadius, + }; +} + function normalizeWindowArea(raw: unknown, index: number): ChildWindowAreaConfig { const source = (raw ?? {}) as Partial; return { @@ -49,6 +77,7 @@ function normalizeWindowArea(raw: unknown, index: number): ChildWindowAreaConfig typeof source.windowId === "number" && Number.isFinite(source.windowId) && source.windowId > 0 ? Math.floor(source.windowId) : index + 1, + isClockWindow: source.isClockWindow === true, width: normalizeFontSize(source.width, 220), height: normalizeFontSize(source.height, 48), x: typeof source.x === "number" && Number.isFinite(source.x) ? Math.max(0, Math.floor(source.x)) : 0, @@ -58,6 +87,11 @@ function normalizeWindowArea(raw: unknown, index: number): ChildWindowAreaConfig ? source.windowNumber : String(index + 1), windowNumberCircle: source.windowNumberCircle === true, + windowNumberCircleStyle: normalizeCircleStyle(source.windowNumberCircleStyle, { + size: 36, + borderWidth: 1, + borderRadius: 18, + }), windowNumberStyle: normalizeTextStyle(source.windowNumberStyle, { fontSize: 16, color: "#ffffff", @@ -78,6 +112,27 @@ function normalizeWindowArea(raw: unknown, index: number): ChildWindowAreaConfig }; } +function normalizeSubtitleArea(raw: unknown, index: number): SubtitleAreaConfig { + const source = (raw ?? {}) as Partial; + return { + id: typeof source.id === "string" && source.id.trim() ? source.id : `subtitle-${index + 1}`, + width: normalizeFontSize(source.width, 320), + height: normalizeFontSize(source.height, 32), + x: typeof source.x === "number" && Number.isFinite(source.x) ? Math.max(0, Math.floor(source.x)) : 0, + y: typeof source.y === "number" && Number.isFinite(source.y) ? Math.max(0, Math.floor(source.y)) : 0, + text: typeof source.text === "string" ? source.text : "滚动字幕示例", + textStyle: normalizeTextStyle(source.textStyle, { + fontSize: 16, + color: "#ffffff", + fontWeight: 500, + }), + speed: + typeof source.speed === "number" && Number.isFinite(source.speed) && source.speed > 0 + ? source.speed + : 80, + }; +} + /** * 统一校验并归一化配置,避免脏数据污染运行态。 */ @@ -90,6 +145,7 @@ function normalizeConfig(raw: unknown): BroadcastConfig { const screenWidth = normalizeScreenWidth(window.screen.width || 1920); const segmentsRaw = Array.isArray(source.segments) ? source.segments : []; const windowAreasRaw = Array.isArray(source.windowAreas) ? source.windowAreas : []; + const subtitleAreasRaw = Array.isArray(source.subtitleAreas) ? source.subtitleAreas : []; return { totalWidth: Math.min(totalWidth, screenWidth), segmentHeight, @@ -99,6 +155,7 @@ function normalizeConfig(raw: unknown): BroadcastConfig { normalizeSegmentConfigItem(item, index, screenWidth, segmentHeight), ), windowAreas: windowAreasRaw.map((item, index) => normalizeWindowArea(item, index)), + subtitleAreas: subtitleAreasRaw.map((item, index) => normalizeSubtitleArea(item, index)), }; } diff --git a/broadcast-client/src/services/subtitleAreaSliceService.ts b/broadcast-client/src/services/subtitleAreaSliceService.ts new file mode 100644 index 0000000..e01bdfe --- /dev/null +++ b/broadcast-client/src/services/subtitleAreaSliceService.ts @@ -0,0 +1,51 @@ +import type { SubtitleAreaConfig } from "../models/config"; +import type { Segment } from "../models/ruler"; + +export interface SubtitleAreaSlice { + area: SubtitleAreaConfig; + segmentIndex: number; + renderLeft: number; + renderTop: number; + renderWidth: number; + renderHeight: number; + clipOffset: number; +} + +/** + * 对滚动字幕区域按分段做横向切片,支持跨段连续显示。 + */ +export function buildSubtitleAreaSlices( + areas: SubtitleAreaConfig[], + segments: Segment[], +): SubtitleAreaSlice[] { + const slices: SubtitleAreaSlice[] = []; + + for (const area of areas) { + const areaStart = area.x; + const areaEnd = area.x + area.width; + + for (const segment of segments) { + const segmentStart = segment.sourceX; + const segmentEnd = segmentStart + segment.sliceWidth; + + const visibleStart = Math.max(areaStart, segmentStart); + const visibleEnd = Math.min(areaEnd, segmentEnd); + const visibleWidth = visibleEnd - visibleStart; + if (visibleWidth <= 0) { + continue; + } + + slices.push({ + area, + segmentIndex: segment.index, + renderLeft: visibleStart - segmentStart, + renderTop: area.y, + renderWidth: visibleWidth, + renderHeight: area.height, + clipOffset: Math.max(0, segmentStart - areaStart), + }); + } + } + + return slices; +} diff --git a/broadcast-client/src/services/tickService.ts b/broadcast-client/src/services/tickService.ts index 1a02ada..d19d309 100644 --- a/broadcast-client/src/services/tickService.ts +++ b/broadcast-client/src/services/tickService.ts @@ -1,6 +1,7 @@ import { DEFAULT_TOTAL_WIDTH, MAJOR_TICK_GAP, + MID_TICK_GAP, MINOR_TICK_GAP, type Tick, } from "../models/ruler"; @@ -22,6 +23,8 @@ export function buildTicks(totalWidth: number = DEFAULT_TOTAL_WIDTH): Tick[] { for (let x = 0; x <= safeTotalWidth; x += MINOR_TICK_GAP) { if (x % MAJOR_TICK_GAP === 0) { ticks.push({ x, type: "major", label: String(x) }); + } else if (x % MAJOR_TICK_GAP === MID_TICK_GAP) { + ticks.push({ x, type: "mid" }); } else { ticks.push({ x, type: "minor" }); } diff --git a/broadcast-client/src/styles.css b/broadcast-client/src/styles.css index 43a00f2..f08dcc6 100644 --- a/broadcast-client/src/styles.css +++ b/broadcast-client/src/styles.css @@ -54,6 +54,11 @@ body, opacity: 0.85; } +.tick.mid { + height: 24px; + opacity: 0.9; +} + .tick.major { height: 34px; background: #c4d2ea; @@ -231,6 +236,13 @@ body, margin-top: 14px; } +.config-top-status { + display: flex; + align-items: center; + min-height: 28px; + margin-bottom: 10px; +} + .btn { border: 1px solid #c8c8c8; background: #fff; @@ -358,6 +370,18 @@ body, display: flex; } +.clock-window { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.clock-text { + line-height: 1; +} + .window-no-region { flex: 1; display: flex; @@ -391,3 +415,42 @@ body, justify-content: center; text-align: center; } + +.subtitle-areas-layer { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.subtitle-area { + position: absolute; + overflow: hidden; + background: transparent; +} + +.subtitle-area-inner { + position: relative; + overflow: hidden; +} + +.subtitle-text { + position: absolute; + top: 50%; + white-space: nowrap; + transform: translateY(-50%); + animation-name: subtitle-move-right; + animation-timing-function: linear; + animation-iteration-count: infinite; +} + +@keyframes subtitle-move-right { + 0% { + left: -100%; + } + 100% { + left: 100%; + } +} diff --git a/broadcast-client/src/views/BroadcastView.vue b/broadcast-client/src/views/BroadcastView.vue index 66302fb..d07667c 100644 --- a/broadcast-client/src/views/BroadcastView.vue +++ b/broadcast-client/src/views/BroadcastView.vue @@ -12,6 +12,7 @@ :total-width="totalWidth" :show-ruler="showRuler" :window-area-slices="windowAreaSlices" + :subtitle-area-slices="subtitleAreaSlices" /> @@ -26,6 +27,7 @@ import { useRulerTicks } from "../composables/useRulerTicks"; import { useScreenInfo } from "../composables/useScreenInfo"; import type { ChildWindowAreaConfig } from "../models/config"; import { buildSegmentsFromConfig } from "../services/segmentService"; +import { buildSubtitleAreaSlices } from "../services/subtitleAreaSliceService"; import { buildWindowAreaSlices } from "../services/windowAreaSliceService"; const { config } = useBroadcastConfig(); @@ -67,6 +69,9 @@ const containerHeight = computed(() => { }); const { ticks } = useRulerTicks(totalWidth); const windowAreaSlices = computed(() => buildWindowAreaSlices(windowAreas.value, segments.value)); +const subtitleAreaSlices = computed(() => + buildSubtitleAreaSlices(config.value.subtitleAreas, segments.value), +); /** * 右键触发原生菜单(配置窗口 / 退出)。 diff --git a/broadcast-client/src/views/ConfigView.vue b/broadcast-client/src/views/ConfigView.vue index 2c1f533..01be9c6 100644 --- a/broadcast-client/src/views/ConfigView.vue +++ b/broadcast-client/src/views/ConfigView.vue @@ -1,6 +1,11 @@