feat(app): configure background tray behavior

This commit is contained in:
t8y2 committed 2026-05-22 02:24:16 +08:00
1 parent cf520a7ee1
commit bc41936e48
12 files changed
+282 -35

No files matched your search

+98 -13
View File
@@ -1,6 +1,7 @@
use std::path::Path;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
use crate::ai::{AiChatMessage, AiConfig, AiConversation};
@@ -12,6 +13,17 @@ pub struct Storage {
db: SqlitePool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DesktopSettings {
pub run_in_background: bool,
}
impl Default for DesktopSettings {
fn default() -> Self {
Self { run_in_background: true }
}
}
const SCHEMA_STATEMENTS: &[&str] = &[
"CREATE TABLE IF NOT EXISTS connections (
id TEXT PRIMARY KEY,
@@ -274,8 +286,25 @@ impl Storage {
// ---------------------------------------------------------------------------
impl Storage {
pub async fn save_password_hash(&self, hash: &str) -> Result<(), String> {
let json = serde_json::json!({ "password_hash": hash }).to_string();
async fn load_app_settings_json(&self) -> Result<serde_json::Map<String, serde_json::Value>, String> {
let row: Option<(String,)> = sqlx::query_as("SELECT settings_json FROM app_settings WHERE id = 1")
.fetch_optional(&self.db)
.await
.map_err(|e| e.to_string())?;
let Some((json,)) = row else {
return Ok(serde_json::Map::new());
};
match serde_json::from_str::<serde_json::Value>(&json).map_err(|e| e.to_string())? {
serde_json::Value::Object(map) => Ok(map),
_ => Ok(serde_json::Map::new()),
}
}
async fn save_app_settings_json(
&self,
settings: &serde_json::Map<String, serde_json::Value>,
) -> Result<(), String> {
let json = serde_json::Value::Object(settings.clone()).to_string();
sqlx::query("INSERT OR REPLACE INTO app_settings (id, settings_json) VALUES (1, ?)")
.bind(&json)
.execute(&self.db)
@@ -284,18 +313,31 @@ impl Storage {
Ok(())
}
pub async fn save_password_hash(&self, hash: &str) -> Result<(), String> {
let mut settings = self.load_app_settings_json().await?;
settings.insert("password_hash".to_string(), serde_json::Value::String(hash.to_string()));
self.save_app_settings_json(&settings).await
}
pub async fn load_password_hash(&self) -> Result<Option<String>, String> {
let row: Option<(String,)> = sqlx::query_as("SELECT settings_json FROM app_settings WHERE id = 1")
.fetch_optional(&self.db)
.await
.map_err(|e| e.to_string())?;
match row {
Some((json,)) => {
let v: serde_json::Value = serde_json::from_str(&json).map_err(|e| e.to_string())?;
Ok(v.get("password_hash").and_then(|v| v.as_str()).map(|s| s.to_string()))
}
None => Ok(None),
}
let settings = self.load_app_settings_json().await?;
Ok(settings.get("password_hash").and_then(|v| v.as_str()).map(|s| s.to_string()))
}
pub async fn save_desktop_settings(&self, desktop_settings: &DesktopSettings) -> Result<(), String> {
let mut settings = self.load_app_settings_json().await?;
settings.insert("run_in_background".to_string(), serde_json::Value::Bool(desktop_settings.run_in_background));
self.save_app_settings_json(&settings).await
}
pub async fn load_desktop_settings(&self) -> Result<DesktopSettings, String> {
let settings = self.load_app_settings_json().await?;
Ok(DesktopSettings {
run_in_background: settings
.get("run_in_background")
.and_then(|value| value.as_bool())
.unwrap_or_else(|| DesktopSettings::default().run_in_background),
})
}
}
@@ -930,3 +972,46 @@ async fn persist_secret_in_tx(
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{DesktopSettings, Storage};
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_db_path(name: &str) -> std::path::PathBuf {
let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
std::env::temp_dir().join(format!("dbx-storage-{name}-{}-{stamp}.db", std::process::id()))
}
#[tokio::test]
async fn desktop_settings_default_to_background_enabled() {
let path = temp_db_path("desktop-settings-default");
let storage = Storage::open(&path).await.unwrap();
assert_eq!(storage.load_desktop_settings().await.unwrap(), DesktopSettings { run_in_background: true });
}
#[tokio::test]
async fn desktop_settings_preserve_existing_password_hash() {
let path = temp_db_path("desktop-settings-preserve-password");
let storage = Storage::open(&path).await.unwrap();
storage.save_password_hash("hash-1").await.unwrap();
storage.save_desktop_settings(&DesktopSettings { run_in_background: false }).await.unwrap();
assert_eq!(storage.load_password_hash().await.unwrap(), Some("hash-1".to_string()));
assert_eq!(storage.load_desktop_settings().await.unwrap(), DesktopSettings { run_in_background: false });
}
#[tokio::test]
async fn password_hash_preserves_existing_desktop_settings() {
let path = temp_db_path("password-preserve-desktop-settings");
let storage = Storage::open(&path).await.unwrap();
storage.save_desktop_settings(&DesktopSettings { run_in_background: false }).await.unwrap();
storage.save_password_hash("hash-2").await.unwrap();
assert_eq!(storage.load_password_hash().await.unwrap(), Some("hash-2".to_string()));
assert_eq!(storage.load_desktop_settings().await.unwrap(), DesktopSettings { run_in_background: false });
}
}