UI code cleanup

This commit is contained in:
Jakub Hlusička 2026-01-31 15:24:36 +01:00
parent 2b8dfa7b44
commit 515b3ff4fd
2 changed files with 306 additions and 430 deletions

View file

@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use const_gen::*; use const_gen::*;
use embuild::cmd; use embuild::cmd;
use indoc::{formatdoc, writedoc}; use indoc::writedoc;
use json::JsonValue; use json::JsonValue;
use slint_build::{CompilerConfiguration, EmbedResourcesKind}; use slint_build::{CompilerConfiguration, EmbedResourcesKind};
use xz2::read::XzEncoder; use xz2::read::XzEncoder;
@ -63,6 +63,7 @@ fn main() {
#[derive(Debug)] #[derive(Debug)]
struct NotBuilt { struct NotBuilt {
#[allow(unused)]
lib_build_dir: String, lib_build_dir: String,
} }

View file

@ -46,120 +46,23 @@ use crate::{
PartitionAcid, ReadTransactionExt, PartitionAcid, ReadTransactionExt,
}, },
ffi::alloc::__spre_free, ffi::alloc::__spre_free,
ui::backend::SlintBackend, ui::{
backend::SlintBackend,
messages::{
CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, CallbackMessageUsers,
},
storage::SpectreUsersConfig,
},
util::DurationExt, util::DurationExt,
}; };
pub mod backend; pub mod backend;
pub mod messages;
pub mod storage;
pub mod window_adapter; pub mod window_adapter;
slint::include_modules!(); slint::include_modules!();
#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)]
struct SpectreUsersConfig {
users: Vec<SpectreUserConfig>,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
struct SpectreUserConfig {
username: String,
#[serde(with = "serde_bytes")]
encrypted_key: Key,
#[serde(with = "serde_bytes")]
key_id: [u8; 32],
}
trait ReprConvert: Copy {
type Repr: Copy;
fn into_repr(self) -> Self::Repr;
fn from_repr(repr: Self::Repr) -> Self;
}
impl ReprConvert for SpectreAlgorithm {
type Repr = u32;
fn from_repr(repr: Self::Repr) -> Self {
unsafe { core::mem::transmute::<Self::Repr, Self>(repr) }
}
fn into_repr(self) -> Self::Repr {
self as Self::Repr
}
}
impl ReprConvert for SpectreResultType {
type Repr = u32;
fn from_repr(repr: Self::Repr) -> Self {
unsafe { core::mem::transmute::<Self::Repr, Self>(repr) }
}
fn into_repr(self) -> Self::Repr {
self as Self::Repr
}
}
mod with_repr {
use serde::{Deserialize, Deserializer, Serializer};
use super::ReprConvert;
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: ReprConvert<Repr = u32>,
S: Serializer,
{
serializer.serialize_u32(value.into_repr())
}
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: ReprConvert<Repr = u32>,
D: Deserializer<'de>,
{
<u32 as Deserialize>::deserialize(deserializer).map(T::from_repr)
}
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
struct SpectreSiteConfig {
#[serde(with = "with_repr")]
algorithm: SpectreAlgorithm,
counter: SpectreCounter::Type,
#[serde(rename = "type")]
#[serde(with = "with_repr")]
result_type: SpectreResultType,
password: Option<String>,
#[serde(with = "with_repr")]
login_type: SpectreResultType,
login_name: Option<String>,
uses: u32,
last_used: NaiveDateTime,
}
impl Default for SpectreSiteConfig {
fn default() -> Self {
Self {
algorithm: SpectreAlgorithm::Current,
counter: SpectreCounter::Default,
result_type: SpectreResultType::SpectreResultDefaultResult,
password: None,
login_type: SpectreResultType::SpectreResultDefaultLogin,
login_name: None,
uses: 0,
last_used: Default::default(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct SpectreSite {
username: String,
site_name: String,
config: SpectreSiteConfig,
}
fn spectre_derive_user_key(username: &CStr, password: &CStr) -> SpectreUserKey { fn spectre_derive_user_key(username: &CStr, password: &CStr) -> SpectreUserKey {
let user_key_start = Instant::now(); let user_key_start = Instant::now();
@ -282,6 +185,12 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
.unwrap(); .unwrap();
slint::platform::set_platform(Box::new(backend)).expect("backend already initialized"); slint::platform::set_platform(Box::new(backend)).expect("backend already initialized");
let main = AppWindow::new().unwrap();
let state = State::new(db, main).await;
state.borrow().run_event_loop().await;
}
struct State { struct State {
window: AppWindow, window: AppWindow,
db: AcidDatabase, db: AcidDatabase,
@ -296,6 +205,105 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
} }
impl State { impl State {
async fn new(db: AcidDatabase, main: AppWindow) -> Rc<RefCell<Self>> {
let state = Rc::new(RefCell::new(State {
window: main.clone_strong(),
users: {
let read = db.read_transaction().await;
let mut buffer = vec![0_u8; 128];
match read
.read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer)
.await
{
Ok(bytes) => postcard::from_bytes::<SpectreUsersConfig>(&bytes).unwrap(),
Err(ekv::ReadError::KeyNotFound) => Default::default(),
Err(error) => panic!("Failed to read the users config: {error:?}"),
}
},
db,
view: AppState::Login,
state_login: Default::default(),
state_users: Default::default(),
state_user_edit: Default::default(),
state_user_sites: Default::default(),
}));
main.on_enter_view({
let state = state.clone();
move |view| {
state.borrow_mut().set_view(view, true);
}
});
main.on_escape({
let state = state.clone();
move || {
state
.borrow_mut()
.process_callback_message(CallbackMessage::Escape);
}
});
main.on_login_pw_accepted({
let state = state.clone();
move |username, password| {
state
.borrow_mut()
.process_callback_message(CallbackMessage::Login(
CallbackMessageLogin::PwAccepted { username, password },
));
}
});
main.on_users_edit_user({
let state = state.clone();
move |username, new| {
state
.borrow_mut()
.process_callback_message(CallbackMessage::Users(
CallbackMessageUsers::EditUser { username, new },
));
}
});
main.on_user_edit_compute_identicon({
let state = state.clone();
move |encrypted_key, password| {
state
.borrow_mut()
.process_callback_message(CallbackMessage::UserEdit(
CallbackMessageUserEdit::ComputeIdenticon {
encrypted_key,
password,
},
));
}
});
main.on_user_edit_confirm({
let state = state.clone();
move |encrypted_key| {
state
.borrow_mut()
.process_callback_message(CallbackMessage::UserEdit(
CallbackMessageUserEdit::Confirm { encrypted_key },
));
}
});
// let sites = Rc::new(VecModel::default());
// sites.push("First".into());
// sites.push("Second".into());
// main.set_sites(ModelRc::new(ModelRc::new(sites.clone()).map(
// |mut site: StandardListViewItem| {
// site.text += "10";
// site
// },
// )));
state
}
fn process_callback_message(&mut self, message: CallbackMessage) { fn process_callback_message(&mut self, message: CallbackMessage) {
match self.view { match self.view {
AppState::Login => StateLogin::process_callback_message(self, message), AppState::Login => StateLogin::process_callback_message(self, message),
@ -318,6 +326,24 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
} }
} }
} }
/// Instead of having a `loop` in the non-async `SlintBackend::run_event_loop`, we achieve
/// async by having only one iteration of the loop run, and `await`ing here.
/// The following block is analogous to `main.run()`.
async fn run_event_loop(&self) -> ! {
self.window.show().unwrap();
loop {
slint::run_event_loop().unwrap();
SIGNAL_LCD_SUBMIT.signal(());
#[cfg(feature = "limit-fps")]
embassy_time::Timer::after(FRAME_DURATION_MIN).await;
SIGNAL_UI_RENDER.wait().await;
}
#[expect(unreachable_code)]
self.window.hide().unwrap();
}
} }
trait AppViewTrait { trait AppViewTrait {
@ -331,10 +357,10 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
fn process_callback_message(state: &mut State, message: CallbackMessage) { fn process_callback_message(state: &mut State, message: CallbackMessage) {
match message { match message {
CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }) => { CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }) => {
let username_c = CString::new(&*username) let username_c =
.expect("Username cannot be converted to a C string."); CString::new(&*username).expect("Username cannot be converted to a C string.");
let password_c = CString::new(&*password) let password_c =
.expect("Password cannot be converted to a C string."); CString::new(&*password).expect("Password cannot be converted to a C string.");
let user_key = spectre_derive_user_key(&username_c, &password_c); let user_key = spectre_derive_user_key(&username_c, &password_c);
// let site_key_start = Instant::now(); // let site_key_start = Instant::now();
@ -417,8 +443,8 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
}) => { }) => {
let username_c = CString::new(&*state.state_user_edit.username) let username_c = CString::new(&*state.state_user_edit.username)
.expect("Username cannot be converted to a C string."); .expect("Username cannot be converted to a C string.");
let password_c = CString::new(&*password) let password_c =
.expect("Password cannot be converted to a C string."); CString::new(&*password).expect("Password cannot be converted to a C string.");
// let user_key = spectre_derive_user_key(&username_c, &password_c); // let user_key = spectre_derive_user_key(&username_c, &password_c);
let identicon: SharedString = unsafe { let identicon: SharedString = unsafe {
let identicon = spectre_api_sys::spectre_identicon( let identicon = spectre_api_sys::spectre_identicon(
@ -462,154 +488,3 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
} }
impl AppViewTrait for StateUserSites {} impl AppViewTrait for StateUserSites {}
enum CallbackMessage {
/// The escape key was pressed.
Escape,
Login(CallbackMessageLogin),
Users(CallbackMessageUsers),
UserEdit(CallbackMessageUserEdit),
UserSites(CallbackMessageUserSites),
}
enum CallbackMessageLogin {
PwAccepted {
username: SharedString,
password: SharedString,
},
}
enum CallbackMessageUsers {
EditUser { username: SharedString, new: bool },
}
enum CallbackMessageUserEdit {
ComputeIdenticon {
encrypted_key: SharedString,
password: SharedString,
},
Confirm {
encrypted_key: SharedString,
},
}
enum CallbackMessageUserSites {}
let main = AppWindow::new().unwrap();
let state = Rc::new(RefCell::new(State {
window: main.clone_strong(),
users: {
let read = db.read_transaction().await;
let mut buffer = vec![0_u8; 128];
match read
.read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer)
.await
{
Ok(bytes) => postcard::from_bytes::<SpectreUsersConfig>(&bytes).unwrap(),
Err(ekv::ReadError::KeyNotFound) => Default::default(),
Err(error) => panic!("Failed to read the users config: {error:?}"),
}
},
db,
view: AppState::Login,
state_login: Default::default(),
state_users: Default::default(),
state_user_edit: Default::default(),
state_user_sites: Default::default(),
}));
main.on_enter_view({
let state = state.clone();
move |view| {
state.borrow_mut().set_view(view, true);
}
});
main.on_escape({
let state = state.clone();
move || {
state
.borrow_mut()
.process_callback_message(CallbackMessage::Escape);
}
});
main.on_login_pw_accepted({
let state = state.clone();
move |username, password| {
state
.borrow_mut()
.process_callback_message(CallbackMessage::Login(
CallbackMessageLogin::PwAccepted { username, password },
));
}
});
main.on_users_edit_user({
let state = state.clone();
move |username, new| {
state
.borrow_mut()
.process_callback_message(CallbackMessage::Users(CallbackMessageUsers::EditUser {
username,
new,
}));
}
});
main.on_user_edit_compute_identicon({
let state = state.clone();
move |encrypted_key, password| {
state
.borrow_mut()
.process_callback_message(CallbackMessage::UserEdit(
CallbackMessageUserEdit::ComputeIdenticon {
encrypted_key,
password,
},
));
}
});
main.on_user_edit_confirm({
let state = state.clone();
move |encrypted_key| {
state
.borrow_mut()
.process_callback_message(CallbackMessage::UserEdit(
CallbackMessageUserEdit::Confirm { encrypted_key },
));
}
});
// let sites = Rc::new(VecModel::default());
// sites.push("First".into());
// sites.push("Second".into());
// main.set_sites(ModelRc::new(ModelRc::new(sites.clone()).map(
// |mut site: StandardListViewItem| {
// site.text += "10";
// site
// },
// )));
run_event_loop(main).await;
}
/// Instead of having a `loop` in the non-async `SlintBackend::run_event_loop`, we achieve
/// async by having only one iteration of the loop run, and `await`ing here.
/// The following block is analogous to `main.run()`.
async fn run_event_loop(main: AppWindow) {
main.show().unwrap();
loop {
slint::run_event_loop().unwrap();
SIGNAL_LCD_SUBMIT.signal(());
#[cfg(feature = "limit-fps")]
embassy_time::Timer::after(FRAME_DURATION_MIN).await;
SIGNAL_UI_RENDER.wait().await;
}
#[expect(unreachable_code)]
main.hide().unwrap();
}