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:
@@ -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();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user