Do not offer an import where one cannot be done
The Import button went into the library header unconditionally, so an Android user got a page that opens, finds nothing, and cannot be pressed — worse than no page at all, because it reads as broken rather than as absent. `dr_plat::imports_supported()` answers the question the interface actually has, which is not "did we find a volume". An empty list on Linux means plug one in; false here means it cannot be done on this device however hard the user tries. It is false on Android for two reasons that both have to be fixed before it changes: there is no mount table to read and no path to type, and nothing implements WritableStorage except LocalStorage. The button is hidden rather than disabled. The buttons beside it come and go with the selection — unavailable now, available in a moment — where this one never will be here, and a permanently disabled control teaches the reader that the row lies. What this gates is the interface, not the engine. dr-ingest takes storage traits and never a path, and cross-compiles to aarch64-linux-android today; it should need no changes when a SAF implementation lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,4 +15,4 @@ pub use storage::{
|
|||||||
DirRef, Entry, LocalStorage, NewFile, Node, SeekableRead, Storage, StorageError,
|
DirRef, Entry, LocalStorage, NewFile, Node, SeekableRead, Storage, StorageError,
|
||||||
WritableStorage,
|
WritableStorage,
|
||||||
};
|
};
|
||||||
pub use volumes::{volumes, Volume};
|
pub use volumes::{imports_supported, volumes, Volume};
|
||||||
|
|||||||
@@ -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.
|
/// Every mounted volume that might hold photographs.
|
||||||
///
|
///
|
||||||
/// Ordered with the likely cards first and, within each group, by label, so
|
/// 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());
|
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]
|
#[test]
|
||||||
fn asking_where_there_is_no_answer_is_not_an_error() {
|
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
|
// The call must work on a machine with no card, in CI, and on a
|
||||||
|
|||||||
@@ -660,6 +660,15 @@ component HeaderActions inherits HorizontalLayout {
|
|||||||
callback change-library();
|
callback change-library();
|
||||||
callback toggle-pin-scope();
|
callback toggle-pin-scope();
|
||||||
callback sync-now();
|
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 <bool> can-import: false;
|
||||||
|
|
||||||
callback rescan();
|
callback rescan();
|
||||||
callback open-import();
|
callback open-import();
|
||||||
callback open-settings();
|
callback open-settings();
|
||||||
@@ -796,7 +805,7 @@ component HeaderActions inherits HorizontalLayout {
|
|||||||
// for the same reason Rescan is — an import writes files the running scan
|
// 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
|
// would then half-see, and the two disagreeing about what is in a folder
|
||||||
// is a worse outcome than waiting.
|
// is a worse outcome than waiting.
|
||||||
if !root.scanning: Button {
|
if !root.scanning && root.can-import: Button {
|
||||||
text: "Import";
|
text: "Import";
|
||||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||||
clicked => { root.open-import(); }
|
clicked => { root.open-import(); }
|
||||||
@@ -938,6 +947,11 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
/// Back to the launch screen, to change library or account.
|
/// Back to the launch screen, to change library or account.
|
||||||
callback change-library();
|
callback change-library();
|
||||||
/// Open the settings page.
|
/// 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 <bool> can-import: false;
|
||||||
|
|
||||||
callback open-import();
|
callback open-import();
|
||||||
callback open-settings();
|
callback open-settings();
|
||||||
|
|
||||||
@@ -1350,6 +1364,7 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
scanning: root.scanning;
|
scanning: root.scanning;
|
||||||
syncing: root.syncing;
|
syncing: root.syncing;
|
||||||
scope-pinned: root.scope-pinned;
|
scope-pinned: root.scope-pinned;
|
||||||
|
can-import: root.can-import;
|
||||||
settings-armed: root.settings-armed;
|
settings-armed: root.settings-armed;
|
||||||
settings-summary: root.settings-summary;
|
settings-summary: root.settings-summary;
|
||||||
exporting: root.exporting;
|
exporting: root.exporting;
|
||||||
@@ -1425,6 +1440,7 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
scanning: root.scanning;
|
scanning: root.scanning;
|
||||||
syncing: root.syncing;
|
syncing: root.syncing;
|
||||||
scope-pinned: root.scope-pinned;
|
scope-pinned: root.scope-pinned;
|
||||||
|
can-import: root.can-import;
|
||||||
settings-armed: root.settings-armed;
|
settings-armed: root.settings-armed;
|
||||||
settings-summary: root.settings-summary;
|
settings-summary: root.settings-summary;
|
||||||
exporting: root.exporting;
|
exporting: root.exporting;
|
||||||
|
|||||||
Reference in New Issue
Block a user