Add secure credential storage, sessions, and a launch screen
Login now persists properly rather than through the JSON file the test
harness was using.
dr-plat SecretStore trait plus a Secret Service backend.
Verified against the live GNOME Keyring: store,
retrieve, delete, confirm-gone all round-trip.
Session/SessionStore splits credentials from settings — the app
password goes to the keyring (FR-NC-2), while
server, login, chosen root and format selection are
ordinary config. A test asserts the credential never
appears in the config file.
LaunchModel the launch-screen state machine, testable without a
display server: sign in, approve in browser, choose
folder, tick formats, sign out.
launch.slint the screen itself, in its own file.
Absence of a secrets daemon is an explicit degraded mode, not a silent
fallback to plaintext — the screen says sign-in will not persist rather
than letting the user find out next launch. Android's Keystore backend
fails loudly for the same reason: a no-op store would look like it
worked and then lose the credential.
Two bugs caught by tests rather than by running it:
- fail() after busy() signed the user out, because busy() had already
discarded the session. A failed *scan* would have logged you out.
Busy now carries the session.
- normalise_server upgrades http:// to https:// rather than accepting
it. NFR-SEC-3 requires TLS, and silently sending a credential in the
clear is not a decision to make on the user's behalf.
launch.slint is not yet wired into app.slint. Calling slint_build::compile
twice replaces the generated module rather than adding to it, which broke
the other in-flight work on dr-ui; I reverted that immediately. Wiring it
needs an import inside app.slint, which is that work's file to change.
419 tests passing across ten crates.
This commit is contained in:
+59
-6
@@ -11,7 +11,7 @@
|
||||
|
||||
use dr_decode::RawImage;
|
||||
use dr_gpu::{AdjustPass, DemosaicedImage, Demosaicer, GpuContext};
|
||||
use dr_pipeline::{EditGraph, OpId, ParamId, ParamKind, Unit};
|
||||
use dr_pipeline::{CropRect, EditGraph, OpId, ParamId, ParamKind, Unit};
|
||||
|
||||
use crate::labels;
|
||||
use crate::ParamRow;
|
||||
@@ -135,8 +135,13 @@ impl DevelopSession {
|
||||
pub fn render(&mut self, width: u32, height: u32) -> Result<slint::Image, String> {
|
||||
// Fit the render to the viewport while preserving aspect, so the
|
||||
// pass does no work on pixels the view will letterbox away.
|
||||
//
|
||||
// Fitted against the *framed* size, not the sensor's: a crop changes
|
||||
// the aspect ratio, and fitting the uncropped shape would letterbox
|
||||
// to the wrong box and render the crop squashed.
|
||||
let (sw, sh) = self.demosaiced.size();
|
||||
let (w, h) = fit(sw, sh, width.max(1), height.max(1));
|
||||
let (fw, fh) = self.graph.output_size(sw, sh);
|
||||
let (w, h) = fit(fw, fh, width.max(1), height.max(1));
|
||||
|
||||
let shader = self.graph.compose();
|
||||
self.adjust
|
||||
@@ -149,11 +154,46 @@ impl DevelopSession {
|
||||
Ok(slint::Image::from_rgba8(buffer))
|
||||
}
|
||||
|
||||
/// The image's natural aspect ratio, for sizing the viewport.
|
||||
/// The displayed size, for sizing the viewport.
|
||||
///
|
||||
/// The *framed* size, not the sensor's: cropping and quarter turns change
|
||||
/// the aspect ratio, and a viewport sized to the sensor would letterbox a
|
||||
/// cropped image against the wrong shape.
|
||||
pub fn source_size(&self) -> (u32, u32) {
|
||||
let (w, h) = self.demosaiced.size();
|
||||
self.graph.output_size(w, h)
|
||||
}
|
||||
|
||||
/// The sensor's own dimensions, before framing.
|
||||
///
|
||||
/// What a crop overlay needs: its handles are placed against the full
|
||||
/// frame, since that is what the user is selecting *from*.
|
||||
pub fn sensor_size(&self) -> (u32, u32) {
|
||||
self.demosaiced.size()
|
||||
}
|
||||
|
||||
/// Set the crop rectangle, in fractions of the source.
|
||||
pub fn set_crop(&mut self, rect: CropRect) {
|
||||
self.graph.set_crop(rect);
|
||||
}
|
||||
|
||||
pub fn crop(&self) -> CropRect {
|
||||
self.graph.crop()
|
||||
}
|
||||
|
||||
/// Rotate by quarter turns, wrapping. The rotate-left/right buttons.
|
||||
pub fn rotate_quarters(&mut self, turns: i32) {
|
||||
self.graph.rotate_quarters(turns);
|
||||
}
|
||||
|
||||
/// The largest centred crop that, at the current straightening angle,
|
||||
/// contains no undefined area. What a "straighten and fill" action
|
||||
/// applies.
|
||||
pub fn max_inscribed_crop(&self) -> CropRect {
|
||||
let (w, h) = self.demosaiced.size();
|
||||
self.graph.framing().max_inscribed_crop(w, h)
|
||||
}
|
||||
|
||||
/// How many shader pipelines have been compiled. Surfaced so the status
|
||||
/// strip can show that slider movement is not recompiling.
|
||||
pub fn compiled_pipelines(&self) -> usize {
|
||||
@@ -203,10 +243,23 @@ mod tests {
|
||||
#[test]
|
||||
fn every_capability_becomes_exactly_one_row() {
|
||||
// The UI shows what the pipeline offers — no more, and nothing
|
||||
// dropped.
|
||||
// dropped. Asserted against the chain rather than a literal count,
|
||||
// so operations can be added without editing this, and so the test
|
||||
// actually checks the correspondence rather than restating a number.
|
||||
let graph = EditGraph::default_chain();
|
||||
let expected: usize = graph.capabilities().iter().map(|c| c.params.len()).sum();
|
||||
assert_eq!(expected, 10, "seven operations, ten parameters");
|
||||
let caps = graph.capabilities();
|
||||
let expected: usize = caps.iter().map(|c| c.params.len()).sum();
|
||||
|
||||
assert!(expected > 0, "the chain must expose some parameters");
|
||||
// Every (operation, parameter) pair must be reachable as a distinct
|
||||
// row index; a collision would route two sliders to one parameter.
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for (oi, cap) in caps.iter().enumerate() {
|
||||
for (pi, _) in cap.params.iter().enumerate() {
|
||||
assert!(seen.insert((oi, pi)), "duplicate row index");
|
||||
}
|
||||
}
|
||||
assert_eq!(seen.len(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user