This commit is contained in:
Chris233 committed 2026-08-09 13:58:28 +08:00
commit 76357a0f8c
1 file changed
+151 -58
+151 -58
View File
@@ -134,7 +134,8 @@ use tauri::{
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use tauri::{WebviewUrl, WebviewWindowBuilder};
use crate::types::PolishMode;
#[cfg(not(mobile))]
use crate::types::StylePack;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -854,13 +855,16 @@ struct TrayMenu {
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(not(mobile))]
struct TrayPolishModeMenuEntry {
struct TrayStylePackMenuEntry {
id: String,
label: &'static str,
mode: PolishMode,
pack_id: String,
label: String,
checked: bool,
}
#[cfg(not(mobile))]
const TRAY_STYLE_PACK_MENU_ID_PREFIX: &str = "style-pack-id-";
fn tray_style_menu_enabled() -> bool {
#[cfg(all(not(mobile), target_os = "windows"))]
return true;
@@ -869,32 +873,44 @@ fn tray_style_menu_enabled() -> bool {
}
#[cfg(not(mobile))]
fn tray_polish_mode_menu_entries(selected: PolishMode) -> Vec<TrayPolishModeMenuEntry> {
[
(PolishMode::Raw, "style-raw"),
(PolishMode::Light, "style-light"),
(PolishMode::Structured, "style-structured"),
(PolishMode::Formal, "style-formal"),
]
.into_iter()
.map(|(mode, id)| TrayPolishModeMenuEntry {
id: id.to_string(),
label: mode.display_name(),
mode,
checked: mode == selected,
})
.collect()
fn tray_style_pack_menu_id(pack_id: &str) -> String {
format!("{TRAY_STYLE_PACK_MENU_ID_PREFIX}{pack_id}")
}
#[cfg(not(mobile))]
fn parse_tray_polish_mode_id(id: &str) -> Option<PolishMode> {
match id {
"style-raw" => Some(PolishMode::Raw),
"style-light" => Some(PolishMode::Light),
"style-structured" => Some(PolishMode::Structured),
"style-formal" => Some(PolishMode::Formal),
_ => None,
}
fn parse_tray_style_pack_menu_id(id: &str) -> Option<&str> {
let pack_id = id.strip_prefix(TRAY_STYLE_PACK_MENU_ID_PREFIX)?;
(!pack_id.is_empty()).then_some(pack_id)
}
#[cfg(not(mobile))]
fn tray_style_pack_menu_entries(
packs: &[StylePack],
active_style_pack_id: &str,
) -> Vec<TrayStylePackMenuEntry> {
packs
.iter()
.filter(|pack| pack.enabled)
.map(|pack| TrayStylePackMenuEntry {
id: tray_style_pack_menu_id(&pack.id),
pack_id: pack.id.clone(),
label: if pack.name.trim().is_empty() {
pack.id.clone()
} else {
pack.name.clone()
},
checked: pack.id == active_style_pack_id,
})
.collect()
}
#[cfg(not(mobile))]
fn resolve_tray_style_pack_id<'a>(id: &'a str, packs: &[StylePack]) -> Option<&'a str> {
let pack_id = parse_tray_style_pack_menu_id(id)?;
packs
.iter()
.any(|pack| pack.enabled && pack.id == pack_id)
.then_some(pack_id)
}
#[cfg(not(mobile))]
@@ -929,13 +945,12 @@ fn build_style_tray_menu<M: Manager<tauri::Wry>>(
coordinator: &Arc<coordinator::Coordinator>,
) -> tauri::Result<StyleTrayMenu> {
let prefs = coordinator.prefs().get();
let selected = coordinator
.style_packs()
.get_or_default_active(&prefs.active_style_pack_id)
.map(|pack| pack.base_mode)
.unwrap_or(prefs.default_mode);
let packs = coordinator.style_packs().list().unwrap_or_else(|err| {
log::warn!("[tray] list style packs for tray menu failed: {err}");
Vec::new()
});
let mut submenu = SubmenuBuilder::with_id(app, "style", "输出风格");
for entry in tray_polish_mode_menu_entries(selected) {
for entry in tray_style_pack_menu_entries(&packs, &prefs.active_style_pack_id) {
let item = CheckMenuItemBuilder::with_id(&entry.id, entry.label)
.checked(entry.checked)
.build(app)?;
@@ -1166,12 +1181,23 @@ fn handle_microphone_tray_menu_event(app: &AppHandle, id: &str) {
#[cfg(not(mobile))]
fn handle_style_tray_menu_event(app: &AppHandle, id: &str) -> bool {
let Some(mode) = parse_tray_polish_mode_id(id) else {
let Some(pack_id) = parse_tray_style_pack_menu_id(id) else {
return false;
};
let coord = app.state::<Arc<coordinator::Coordinator>>();
if let Err(err) = commands::activate_builtin_style_mode(&coord, app, mode) {
log::warn!("[tray] activate builtin style mode failed: {err}");
let packs = match coord.style_packs().list() {
Ok(packs) => packs,
Err(err) => {
log::warn!("[tray] validate style pack tray item failed: {err}");
return true;
}
};
if resolve_tray_style_pack_id(id, &packs).is_none() {
log::warn!("[tray] ignore stale or disabled style pack tray item id={pack_id}");
return true;
}
if let Err(err) = commands::activate_style_pack_by_id(&coord, app, pack_id) {
log::warn!("[tray] activate style pack from tray failed: {err}");
return true;
}
if let Err(err) = refresh_tray_microphone_menu(app) {
@@ -2920,11 +2946,12 @@ mod tests {
use super::{
bottom_center_position, bottom_visual_position, capsule_height_for_qa,
capsule_visual_height, capsule_window_bounds, clamp_to_monitor, frame_contains_point,
frame_distance_to_point_squared, logical_monitor_frame, parse_tray_polish_mode_id,
rotate_log_if_too_large, tray_polish_mode_menu_entries, tray_style_menu_enabled,
LogicalMonitorFrame, LOG_ROTATE_LIMIT_BYTES,
frame_distance_to_point_squared, logical_monitor_frame, parse_tray_style_pack_menu_id,
resolve_tray_style_pack_id, rotate_log_if_too_large, tray_style_menu_enabled,
tray_style_pack_menu_entries, tray_style_pack_menu_id, LogicalMonitorFrame,
LOG_ROTATE_LIMIT_BYTES,
};
use crate::types::PolishMode;
use crate::types::{builtin_style_pack_for_mode, PolishMode, StylePack, StylePackKind};
use std::io::Write;
#[test]
@@ -2937,43 +2964,109 @@ mod tests {
}
#[test]
fn tray_style_menu_lists_builtin_modes_in_expected_order() {
let entries = tray_polish_mode_menu_entries(PolishMode::Structured);
fn tray_style_menu_lists_enabled_packs_and_marks_active_id() {
let imported = StylePack {
id: "imported.meeting".into(),
name: "会议纪要".into(),
kind: StylePackKind::Imported,
base_mode: PolishMode::Structured,
..StylePack::default()
};
let duplicate_base_mode = StylePack {
id: "imported.structured".into(),
name: "自定义结构化".into(),
kind: StylePackKind::Imported,
base_mode: PolishMode::Structured,
..StylePack::default()
};
let disabled = StylePack {
id: "imported.disabled".into(),
name: "已禁用".into(),
kind: StylePackKind::Imported,
base_mode: PolishMode::Structured,
enabled: false,
..StylePack::default()
};
let packs = vec![
builtin_style_pack_for_mode(PolishMode::Raw),
builtin_style_pack_for_mode(PolishMode::Light),
builtin_style_pack_for_mode(PolishMode::Structured),
builtin_style_pack_for_mode(PolishMode::Formal),
imported,
duplicate_base_mode,
disabled,
];
let entries = tray_style_pack_menu_entries(&packs, "imported.meeting");
assert_eq!(
entries
.iter()
.map(|entry| (entry.id.as_str(), entry.label, entry.mode, entry.checked))
.map(|entry| (entry.pack_id.as_str(), entry.label.as_str(), entry.checked))
.collect::<Vec<_>>(),
vec![
("style-raw", "原文", PolishMode::Raw, false),
("style-light", "轻度润色", PolishMode::Light, false),
("style-structured", "清晰结构", PolishMode::Structured, true),
("style-formal", "正式表达", PolishMode::Formal, false),
("builtin.raw", "原文", false),
("builtin.light", "轻度润色", false),
("builtin.structured", "清晰结构", false),
("builtin.formal", "正式表达", false),
("imported.meeting", "会议纪要", true),
("imported.structured", "自定义结构化", false),
]
);
assert_eq!(
entries
.iter()
.filter(|entry| entry.checked)
.map(|entry| entry.pack_id.as_str())
.collect::<Vec<_>>(),
vec!["imported.meeting"]
);
assert_eq!(entries[0].id, tray_style_pack_menu_id("builtin.raw"));
}
#[test]
fn tray_style_menu_id_parsing_accepts_only_style_items() {
fn tray_style_menu_ids_are_stable_and_collision_safe() {
let first = tray_style_pack_menu_id("imported.meeting");
assert_eq!(first, "style-pack-id-imported.meeting");
assert_eq!(first, tray_style_pack_menu_id("imported.meeting"));
assert_ne!(first, tray_style_pack_menu_id("imported.structured"));
assert_ne!(first, "style-structured");
}
#[test]
fn tray_style_menu_id_parsing_rejects_malformed_and_stale_items() {
let packs = vec![
builtin_style_pack_for_mode(PolishMode::Raw),
StylePack {
id: "imported.disabled".into(),
name: "已禁用".into(),
kind: StylePackKind::Imported,
base_mode: PolishMode::Raw,
enabled: false,
..StylePack::default()
},
];
assert_eq!(
parse_tray_polish_mode_id("style-raw"),
Some(PolishMode::Raw)
parse_tray_style_pack_menu_id(&tray_style_pack_menu_id("builtin.raw")),
Some("builtin.raw")
);
assert_eq!(
parse_tray_polish_mode_id("style-light"),
Some(PolishMode::Light)
resolve_tray_style_pack_id(&tray_style_pack_menu_id("builtin.raw"), &packs),
Some("builtin.raw")
);
assert_eq!(
parse_tray_polish_mode_id("style-structured"),
Some(PolishMode::Structured)
resolve_tray_style_pack_id(&tray_style_pack_menu_id("imported.disabled"), &packs),
None
);
assert_eq!(
parse_tray_polish_mode_id("style-formal"),
Some(PolishMode::Formal)
resolve_tray_style_pack_id(&tray_style_pack_menu_id("imported.deleted"), &packs),
None
);
assert_eq!(parse_tray_polish_mode_id("toggle"), None);
assert_eq!(parse_tray_polish_mode_id("mic-default"), None);
assert_eq!(parse_tray_style_pack_menu_id("style-pack-id-"), None);
assert_eq!(parse_tray_style_pack_menu_id("style-raw"), None);
assert_eq!(parse_tray_style_pack_menu_id("toggle"), None);
assert_eq!(parse_tray_style_pack_menu_id("mic-default"), None);
}
#[test]