More GUI progress

This commit is contained in:
Jakub Hlusička 2026-01-27 02:46:53 +01:00
parent 9c2a614aff
commit 8426852d7c
8 changed files with 504 additions and 96 deletions

11
firmware/Cargo.lock generated
View file

@ -53,6 +53,7 @@ dependencies = [
"rmk", "rmk",
"rtt-target", "rtt-target",
"serde", "serde",
"serde_bytes",
"sha2", "sha2",
"slint", "slint",
"slint-build", "slint-build",
@ -6499,6 +6500,16 @@ dependencies = [
"typeid", "typeid",
] ]
[[package]]
name = "serde_bytes"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
"serde",
"serde_core",
]
[[package]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.228" version = "1.0.228"

View file

@ -84,6 +84,7 @@ embedded-storage-async = "0.4.1"
postcard = { version = "1.1", default-features = false, features = ["alloc", "postcard-derive"] } # TODO: defmt postcard = { version = "1.1", default-features = false, features = ["alloc", "postcard-derive"] } # TODO: defmt
serde = { version = "1.0", default-features = false, features = ["derive"] } serde = { version = "1.0", default-features = false, features = ["derive"] }
# serde_with = { version = "3.16", default-features = false, features = ["alloc", "macros"] } # serde_with = { version = "3.16", default-features = false, features = ["alloc", "macros"] }
serde_bytes = { version = "0.11.19", default-features = false, features = ["alloc"] }
chrono = { version = "0.4.43", default-features = false, features = ["alloc", "serde"] } # TODO: defmt chrono = { version = "0.4.43", default-features = false, features = ["alloc", "serde"] } # TODO: defmt
# Crates for serial UART CLI # Crates for serial UART CLI

View file

@ -97,6 +97,7 @@ esp_bootloader_esp_idf::esp_app_desc!();
// Memory allocation regions. // Memory allocation regions.
// These can be debugged using `xtensa-esp32s3-elf-size -A <path-to-binary>`. // These can be debugged using `xtensa-esp32s3-elf-size -A <path-to-binary>`.
// A panic such as `memory allocation of 3740121773 bytes failed` is caused by a heap overflow. The size is `DEEDBAAD` in hex.
/// Total heap size /// Total heap size
const HEAP_SIZE: usize = 128 * 1024; const HEAP_SIZE: usize = 128 * 1024;
@ -158,7 +159,7 @@ async fn main(_spawner: Spawner) {
// Use the internal DRAM as the heap. // Use the internal DRAM as the heap.
// Memory reclaimed from the esp-idf bootloader. // Memory reclaimed from the esp-idf bootloader.
const HEAP_SIZE_RECLAIMED: usize = 64 * 1024; const HEAP_SIZE_RECLAIMED: usize = 72 * 1024;
esp_alloc::heap_allocator!(#[ram(reclaimed)] size: HEAP_SIZE_RECLAIMED); esp_alloc::heap_allocator!(#[ram(reclaimed)] size: HEAP_SIZE_RECLAIMED);
esp_alloc::heap_allocator!(size: HEAP_SIZE - HEAP_SIZE_RECLAIMED); esp_alloc::heap_allocator!(size: HEAP_SIZE - HEAP_SIZE_RECLAIMED);
info!("Heap initialized! {:#?}", esp_alloc::HEAP.stats()); info!("Heap initialized! {:#?}", esp_alloc::HEAP.stats());

View file

@ -1,6 +1,8 @@
// #![cfg_attr(not(feature = "simulator"), no_main)] // #![cfg_attr(not(feature = "simulator"), no_main)]
use core::{ use core::{
cell::RefCell,
ffi::CStr,
iter::Chain, iter::Chain,
ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
}; };
@ -9,6 +11,7 @@ use alloc::{
borrow::Cow, borrow::Cow,
boxed::Box, boxed::Box,
ffi::CString, ffi::CString,
format,
rc::Rc, rc::Rc,
string::{String, ToString}, string::{String, ToString},
vec, vec,
@ -24,11 +27,14 @@ use esp_hal::rng::Trng;
use esp_storage::FlashStorage; use esp_storage::FlashStorage;
use itertools::Itertools; use itertools::Itertools;
use log::{info, warn}; use log::{info, warn};
use password_hash::Key;
use rmk::futures::TryFutureExt; use rmk::futures::TryFutureExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_bytes::Bytes;
use slint::{ModelRc, SharedString, StandardListViewItem, VecModel}; use slint::{ModelRc, SharedString, StandardListViewItem, VecModel};
use spectre_api_sys::{ use spectre_api_sys::{
SpectreAlgorithm, SpectreCounter, SpectreKeyPurpose, SpectreResultType, SpectreUserKey, SpectreAlgorithm, SpectreCounter, SpectreKeyID, SpectreKeyPurpose, SpectreResultType,
SpectreUserKey,
}; };
#[cfg(feature = "limit-fps")] #[cfg(feature = "limit-fps")]
@ -39,6 +45,7 @@ use crate::{
AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers, AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers,
PartitionAcid, ReadTransactionExt, PartitionAcid, ReadTransactionExt,
}, },
ffi::alloc::__spre_free,
ui::backend::SlintBackend, ui::backend::SlintBackend,
util::DurationExt, util::DurationExt,
}; };
@ -48,7 +55,7 @@ pub mod window_adapter;
slint::include_modules!(); slint::include_modules!();
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] #[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)]
struct SpectreUsersConfig { struct SpectreUsersConfig {
users: Vec<SpectreUserConfig>, users: Vec<SpectreUserConfig>,
} }
@ -56,6 +63,10 @@ struct SpectreUsersConfig {
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] #[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
struct SpectreUserConfig { struct SpectreUserConfig {
username: String, username: String,
#[serde(with = "serde_bytes")]
encrypted_key: Key,
#[serde(with = "serde_bytes")]
key_id: [u8; 32],
} }
trait ReprConvert: Copy { trait ReprConvert: Copy {
@ -149,6 +160,29 @@ struct SpectreSite {
config: SpectreSiteConfig, config: SpectreSiteConfig,
} }
fn spectre_derive_user_key(username: &CStr, password: &CStr) -> SpectreUserKey {
let user_key_start = Instant::now();
unsafe {
let user_key = &*spectre_api_sys::spectre_user_key(
username.as_ptr(),
password.as_ptr(),
SpectreAlgorithm::Current,
);
let user_key_duration = Instant::now().duration_since(user_key_start);
warn!(
"User key derived in {} seconds:\n{user_key:02x?}",
user_key_duration.display_as_secs()
);
let user_key_stack = user_key.clone();
// TODO: Erase memory before freeing
__spre_free(user_key as *const _ as *mut _);
user_key_stack
}
}
#[embassy_executor::task] #[embassy_executor::task]
pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) { pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) {
let db = AcidDatabase::mount(flash_part_acid).await; let db = AcidDatabase::mount(flash_part_acid).await;
@ -156,6 +190,8 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
let value = SpectreUsersConfig { let value = SpectreUsersConfig {
users: vec![SpectreUserConfig { users: vec![SpectreUserConfig {
username: "test".to_string(), username: "test".to_string(),
encrypted_key: [0; _],
key_id: [0; _],
}], }],
}; };
@ -242,13 +278,303 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
slint::platform::set_platform(Box::new(backend)).expect("backend already initialized"); slint::platform::set_platform(Box::new(backend)).expect("backend already initialized");
struct State {
window: AppWindow,
db: AcidDatabase,
users: SpectreUsersConfig,
/// Currently active view.
view: AppState,
// Retained state for each view.
state_login: StateLogin,
state_users: StateUsers,
state_user_edit: StateUserEdit,
state_user_sites: Option<StateUserSites>,
}
impl State {
fn process_callback_message(&mut self, message: CallbackMessage) {
match self.view {
AppState::Login => StateLogin::process_callback_message(self, message),
AppState::Users => StateUsers::process_callback_message(self, message),
AppState::UserEdit => StateUserEdit::process_callback_message(self, message),
AppState::UserSites => StateUserSites::process_callback_message(self, message),
}
}
fn set_view(&mut self, view: AppState, reset: bool) {
self.view = view;
self.window.set_app_state(view);
if reset {
match self.view {
AppState::Login => self.state_login = Default::default(),
AppState::Users => self.state_users = Default::default(),
AppState::UserEdit => self.state_user_edit = Default::default(),
AppState::UserSites => self.state_user_sites = Default::default(),
}
}
}
}
trait AppViewTrait {
fn process_callback_message(state: &mut State, message: CallbackMessage) {}
}
#[derive(Default)]
struct StateLogin {}
impl AppViewTrait for StateLogin {
fn process_callback_message(state: &mut State, message: CallbackMessage) {
match message {
CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }) => {
let username_c = CString::new(&*username)
.expect("Username cannot be converted to a C string.");
let password_c = CString::new(&*password)
.expect("Password cannot be converted to a C string.");
let user_key = spectre_derive_user_key(&username_c, &password_c);
// let site_key_start = Instant::now();
// let site_key = &*spectre_api_sys::spectre_site_key(
// user_key as *const SpectreUserKey,
// c"example.org".as_ptr(),
// SpectreCounter::Initial,
// SpectreKeyPurpose::Authentication,
// c"".as_ptr(),
// );
// let site_key_duration = Instant::now().duration_since(site_key_start);
// warn!(
// "Site key derived in {} seconds:\n{site_key:02x?}",
// site_key_duration.display_as_secs()
// );
// TODO: Erase memory before freeing
// __spre_free(site_key as *const _ as *mut _);
let Some(user) = state
.users
.users
.iter()
.find(|user| &*user.username == &*username)
else {
return;
};
if user.key_id == user_key.keyID.bytes {
info!("Correct password entered for user {username:?}.");
state.state_user_sites = Some(StateUserSites { username, user_key });
state.set_view(AppState::UserSites, true);
} else {
warn!("Incorrect password entered for user {username:?}.");
// TODO: Clear the input
}
}
_ => (),
}
}
}
#[derive(Default)]
struct StateUsers {}
impl AppViewTrait for StateUsers {
fn process_callback_message(state: &mut State, message: CallbackMessage) {
match message {
CallbackMessage::Escape => {
state.set_view(AppState::Login, false);
}
CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }) => {
state.state_user_edit = StateUserEdit {
username: username.clone(),
new,
hashed: None,
};
state.window.set_user_edit_username(username);
state.set_view(AppState::UserEdit, false);
}
_ => (),
}
}
}
#[derive(Default)]
struct StateUserEdit {
username: SharedString,
new: bool,
hashed: Option<ProposedPassword>,
}
impl AppViewTrait for StateUserEdit {
fn process_callback_message(state: &mut State, message: CallbackMessage) {
match message {
CallbackMessage::Escape => {
state.set_view(AppState::Users, false);
}
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon {
encrypted_key,
password,
}) => {
let username_c = CString::new(&*state.state_user_edit.username)
.expect("Username cannot be converted to a C string.");
let password_c = CString::new(&*password)
.expect("Password cannot be converted to a C string.");
// let user_key = spectre_derive_user_key(&username_c, &password_c);
let identicon: SharedString = unsafe {
let identicon = spectre_api_sys::spectre_identicon(
username_c.as_ptr(),
password_c.as_ptr(),
);
// TODO: identicon.color
format!(
"{}{}{}{}",
CStr::from_ptr(identicon.leftArm).to_str().unwrap(),
CStr::from_ptr(identicon.body).to_str().unwrap(),
CStr::from_ptr(identicon.rightArm).to_str().unwrap(),
CStr::from_ptr(identicon.accessory).to_str().unwrap()
)
.into()
};
state.window.set_user_edit_identicon(identicon.clone());
state.state_user_edit.hashed = Some(ProposedPassword {
encrypted_key,
identicon,
})
}
CallbackMessage::UserEdit(CallbackMessageUserEdit::Confirm { encrypted_key }) => {
// TODO
}
_ => (),
}
}
}
struct ProposedPassword {
encrypted_key: SharedString,
identicon: SharedString,
}
struct StateUserSites {
username: SharedString,
user_key: SpectreUserKey,
}
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 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({ main.on_login_pw_accepted({
let main = main.clone_strong(); let state = state.clone();
move |username, password| { move |username, password| {
info!("username = {username:?}, password = {password:?}"); state
main.set_app_state(AppState::UserSites); .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 },
));
} }
}); });
@ -262,42 +588,6 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition
// }, // },
// ))); // )));
main.on_site_pw_accepted(|string| {
warn!("Accepted: {string}");
let Ok(c_string) = CString::new(&*string) else {
warn!("String cannot be converted to a C string: {string:?}");
return;
};
unsafe {
let user_key_start = Instant::now();
let user_key = &*spectre_api_sys::spectre_user_key(
c"test".as_ptr(),
c_string.as_ptr(),
SpectreAlgorithm::Current,
);
let user_key_duration = Instant::now().duration_since(user_key_start);
warn!(
"User key derived in {} seconds:\n{user_key:02x?}",
user_key_duration.display_as_secs()
);
let site_key_start = Instant::now();
let site_key = &*spectre_api_sys::spectre_site_key(
user_key as *const SpectreUserKey,
c"example.org".as_ptr(),
SpectreCounter::Initial,
SpectreKeyPurpose::Authentication,
c"".as_ptr(),
);
let site_key_duration = Instant::now().duration_since(site_key_start);
warn!(
"Site key derived in {} seconds:\n{site_key:02x?}",
site_key_duration.display_as_secs()
);
// TODO: Free memory
}
});
run_event_loop(main).await; run_event_loop(main).await;
} }

