feat: FTPS, AppError, Site Manager, tests, multi-window, auto-clear, CI/CD, virtual scroll
- FTPS: реализован FtpsClient через suppaftp + async-native-tls - AppError: кастомный enum с Serialize вместо Result<T, String> - Site Manager UI: создание/редактирование/удаление сайтов - Тесты: 53 тестов (domain, storage, transfer, fs, remote) - Multi-window: новое окно + сохранение геометрии - Auto-clear: воркер чистит завершённые задачи по таймеру - CI/CD: 3-ступенчатый пайплайн (lint→build→test) с кэшированием - Virtual scroll: fileTable с рендером только видимых строк - UI: исправлены close-кнопки, color-scheme для select, скриншоты в README
This commit is contained in:
parent
545dd4ed0b
commit
e188fbef36
52 changed files with 5188 additions and 523 deletions
363
src/commands.rs
363
src/commands.rs
|
|
@ -4,13 +4,20 @@ use std::sync::Arc;
|
|||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use tauri::State;
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::domain::connection::{ConnectionParams, Protocol};
|
||||
use crate::domain::error::AppError;
|
||||
use crate::domain::file_entry::FileEntry;
|
||||
use crate::domain::site::Site;
|
||||
use crate::domain::transfer::{TaskState, TransferKind, TransferTask};
|
||||
use crate::domain::window_state::WindowState;
|
||||
use crate::fs::remote::RemoteRegistry;
|
||||
use crate::protocols::{RemoteFs, ftp::FtpClient, sftp::SftpClient};
|
||||
use crate::protocols::{
|
||||
RemoteFs,
|
||||
ftp::{FtpClient, FtpsClient},
|
||||
sftp::SftpClient,
|
||||
};
|
||||
use crate::storage::db::{self, HistoryRow};
|
||||
use crate::transfer::queue::TransferQueue;
|
||||
|
||||
|
|
@ -19,12 +26,16 @@ pub struct AppState {
|
|||
pub registry: Arc<RemoteRegistry>,
|
||||
pub queue: TransferQueue,
|
||||
pub max_concurrent: Arc<AtomicU32>,
|
||||
pub auto_clear_secs: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
// --- Connections ---------------------------------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn connect(state: State<'_, AppState>, params: ConnectionParams) -> Result<String, String> {
|
||||
pub async fn connect(
|
||||
state: State<'_, AppState>,
|
||||
params: ConnectionParams,
|
||||
) -> Result<String, AppError> {
|
||||
let registry = state.registry.clone();
|
||||
let db = state.db.clone();
|
||||
|
||||
|
|
@ -44,40 +55,44 @@ pub async fn connect(state: State<'_, AppState>, params: ConnectionParams) -> Re
|
|||
let fs: Arc<dyn RemoteFs> = match params.protocol {
|
||||
Protocol::Sftp => {
|
||||
if let Some(key_path) = ¶ms.key_path {
|
||||
Arc::new(
|
||||
SftpClient::connect_key(
|
||||
¶ms.host,
|
||||
params.port,
|
||||
¶ms.username,
|
||||
key_path,
|
||||
params.password.as_deref(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?,
|
||||
)
|
||||
Arc::new(SftpClient::connect_key(
|
||||
¶ms.host,
|
||||
params.port,
|
||||
¶ms.username,
|
||||
key_path,
|
||||
params.password.as_deref(),
|
||||
)?)
|
||||
} else if let Some(password) = ¶ms.password {
|
||||
Arc::new(
|
||||
SftpClient::connect_password(¶ms.host, params.port, ¶ms.username, password)
|
||||
.map_err(|e| e.to_string())?,
|
||||
)
|
||||
Arc::new(SftpClient::connect_password(
|
||||
¶ms.host,
|
||||
params.port,
|
||||
¶ms.username,
|
||||
password,
|
||||
)?)
|
||||
} else {
|
||||
Arc::new(
|
||||
SftpClient::connect_auto(¶ms.host, params.port, ¶ms.username)
|
||||
.map_err(|e| e.to_string())?,
|
||||
)
|
||||
Arc::new(SftpClient::connect_auto(
|
||||
¶ms.host,
|
||||
params.port,
|
||||
¶ms.username,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
Protocol::Ftp => {
|
||||
let password = params
|
||||
.password
|
||||
.clone()
|
||||
.ok_or("Password is required for FTP connections.")?;
|
||||
let password = params.password.clone().ok_or_else(|| {
|
||||
AppError::InvalidInput("Password is required for FTP connections.".into())
|
||||
})?;
|
||||
Arc::new(
|
||||
FtpClient::connect(¶ms.host, params.port, ¶ms.username, &password)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?,
|
||||
FtpClient::connect(¶ms.host, params.port, ¶ms.username, &password).await?,
|
||||
)
|
||||
}
|
||||
Protocol::Ftps => {
|
||||
let password = params.password.clone().ok_or_else(|| {
|
||||
AppError::InvalidInput("Password is required for FTPS connections.".into())
|
||||
})?;
|
||||
Arc::new(
|
||||
FtpsClient::connect(¶ms.host, params.port, ¶ms.username, &password).await?,
|
||||
)
|
||||
}
|
||||
Protocol::Ftps => return Err("FTPS not yet implemented".into()),
|
||||
};
|
||||
|
||||
registry.insert(params.id.clone(), fs);
|
||||
|
|
@ -104,11 +119,11 @@ pub fn disconnect(state: State<'_, AppState>, connection_id: String) {
|
|||
|
||||
// --- Remote file ops -------------------------------------------------------
|
||||
|
||||
fn get_remote(state: &AppState, connection_id: &str) -> Result<Arc<dyn RemoteFs>, String> {
|
||||
fn get_remote(state: &AppState, connection_id: &str) -> Result<Arc<dyn RemoteFs>, AppError> {
|
||||
state
|
||||
.registry
|
||||
.get(connection_id)
|
||||
.ok_or_else(|| "connection not found".to_string())
|
||||
.ok_or_else(|| AppError::NotFound("connection not found".into()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -116,11 +131,8 @@ pub async fn remote_list(
|
|||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
path: String,
|
||||
) -> Result<Vec<FileEntry>, String> {
|
||||
get_remote(&state, &connection_id)?
|
||||
.list(&path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
) -> Result<Vec<FileEntry>, AppError> {
|
||||
Ok(get_remote(&state, &connection_id)?.list(&path).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -128,11 +140,8 @@ pub async fn remote_stat(
|
|||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
path: String,
|
||||
) -> Result<FileEntry, String> {
|
||||
get_remote(&state, &connection_id)?
|
||||
.stat(&path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
) -> Result<FileEntry, AppError> {
|
||||
Ok(get_remote(&state, &connection_id)?.stat(&path).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -140,11 +149,8 @@ pub async fn remote_mkdir(
|
|||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
path: String,
|
||||
) -> Result<(), String> {
|
||||
get_remote(&state, &connection_id)?
|
||||
.mkdir(&path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
) -> Result<(), AppError> {
|
||||
Ok(get_remote(&state, &connection_id)?.mkdir(&path).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -153,11 +159,10 @@ pub async fn remote_rename(
|
|||
connection_id: String,
|
||||
from: String,
|
||||
to: String,
|
||||
) -> Result<(), String> {
|
||||
get_remote(&state, &connection_id)?
|
||||
) -> Result<(), AppError> {
|
||||
Ok(get_remote(&state, &connection_id)?
|
||||
.rename(&from, &to)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -165,18 +170,15 @@ pub async fn remote_delete(
|
|||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
path: String,
|
||||
) -> Result<(), String> {
|
||||
get_remote(&state, &connection_id)?
|
||||
.delete(&path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
) -> Result<(), AppError> {
|
||||
Ok(get_remote(&state, &connection_id)?.delete(&path).await?)
|
||||
}
|
||||
|
||||
// --- Local file ops --------------------------------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_list(path: String) -> Result<Vec<FileEntry>, String> {
|
||||
crate::fs::local::list(&path).map_err(|e| e.to_string())
|
||||
pub fn local_list(path: String) -> Result<Vec<FileEntry>, AppError> {
|
||||
Ok(crate::fs::local::list(&path)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -185,28 +187,28 @@ pub fn local_home_dir() -> String {
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_mkdir(path: String) -> Result<(), String> {
|
||||
crate::fs::local::mkdir(&path).map_err(|e| e.to_string())
|
||||
pub fn local_mkdir(path: String) -> Result<(), AppError> {
|
||||
Ok(crate::fs::local::mkdir(&path)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_rename(from: String, to: String) -> Result<(), String> {
|
||||
crate::fs::local::rename(&from, &to).map_err(|e| e.to_string())
|
||||
pub fn local_rename(from: String, to: String) -> Result<(), AppError> {
|
||||
Ok(crate::fs::local::rename(&from, &to)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_delete(path: String) -> Result<(), String> {
|
||||
crate::fs::local::delete(&path).map_err(|e| e.to_string())
|
||||
pub fn local_delete(path: String) -> Result<(), AppError> {
|
||||
Ok(crate::fs::local::delete(&path)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_open(path: String) -> Result<(), String> {
|
||||
crate::fs::local::open(&path).map_err(|e| e.to_string())
|
||||
pub fn local_open(path: String) -> Result<(), AppError> {
|
||||
Ok(crate::fs::local::open(&path)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_move_into(src_path: String, dest_dir: String) -> Result<(), String> {
|
||||
crate::fs::local::move_into(&src_path, &dest_dir).map_err(|e| e.to_string())
|
||||
pub fn local_move_into(src_path: String, dest_dir: String) -> Result<(), AppError> {
|
||||
Ok(crate::fs::local::move_into(&src_path, &dest_dir)?)
|
||||
}
|
||||
|
||||
// --- Transfers --------------------------------------------------------------
|
||||
|
|
@ -222,7 +224,14 @@ pub fn enqueue_transfer(
|
|||
file_name: String,
|
||||
total_bytes: u64,
|
||||
) -> String {
|
||||
let task = TransferTask::new(kind, connection_id, local_path, remote_path, file_name, total_bytes);
|
||||
let task = TransferTask::new(
|
||||
kind,
|
||||
connection_id,
|
||||
local_path,
|
||||
remote_path,
|
||||
file_name,
|
||||
total_bytes,
|
||||
);
|
||||
let id = task.id.clone();
|
||||
state.queue.push(task);
|
||||
id
|
||||
|
|
@ -264,51 +273,79 @@ pub fn set_max_concurrent(state: State<'_, AppState>, n: u32) {
|
|||
// --- Sites / bookmarks / history / settings ---------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_sites(state: State<'_, AppState>) -> Result<Vec<Site>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::get_sites(&conn).map_err(|e| e.to_string())
|
||||
pub fn list_sites(state: State<'_, AppState>) -> Result<Vec<Site>, AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::get_sites(&conn)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_site(state: State<'_, AppState>, site: Site) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::save_site(&conn, &site).map_err(|e| e.to_string())
|
||||
pub fn save_site(state: State<'_, AppState>, site: Site) -> Result<(), AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::save_site(&conn, &site)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_site(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::delete_site(&conn, &id).map_err(|e| e.to_string())
|
||||
pub fn delete_site(state: State<'_, AppState>, id: String) -> Result<(), AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::delete_site(&conn, &id)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_bookmarks(state: State<'_, AppState>) -> Result<Vec<(i64, String, String)>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::get_bookmarks(&conn).map_err(|e| e.to_string())
|
||||
pub fn list_bookmarks(state: State<'_, AppState>) -> Result<Vec<(i64, String, String)>, AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::get_bookmarks(&conn)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_bookmark(state: State<'_, AppState>, name: String, path: String) -> Result<i64, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::add_bookmark(&conn, &name, &path).map_err(|e| e.to_string())
|
||||
pub fn add_bookmark(
|
||||
state: State<'_, AppState>,
|
||||
name: String,
|
||||
path: String,
|
||||
) -> Result<i64, AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::add_bookmark(&conn, &name, &path)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_bookmark(state: State<'_, AppState>, id: i64) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::remove_bookmark(&conn, id).map_err(|e| e.to_string())
|
||||
pub fn remove_bookmark(state: State<'_, AppState>, id: i64) -> Result<(), AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::remove_bookmark(&conn, id)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_history(state: State<'_, AppState>) -> Result<Vec<HistoryRow>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::get_history(&conn).map_err(|e| e.to_string())
|
||||
pub fn list_history(state: State<'_, AppState>) -> Result<Vec<HistoryRow>, AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::get_history(&conn)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn clear_history(state: State<'_, AppState>) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::clear_history(&conn).map_err(|e| e.to_string())
|
||||
pub fn clear_history(state: State<'_, AppState>) -> Result<(), AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::clear_history(&conn)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -317,32 +354,47 @@ pub fn find_history_conn_id(
|
|||
host: String,
|
||||
port: u16,
|
||||
username: String,
|
||||
) -> Result<Option<String>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::find_history_conn_id(&conn, &host, port, &username).map_err(|e| e.to_string())
|
||||
) -> Result<Option<String>, AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::find_history_conn_id(&conn, &host, port, &username)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_pref(state: State<'_, AppState>, key: String) -> Result<Option<String>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
pub fn get_pref(state: State<'_, AppState>, key: String) -> Result<Option<String>, AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::get_setting(&conn, &key))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_pref(state: State<'_, AppState>, key: String, value: String) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::set_setting(&conn, &key, &value).map_err(|e| e.to_string())
|
||||
pub fn set_pref(state: State<'_, AppState>, key: String, value: String) -> Result<(), AppError> {
|
||||
if key == "auto_clear_completed_secs" && let Ok(secs) = value.parse::<u32>() {
|
||||
state.auto_clear_secs.store(secs, Ordering::Relaxed);
|
||||
}
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(db::set_setting(&conn, &key, &value)?)
|
||||
}
|
||||
|
||||
/// Удалить пароль из сохранённого сайта (очистить поле password в БД).
|
||||
#[tauri::command]
|
||||
pub fn delete_password(state: State<'_, AppState>, site_id: String) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
pub fn delete_password(state: State<'_, AppState>, site_id: String) -> Result<(), AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
conn.execute(
|
||||
"UPDATE sites SET password = NULL WHERE id = ?1",
|
||||
rusqlite::params![site_id],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -381,5 +433,110 @@ pub fn platform_info() -> PlatformInfo {
|
|||
} else {
|
||||
(None, None)
|
||||
};
|
||||
PlatformInfo { os, desktop, session }
|
||||
PlatformInfo {
|
||||
os,
|
||||
desktop,
|
||||
session,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Multi-window support ---------------------------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_window_state(
|
||||
state: State<'_, AppState>,
|
||||
window_state: WindowState,
|
||||
) -> Result<(), AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
let key = format!("window_state_{}", window_state.label);
|
||||
let json =
|
||||
serde_json::to_string(&window_state).map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
db::set_setting(&conn, &key, &json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn load_window_state(
|
||||
state: State<'_, AppState>,
|
||||
label: String,
|
||||
) -> Result<Option<WindowState>, AppError> {
|
||||
let conn = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
let key = format!("window_state_{}", label);
|
||||
match db::get_setting(&conn, &key) {
|
||||
Some(json) => {
|
||||
let ws: WindowState =
|
||||
serde_json::from_str(&json).map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
Ok(Some(ws))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn new_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let label = format!("browser-{}", uuid::Uuid::new_v4());
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&label,
|
||||
tauri::WebviewUrl::App("/".into()),
|
||||
)
|
||||
.title("Wherry")
|
||||
.inner_size(1280.0, 800.0)
|
||||
.min_inner_size(900.0, 600.0)
|
||||
.center()
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let db = state.db.clone();
|
||||
let app_handle = app_handle.clone();
|
||||
let label_clone = label.clone();
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api: _, .. } = event
|
||||
&& let Some(win) = app_handle.get_webview_window(&label_clone)
|
||||
{
|
||||
save_window_state_internal(&win, &db);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn save_window_state_internal(
|
||||
window: &tauri::WebviewWindow,
|
||||
db: &Arc<std::sync::Mutex<rusqlite::Connection>>,
|
||||
) {
|
||||
let label = window.label().to_string();
|
||||
let pos = window.outer_position().ok();
|
||||
let size = window
|
||||
.outer_size()
|
||||
.unwrap_or(tauri::PhysicalSize {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
});
|
||||
let maximized = window.is_maximized().unwrap_or(false);
|
||||
|
||||
let ws = WindowState {
|
||||
label: label.clone(),
|
||||
x: pos.map(|p| p.x),
|
||||
y: pos.map(|p| p.y),
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
maximized,
|
||||
};
|
||||
|
||||
if let Ok(conn) = db.lock() {
|
||||
let key = format!("window_state_{}", label);
|
||||
if let Ok(json) = serde_json::to_string(&ws) {
|
||||
let _ = db::set_setting(&conn, &key, &json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue