use crate::{
LibraryModel, MainWindow, RemoteWorkerModel,
bridge::library::{
client as library_client,
mirror::{self as library_mirror, MirrorHandle, MirrorOutcome, MirrorProgress},
source::{LibrarySource, spawn_library_request},
},
gui::show_toast,
settings::{SettingsPersister, WorkerSettings},
};
use indicatrix_net::library::{AttributeRangesWire, LibraryRequest, LibraryResponse};
use indicatrix_vault::db::sqlite::Database;
use slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};
use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, Mutex, PoisonError},
};
pub fn setup_library_source_callbacks(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
settings_store: &Arc<SettingsPersister>,
) {
let db_switch = Arc::clone(db);
let source_switch = Arc::clone(source);
let settings_switch = settings_store.clone();
let ui_weak = ui.as_weak();
ui.global::<RemoteWorkerModel>().on_switch_library_source(move |idx: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if idx < 0 {
crate::gui::library::search::bump_search_seq();
*source_switch
.lock()
.unwrap_or_else(PoisonError::into_inner) = LibrarySource::Local;
apply_source_badge(&ui, &LibrarySource::Local);
ui.global::<RemoteWorkerModel>().set_library_source_worker_index(-1);
crate::gui::library::diagram_list::load_filter_options_and_initial_list(&ui, &db_switch);
show_toast(&ui, "Switched to the local library.", "info");
return;
}
let Some(worker) = settings_switch.snapshot().settings.remote_worker() else {
show_toast(&ui, "No remote coordinator is configured.", "error");
return;
};
ui.global::<LibraryModel>().set_status_message(format!("Connecting to {}...", worker_display_name(&worker)).into());
let switch_seq = crate::gui::library::search::bump_search_seq();
let source_probe = Arc::clone(&source_switch);
let ui_weak_result = ui.as_weak();
let worker_for_probe = worker;
std::thread::spawn(move || {
let result = library_client::probe(&worker_for_probe);
let _ = ui_weak_result.upgrade_in_event_loop(move |ui| {
if !crate::gui::library::search::is_current_search(switch_seq) {
return;
}
match result {
Ok(info) if info.library => {
crate::gui::library::search::bump_search_seq();
let new_source = LibrarySource::Remote(worker_for_probe);
*source_probe.lock().unwrap_or_else(PoisonError::into_inner) =
new_source.clone();
apply_source_badge(&ui, &new_source);
ui.global::<RemoteWorkerModel>().set_library_source_worker_index(0);
if let Some(w) = new_source.worker() {
load_filter_options_and_initial_list_remote(&ui, w.clone());
}
show_toast(&ui, &format!("Now browsing: {}", new_source.label()), "success");
ui.global::<LibraryModel>().set_status_message(
"\"My designs\" and the just-imported filter apply to the local \
library only."
.into(),
);
}
Ok(_) => {
show_toast(
&ui,
"That remote is reachable but does not serve a design library -- staying on the current library.",
"error",
);
ui.global::<LibraryModel>().set_status_message("Ready.".into());
}
Err(e) => {
show_toast(&ui, &format!("Could not switch library: {e}"), "error");
ui.global::<LibraryModel>().set_status_message("Ready.".into());
}
}
});
});
});
}
fn apply_source_badge(ui: &MainWindow, source: &LibrarySource) {
ui.global::<RemoteWorkerModel>()
.set_library_source_label(source.label().into());
ui.global::<RemoteWorkerModel>()
.set_library_is_remote(source.is_remote());
crate::gui::rough_plan::refresh_links_enabled();
}
fn load_filter_options_and_initial_list_remote(ui: &MainWindow, worker: WorkerSettings) {
let seq = crate::gui::library::search::bump_search_seq();
let worker_for_search = worker.clone();
crate::bridge::library::source::spawn_library_request(
ui.as_weak(),
worker,
LibraryRequest::FilterOptions,
move |ui, result| {
if !crate::gui::library::search::is_current_search(seq) {
return;
}
match result {
Ok(LibraryResponse::FilterOptions {
shapes,
gears,
ranges,
}) => {
apply_shape_gear_options(ui, &shapes, &gears);
apply_range_wire_to_ui(ui, &ranges);
crate::gui::library::diagram_list::reset_search_and_filter_ui_inputs(ui);
}
Ok(_) => {
ui.global::<LibraryModel>().set_status_message(
"Unexpected reply loading remote filter options.".into(),
);
}
Err(e) => ui.global::<LibraryModel>().set_status_message(
format!("Could not load remote filter options: {e}").into(),
),
}
crate::gui::library::search::refresh_diagram_list_remote(
ui,
worker_for_search,
"",
"All Shapes",
"All Gears",
);
},
);
}
fn worker_display_name(worker: &WorkerSettings) -> &str {
if worker.name.trim().is_empty() {
&worker.address
} else {
&worker.name
}
}
fn apply_shape_gear_options(ui: &MainWindow, shapes: &[String], gears: &[String]) {
let mut shape_opts = vec!["All Shapes".to_string()];
shape_opts.extend(shapes.iter().cloned());
let shape_model: Vec<SharedString> = shape_opts.into_iter().map(Into::into).collect();
ui.global::<LibraryModel>()
.set_shape_options(ModelRc::new(VecModel::from(shape_model)));
let mut gear_opts = vec!["All Gears".to_string()];
gear_opts.extend(gears.iter().cloned());
let gear_model: Vec<SharedString> = gear_opts.into_iter().map(Into::into).collect();
ui.global::<LibraryModel>()
.set_gear_options(ModelRc::new(VecModel::from(gear_model)));
ui.global::<LibraryModel>().set_selected_shape_index(0);
ui.global::<LibraryModel>().set_selected_gear_index(0);
}
fn apply_range_wire_to_ui(ui: &MainWindow, ranges: &AttributeRangesWire) {
ui.global::<LibraryModel>()
.set_ri_bounds_min(ranges.ri.0 as f32);
ui.global::<LibraryModel>()
.set_ri_bounds_max(ranges.ri.1 as f32);
ui.global::<LibraryModel>()
.set_ri_filter_min(ranges.ri.0 as f32);
ui.global::<LibraryModel>()
.set_ri_filter_max(ranges.ri.1 as f32);
ui.global::<LibraryModel>()
.set_lw_bounds_min(ranges.lw_ratio.0 as f32);
ui.global::<LibraryModel>()
.set_lw_bounds_max(ranges.lw_ratio.1 as f32);
ui.global::<LibraryModel>()
.set_lw_filter_min(ranges.lw_ratio.0 as f32);
ui.global::<LibraryModel>()
.set_lw_filter_max(ranges.lw_ratio.1 as f32);
ui.global::<LibraryModel>()
.set_volume_bounds_min(ranges.volume.0 as f32);
ui.global::<LibraryModel>()
.set_volume_bounds_max(ranges.volume.1 as f32);
ui.global::<LibraryModel>()
.set_volume_filter_min(ranges.volume.0 as f32);
ui.global::<LibraryModel>()
.set_volume_filter_max(ranges.volume.1 as f32);
ui.global::<LibraryModel>()
.set_facets_bounds_min(ranges.facets.0 as f32);
ui.global::<LibraryModel>()
.set_facets_bounds_max(ranges.facets.1 as f32);
ui.global::<LibraryModel>()
.set_facets_filter_min(ranges.facets.0 as f32);
ui.global::<LibraryModel>()
.set_facets_filter_max(ranges.facets.1 as f32);
}
pub fn setup_mirror_sync_callbacks(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
settings_store: &Arc<SettingsPersister>,
) {
let handle: Rc<RefCell<Option<MirrorHandle>>> = Rc::new(RefCell::new(None));
let db_start = Arc::clone(db);
let settings_start = settings_store.clone();
let handle_start = handle.clone();
let ui_weak_start = ui.as_weak();
ui.global::<RemoteWorkerModel>()
.on_start_mirror_sync(move || {
let Some(ui) = ui_weak_start.upgrade() else {
return;
};
let Some(worker) = settings_start.snapshot().settings.remote_worker() else {
show_toast(&ui, "No remote coordinator is configured.", "error");
return;
};
ui.global::<RemoteWorkerModel>()
.set_mirror_in_progress(true);
ui.global::<RemoteWorkerModel>()
.set_mirror_progress_fraction(0.0);
ui.global::<RemoteWorkerModel>().set_mirror_has_error(false);
ui.global::<RemoteWorkerModel>()
.set_mirror_status_text("Listing the remote library...".into());
let new_handle = library_mirror::spawn_mirror_sync(
ui.as_weak(),
Arc::clone(&db_start),
worker,
library_mirror::MirrorOptions::default(),
|ui, progress| on_mirror_progress(ui, &progress),
|ui, outcome| on_mirror_done(ui, &outcome),
);
*handle_start.borrow_mut() = Some(new_handle);
});
let handle_cancel = handle;
ui.global::<RemoteWorkerModel>()
.on_cancel_mirror_sync(move || {
if let Some(h) = handle_cancel.borrow().as_ref() {
h.cancel();
}
});
}
fn on_mirror_progress(ui: &MainWindow, progress: &MirrorProgress) {
let total = progress.counts.total_found.max(1);
let fraction = progress.processed as f32 / total as f32;
ui.global::<RemoteWorkerModel>()
.set_mirror_progress_fraction(fraction);
ui.global::<RemoteWorkerModel>().set_mirror_status_text(
format!(
"{}/{} -- {} (new {}, updated {}, unchanged {}, local conflicts {}, failed {})",
progress.processed,
progress.counts.total_found,
progress.current_title,
progress.counts.new_count,
progress.counts.updated_count,
progress.counts.skipped_unchanged,
progress.counts.local_conflicts_skipped,
progress.counts.failed,
)
.into(),
);
}
fn orphaned_mirror_states_suffix(orphaned: u64) -> String {
if orphaned == 0 {
String::new()
} else {
format!(" ({orphaned} design(s) skipped -- deleted locally.)")
}
}
fn on_mirror_done(ui: &MainWindow, outcome: &MirrorOutcome) {
ui.global::<RemoteWorkerModel>()
.set_mirror_in_progress(false);
let (text, is_error, toast_kind) = match outcome {
MirrorOutcome::Completed(c) => (
format!(
"Mirror complete: {} new, {} updated, {} unchanged, {} deleted locally \
(skipped), {} local conflicts skipped, {} failed (of {} found).{}",
c.new_count,
c.updated_count,
c.skipped_unchanged,
c.skipped_deleted,
c.local_conflicts_skipped,
c.failed,
c.total_found,
orphaned_mirror_states_suffix(c.orphaned_mirror_states),
),
c.failed > 0,
if c.failed > 0 { "error" } else { "success" },
),
MirrorOutcome::Cancelled(c) => (
format!(
"Mirror cancelled after {} new, {} updated, {} unchanged, {} deleted \
locally (skipped), {} local conflicts skipped (of {} found so far).{}",
c.new_count,
c.updated_count,
c.skipped_unchanged,
c.skipped_deleted,
c.local_conflicts_skipped,
c.total_found,
orphaned_mirror_states_suffix(c.orphaned_mirror_states),
),
false,
"info",
),
MirrorOutcome::Failed(msg) => (format!("Mirror failed: {msg}"), true, "error"),
};
ui.global::<RemoteWorkerModel>()
.set_mirror_has_error(is_error);
ui.global::<RemoteWorkerModel>()
.set_mirror_status_text(text.clone().into());
show_toast(ui, &text, toast_kind);
}
#[derive(Debug)]
pub struct RemoteDesignSource {
pub file_name: String,
pub asc_text: String,
}
pub fn fetch_remote_design_source(
ui_weak: Weak<MainWindow>,
worker: WorkerSettings,
entry_id: i64,
on_done: impl FnOnce(&MainWindow, Result<RemoteDesignSource, String>) + Send + 'static,
) {
spawn_library_request(
ui_weak,
worker,
LibraryRequest::FetchDesignSource { entry_id },
move |ui, result| on_done(ui, map_design_source_response(result)),
);
}
fn map_design_source_response(
result: Result<LibraryResponse, library_client::LibraryClientError>,
) -> Result<RemoteDesignSource, String> {
match result {
Ok(LibraryResponse::DesignSource {
file_name,
asc_text,
..
}) => Ok(RemoteDesignSource {
file_name,
asc_text,
}),
Ok(LibraryResponse::NotFound) => {
Err("That design no longer exists on the remote library.".to_string())
}
Ok(LibraryResponse::DesignSourceNotAvailable) => Err(
"This design has no attached .asc, .gem or .gcs file on the remote library -- \
nothing to load."
.to_string(),
),
Ok(LibraryResponse::Error(e)) => Err(e.message),
Ok(_) => Err("Unexpected reply fetching the remote design source.".to_string()),
Err(e) => Err(format!("Could not reach the remote library: {e}")),
}
}
#[cfg(test)]
mod remote_design_source_tests {
use super::{RemoteDesignSource, map_design_source_response};
use indicatrix_net::{library::LibraryResponse, messages::ErrorMsg};
#[test]
fn a_design_source_reply_maps_to_ok() {
let result = map_design_source_response(Ok(LibraryResponse::DesignSource {
entry_id: 7,
file_name: "round-brilliant.asc".to_string(),
asc_text: "GemCad 5.0\n".to_string(),
}));
let RemoteDesignSource {
file_name,
asc_text,
} = result.expect("DesignSource must map to Ok");
assert_eq!(file_name, "round-brilliant.asc");
assert_eq!(asc_text, "GemCad 5.0\n");
}
#[test]
fn not_found_maps_to_a_distinct_message_from_not_available() {
let not_found = map_design_source_response(Ok(LibraryResponse::NotFound))
.expect_err("NotFound must map to Err");
let not_available =
map_design_source_response(Ok(LibraryResponse::DesignSourceNotAvailable))
.expect_err("DesignSourceNotAvailable must map to Err");
assert_ne!(
not_found, not_available,
"the two failure reasons must not read the same to the user"
);
}
#[test]
fn a_server_error_reply_surfaces_its_own_message() {
let err = map_design_source_response(Ok(LibraryResponse::Error(ErrorMsg {
code: indicatrix_net::messages::error_codes::LIBRARY_FAILED,
message: "internal error serving the design library".to_string(),
request_id: None,
})))
.expect_err("Error must map to Err");
assert_eq!(err, "internal error serving the design library");
}
#[test]
fn an_unexpected_reply_shape_is_reported_rather_than_panicking() {
let err = map_design_source_response(Ok(LibraryResponse::FilterOptions {
shapes: Vec::new(),
gears: Vec::new(),
ranges: indicatrix_net::library::AttributeRangesWire {
ri: (0.0, 0.0),
lw_ratio: (0.0, 0.0),
volume: (0.0, 0.0),
facets: (0, 0),
},
}))
.expect_err("an unrelated reply shape must map to Err, not panic");
assert!(err.contains("Unexpected reply"));
}
}