1
0
forked from GRIN/grim

Build 9: QR scan, camera fixes, first-run onboarding, sweep fixes

Camera/QR (validated live with a v4l2loopback virtual camera):
- decode non-MJPEG frames on Linux (raw YUYV was undecodable: eternal
  spinner), enumerate devices off the UI thread without unwrap, open
  cameras by their real device index (list position broke whenever
  /dev/video0 was absent), show "No camera found" after 5s
- scan-to-pay: QR button in the send recipient search row and the
  mobile home header; in-surface camera panel feeding recipient
  resolution; only text payloads accepted (seeds/slatepacks refused)
- receive QR was unscannable: full cells (0.5px gaps fragmented finder
  patterns at 4.5px cells), always ink-on-white plate (inverted codes
  fail many scanners), center mark 26% -> 19% (zbar chokes above);
  rqrr+zbar both decode the live card now; unit tests cover the exact
  widget geometry

First-run onboarding (replaces the stock empty state only; the wallet
list, add-wallet modal and GRIM creation flow stay for later wallets):
- intro -> node choice (Private integrated / Instant external with
  URL) -> create or restore (wraps MnemonicSetup; word grid, paste,
  SeedQR scan) -> optional @username claim with prominent skip
- fonts bind at creation context so frame one can use Geist families

Sweep fixes (22 confirmed findings, the simple ones):
- yellow theme: active sidebar nav was dark-on-dark (P1)
- window frame ring painted with theme bg: transparent clear color
  rendered as a black band without a compositor (P2)
- import/rotate identity inputs get visible field wells (P2)
- receive buttons: verb labels + transient "Copied" feedback
- review hero card fills the column; fee row copy; pay-tab stale and
  duplicated hints; unlock modal copy; node card subtitle truncation
- build number stays current (.git/logs/HEAD rerun trigger)

Backup restore: import accepts the export-time password and re-encrypts
under the current wallet password; cross-device restores work.

31 lib tests green. Mainnet-validated: both flows restored real
wallets; 0.1 grin sent B->A through NIP-17 over Tor with async
open/close ping-pong; tx 71fbfce4f591 posted on-chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-11 04:07:53 -04:00
parent 908df117e6
commit d8cf06b577
16 changed files with 1412 additions and 125 deletions
+38 -13
View File
@@ -62,17 +62,22 @@ impl Desktop {
use nokhwa::Camera;
use nokhwa::pixel_format::RgbFormat;
use nokhwa::utils::ApiBackend;
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
let devices = nokhwa::query(ApiBackend::Auto).unwrap();
cameras_amount.store(devices.len(), Ordering::Relaxed);
let index = camera_index.load(Ordering::Relaxed);
if devices.is_empty() || index >= devices.len() {
return;
}
use nokhwa::utils::{FrameFormat, RequestedFormat, RequestedFormatType};
thread::spawn(move || {
let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
// Device enumeration does IO — keep it off the UI thread, and
// treat a backend error the same as "no cameras".
let devices = nokhwa::query(ApiBackend::Auto).unwrap_or_default();
cameras_amount.store(devices.len(), Ordering::Relaxed);
let index = camera_index.load(Ordering::Relaxed);
if devices.is_empty() || index >= devices.len() {
return;
}
// Open by the enumerated device's own index, not the list
// position: on v4l they differ whenever /dev/video0 is absent
// (first camera at video1, loopback-only setups, …).
let index = devices[index].index().clone();
let requested =
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate);
// Create and open camera.
@@ -89,9 +94,29 @@ impl Desktop {
}
// Get a frame.
if let Ok(frame) = camera.frame() {
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((frame.buffer().to_vec(), 0));
// Consumers expect an encoded image. MJPEG frames
// already are; anything else (YUYV, NV12, …) must
// be decoded to RGB and re-encoded, or the readers
// fail on the raw buffer and show a spinner forever.
let bytes = if frame.source_frame_format() == FrameFormat::MJPEG {
Some(frame.buffer().to_vec())
} else if let Ok(image) = frame.decode_image::<RgbFormat>() {
let mut bytes: Vec<u8> = Vec::new();
image
.write_to(
&mut std::io::Cursor::new(&mut bytes),
image::ImageFormat::Jpeg,
)
.ok()
.map(|_| bytes)
} else {
None
};
if let Some(bytes) = bytes {
// Save image.
let mut w_image = LAST_CAMERA_IMAGE.write();
*w_image = Some((bytes, 0));
}
} else {
// Clear image.
let mut w_image = LAST_CAMERA_IMAGE.write();
@@ -99,7 +124,7 @@ impl Desktop {
break;
}
}
camera.stop_stream().unwrap();
let _ = camera.stop_stream();
};
}
});