View file

@ -5,6 +5,8 @@ import { IconButton } from "widgets/icon-button.slint";
export component LoginView inherits VerticalLayout { export component LoginView inherits VerticalLayout {
padding: Style.spacing; padding: Style.spacing;
spacing: Style.spacing; spacing: Style.spacing;
in property <[string]> usernames <=> combo_box_username.model;
callback users_clicked();
callback pw_accepted(string, string); callback pw_accepted(string, string);
Rectangle { } Rectangle { }
@ -12,11 +14,12 @@ export component LoginView inherits VerticalLayout {
spacing: Style.spacing; spacing: Style.spacing;
IconButton { IconButton {
icon: @image-url("images/users.svg"); icon: @image-url("images/users.svg");
clicked => {
users_clicked();
}
} }
combo_box_username := ComboBox { combo_box_username := ComboBox { }
model: ["first", "second"];
}
line_edit_user_pw := LineEdit { line_edit_user_pw := LineEdit {
input-type: InputType.password; input-type: InputType.password;

View file

@ -1,13 +1,3 @@
/*
import {
Button,
VerticalBox,
LineEdit,
GridBox,
TabWidget,
Button,
} from "std-widgets.slint";
*/
import { import {
Button, Button,
VerticalBox, VerticalBox,
@ -18,58 +8,90 @@ import {
ComboBox, ComboBox,
} from "std-widgets.slint"; } from "std-widgets.slint";
// Implementations of standard widgets can be found at https://github.com/slint-ui/slint/tree/master/internal/compiler/widgets
// See https://github.com/slint-ui/slint/issues/4956 for issues with fonts. // See https://github.com/slint-ui/slint/issues/4956 for issues with fonts.
import "../fonts/IBM_Plex_Mono/IBMPlexMono-Regular.ttf"; import "../fonts/IBM_Plex_Mono/IBMPlexMono-Regular.ttf";
import { UserSitesView } from "user-sites-view.slint"; import { UserSitesView } from "user-sites-view.slint";
import { Style } from "globals.slint"; import { Style } from "globals.slint";
import { UsersView } from "users-view.slint"; import { UsersView } from "users-view.slint";
import { LoginView } from "login-view.slint"; import { LoginView } from "login-view.slint";
import { UserEditView } from "user-edit-view.slint";
export enum AppState { export enum AppState {
login, login,
user-sites,
users, users,
user-edit,
user-sites,
} }
export component AppWindow inherits Window { export component AppWindow inherits Window {
// Special characters to generate pre-render glyphs for.
in property <string> dummy: "ÄÖÜÁÉÍÓÚÝŔŚĹŹĆŃĚĽŽŠČŘĎŤŇŮÅäöüáéíóúýŕśĺźćńěľžščřďťňůåß„“”‘’—–@&$%+=¡¿¢£$¥€²³¼½¬¤¦§©®™°´ˇ¨"; in property <string> dummy: "ÄÖÜÁÉÍÓÚÝŔŚĹŹĆŃĚĽŽŠČŘĎŤŇŮÅäöüáéíóúýŕśĺźćńěľžščřďťňůåß„“”‘’—–@&$%+=¡¿¢£$¥€²³¼½¬¤¦§©®™°´ˇ¨";
// in property <string> dummy_identicon_symbols: "╔╚╰═█░▒▓☺☻╗╝╯═◈◎◐◑◒◓☀☁☂☃☄★☆☎☏⎈⌂☘☢☣☕⌚⌛⏰⚡⛄⛅☔♔♕♖♗♘♙♚♛♜♝♞♟♨♩♪♫⚐⚑⚔⚖⚙⚠⌘⏎✄✆✈✉✌";
default-font-family: "IBM Plex Mono"; default-font-family: "IBM Plex Mono";
default-font-size: 16pt; default-font-size: 16pt;
height: 368px; height: 368px;
width: 960px; width: 960px;
forward-focus: focus-scope;
in property <AppState> app-state: AppState.login; in property <AppState> app-state: AppState.login;
callback escape();
callback enter-view(view: AppState);
// Login View // Login View
callback login_pw_accepted <=> login_view.pw_accepted; callback login_pw_accepted <=> login_view.pw_accepted;
// Sites View // Users View
callback users_edit_user <=> users_view.edit_user;
// User Edit View
in property <string> user_edit_username <=> user_edit_view.username;
in-out property <string> user_edit_identicon <=> user_edit_view.identicon;
callback user_edit_compute_identicon <=> user_edit_view.compute_identicon;
callback user_edit_confirm <=> user_edit_view.confirm;
// User Sites View
in property <[StandardListViewItem]> sites <=> user_sites_view.model; in property <[StandardListViewItem]> sites <=> user_sites_view.model;
callback site_pw_edited <=> user_sites_view.pw_edited; callback site_pw_edited <=> user_sites_view.pw_edited;
callback site_pw_accepted <=> user_sites_view.pw_accepted; callback site_pw_accepted <=> user_sites_view.pw_accepted;
// Sites View focus-scope := FocusScope {
in property <[StandardListViewItem]> usernames <=> users_view.model; key-pressed(event) => {
callback user_username_edited <=> users_view.pw_edited; if event.text == "\u{1b}" {
callback user_username_accepted <=> users_view.pw_accepted; root.escape();
VerticalBox { EventResult.accept
width: 960px; } else {
height: 368px; EventResult.reject
padding: 0px;
padding-top: 120px;
padding-bottom: 8px;
Rectangle {
height: 240px;
background: #00141d;
// For debugging bounds.
// border-color: #ffcf00;
// border-width: 1px;
login_view := LoginView {
visible: app-state == AppState.login;
} }
}
vertical-box := VerticalBox {
width: 960px;
height: 368px;
padding: 0px;
padding-top: 120px;
padding-bottom: 8px;
Rectangle {
height: 240px;
background: #00141d;
// For debugging bounds.
// border-color: #ffcf00;
// border-width: 1px;
login_view := LoginView {
visible: app-state == AppState.login;
users_clicked => {
enter-view(AppState.users);
}
}
user_sites_view := UserSitesView { users_view := UsersView {
visible: app-state == AppState.user-sites; visible: app-state == AppState.users;
} }
users_view := UsersView { user_edit_view := UserEditView {
visible: app-state == AppState.users; visible: app-state == AppState.user-edit;
cancel => {
root.escape();
}
}
user_sites_view := UserSitesView {
visible: app-state == AppState.user-sites;
}
} }
} }
} }

View file

@ -0,0 +1,64 @@
import { LineEdit, StandardListView, Button } from "std-widgets.slint";
import { Style } from "globals.slint";
import { IconButton } from "widgets/icon-button.slint";
export component UserEditView inherits HorizontalLayout {
padding: Style.spacing;
spacing: Style.spacing;
in property <string> username;
in-out property <string> identicon;
callback compute_identicon(encrypted_key: string, password: string);
callback confirm(encrypted_key: string);
callback cancel <=> button_cancel.clicked;
VerticalLayout {
spacing: Style.spacing;
Rectangle { }
Text {
text: "Enter " + username + "'s encrypted key and press Enter:";
}
line_edit_encrypted_key := LineEdit {
input-type: InputType.text;
placeholder-text: "Encrypted key";
edited(text) => {
root.identicon = "";
}
accepted(text) => {
line_edit_password.focus();
}
}
line_edit_password := LineEdit {
input-type: InputType.text;
placeholder-text: "Password";
edited(text) => {
root.identicon = "";
}
accepted(text) => {
compute_identicon(line_edit_encrypted_key.text, text);
}
}
Text {
text: identicon.is-empty ? "" : ("Check the identicon: " + identicon);
}
HorizontalLayout {
spacing: Style.spacing;
button_cancel := Button {
text: "Cancel";
}
button_confirm := Button {
text: "Confirm";
enabled: !identicon.is-empty;
clicked => {
confirm(line_edit_encrypted_key.text);
}
}
}
Rectangle { }
}
}

View file

@ -5,26 +5,42 @@ import { IconButton } from "widgets/icon-button.slint";
export component UsersView inherits HorizontalLayout { export component UsersView inherits HorizontalLayout {
padding: Style.spacing; padding: Style.spacing;
spacing: Style.spacing; spacing: Style.spacing;
in property <[StandardListViewItem]> model <=> list_view_sites.model; callback edit_user(username: string, new: bool);
in-out property <int> current-item <=> list_view_sites.current-item;
callback pw_edited <=> line_edit_site_pw.edited;
callback pw_accepted <=> line_edit_site_pw.accepted;
VerticalLayout { VerticalLayout {
spacing: Style.spacing; spacing: Style.spacing * 2;
Text { VerticalLayout {
text: "Username:"; spacing: Style.spacing;
Text {
text: "Add new user:";
}
line_edit_site_pw := LineEdit {
input-type: InputType.text;
placeholder-text: "Full Name";
accepted(text) => {
edit_user(text, true);
}
}
} }
line_edit_site_pw := LineEdit { VerticalLayout {
input-type: InputType.text; spacing: Style.spacing;
placeholder-text: "Full Name"; Text {
} text: "Edit existing user:";
}
list_view_sites := StandardListView { FocusScope {
model: [ forward-focus: list_view_sites;
{ text: "Test" }, key-pressed(event) => {
{ text: "Test" }, if event.text == "\u{0D}" /* enter */ || event.text == " " /* space */ {
]; edit_user(list_view_sites.current-item, false);
EventResult.accept
} else {
EventResult.reject
}
}
list_view_sites := StandardListView { }
}
} }
} }