Let the top level be chosen as the library root
Confirming "/" in the folder picker set an empty root, which the launch model read as no root at all: "Open library" stayed disabled after the question had plainly been answered, and a folder library — whose folder is the whole library — could never be opened without first descending into a subfolder of it. The empty string was carrying two meanings. Record the choice as its own fact on the account (`root_chosen`, defaulted so existing configuration loads unchanged), treat a folder endpoint as chosen by definition, and let the launch screen say so: a folder is shown as a LIBRARY rather than an ACCOUNT, the second question becomes an optional "scan only a subfolder", and the library header names the folder instead of calling it "· whole account".
This commit is contained in:
+79
-10
@@ -218,6 +218,43 @@ impl LaunchModel {
|
||||
self.session().map(|s| s.root.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Whether a root has been settled on, including the top level itself.
|
||||
///
|
||||
/// Three ways to have one: a subfolder is named; the picker confirmed
|
||||
/// the top level, which `Account::root_chosen` records because the
|
||||
/// empty string cannot; or the endpoint is a folder, whose top level is
|
||||
/// the library by definition.
|
||||
pub fn root_is_chosen(&self) -> bool {
|
||||
self.session()
|
||||
.is_some_and(|s| !s.root.is_empty() || s.root_chosen)
|
||||
|| self.endpoint_is_library()
|
||||
}
|
||||
|
||||
/// What the signed-in section shows for the root: empty when nothing is
|
||||
/// chosen, `/` for the top level, else the path.
|
||||
pub fn library_root_label(&self) -> String {
|
||||
let root = self.library_root();
|
||||
if root.is_empty() && self.root_is_chosen() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
root
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the endpoint itself is the library.
|
||||
///
|
||||
/// A folder account has no sub-root to choose: the directory the user
|
||||
/// typed is the whole library, and asking them to pick a "library
|
||||
/// folder" inside it a second time reads as though the first answer was
|
||||
/// not taken. A cloud account is the opposite — its endpoint is an
|
||||
/// entire server-side tree, and a root inside it is required (see
|
||||
/// [`can_open_library`](Self::can_open_library)). Keyed on the absence
|
||||
/// of a login, the same fact [`prefill`] keys on, rather than on the
|
||||
/// connector's id.
|
||||
pub fn endpoint_is_library(&self) -> bool {
|
||||
self.session().is_some_and(|s| s.login.is_empty())
|
||||
}
|
||||
|
||||
/// The login URL while approval is pending.
|
||||
pub fn login_url(&self) -> String {
|
||||
match &self.state {
|
||||
@@ -228,11 +265,13 @@ impl LaunchModel {
|
||||
|
||||
/// Whether "Open library" should be clickable.
|
||||
///
|
||||
/// Requires a signed-in account *and* a chosen folder: opening without one
|
||||
/// would scan the whole account, which on a real library is thousands of
|
||||
/// directories the user did not ask for.
|
||||
/// A cloud account requires a signed-in session *and* a chosen folder:
|
||||
/// opening without one would scan the whole account, which on a real
|
||||
/// library is thousands of directories the user did not ask for. Chosen
|
||||
/// is the operative word, not non-empty — the top level is a legitimate
|
||||
/// choice, and a folder library is the whole folder by definition.
|
||||
pub fn can_open_library(&self) -> bool {
|
||||
self.is_signed_in() && !self.library_root().is_empty()
|
||||
self.is_signed_in() && self.root_is_chosen()
|
||||
}
|
||||
|
||||
/// What the app should do on startup.
|
||||
@@ -386,6 +425,9 @@ impl LaunchModel {
|
||||
let path = self.browser.as_ref()?.path.clone();
|
||||
let mut session = self.session()?.clone();
|
||||
session.root = path;
|
||||
// Even when `path` is the top level: that is a choice, and the
|
||||
// empty string alone would read as none having been made.
|
||||
session.root_chosen = true;
|
||||
self.browser = None;
|
||||
self.state = LaunchState::SignedIn {
|
||||
session: session.clone(),
|
||||
@@ -547,17 +589,32 @@ mod tests {
|
||||
// Everything past sign-in is backend-neutral, and this is the check
|
||||
// that keeps it so: no branch on the account's connector below here.
|
||||
let mut m = LaunchModel::default();
|
||||
m.signed_in(folder_with_root(""));
|
||||
assert!(m.is_signed_in());
|
||||
assert!(!m.can_open_library(), "no root chosen yet");
|
||||
assert_eq!(m.startup_action(false), Startup::ShowLaunchScreen);
|
||||
|
||||
m.signed_in(folder_with_root("2026"));
|
||||
assert!(m.is_signed_in());
|
||||
assert!(m.can_open_library());
|
||||
assert_eq!(m.startup_action(false), Startup::OpenLibrary);
|
||||
assert_eq!(m.account_label(), "/mnt/photos/2026");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_folder_is_the_whole_library_without_choosing_a_root() {
|
||||
// The directory typed on the launch screen is the answer to "which
|
||||
// folder"; a second question with the same name is what confused
|
||||
// the first person to open one. A server account still needs a
|
||||
// root, because its endpoint is the entire tree.
|
||||
let mut m = LaunchModel::default();
|
||||
m.signed_in(folder_with_root(""));
|
||||
assert!(m.endpoint_is_library());
|
||||
assert!(m.can_open_library(), "the folder itself is the library");
|
||||
assert_eq!(m.library_root_label(), "/");
|
||||
assert_eq!(m.startup_action(false), Startup::OpenLibrary);
|
||||
|
||||
m.signed_in(session_with_root(""));
|
||||
assert!(!m.endpoint_is_library());
|
||||
assert!(!m.can_open_library(), "a server account needs a root");
|
||||
assert_eq!(m.startup_action(false), Startup::ShowLaunchScreen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_toggles_apply_and_out_of_range_is_ignored() {
|
||||
let mut m = LaunchModel::default();
|
||||
@@ -665,13 +722,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn confirming_at_the_root_selects_the_whole_account() {
|
||||
// Legitimate: a user may keep everything at the top level.
|
||||
// Legitimate: a user may keep everything at the top level — and it
|
||||
// must then open. It used to be recorded as an empty root, which
|
||||
// is indistinguishable from no root, so "Open library" stayed
|
||||
// disabled after the picker had plainly been answered.
|
||||
let mut m = LaunchModel::default();
|
||||
m.signed_in(session_with_root(""));
|
||||
assert!(!m.can_open_library(), "nothing chosen yet");
|
||||
assert_eq!(m.library_root_label(), "");
|
||||
m.open_browser();
|
||||
|
||||
let session = m.choose_current_folder().expect("a session");
|
||||
assert_eq!(session.root, "");
|
||||
assert!(
|
||||
session.root_chosen,
|
||||
"the choice survives to the config file"
|
||||
);
|
||||
assert!(m.can_open_library());
|
||||
assert_eq!(m.library_root_label(), "/");
|
||||
assert_eq!(m.startup_action(false), Startup::OpenLibrary);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user