Choose the export folder by walking the server, not by typing it
The destination for a Nextcloud export was a text field. Nobody recalls the exact spelling of a path three levels down, and getting it wrong does not fail — `create_dir` makes whatever was typed, so a misremembered folder becomes a new one at the root and the exports are somewhere nobody looks. So it is picked the way the library root is picked, using the same `FolderBrowser` model the launch screen drives: up, into, and "use this folder", confirming the folder currently *shown* rather than one selected in the list. Same rule in both places, so the phrase means one thing. The model is shared; the worker is not. `settings_ui::spawn_folder_list` is a near-twin of the launch screen's, because that one reaches into the `LaunchController` for its session and reports onto the launch screen's error line, while this one is handed credentials and writes to the settings page. Factoring them together needs a function taking both controllers or a trait implemented twice to abstract two call sites — more machinery than the twenty lines it saves. What matters is shared already: navigation behaves identically because both drive the same model. The callbacks are wired in `lib.rs` rather than in `settings_ui::wire`, because listing a remote folder needs credentials and the settings page holds no session on purpose — it is reachable before a library is opened and must not depend on one existing. With no account the picker says to sign in first, rather than showing an empty list that reads as a server with no folders. Details that are decisions rather than accidents: the picker opens at the library root rather than at whatever half-typed path is in the field, which would list nothing and look broken. The listing area is a fixed 180px, since a folder with sixty children would otherwise push the rest of the settings page off the bottom. "Up" is disabled at the root rather than hidden, so the row does not jump as the user navigates. A failed listing leaves the picker open on the folder it was showing — where the user had got to is not something to discard over a dropped request. And the chosen folder saves immediately like every other setting on a page that has no Save button. The poll timer lives on the controller for the reason `LaunchController` keeps its own there: a `slint::Timer` stops when dropped, so one local to the function that starts it would be collected before the listing arrived. Carries in-flight work from a parallel session — a segmentation pass in dr-gpu, a sidecar cache, and the develop panel's continuing changes. 1020 tests pass, fmt clean. One clippy warning remains and is not mine: `sidecar_cache::dir` is unused while that work is in progress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+129
-2
@@ -28,6 +28,7 @@ mod net_runtime;
|
||||
mod presets;
|
||||
mod settings_store;
|
||||
mod settings_ui;
|
||||
mod sidecar_cache;
|
||||
mod trash;
|
||||
|
||||
use std::cell::RefCell;
|
||||
@@ -768,6 +769,127 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
library.set_keep_opened_originals(stored.cache.keep_opened_originals);
|
||||
}
|
||||
|
||||
// --- the export folder picker ----------------------------------------
|
||||
//
|
||||
// Wired here rather than inside `settings_ui::wire` because listing a
|
||||
// remote folder needs credentials, and the settings page deliberately
|
||||
// holds no session — it is reachable before a library is opened and must
|
||||
// not depend on one existing.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = settings.clone();
|
||||
let library = library.clone();
|
||||
|
||||
// Every entry point needs the same three things, so they are fetched
|
||||
// once here rather than at four call sites.
|
||||
let start = move |ctl: &Rc<settings_ui::SettingsController>,
|
||||
weak: &slint::Weak<AppWindow>,
|
||||
library: &Rc<library_ui::LibraryController>,
|
||||
path: String| {
|
||||
match library.session() {
|
||||
Some((creds, session)) => {
|
||||
settings_ui::spawn_folder_list(
|
||||
weak.clone(),
|
||||
ctl.clone(),
|
||||
creds,
|
||||
session.user_id.clone(),
|
||||
path,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
// No account, so nothing to browse. Said plainly rather
|
||||
// than left as an empty list, which would read as a server
|
||||
// with no folders on it.
|
||||
ctl.set_error("Sign in to a library before choosing a folder on it.");
|
||||
ctl.browser.replace(None);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let (weak, ctl, library, start) =
|
||||
(weak.clone(), ctl.clone(), library.clone(), start);
|
||||
window.on_settings_browse_open_picker(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// Opens on the library root rather than on whatever the
|
||||
// destination field happens to contain: a half-typed path
|
||||
// would list nothing and look like a broken picker.
|
||||
ctl.browser.replace(Some(launch::FolderBrowser {
|
||||
path: String::new(),
|
||||
entries: Vec::new(),
|
||||
loading: true,
|
||||
}));
|
||||
start(&ctl, &weak, &library, String::new());
|
||||
settings_ui::render(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let (weak, ctl, library, start) =
|
||||
(weak.clone(), ctl.clone(), library.clone(), start);
|
||||
window.on_settings_browse_into(move |name| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let path = {
|
||||
let mut browser = ctl.browser.borrow_mut();
|
||||
let Some(b) = browser.as_mut() else { return };
|
||||
let path = b.child_path(&name);
|
||||
b.path = path.clone();
|
||||
b.entries.clear();
|
||||
b.loading = true;
|
||||
path
|
||||
};
|
||||
start(&ctl, &weak, &library, path);
|
||||
settings_ui::render(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let (weak, ctl, library, start) =
|
||||
(weak.clone(), ctl.clone(), library.clone(), start);
|
||||
window.on_settings_browse_up(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let path = {
|
||||
let mut browser = ctl.browser.borrow_mut();
|
||||
let Some(b) = browser.as_mut() else { return };
|
||||
let Some(path) = b.parent_path() else { return };
|
||||
b.path = path.clone();
|
||||
b.entries.clear();
|
||||
b.loading = true;
|
||||
path
|
||||
};
|
||||
start(&ctl, &weak, &library, path);
|
||||
settings_ui::render(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let (weak, ctl) = (weak.clone(), ctl.clone());
|
||||
window.on_settings_browse_confirm(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// The folder being *shown* is the one chosen, matching the
|
||||
// library picker — so "use this one" means the same thing in
|
||||
// both places rather than depending on a selection the list
|
||||
// does not have.
|
||||
let chosen = ctl.browser.borrow().as_ref().map(|b| b.path.clone());
|
||||
if let Some(path) = chosen {
|
||||
ctl.set_destination(path);
|
||||
}
|
||||
ctl.browser.replace(None);
|
||||
settings_ui::render(&w, &ctl);
|
||||
refresh_export_label(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let (weak, ctl) = (weak.clone(), ctl.clone());
|
||||
window.on_settings_browse_cancel(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
ctl.browser.replace(None);
|
||||
settings_ui::render(&w, &ctl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let lib = library.clone();
|
||||
let ctl = settings.clone();
|
||||
let weak = window.as_weak();
|
||||
@@ -1134,8 +1256,13 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
||||
// it to — and starting it here means the edit is ready when the
|
||||
// photograph is, instead of the image appearing at its defaults
|
||||
// and visibly changing a moment later.
|
||||
let sidecar_rx =
|
||||
library::spawn_sidecar_fetch(creds.clone(), user_id.clone(), path.clone());
|
||||
let sidecar_rx = library::spawn_sidecar_fetch(
|
||||
creds.clone(),
|
||||
user_id.clone(),
|
||||
path.clone(),
|
||||
library.sidecar_cache_dir().unwrap_or_default(),
|
||||
library.is_offline(),
|
||||
);
|
||||
|
||||
// TRACES: FR-NC-6a
|
||||
// The cache is consulted first, so a second open of the same
|
||||
|
||||
Reference in New Issue
Block a user