fix: preserve active style across settings saves (#747)
Co-authored-by: Chris233 <h-chris233@outlook.com>
This commit is contained in:
1 parent
4242020ae9
commit
f2c23723ee
3 files changed
+204
-11
No files matched your search
@@ -13,6 +13,14 @@ pub fn get_default_style_system_prompts() -> StyleSystemPrompts {
|
||||
pub(crate) trait SettingsWriter {
|
||||
fn read_settings(&self) -> UserPreferences;
|
||||
fn write_settings(&self, prefs: UserPreferences) -> Result<(), String>;
|
||||
fn write_settings_preserving_current_style_preferences(
|
||||
&self,
|
||||
mut prefs: UserPreferences,
|
||||
) -> Result<(), String> {
|
||||
let current = self.read_settings();
|
||||
prefs.preserve_style_preferences_from(¤t);
|
||||
self.write_settings(prefs)
|
||||
}
|
||||
fn sync_active_asr_provider(&self, provider: &str) -> Result<(), String>;
|
||||
fn refresh_dictation_hotkey(&self);
|
||||
fn refresh_qa_hotkey(&self);
|
||||
@@ -32,6 +40,15 @@ impl SettingsWriter for Coordinator {
|
||||
self.prefs().set(prefs).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn write_settings_preserving_current_style_preferences(
|
||||
&self,
|
||||
prefs: UserPreferences,
|
||||
) -> Result<(), String> {
|
||||
self.prefs()
|
||||
.set_preserving_current_style_preferences(prefs)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn sync_active_asr_provider(&self, provider: &str) -> Result<(), String> {
|
||||
self.sync_active_asr_provider_to_vault(provider)
|
||||
}
|
||||
@@ -74,6 +91,13 @@ impl<T: SettingsWriter + ?Sized> SettingsWriter for Arc<T> {
|
||||
(**self).write_settings(prefs)
|
||||
}
|
||||
|
||||
fn write_settings_preserving_current_style_preferences(
|
||||
&self,
|
||||
prefs: UserPreferences,
|
||||
) -> Result<(), String> {
|
||||
(**self).write_settings_preserving_current_style_preferences(prefs)
|
||||
}
|
||||
|
||||
fn sync_active_asr_provider(&self, provider: &str) -> Result<(), String> {
|
||||
(**self).sync_active_asr_provider(provider)
|
||||
}
|
||||
@@ -162,7 +186,7 @@ pub(crate) fn persist_settings_with_keyboard_apply<T: SettingsWriter>(
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(error) = coord.write_settings(prefs.clone()) {
|
||||
if let Err(error) = coord.write_settings_preserving_current_style_preferences(prefs.clone()) {
|
||||
if active_asr_provider_changed {
|
||||
match coord.sync_active_asr_provider(&previous.active_asr_provider) {
|
||||
Ok(()) => {
|
||||
@@ -180,11 +204,13 @@ pub(crate) fn persist_settings_with_keyboard_apply<T: SettingsWriter>(
|
||||
}
|
||||
Err(rollback_error) => {
|
||||
// ASR vault 无法回滚时 roll-forward prefs;键盘列表保持新状态,避免三者分叉。
|
||||
coord.write_settings(prefs).map_err(|roll_forward_error| {
|
||||
format!(
|
||||
"{error}; additionally failed to restore active ASR provider: {rollback_error}; additionally failed to preserve active ASR provider consistency: {roll_forward_error}"
|
||||
)
|
||||
})?;
|
||||
coord
|
||||
.write_settings_preserving_current_style_preferences(prefs)
|
||||
.map_err(|roll_forward_error| {
|
||||
format!(
|
||||
"{error}; additionally failed to restore active ASR provider: {rollback_error}; additionally failed to preserve active ASR provider consistency: {roll_forward_error}"
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
} else if windows_keyboard_list_changed {
|
||||
@@ -241,7 +267,8 @@ pub fn set_settings(
|
||||
// 广播给所有 webview。issue #205:QaPanel 跑在独立 webview,
|
||||
// 没有 HotkeySettingsContext,必须靠事件感知录音键变化,否则面板可见时
|
||||
// 用户改键会让浮窗里的 "{recordHotkey}" 文案一直停留在旧值。
|
||||
persist_settings(&*coord, prefs.clone())?;
|
||||
persist_settings(&*coord, prefs)?;
|
||||
let prefs = coord.prefs().get();
|
||||
#[cfg(target_os = "android")]
|
||||
coord.apply_android_overlay_settings_change(&remote_prev, &prefs);
|
||||
// refresh_tray_microphone_menu 内部会调用 NSStatusItem.set_menu,必须在主线程上跑。
|
||||
@@ -284,7 +311,8 @@ pub fn set_settings(
|
||||
let packs = coord.style_packs().list().map_err(|e| e.to_string())?;
|
||||
sync_style_pack_preferences(&mut prefs, &packs);
|
||||
prefs.android_overlay_trigger = prefs.android_overlay_trigger.normalized();
|
||||
persist_settings(&*coord, prefs.clone())?;
|
||||
persist_settings(&*coord, prefs)?;
|
||||
let prefs = coord.prefs().get();
|
||||
#[cfg(target_os = "android")]
|
||||
coord.apply_android_overlay_settings_change(&previous, &prefs);
|
||||
let _ = app.emit("prefs:changed", &prefs);
|
||||
@@ -292,6 +320,109 @@ pub fn set_settings(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Default)]
|
||||
struct RaceSettingsWriter {
|
||||
reads: Mutex<Vec<UserPreferences>>,
|
||||
saved: Mutex<Option<UserPreferences>>,
|
||||
}
|
||||
|
||||
impl SettingsWriter for RaceSettingsWriter {
|
||||
fn read_settings(&self) -> UserPreferences {
|
||||
let mut reads = self.reads.lock().unwrap();
|
||||
if reads.is_empty() {
|
||||
return self.saved.lock().unwrap().clone().unwrap_or_default();
|
||||
}
|
||||
reads.remove(0)
|
||||
}
|
||||
|
||||
fn write_settings(&self, prefs: UserPreferences) -> Result<(), String> {
|
||||
*self.saved.lock().unwrap() = Some(prefs);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_active_asr_provider(&self, _provider: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn refresh_dictation_hotkey(&self) {}
|
||||
|
||||
fn refresh_qa_hotkey(&self) {}
|
||||
|
||||
fn refresh_combo_hotkey(&self) {}
|
||||
|
||||
fn refresh_translation_hotkey(&self) {}
|
||||
|
||||
fn refresh_switch_style_hotkey(&self) {}
|
||||
|
||||
fn refresh_open_app_hotkey(&self) {}
|
||||
|
||||
fn refresh_coding_agent_hotkey(&self) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_save_preserves_current_style_preferences_before_write() {
|
||||
let packs = crate::types::builtin_style_packs();
|
||||
let current = UserPreferences {
|
||||
default_mode: PolishMode::Light,
|
||||
active_style_pack_id: builtin_style_pack_id(PolishMode::Light).to_string(),
|
||||
..UserPreferences::default()
|
||||
};
|
||||
let mut stale_settings_payload = UserPreferences {
|
||||
default_mode: PolishMode::Formal,
|
||||
active_style_pack_id: builtin_style_pack_id(PolishMode::Formal).to_string(),
|
||||
..UserPreferences::default()
|
||||
};
|
||||
|
||||
stale_settings_payload.preserve_style_preferences_from(¤t);
|
||||
sync_style_pack_preferences(&mut stale_settings_payload, &packs);
|
||||
|
||||
assert_eq!(
|
||||
stale_settings_payload.active_style_pack_id,
|
||||
builtin_style_pack_id(PolishMode::Light)
|
||||
);
|
||||
assert_eq!(stale_settings_payload.default_mode, PolishMode::Light);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persist_settings_keeps_style_change_that_lands_before_write() {
|
||||
let active_before_request = UserPreferences {
|
||||
default_mode: PolishMode::Formal,
|
||||
active_style_pack_id: builtin_style_pack_id(PolishMode::Formal).to_string(),
|
||||
..UserPreferences::default()
|
||||
};
|
||||
let active_before_write = UserPreferences {
|
||||
default_mode: PolishMode::Light,
|
||||
active_style_pack_id: builtin_style_pack_id(PolishMode::Light).to_string(),
|
||||
..UserPreferences::default()
|
||||
};
|
||||
let stale_payload = UserPreferences {
|
||||
default_mode: PolishMode::Formal,
|
||||
active_style_pack_id: builtin_style_pack_id(PolishMode::Formal).to_string(),
|
||||
microphone_device_name: "External Mic".to_string(),
|
||||
..UserPreferences::default()
|
||||
};
|
||||
let writer = RaceSettingsWriter {
|
||||
reads: Mutex::new(vec![active_before_request, active_before_write]),
|
||||
saved: Mutex::new(None),
|
||||
};
|
||||
|
||||
persist_settings(&writer, stale_payload).unwrap();
|
||||
|
||||
let saved = writer.saved.lock().unwrap().clone().expect("prefs saved");
|
||||
assert_eq!(
|
||||
saved.active_style_pack_id,
|
||||
builtin_style_pack_id(PolishMode::Light)
|
||||
);
|
||||
assert_eq!(saved.default_mode, PolishMode::Light);
|
||||
assert_eq!(saved.microphone_device_name, "External Mic");
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── release channel (Beta opt-in) ───────────────────────────
|
||||
//
|
||||
// 渠道偏好的写入路径跟 set_settings 复用 persist_settings:保持热键兜底归一化
|
||||
@@ -321,8 +452,8 @@ pub fn set_update_channel(
|
||||
return Ok(());
|
||||
}
|
||||
prefs.update_channel = channel;
|
||||
persist_settings(&*coord, prefs.clone())?;
|
||||
let _ = app.emit("prefs:changed", &prefs);
|
||||
persist_settings(&*coord, prefs)?;
|
||||
let _ = app.emit("prefs:changed", &coord.prefs().get());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -97,11 +97,25 @@ impl PreferencesStore {
|
||||
*guard = prefs;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_preserving_current_style_preferences(
|
||||
&self,
|
||||
mut prefs: UserPreferences,
|
||||
) -> Result<()> {
|
||||
let mut guard = self.state.lock();
|
||||
prefs.preserve_style_preferences_from(&guard);
|
||||
let json = serde_json::to_vec_pretty(&prefs).context("encode prefs failed")?;
|
||||
atomic_write(&self.path, &json)?;
|
||||
*guard = prefs;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::read_preferences;
|
||||
use super::{read_preferences, PreferencesStore};
|
||||
use crate::types::{builtin_style_pack_id, PolishMode, UserPreferences};
|
||||
use parking_lot::Mutex;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -142,4 +156,42 @@ mod tests {
|
||||
|
||||
let _ = fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_preserving_current_style_preferences_keeps_store_style_fields() {
|
||||
let tmp: PathBuf =
|
||||
std::env::temp_dir().join(format!("openless-prefs-test-{}", uuid::Uuid::new_v4()));
|
||||
fs::create_dir_all(&tmp).expect("create temp dir");
|
||||
let path = tmp.join("preferences.json");
|
||||
let current = UserPreferences {
|
||||
default_mode: PolishMode::Light,
|
||||
active_style_pack_id: "local.light-cleanup".to_string(),
|
||||
..UserPreferences::default()
|
||||
};
|
||||
let store = PreferencesStore {
|
||||
path,
|
||||
state: Mutex::new(current),
|
||||
};
|
||||
let incoming = UserPreferences {
|
||||
default_mode: PolishMode::Formal,
|
||||
active_style_pack_id: builtin_style_pack_id(PolishMode::Formal).to_string(),
|
||||
microphone_device_name: "External Mic".to_string(),
|
||||
..UserPreferences::default()
|
||||
};
|
||||
|
||||
store
|
||||
.set_preserving_current_style_preferences(incoming)
|
||||
.expect("save prefs");
|
||||
|
||||
let saved = store.get();
|
||||
assert_eq!(saved.default_mode, PolishMode::Light);
|
||||
assert_eq!(saved.active_style_pack_id, "local.light-cleanup");
|
||||
assert_eq!(saved.microphone_device_name, "External Mic");
|
||||
let saved_on_disk = read_preferences(&store.path).expect("read saved prefs");
|
||||
assert_eq!(saved_on_disk.default_mode, PolishMode::Light);
|
||||
assert_eq!(saved_on_disk.active_style_pack_id, "local.light-cleanup");
|
||||
assert_eq!(saved_on_disk.microphone_device_name, "External Mic");
|
||||
|
||||
let _ = fs::remove_dir_all(&tmp);
|
||||
}
|
||||
}
|
||||
@@ -879,6 +879,16 @@ pub struct UserPreferences {
|
||||
pub android_overlay_size_dp: u32,
|
||||
}
|
||||
|
||||
impl UserPreferences {
|
||||
pub(crate) fn preserve_style_preferences_from(&mut self, current: &Self) {
|
||||
self.default_mode = current.default_mode;
|
||||
self.enabled_modes = current.enabled_modes.clone();
|
||||
self.active_style_pack_id = current.active_style_pack_id.clone();
|
||||
self.style_system_prompts = current.style_system_prompts.clone();
|
||||
self.custom_style_prompts = current.custom_style_prompts.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn default_local_asr_model() -> String {
|
||||
"qwen3-asr-0.6b".into()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user