2025-12-29 19:36:00 +01:00
|
|
|
// #![cfg_attr(not(feature = "simulator"), no_main)]
|
|
|
|
|
|
2026-02-13 03:00:46 +01:00
|
|
|
use core::{cell::RefCell, ffi::CStr, pin::Pin};
|
2026-02-08 21:04:28 +01:00
|
|
|
|
|
|
|
|
use alloc::{
|
|
|
|
|
borrow::Cow,
|
|
|
|
|
boxed::Box,
|
|
|
|
|
ffi::CString,
|
|
|
|
|
format,
|
|
|
|
|
rc::Rc,
|
|
|
|
|
string::{String, ToString},
|
|
|
|
|
vec,
|
2026-02-09 02:14:06 +01:00
|
|
|
vec::Vec,
|
2026-02-08 21:04:28 +01:00
|
|
|
};
|
2026-01-31 15:36:36 +01:00
|
|
|
use embassy_time::Instant;
|
2026-02-04 03:14:21 +01:00
|
|
|
use hex::FromHexError;
|
2026-02-08 21:04:28 +01:00
|
|
|
use i_slint_core::model::{ModelChangeListener, ModelChangeListenerContainer};
|
2026-02-08 01:17:33 +01:00
|
|
|
use log::{error, info, warn};
|
2026-02-04 03:14:21 +01:00
|
|
|
use password_hash::Key;
|
2026-02-08 21:04:28 +01:00
|
|
|
use slint::{
|
|
|
|
|
Model, ModelExt, ModelNotify, ModelRc, ModelTracker, SharedString, StandardListViewItem,
|
|
|
|
|
VecModel,
|
|
|
|
|
};
|
|
|
|
|
use spectre_api_sys::{
|
|
|
|
|
SpectreAlgorithm, SpectreCounter, SpectreKeyID, SpectreKeyPurpose, SpectreResultType,
|
2026-02-13 03:00:46 +01:00
|
|
|
SpectreUserKey,
|
2026-02-08 21:04:28 +01:00
|
|
|
};
|
2026-01-10 19:21:13 +01:00
|
|
|
|
|
|
|
|
#[cfg(feature = "limit-fps")]
|
|
|
|
|
use crate::FRAME_DURATION_MIN;
|
2026-01-24 21:12:25 +01:00
|
|
|
use crate::{
|
2026-02-22 00:59:01 +01:00
|
|
|
PSRAM_ALLOCATOR,
|
2026-02-08 21:04:28 +01:00
|
|
|
db::{
|
2026-02-09 02:14:06 +01:00
|
|
|
AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers,
|
|
|
|
|
PartitionAcid, ReadTransactionExt,
|
2026-02-08 21:04:28 +01:00
|
|
|
},
|
2026-02-04 03:14:21 +01:00
|
|
|
ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY},
|
2026-02-13 02:50:32 +01:00
|
|
|
proxy::OUTPUT_STRING_CHANNEL,
|
2026-01-31 15:24:36 +01:00
|
|
|
ui::{
|
|
|
|
|
backend::SlintBackend,
|
2026-02-22 00:59:01 +01:00
|
|
|
dpi::FRAMES_SKIPPED,
|
2026-01-31 15:24:36 +01:00
|
|
|
messages::{
|
2026-02-08 21:04:28 +01:00
|
|
|
CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit,
|
|
|
|
|
CallbackMessageUserSites, CallbackMessageUsers, LoginResult,
|
2026-01-31 15:24:36 +01:00
|
|
|
},
|
2026-02-08 21:04:28 +01:00
|
|
|
storage::{SpectreSite, SpectreSiteConfig, SpectreUserConfig, SpectreUsersConfig},
|
2026-01-31 15:24:36 +01:00
|
|
|
},
|
2026-01-24 21:12:25 +01:00
|
|
|
util::DurationExt,
|
|
|
|
|
};
|
2026-01-10 19:21:13 +01:00
|
|
|
|
2025-12-31 22:24:26 +01:00
|
|
|
pub mod backend;
|
2026-02-14 20:03:32 +01:00
|
|
|
pub mod dpi;
|
2026-01-31 15:24:36 +01:00
|
|
|
pub mod messages;
|
|
|
|
|
pub mod storage;
|
2026-01-10 19:21:13 +01:00
|
|
|
pub mod window_adapter;
|
2025-12-31 22:24:26 +01:00
|
|
|
|
2025-12-29 19:36:00 +01:00
|
|
|
slint::include_modules!();
|
2026-01-10 19:21:13 +01:00
|
|
|
|
2026-02-08 01:17:33 +01:00
|
|
|
fn spectre_derive_user_key(
|
|
|
|
|
username: &CStr,
|
|
|
|
|
password: &CStr,
|
|
|
|
|
encrypted_key: Option<Key>,
|
|
|
|
|
) -> SpectreUserKey {
|
|
|
|
|
if let Some(encrypted_key) = encrypted_key {
|
|
|
|
|
critical_section::with(|cs| {
|
|
|
|
|
ACTIVE_ENCRYPTED_USER_KEY.borrow(cs).set(encrypted_key);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 02:46:53 +01:00
|
|
|
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()
|
|
|
|
|
);
|
2026-01-31 15:36:36 +01:00
|
|
|
let user_key_stack = *user_key;
|
2026-01-27 02:46:53 +01:00
|
|
|
|
|
|
|
|
// TODO: Erase memory before freeing
|
|
|
|
|
__spre_free(user_key as *const _ as *mut _);
|
|
|
|
|
|
|
|
|
|
user_key_stack
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 21:04:28 +01:00
|
|
|
fn spectre_derive_site_password(user_key: &SpectreUserKey, site_name: &CStr) -> String {
|
|
|
|
|
unsafe {
|
|
|
|
|
let site_password_c = &*spectre_api_sys::spectre_site_result(
|
|
|
|
|
user_key as *const SpectreUserKey,
|
|
|
|
|
site_name.as_ptr(),
|
|
|
|
|
SpectreResultType::SpectreResultDefaultResult,
|
|
|
|
|
core::ptr::null(),
|
|
|
|
|
SpectreCounter::Initial,
|
|
|
|
|
SpectreKeyPurpose::Authentication,
|
|
|
|
|
core::ptr::null(),
|
|
|
|
|
);
|
|
|
|
|
let site_password = CStr::from_ptr(site_password_c)
|
|
|
|
|
.to_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.to_string();
|
|
|
|
|
__spre_free(site_password_c as *const _ as *mut _);
|
|
|
|
|
site_password
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 19:21:13 +01:00
|
|
|
#[embassy_executor::task]
|
2026-01-24 00:42:16 +01:00
|
|
|
pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) {
|
2026-01-24 21:12:25 +01:00
|
|
|
let db = AcidDatabase::mount(flash_part_acid).await;
|
2026-01-25 01:45:25 +01:00
|
|
|
|
2026-01-24 21:12:25 +01:00
|
|
|
// TODO:
|
|
|
|
|
// * Store a config as a versioned postcard-serialized struct
|
|
|
|
|
// * Store accounts and sites as ranges in the DB
|
2026-01-24 00:42:16 +01:00
|
|
|
|
2026-01-28 22:46:14 +01:00
|
|
|
i_slint_core::properties::ALLOCATOR
|
|
|
|
|
.set(&PSRAM_ALLOCATOR)
|
|
|
|
|
.ok()
|
|
|
|
|
.unwrap();
|
2026-01-10 19:21:13 +01:00
|
|
|
slint::platform::set_platform(Box::new(backend)).expect("backend already initialized");
|
|
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
let main = AppWindow::new().unwrap();
|
|
|
|
|
let state = State::new(db, main).await;
|
2026-02-04 03:14:21 +01:00
|
|
|
let window = state.borrow().window.clone_strong();
|
2026-01-27 02:46:53 +01:00
|
|
|
|
2026-02-04 03:14:21 +01:00
|
|
|
State::run_event_loop(window).await;
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-01-27 02:46:53 +01:00
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
struct State {
|
|
|
|
|
window: AppWindow,
|
2026-02-04 03:14:21 +01:00
|
|
|
db: Rc<AcidDatabase>,
|
2026-01-31 15:24:36 +01:00
|
|
|
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>,
|
|
|
|
|
}
|
2026-01-27 02:46:53 +01:00
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
impl State {
|
|
|
|
|
async fn new(db: AcidDatabase, main: AppWindow) -> Rc<RefCell<Self>> {
|
2026-02-15 02:33:42 +01:00
|
|
|
let users = Self::load_users(&db).await;
|
2026-02-08 01:17:33 +01:00
|
|
|
let usernames = users.users.clone().map(|user| user.username);
|
|
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
let state = Rc::new(RefCell::new(State {
|
|
|
|
|
window: main.clone_strong(),
|
2026-02-08 01:17:33 +01:00
|
|
|
users,
|
2026-02-04 03:14:21 +01:00
|
|
|
db: Rc::new(db),
|
2026-01-31 15:24:36 +01:00
|
|
|
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| {
|
2026-02-08 01:17:33 +01:00
|
|
|
state.borrow_mut().set_view(view, true, true);
|
2026-01-27 02:46:53 +01:00
|
|
|
}
|
2026-01-31 15:24:36 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
main.on_escape({
|
|
|
|
|
let state = state.clone();
|
|
|
|
|
move || {
|
2026-02-04 03:14:21 +01:00
|
|
|
State::process_callback_message(&state, CallbackMessage::Escape);
|
2026-01-27 02:46:53 +01:00
|
|
|
}
|
2026-01-31 15:24:36 +01:00
|
|
|
});
|
|
|
|
|
|
2026-02-08 01:17:33 +01:00
|
|
|
main.set_login_usernames(ModelRc::new(usernames));
|
|
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
main.on_login_pw_accepted({
|
|
|
|
|
let state = state.clone();
|
2026-02-08 01:17:33 +01:00
|
|
|
move |user_index, username, password| {
|
2026-02-04 03:14:21 +01:00
|
|
|
State::process_callback_message(
|
|
|
|
|
&state,
|
2026-02-08 01:17:33 +01:00
|
|
|
CallbackMessage::Login(CallbackMessageLogin::PwAccepted {
|
|
|
|
|
user_index,
|
|
|
|
|
username,
|
|
|
|
|
password,
|
|
|
|
|
}),
|
2026-02-04 03:14:21 +01:00
|
|
|
);
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-09 02:14:06 +01:00
|
|
|
main.on_login_test_string_accepted(|string| {
|
|
|
|
|
slint::spawn_local(async move {
|
|
|
|
|
OUTPUT_STRING_CHANNEL.send(string.to_string()).await;
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
main.on_users_edit_user({
|
|
|
|
|
let state = state.clone();
|
|
|
|
|
move |username, new| {
|
2026-02-04 03:14:21 +01:00
|
|
|
State::process_callback_message(
|
|
|
|
|
&state,
|
|
|
|
|
CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }),
|
|
|
|
|
);
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
main.on_user_edit_compute_identicon({
|
|
|
|
|
let state = state.clone();
|
2026-02-04 03:14:21 +01:00
|
|
|
move |password| {
|
|
|
|
|
State::process_callback_message(
|
|
|
|
|
&state,
|
|
|
|
|
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon {
|
|
|
|
|
password,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
main.on_user_edit_compute_key_id({
|
|
|
|
|
let state = state.clone();
|
|
|
|
|
move |key| {
|
|
|
|
|
State::process_callback_message(
|
|
|
|
|
&state,
|
|
|
|
|
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeKeyId { key }),
|
|
|
|
|
);
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
main.on_user_edit_confirm({
|
|
|
|
|
let state = state.clone();
|
2026-02-08 21:04:28 +01:00
|
|
|
move |_encrypted_key| {
|
|
|
|
|
State::process_callback_message(
|
|
|
|
|
&state,
|
|
|
|
|
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
main.on_user_sites_site_name_edited({
|
|
|
|
|
let state = state.clone();
|
|
|
|
|
move |query| {
|
|
|
|
|
State::process_callback_message(
|
|
|
|
|
&state,
|
|
|
|
|
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameEdited { query }),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
main.on_user_sites_site_name_accepted({
|
|
|
|
|
let state = state.clone();
|
|
|
|
|
move |site_list_index| {
|
2026-02-04 03:14:21 +01:00
|
|
|
State::process_callback_message(
|
|
|
|
|
&state,
|
2026-02-08 21:04:28 +01:00
|
|
|
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameAccepted {
|
|
|
|
|
site_list_index,
|
2026-02-04 03:14:21 +01:00
|
|
|
}),
|
|
|
|
|
);
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-01-27 02:46:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 03:14:21 +01:00
|
|
|
async fn load_users(db: &AcidDatabase) -> SpectreUsersConfig {
|
|
|
|
|
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:?}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn save_users(db: &AcidDatabase, users: &SpectreUsersConfig) {
|
|
|
|
|
let mut write = db.write_transaction().await;
|
|
|
|
|
let buffer = postcard::to_allocvec(&users).unwrap();
|
|
|
|
|
write
|
|
|
|
|
.write(&DbKey::new(DbPathSpectreUsers), &buffer)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
write.commit().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 21:04:28 +01:00
|
|
|
fn make_site_list(
|
|
|
|
|
sites: &Rc<VecModel<SpectreSite>>,
|
|
|
|
|
query: &SharedString,
|
|
|
|
|
) -> Rc<VecModel<SiteListEntry>> {
|
|
|
|
|
let site_list = Rc::new(VecModel::default());
|
|
|
|
|
|
|
|
|
|
for site in sites.iter() {
|
|
|
|
|
site_list.push(SiteListEntry::Existing(site.site_name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !query.is_empty() {
|
|
|
|
|
site_list.push(SiteListEntry::New(query.clone()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
site_list
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_site_list(&mut self) {
|
|
|
|
|
if let Some(StateUserSites {
|
|
|
|
|
sites,
|
|
|
|
|
query,
|
|
|
|
|
site_list,
|
|
|
|
|
..
|
|
|
|
|
}) = self.state_user_sites.as_mut()
|
|
|
|
|
{
|
|
|
|
|
*site_list = State::make_site_list(sites, query);
|
|
|
|
|
self.window
|
|
|
|
|
.set_user_sites_sites(ModelRc::from(Rc::new(site_list.clone().map(|site| {
|
|
|
|
|
let mut item = StandardListViewItem::default();
|
|
|
|
|
item.text = site.to_string();
|
|
|
|
|
item
|
|
|
|
|
}))));
|
|
|
|
|
} else {
|
|
|
|
|
self.window.invoke_user_sites_site_name_clear();
|
|
|
|
|
self.window.set_user_sites_sites(Default::default());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 03:14:21 +01:00
|
|
|
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
|
|
|
|
let view = state_rc.borrow().view;
|
|
|
|
|
match view {
|
|
|
|
|
AppState::Login => StateLogin::process_callback_message(state_rc, message),
|
|
|
|
|
AppState::Users => StateUsers::process_callback_message(state_rc, message),
|
|
|
|
|
AppState::UserEdit => StateUserEdit::process_callback_message(state_rc, message),
|
|
|
|
|
AppState::UserSites => StateUserSites::process_callback_message(state_rc, message),
|
2026-01-27 02:46:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 01:17:33 +01:00
|
|
|
fn reset_view(&mut self) {
|
|
|
|
|
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(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_view(&mut self, view: AppState, reset_source: bool, reset_target: bool) {
|
|
|
|
|
if reset_source {
|
|
|
|
|
self.reset_view();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
self.view = view;
|
|
|
|
|
self.window.set_app_state(view);
|
2026-01-27 02:46:53 +01:00
|
|
|
|
2026-02-08 01:17:33 +01:00
|
|
|
if reset_target {
|
|
|
|
|
self.reset_view();
|
2026-01-27 02:46:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
/// 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()`.
|
2026-02-04 03:14:21 +01:00
|
|
|
async fn run_event_loop(window: AppWindow) -> ! {
|
|
|
|
|
window.show().unwrap();
|
2026-01-31 15:24:36 +01:00
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
slint::run_event_loop().unwrap();
|
2026-02-22 00:59:01 +01:00
|
|
|
// SIGNAL_LCD_SUBMIT.signal(());
|
2026-01-31 15:24:36 +01:00
|
|
|
#[cfg(feature = "limit-fps")]
|
|
|
|
|
embassy_time::Timer::after(FRAME_DURATION_MIN).await;
|
2026-02-22 00:59:01 +01:00
|
|
|
let frames_skipped = FRAMES_SKIPPED.wait().await;
|
|
|
|
|
|
|
|
|
|
if frames_skipped > 0 {
|
|
|
|
|
error!("Renderer missed {frames_skipped} frames.");
|
|
|
|
|
}
|
|
|
|
|
// SIGNAL_UI_RENDER.wait().await;
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-01-27 02:46:53 +01:00
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
#[expect(unreachable_code)]
|
2026-02-04 03:14:21 +01:00
|
|
|
window.hide().unwrap();
|
2026-01-27 02:46:53 +01:00
|
|
|
}
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-01-27 02:46:53 +01:00
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
trait AppViewTrait {
|
2026-02-04 03:14:21 +01:00
|
|
|
fn process_callback_message(_state_rc: &Rc<RefCell<State>>, _message: CallbackMessage) {}
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-01-27 02:46:53 +01:00
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
#[derive(Default)]
|
|
|
|
|
struct StateLogin {}
|
|
|
|
|
|
|
|
|
|
impl AppViewTrait for StateLogin {
|
2026-02-04 03:14:21 +01:00
|
|
|
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
|
|
|
|
let mut state = state_rc.borrow_mut();
|
2026-02-08 21:04:28 +01:00
|
|
|
match message {
|
|
|
|
|
CallbackMessage::Login(CallbackMessageLogin::PwAccepted {
|
|
|
|
|
user_index,
|
|
|
|
|
username: _,
|
|
|
|
|
password,
|
|
|
|
|
}) => {
|
|
|
|
|
let Some(user) = state.users.users.row_data(user_index as usize) else {
|
|
|
|
|
error!("Failed to find a user with index {user_index}.");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let username_c = CString::new(&*user.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, Some(user.encrypted_key));
|
|
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
// let mut write = db.write_transaction().await;
|
|
|
|
|
// write
|
|
|
|
|
// .write(
|
|
|
|
|
// &DbKey::new(DbPathSpectreUserSite {
|
|
|
|
|
// user_sites: DbPathSpectreUserSites {
|
|
|
|
|
// username: "test".into(),
|
|
|
|
|
// },
|
|
|
|
|
// site: "example.org".into(),
|
|
|
|
|
// }),
|
|
|
|
|
// &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(),
|
|
|
|
|
// )
|
|
|
|
|
// .await
|
|
|
|
|
// .unwrap();
|
|
|
|
|
// write
|
|
|
|
|
// .write(
|
|
|
|
|
// &DbKey::new(DbPathSpectreUserSite {
|
|
|
|
|
// user_sites: DbPathSpectreUserSites {
|
|
|
|
|
// username: "test".into(),
|
|
|
|
|
// },
|
|
|
|
|
// site: "sub.example.org".into(),
|
|
|
|
|
// }),
|
|
|
|
|
// &postcard::to_allocvec(&SpectreSiteConfig::default()).unwrap(),
|
|
|
|
|
// )
|
|
|
|
|
// .await
|
|
|
|
|
// .unwrap();
|
|
|
|
|
// write
|
|
|
|
|
// .write(
|
|
|
|
|
// &DbKey::new(DbPathSpectreUsers),
|
|
|
|
|
// &postcard::to_allocvec(&value).unwrap(),
|
|
|
|
|
// )
|
|
|
|
|
// .await
|
|
|
|
|
// .unwrap();
|
|
|
|
|
// write.commit().await.unwrap();
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
if user.key_id != user_key.keyID.bytes {
|
|
|
|
|
State::process_callback_message(
|
2026-02-13 03:00:46 +01:00
|
|
|
state_rc,
|
2026-02-08 21:04:28 +01:00
|
|
|
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
|
|
|
|
username: user.username,
|
|
|
|
|
result: LoginResult::Failure,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slint::spawn_local({
|
|
|
|
|
let state_rc = state_rc.clone();
|
|
|
|
|
let username = user.username.clone();
|
|
|
|
|
let db = state.db.clone();
|
|
|
|
|
|
|
|
|
|
async move {
|
|
|
|
|
let read = db.read_transaction().await;
|
|
|
|
|
// TODO: https://github.com/embassy-rs/ekv/issues/20
|
|
|
|
|
let mut buffer = vec![0; 256];
|
|
|
|
|
// let slice = read
|
|
|
|
|
// .read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer)
|
|
|
|
|
// .await
|
|
|
|
|
// .unwrap();
|
|
|
|
|
// let read_value = postcard::from_bytes::<SpectreUsersConfig>(&slice).unwrap();
|
|
|
|
|
|
|
|
|
|
let key_sites = DbKey::new(DbPathSpectreUserSites {
|
|
|
|
|
username: Cow::Borrowed(&username),
|
|
|
|
|
});
|
|
|
|
|
let mut key_buffer = [0_u8; ekv::config::MAX_KEY_SIZE];
|
|
|
|
|
let mut cursor = read
|
|
|
|
|
.read_range(key_sites.range_of_children())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
let sites = VecModel::default();
|
|
|
|
|
|
|
|
|
|
while let Some((key_len, value_len)) =
|
|
|
|
|
cursor.next(&mut key_buffer, &mut buffer).await.unwrap()
|
|
|
|
|
{
|
|
|
|
|
let key = DbKey::from_raw(key_buffer[..key_len].into());
|
|
|
|
|
let mut key_segments = key.segments();
|
|
|
|
|
let value = &buffer[..value_len];
|
|
|
|
|
let site_config =
|
2026-02-13 03:00:46 +01:00
|
|
|
postcard::from_bytes::<SpectreSiteConfig>(value).unwrap();
|
2026-02-08 21:04:28 +01:00
|
|
|
let site = SpectreSite {
|
|
|
|
|
config: site_config,
|
|
|
|
|
username: key_segments.nth(2).unwrap().as_ref().into(),
|
|
|
|
|
site_name: key_segments.nth(1).unwrap().as_ref().into(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
info!("site = {:#?}", site);
|
|
|
|
|
sites.push(site);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
State::process_callback_message(
|
|
|
|
|
&state_rc,
|
|
|
|
|
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
|
|
|
|
username: user.username,
|
|
|
|
|
result: LoginResult::Success {
|
|
|
|
|
user_key,
|
|
|
|
|
sites: Rc::new(sites),
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
|
|
|
|
username,
|
|
|
|
|
result: LoginResult::Success { user_key, sites },
|
|
|
|
|
}) => {
|
|
|
|
|
info!("Correct password entered for user {:?}.", username);
|
2026-02-08 01:17:33 +01:00
|
|
|
state.state_user_sites = Some(StateUserSites {
|
2026-02-08 21:04:28 +01:00
|
|
|
username,
|
2026-02-08 01:17:33 +01:00
|
|
|
user_key,
|
2026-02-08 21:04:28 +01:00
|
|
|
query: SharedString::new(),
|
|
|
|
|
sites: sites.clone(),
|
|
|
|
|
site_list: Default::default(),
|
2026-02-08 01:17:33 +01:00
|
|
|
});
|
2026-02-08 21:04:28 +01:00
|
|
|
state.update_site_list();
|
2026-02-08 01:17:33 +01:00
|
|
|
state.set_view(AppState::UserSites, true, false);
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-02-08 21:04:28 +01:00
|
|
|
CallbackMessage::Login(CallbackMessageLogin::LoginResult {
|
|
|
|
|
username,
|
|
|
|
|
result: LoginResult::Failure,
|
|
|
|
|
}) => {
|
|
|
|
|
warn!("Incorrect password entered for user {:?}.", username);
|
|
|
|
|
}
|
|
|
|
|
_ => (),
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-01-27 02:46:53 +01:00
|
|
|
}
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-01-27 02:46:53 +01:00
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
#[derive(Default)]
|
|
|
|
|
struct StateUsers {}
|
2026-01-27 02:46:53 +01:00
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
impl AppViewTrait for StateUsers {
|
2026-02-04 03:14:21 +01:00
|
|
|
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
|
|
|
|
let mut state = state_rc.borrow_mut();
|
2026-01-31 15:24:36 +01:00
|
|
|
match message {
|
|
|
|
|
CallbackMessage::Escape => {
|
2026-02-08 01:17:33 +01:00
|
|
|
state.set_view(AppState::Login, true, false);
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
|
|
|
|
CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }) => {
|
|
|
|
|
state.state_user_edit = StateUserEdit {
|
|
|
|
|
username: username.clone(),
|
|
|
|
|
new,
|
2026-02-04 03:14:21 +01:00
|
|
|
password: None,
|
|
|
|
|
encrypted_key: None,
|
2026-01-31 15:24:36 +01:00
|
|
|
};
|
|
|
|
|
state.window.set_user_edit_username(username);
|
2026-02-08 01:17:33 +01:00
|
|
|
state.set_view(AppState::UserEdit, true, false);
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
2026-01-27 02:46:53 +01:00
|
|
|
}
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-01-27 02:46:53 +01:00
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
#[derive(Default)]
|
|
|
|
|
struct StateUserEdit {
|
|
|
|
|
username: SharedString,
|
|
|
|
|
new: bool,
|
2026-02-04 03:14:21 +01:00
|
|
|
password: Option<SharedString>,
|
|
|
|
|
encrypted_key: Option<(Key, SpectreUserKey)>,
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-01-10 19:21:13 +01:00
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
impl AppViewTrait for StateUserEdit {
|
2026-02-04 03:14:21 +01:00
|
|
|
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
|
|
|
|
let state = state_rc.clone();
|
|
|
|
|
let mut state = state.borrow_mut();
|
2026-01-31 15:24:36 +01:00
|
|
|
match message {
|
|
|
|
|
CallbackMessage::Escape => {
|
2026-02-08 01:17:33 +01:00
|
|
|
state.set_view(AppState::Users, true, false);
|
2026-01-27 02:46:53 +01:00
|
|
|
}
|
2026-02-04 03:14:21 +01:00
|
|
|
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { password }) => {
|
2026-01-31 15:24:36 +01:00
|
|
|
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()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
warn!("Identicon: {identicon} ({identicon:?})");
|
|
|
|
|
state.window.set_user_edit_identicon(identicon.clone());
|
2026-02-04 03:14:21 +01:00
|
|
|
state.state_user_edit.password = Some(password);
|
|
|
|
|
}
|
|
|
|
|
CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeKeyId {
|
|
|
|
|
key: key_string,
|
|
|
|
|
}) => {
|
|
|
|
|
let Some(password) = state.state_user_edit.password.as_ref() else {
|
|
|
|
|
warn!("Attempted to compute a key ID when no password has been entered.");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut key: Key = [0; _];
|
|
|
|
|
if let Err(key_decode_error) = hex::decode_to_slice(&*key_string, &mut key) {
|
|
|
|
|
let message = match key_decode_error {
|
|
|
|
|
FromHexError::InvalidStringLength | FromHexError::OddLength => {
|
|
|
|
|
let required_size = key.len() * 2;
|
|
|
|
|
let provided_size = key_string.len();
|
|
|
|
|
let delta = provided_size as i32 - required_size as i32;
|
|
|
|
|
|
|
|
|
|
if delta < 0 {
|
|
|
|
|
slint::format!("Missing {} characters.", -delta)
|
|
|
|
|
} else if delta > 0 {
|
|
|
|
|
slint::format!("{} too many characters.", delta)
|
|
|
|
|
} else {
|
|
|
|
|
slint::format!("Invalid key length.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
FromHexError::InvalidHexCharacter { c, index } => {
|
|
|
|
|
slint::format!("Invalid character {c:?} at position {index}.")
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
state.window.set_user_edit_key_error(message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.");
|
2026-02-08 01:17:33 +01:00
|
|
|
let user_key = spectre_derive_user_key(&username_c, &password_c, Some(key));
|
2026-02-04 03:14:21 +01:00
|
|
|
|
|
|
|
|
state.window.set_user_edit_key_error(SharedString::new());
|
|
|
|
|
state.window.set_user_edit_key_id(
|
|
|
|
|
CStr::from_bytes_with_nul(&user_key.keyID.hex)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.to_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
state.state_user_edit.encrypted_key = Some((key, user_key));
|
|
|
|
|
}
|
2026-02-08 21:04:28 +01:00
|
|
|
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmRequest) => {
|
2026-02-04 03:14:21 +01:00
|
|
|
let Some((
|
|
|
|
|
encrypted_key,
|
|
|
|
|
SpectreUserKey {
|
|
|
|
|
keyID: SpectreKeyID { bytes: key_id, .. },
|
|
|
|
|
..
|
|
|
|
|
},
|
|
|
|
|
)) = state.state_user_edit.encrypted_key.take()
|
|
|
|
|
else {
|
|
|
|
|
warn!("Encrypted key is not set.");
|
|
|
|
|
return;
|
|
|
|
|
};
|
2026-02-08 01:17:33 +01:00
|
|
|
|
2026-02-04 03:14:21 +01:00
|
|
|
// If a user with that username already exists, overwrite it.
|
2026-02-08 01:17:33 +01:00
|
|
|
let user = SpectreUserConfig {
|
|
|
|
|
username: state.state_user_edit.username.clone(),
|
2026-01-31 15:24:36 +01:00
|
|
|
encrypted_key,
|
2026-02-04 03:14:21 +01:00
|
|
|
key_id,
|
2026-02-08 01:17:33 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut existing_index = None;
|
|
|
|
|
|
|
|
|
|
for index in 0..state.users.users.row_count() {
|
2026-02-13 03:00:46 +01:00
|
|
|
if let Some(current_user) = state.users.users.row_data(index)
|
|
|
|
|
&& current_user.username == user.username
|
|
|
|
|
{
|
|
|
|
|
existing_index = Some(index);
|
2026-02-08 01:17:33 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(existing_index) = existing_index {
|
|
|
|
|
state.users.users.set_row_data(existing_index, user);
|
|
|
|
|
} else {
|
|
|
|
|
state.users.users.push(user);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 03:14:21 +01:00
|
|
|
slint::spawn_local({
|
|
|
|
|
let state_rc = state_rc.clone();
|
|
|
|
|
let db = state.db.clone();
|
|
|
|
|
let users = state.users.clone();
|
2026-02-08 21:04:28 +01:00
|
|
|
|
2026-02-04 03:14:21 +01:00
|
|
|
async move {
|
|
|
|
|
State::save_users(&db, &users).await;
|
|
|
|
|
State::process_callback_message(
|
|
|
|
|
&state_rc,
|
|
|
|
|
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmProcessed),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-31 15:24:36 +01:00
|
|
|
})
|
2026-02-04 03:14:21 +01:00
|
|
|
.unwrap();
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-02-04 03:14:21 +01:00
|
|
|
CallbackMessage::UserEdit(CallbackMessageUserEdit::ConfirmProcessed) => {
|
|
|
|
|
state.state_user_edit = Default::default();
|
2026-02-08 01:17:33 +01:00
|
|
|
state.set_view(AppState::Users, true, true);
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
|
|
|
|
_ => (),
|
2026-01-26 19:21:49 +01:00
|
|
|
}
|
2026-01-31 15:24:36 +01:00
|
|
|
}
|
2026-01-24 00:42:16 +01:00
|
|
|
}
|
2026-01-10 19:21:13 +01:00
|
|
|
|
2026-01-31 15:24:36 +01:00
|
|
|
struct StateUserSites {
|
|
|
|
|
username: SharedString,
|
|
|
|
|
user_key: SpectreUserKey,
|
2026-02-08 21:04:28 +01:00
|
|
|
query: SharedString,
|
|
|
|
|
sites: Rc<VecModel<SpectreSite>>,
|
|
|
|
|
site_list: Rc<VecModel<SiteListEntry>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AppViewTrait for StateUserSites {
|
|
|
|
|
fn process_callback_message(state_rc: &Rc<RefCell<State>>, message: CallbackMessage) {
|
|
|
|
|
let state = state_rc.clone();
|
|
|
|
|
let mut state = state.borrow_mut();
|
|
|
|
|
match message {
|
|
|
|
|
CallbackMessage::Escape => {
|
|
|
|
|
state.set_view(AppState::Login, true, false);
|
|
|
|
|
}
|
|
|
|
|
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameEdited { query }) => {
|
|
|
|
|
if let Some(user_sites) = state.state_user_sites.as_mut() {
|
|
|
|
|
user_sites.query = query;
|
|
|
|
|
state.update_site_list();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CallbackMessage::UserSites(CallbackMessageUserSites::SiteNameAccepted {
|
|
|
|
|
site_list_index,
|
|
|
|
|
}) => {
|
|
|
|
|
let Some(user_sites) = state.state_user_sites.as_mut() else {
|
|
|
|
|
error!("User sites uninitialized.");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let Some(site_list_entry) = user_sites.site_list.row_data(site_list_index as usize)
|
|
|
|
|
else {
|
|
|
|
|
error!("Invalid site list entry index: {site_list_index}");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
warn!("Site name accepted: {site_list_entry:?}");
|
|
|
|
|
let site_name = match site_list_entry {
|
|
|
|
|
SiteListEntry::New(site_name) => site_name,
|
|
|
|
|
SiteListEntry::Existing(site_name) => site_name,
|
|
|
|
|
};
|
|
|
|
|
let site_name_c = CString::new(&*site_name).unwrap();
|
|
|
|
|
let site_password =
|
|
|
|
|
spectre_derive_site_password(&user_sites.user_key, &site_name_c);
|
|
|
|
|
|
|
|
|
|
warn!("Site password: {site_password:?}");
|
2026-02-09 02:14:06 +01:00
|
|
|
|
|
|
|
|
slint::spawn_local({
|
|
|
|
|
let username = user_sites.username.clone();
|
|
|
|
|
let db = state.db.clone();
|
|
|
|
|
async move {
|
2026-02-12 02:39:31 +01:00
|
|
|
// Send password to the host.
|
|
|
|
|
OUTPUT_STRING_CHANNEL.send(site_password).await;
|
|
|
|
|
|
|
|
|
|
// Update the stored site.
|
2026-02-09 02:14:06 +01:00
|
|
|
let mut write = db.write_transaction().await;
|
|
|
|
|
let key = DbKey::new(DbPathSpectreUserSite {
|
|
|
|
|
user_sites: DbPathSpectreUserSites {
|
|
|
|
|
username: Cow::Borrowed(&username),
|
|
|
|
|
},
|
|
|
|
|
site: Cow::Borrowed(&site_name),
|
|
|
|
|
});
|
|
|
|
|
let site = SpectreSiteConfig::default();
|
|
|
|
|
let site_bytes =
|
|
|
|
|
postcard::to_extend(&site, Vec::<u8, _>::new_in(&PSRAM_ALLOCATOR))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
write.write(&key, &site_bytes).await.unwrap();
|
|
|
|
|
write.commit().await.unwrap();
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
2026-02-08 21:04:28 +01:00
|
|
|
}
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct SitesModel<M>(Pin<Box<ModelChangeListenerContainer<SitesModelInner<M>>>>)
|
|
|
|
|
where
|
|
|
|
|
M: Model + 'static;
|
|
|
|
|
|
|
|
|
|
struct SitesModelInner<M>
|
|
|
|
|
where
|
|
|
|
|
M: Model + 'static,
|
|
|
|
|
{
|
|
|
|
|
wrapped_model: M,
|
|
|
|
|
notify: ModelNotify,
|
2026-01-10 19:21:13 +01:00
|
|
|
}
|
2026-01-31 15:24:36 +01:00
|
|
|
|
2026-02-08 21:04:28 +01:00
|
|
|
impl<M> ModelChangeListener for SitesModelInner<M>
|
|
|
|
|
where
|
|
|
|
|
M: Model + 'static,
|
|
|
|
|
{
|
|
|
|
|
fn row_changed(self: Pin<&Self>, row: usize) {
|
|
|
|
|
self.notify
|
|
|
|
|
.row_changed(self.wrapped_model.row_count() - 1 - row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn row_added(self: Pin<&Self>, index: usize, count: usize) {
|
|
|
|
|
let row_count = self.wrapped_model.row_count();
|
|
|
|
|
let old_row_count = row_count - count;
|
|
|
|
|
let index = old_row_count - index;
|
|
|
|
|
self.notify.row_added(index, count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn row_removed(self: Pin<&Self>, index: usize, count: usize) {
|
|
|
|
|
let row_count = self.wrapped_model.row_count();
|
|
|
|
|
self.notify.row_removed(row_count - index, count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reset(self: Pin<&Self>) {
|
|
|
|
|
self.notify.reset()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<M> SitesModel<M>
|
|
|
|
|
where
|
|
|
|
|
M: Model + 'static,
|
|
|
|
|
{
|
|
|
|
|
pub fn new(wrapped_model: M) -> Self {
|
|
|
|
|
let inner = SitesModelInner {
|
|
|
|
|
wrapped_model,
|
|
|
|
|
notify: Default::default(),
|
|
|
|
|
};
|
|
|
|
|
let container = Box::pin(ModelChangeListenerContainer::new(inner));
|
|
|
|
|
container
|
|
|
|
|
.wrapped_model
|
|
|
|
|
.model_tracker()
|
|
|
|
|
.attach_peer(container.as_ref().model_peer());
|
|
|
|
|
Self(container)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns a reference to the inner model
|
|
|
|
|
pub fn source_model(&self) -> &M {
|
|
|
|
|
&self.0.as_ref().get().get_ref().wrapped_model
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<M> Model for SitesModel<M>
|
|
|
|
|
where
|
|
|
|
|
M: Model + 'static,
|
|
|
|
|
{
|
|
|
|
|
type Data = M::Data;
|
|
|
|
|
|
|
|
|
|
fn row_count(&self) -> usize {
|
|
|
|
|
self.0.wrapped_model.row_count()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn row_data(&self, row: usize) -> Option<Self::Data> {
|
|
|
|
|
let count = self.0.wrapped_model.row_count();
|
|
|
|
|
self.0.wrapped_model.row_data(count.checked_sub(row + 1)?)
|
|
|
|
|
}
|
|
|
|
|
fn set_row_data(&self, row: usize, data: Self::Data) {
|
|
|
|
|
let count = self.0.as_ref().wrapped_model.row_count();
|
|
|
|
|
self.0.wrapped_model.set_row_data(count - row - 1, data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn model_tracker(&self) -> &dyn ModelTracker {
|
|
|
|
|
&self.0.notify
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn as_any(&self) -> &dyn core::any::Any {
|
|
|
|
|
self
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub enum SiteListEntry {
|
|
|
|
|
New(SharedString),
|
|
|
|
|
Existing(SharedString),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SiteListEntry {
|
|
|
|
|
pub fn to_string(&self) -> SharedString {
|
|
|
|
|
match self {
|
|
|
|
|
SiteListEntry::New(site) => slint::format!("{site} <Add new site>"),
|
|
|
|
|
SiteListEntry::Existing(site) => site.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|