diff --git a/platform/dr-plat/src/lib.rs b/platform/dr-plat/src/lib.rs index 7869397..a64ae44 100644 --- a/platform/dr-plat/src/lib.rs +++ b/platform/dr-plat/src/lib.rs @@ -15,4 +15,4 @@ pub use storage::{ DirRef, Entry, LocalStorage, NewFile, Node, SeekableRead, Storage, StorageError, WritableStorage, }; -pub use volumes::{volumes, Volume}; +pub use volumes::{imports_supported, volumes, Volume}; diff --git a/platform/dr-plat/src/volumes.rs b/platform/dr-plat/src/volumes.rs index d7c1070..f0fe590 100644 --- a/platform/dr-plat/src/volumes.rs +++ b/platform/dr-plat/src/volumes.rs @@ -56,6 +56,32 @@ impl Volume { } } +/// TRACES: FR-CAT-10 | FR-PLAT-AND-1 | NFR-PORT-1 +/// Whether an import can reach a card on this platform at all. +/// +/// **Not the same question as "did [`volumes`] find anything".** An empty list +/// on Linux means no card is plugged in, and the user can plug one in or type +/// where it is mounted. `false` here means the operation cannot be performed +/// however hard the user tries, and the interface should not offer it. +/// +/// It is `false` on Android, for two reasons that both have to be fixed before +/// it can change: +/// +/// - There is no mount table to read and no path to type. Storage is reached +/// through a tree the user granted, and a removable volume appears there or +/// not at all (ARCH ยง6.9). +/// - Nothing implements [`WritableStorage`](crate::WritableStorage) except +/// [`LocalStorage`](crate::LocalStorage), so there is no destination to write +/// into even once a source is named. +/// +/// The engine above this is already portable โ€” it takes storage traits and +/// never a path โ€” so what this gates is the *interface*, and it stops being +/// `false` when a SAF implementation lands rather than when the importer is +/// rewritten. +pub const fn imports_supported() -> bool { + cfg!(target_os = "linux") +} + /// Every mounted volume that might hold photographs. /// /// Ordered with the likely cards first and, within each group, by label, so @@ -314,6 +340,17 @@ tmpfs /run/user/1000 tmpfs rw,nosuid 0 0 assert!(!fixed.is_likely_card()); } + #[test] + fn a_platform_that_cannot_import_says_so_separately_from_finding_nothing() { + // The two are different claims: an empty list means "plug one in", + // `false` here means "this cannot be done here". An interface that + // conflated them would offer a page that can never be used. + assert_eq!(imports_supported(), cfg!(target_os = "linux")); + if !imports_supported() { + assert!(volumes().is_empty()); + } + } + #[test] fn asking_where_there_is_no_answer_is_not_an_error() { // The call must work on a machine with no card, in CI, and on a diff --git a/ui/dr-ui/ui/library.slint b/ui/dr-ui/ui/library.slint index af6c800..1035ee7 100644 --- a/ui/dr-ui/ui/library.slint +++ b/ui/dr-ui/ui/library.slint @@ -660,6 +660,15 @@ component HeaderActions inherits HorizontalLayout { callback change-library(); callback toggle-pin-scope(); callback sync-now(); + /// TRACES: FR-CAT-10 + /// Whether this platform can reach a card at all (`dr_plat::imports_supported`). + /// + /// Hidden rather than disabled, unlike the buttons above that come and go + /// with the selection: those are unavailable *now* and will be available + /// in a moment, where this one never will be on this device. A permanently + /// disabled control teaches the reader that the row lies. + in property can-import: false; + callback rescan(); callback open-import(); callback open-settings(); @@ -796,7 +805,7 @@ component HeaderActions inherits HorizontalLayout { // for the same reason Rescan is โ€” an import writes files the running scan // would then half-see, and the two disagreeing about what is in a folder // is a worse outcome than waiting. - if !root.scanning: Button { + if !root.scanning && root.can-import: Button { text: "Import"; y: root.centred ? (root.row-height - self.height) / 2 : 0; clicked => { root.open-import(); } @@ -938,6 +947,11 @@ export component LibraryGrid inherits Rectangle { /// Back to the launch screen, to change library or account. callback change-library(); /// Open the settings page. + /// TRACES: FR-CAT-10 + /// Whether this platform can import at all. Forwarded to the header, which + /// hides the button rather than disabling it. + in property can-import: false; + callback open-import(); callback open-settings(); @@ -1350,6 +1364,7 @@ export component LibraryGrid inherits Rectangle { scanning: root.scanning; syncing: root.syncing; scope-pinned: root.scope-pinned; + can-import: root.can-import; settings-armed: root.settings-armed; settings-summary: root.settings-summary; exporting: root.exporting; @@ -1425,6 +1440,7 @@ export component LibraryGrid inherits Rectangle { scanning: root.scanning; syncing: root.syncing; scope-pinned: root.scope-pinned; + can-import: root.can-import; settings-armed: root.settings-armed; settings-summary: root.settings-summary; exporting: root.exporting;