mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-19 07:36:17 +00:00
feat(ui): persist window state and clarify transfer telemetry
- Persist bounded logical main-window geometry with work-area-safe startup restoration. - Persist the Folders collapse preference in SQLite with guarded legacy localStorage migration. - Keep media transfer telemetry truthful and compact the transfer controls across locales. - Add bridge, presentation, persistence, geometry, and configuration regression coverage.
This commit is contained in:
@@ -653,6 +653,14 @@ pub struct SchedulerSettings {
|
||||
pub post_queue_action: PostQueueAction,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../../src/bindings/")]
|
||||
pub struct MainWindowSize {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../../src/bindings/")]
|
||||
@@ -678,6 +686,11 @@ pub struct PersistedSettings {
|
||||
pub speed_limit_preset_values: Vec<f64>,
|
||||
pub logs_enabled: bool,
|
||||
pub is_sidebar_visible: bool,
|
||||
#[serde(default)]
|
||||
pub is_folders_collapsed: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub main_window_size: Option<MainWindowSize>,
|
||||
#[serde(default = "default_sidebar_position")]
|
||||
pub sidebar_position: String,
|
||||
pub active_settings_tab: SettingsTab,
|
||||
|
||||
@@ -3218,6 +3218,7 @@ mod torrent_probe;
|
||||
pub mod torrent;
|
||||
mod settings;
|
||||
mod storage;
|
||||
mod window_geometry;
|
||||
pub use error::AppError;
|
||||
|
||||
// Retained only for compatibility with the optional aria2 diagnostic monitor.
|
||||
@@ -14285,6 +14286,33 @@ pub fn run() {
|
||||
// Build the window only after all command state is registered. This
|
||||
// prevents the frontend from racing startup and invoking IPC before
|
||||
// the database and portable storage layout are available.
|
||||
let startup_size = persisted_settings
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.main_window_size.as_ref())
|
||||
.and_then(|size| crate::window_geometry::normalize_main_window_size(Some(size)))
|
||||
.unwrap_or_else(crate::window_geometry::default_main_window_size);
|
||||
let startup_size = app
|
||||
.primary_monitor()
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|monitor| {
|
||||
let scale_factor = monitor.scale_factor();
|
||||
if !scale_factor.is_finite() || scale_factor <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let work_area = monitor.work_area().size;
|
||||
let logical_width = (work_area.width as f64 / scale_factor).round() as u32;
|
||||
let logical_height = (work_area.height as f64 / scale_factor).round() as u32;
|
||||
Some(crate::window_geometry::clamp_main_window_size(
|
||||
startup_size.clone(),
|
||||
logical_width,
|
||||
logical_height,
|
||||
))
|
||||
})
|
||||
.unwrap_or(startup_size);
|
||||
main_window_builder = main_window_builder
|
||||
.inner_size(startup_size.width as f64, startup_size.height as f64)
|
||||
.prevent_overflow();
|
||||
main_window_builder
|
||||
.build()
|
||||
.map_err(|error| format!("failed to create main window: {error}"))?;
|
||||
|
||||
@@ -386,6 +386,23 @@ fn sanitize_persisted_setting_values(state: &mut Value) {
|
||||
return;
|
||||
};
|
||||
|
||||
let main_window_size = state
|
||||
.get("mainWindowSize")
|
||||
.cloned()
|
||||
.and_then(|value| serde_json::from_value::<crate::ipc::MainWindowSize>(value).ok())
|
||||
.and_then(|size| crate::window_geometry::normalize_main_window_size(Some(&size)));
|
||||
match main_window_size {
|
||||
Some(size) => {
|
||||
state.insert(
|
||||
"mainWindowSize".to_string(),
|
||||
serde_json::to_value(size).expect("main window size is serializable"),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
state.remove("mainWindowSize");
|
||||
}
|
||||
}
|
||||
|
||||
sanitize_integer_setting(state, "maxConcurrentDownloads", |value| value.as_u64().is_some());
|
||||
sanitize_integer_setting(state, "perServerConnections", |value| value.as_i64().is_some());
|
||||
sanitize_integer_setting(state, "maxAutomaticRetries", |value| value.as_i64().is_some());
|
||||
@@ -575,6 +592,9 @@ fn sanitize_allowed_string(
|
||||
}
|
||||
|
||||
fn validate_settings(settings: &mut PersistedSettings) {
|
||||
settings.main_window_size = crate::window_geometry::normalize_main_window_size(
|
||||
settings.main_window_size.as_ref(),
|
||||
);
|
||||
if settings.max_concurrent_downloads == 0 {
|
||||
settings.max_concurrent_downloads = default_settings().max_concurrent_downloads;
|
||||
}
|
||||
@@ -838,6 +858,8 @@ fn default_settings() -> PersistedSettings {
|
||||
speed_limit_preset_values: vec![1.0, 5.0, 10.0],
|
||||
logs_enabled: false,
|
||||
is_sidebar_visible: true,
|
||||
is_folders_collapsed: false,
|
||||
main_window_size: None,
|
||||
sidebar_position: "auto".to_string(),
|
||||
active_settings_tab: SettingsTab::Downloads,
|
||||
scheduler: SchedulerSettings {
|
||||
@@ -1378,6 +1400,51 @@ mod tests {
|
||||
assert!(!default_settings().remember_last_used_download_directory);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_settings_without_geometry_use_no_persisted_size() {
|
||||
let settings = decode_stored_settings(&Value::String(
|
||||
json!({ "state": { "theme": "system" }, "version": 0 }).to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert!(settings.main_window_size.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_main_window_geometry_round_trips() {
|
||||
let settings = decode_stored_settings(&Value::String(
|
||||
json!({
|
||||
"state": { "mainWindowSize": { "width": 1440, "height": 900 } },
|
||||
"version": 6
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
settings
|
||||
.main_window_size
|
||||
.as_ref()
|
||||
.map(|size| (size.width, size.height)),
|
||||
Some((1440, 900))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_and_out_of_range_geometry_is_dropped() {
|
||||
for geometry in [
|
||||
json!({ "width": "1440", "height": 900 }),
|
||||
json!({ "width": 959, "height": 900 }),
|
||||
json!({ "width": 1440, "height": 16_385 }),
|
||||
] {
|
||||
let settings = decode_stored_settings(&Value::String(
|
||||
json!({ "state": { "mainWindowSize": geometry }, "version": 6 }).to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
assert!(settings.main_window_size.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_disabled_last_used_download_directory_setting() {
|
||||
let stored = json!({
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
use crate::ipc::MainWindowSize;
|
||||
|
||||
pub const MAIN_WINDOW_DEFAULT_WIDTH: u32 = 1280;
|
||||
pub const MAIN_WINDOW_DEFAULT_HEIGHT: u32 = 800;
|
||||
pub const MAIN_WINDOW_MIN_WIDTH: u32 = 960;
|
||||
pub const MAIN_WINDOW_MIN_HEIGHT: u32 = 640;
|
||||
pub const MAIN_WINDOW_MAX_WIDTH: u32 = 16_384;
|
||||
pub const MAIN_WINDOW_MAX_HEIGHT: u32 = 16_384;
|
||||
|
||||
pub fn default_main_window_size() -> MainWindowSize {
|
||||
MainWindowSize {
|
||||
width: MAIN_WINDOW_DEFAULT_WIDTH,
|
||||
height: MAIN_WINDOW_DEFAULT_HEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_main_window_size(size: Option<&MainWindowSize>) -> Option<MainWindowSize> {
|
||||
let size = size?;
|
||||
if size.width < MAIN_WINDOW_MIN_WIDTH
|
||||
|| size.height < MAIN_WINDOW_MIN_HEIGHT
|
||||
|| size.width > MAIN_WINDOW_MAX_WIDTH
|
||||
|| size.height > MAIN_WINDOW_MAX_HEIGHT
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(size.clone())
|
||||
}
|
||||
|
||||
pub fn clamp_main_window_size(
|
||||
size: MainWindowSize,
|
||||
work_area_width: u32,
|
||||
work_area_height: u32,
|
||||
) -> MainWindowSize {
|
||||
let width_limit = work_area_width.max(MAIN_WINDOW_MIN_WIDTH);
|
||||
let height_limit = work_area_height.max(MAIN_WINDOW_MIN_HEIGHT);
|
||||
MainWindowSize {
|
||||
width: size.width.min(width_limit),
|
||||
height: size.height.min(height_limit),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
clamp_main_window_size, default_main_window_size, normalize_main_window_size,
|
||||
MAIN_WINDOW_MIN_HEIGHT, MAIN_WINDOW_MIN_WIDTH,
|
||||
};
|
||||
use crate::ipc::MainWindowSize;
|
||||
|
||||
#[test]
|
||||
fn default_size_matches_the_main_window_configuration() {
|
||||
assert_eq!(default_main_window_size().width, 1280);
|
||||
assert_eq!(default_main_window_size().height, 800);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_sizes_outside_the_persisted_bounds() {
|
||||
assert!(normalize_main_window_size(Some(&MainWindowSize {
|
||||
width: MAIN_WINDOW_MIN_WIDTH - 1,
|
||||
height: 800,
|
||||
}))
|
||||
.is_none());
|
||||
assert!(normalize_main_window_size(Some(&MainWindowSize {
|
||||
width: 1280,
|
||||
height: 16_385,
|
||||
}))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_a_valid_size_to_the_available_work_area() {
|
||||
let clamped = clamp_main_window_size(
|
||||
MainWindowSize {
|
||||
width: 1600,
|
||||
height: 1000,
|
||||
},
|
||||
1280,
|
||||
720,
|
||||
);
|
||||
assert_eq!(clamped.width, 1280);
|
||||
assert_eq!(clamped.height, 720);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_the_minimum_when_the_work_area_is_shorter_than_the_minimum() {
|
||||
let clamped = clamp_main_window_size(
|
||||
MainWindowSize {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
},
|
||||
800,
|
||||
500,
|
||||
);
|
||||
assert_eq!(clamped.width, MAIN_WINDOW_MIN_WIDTH);
|
||||
assert_eq!(clamped.height, MAIN_WINDOW_MIN_HEIGHT);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
"create": false,
|
||||
"title": "Firelink",
|
||||
"width": 1280,
|
||||
"height": 760,
|
||||
"height": 800,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"transparent": false
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"create": false,
|
||||
"title": "Firelink",
|
||||
"width": 1280,
|
||||
"height": 760,
|
||||
"height": 800,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"transparent": false,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"create": false,
|
||||
"title": "Firelink",
|
||||
"width": 1280,
|
||||
"height": 760,
|
||||
"height": 800,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"transparent": true,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"create": false,
|
||||
"title": "Firelink",
|
||||
"width": 1280,
|
||||
"height": 760,
|
||||
"height": 800,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"transparent": true,
|
||||
|
||||
Reference in New Issue
Block a user