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:
2026-08-22 19:19:40 +02:00
co-authored by Claude Opus 5
parent f4c21c4e2c
commit b1bf68022d
3 changed files with 55 additions and 2 deletions
+37
View File
@@ -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