Make "Choose folder" an actual folder picker

It previously fetched the folder list and threw it away into a status
line — a button that looked like it worked and did not. Now it opens a
browsable picker: click a folder to descend, ".." to go back, "Use this
folder" to select, "Cancel" to leave the root unchanged.

Descends one level per click because that is what the backend supports:
Depth: infinity is frequently disabled server-side and prohibitively
expensive where it is not (ARCH §8.4).

The chosen root persists immediately on confirm, so it survives a crash
before the library is opened. Confirming at the account root is allowed —
a user may legitimately keep everything at the top level — and cancelling
leaves any previous selection untouched, which a test asserts.

Verified against nextcloud.tourolle.paris at both depths: 30 folders at
the root, 21 year-folders inside PhotosRaw.

19 launch tests, 38 in dr-ui.
This commit is contained in:
2026-08-09 15:50:40 +02:00
parent f630a3ff81
commit 2ca1716a29
10 changed files with 888 additions and 65 deletions
+78 -4
View File
@@ -66,6 +66,20 @@ pub fn render(window: &AppWindow, controller: &LaunchController) {
window.set_launch_status(status.unwrap_or_default().into());
window.set_launch_error(m.error.clone().unwrap_or_default().into());
// Folder picker.
let browsing = m.browser.is_some();
window.set_launch_browsing(browsing);
if let Some(b) = &m.browser {
window.set_launch_browse_path(b.path.clone().into());
window.set_launch_browse_loading(b.loading);
let entries: Vec<slint::SharedString> = b
.entries
.iter()
.map(|e| slint::SharedString::from(e.as_str()))
.collect();
window.set_launch_browse_entries(slint::ModelRc::new(slint::VecModel::from(entries)));
}
let labels: Vec<slint::SharedString> = m
.formats
.iter()
@@ -148,9 +162,9 @@ where
let ctl = controller.clone();
window.on_launch_choose_folder(move || {
let Some(w) = weak.upgrade() else { return };
ctl.model.borrow_mut().busy("Listing folders…");
ctl.model.borrow_mut().open_browser();
render(&w, &ctl);
spawn_folder_list(w.as_weak(), ctl.clone());
spawn_folder_list(w.as_weak(), ctl.clone(), String::new());
});
}
@@ -168,6 +182,66 @@ where
});
}
// --- folder picker navigation ---------------------------------------
{
let weak = window.as_weak();
let ctl = controller.clone();
window.on_launch_browse_into(move |name| {
let Some(w) = weak.upgrade() else { return };
let target = {
let m = ctl.model.borrow();
m.browser.as_ref().map(|b| b.child_path(&name))
};
if let Some(path) = target {
ctl.model.borrow_mut().browse_to(path.clone());
render(&w, &ctl);
spawn_folder_list(w.as_weak(), ctl.clone(), path);
}
});
}
{
let weak = window.as_weak();
let ctl = controller.clone();
window.on_launch_browse_up(move || {
let Some(w) = weak.upgrade() else { return };
let parent = {
let m = ctl.model.borrow();
m.browser.as_ref().and_then(|b| b.parent_path())
};
if let Some(path) = parent {
ctl.model.borrow_mut().browse_to(path.clone());
render(&w, &ctl);
spawn_folder_list(w.as_weak(), ctl.clone(), path);
}
});
}
{
let weak = window.as_weak();
let ctl = controller.clone();
window.on_launch_browse_confirm(move || {
let Some(w) = weak.upgrade() else { return };
let chosen = ctl.model.borrow_mut().choose_current_folder();
if let Some(session) = chosen {
// Persist immediately: a chosen root must survive a crash
// before the library is opened.
if let Err(e) = ctl.store.update(&session) {
log::warn!("saving library root: {e}");
}
log::info!("library root set to /{}", session.root);
}
render(&w, &ctl);
});
}
{
let weak = window.as_weak();
let ctl = controller.clone();
window.on_launch_browse_cancel(move || {
let Some(w) = weak.upgrade() else { return };
ctl.model.borrow_mut().close_browser();
render(&w, &ctl);
});
}
// --- copy the login URL ---------------------------------------------
{
let ctl = controller.clone();
@@ -301,7 +375,7 @@ fn poll_channel(
}
/// List top-level folders so one can be chosen as the library root.
fn spawn_folder_list(weak: slint::Weak<AppWindow>, ctl: Rc<LaunchController>) {
fn spawn_folder_list(weak: slint::Weak<AppWindow>, ctl: Rc<LaunchController>, path: String) {
let Some(session) = ctl.model.borrow().session().cloned() else {
return;
};
@@ -329,7 +403,7 @@ fn spawn_folder_list(weak: slint::Weak<AppWindow>, ctl: Rc<LaunchController>) {
};
rt.block_on(async {
match NextcloudBackend::new(&creds, &user_id) {
Ok(b) => match b.list(&RemotePath::root(), None).await {
Ok(b) => match b.list(&RemotePath::new(&path), None).await {
Ok(entries) => {
let mut dirs: Vec<String> = entries
.iter()