Add user onboarding, keyboard overlay, and DB support
Introduce first-time user onboarding and a navigable on-screen keyboard, plus persistent user storage via a bundled rusqlite database and Tauri API. Backend: add rusqlite dependency, DB initialization in tauri setup, schema migration/backfill for users, and two Tauri commands (get_first_user, create_user) with input sanitization and structured UserRecord. Frontend: add core/users.js (invoke + localStorage fallback), integrate user state initialization, add user setup view/styles and keyboard overlay (JS/CSS), wire views and navigation to show onboarding when needed, and update lock view behavior to coordinate onboarding and passkey flow. Also add @tauri-apps/api dependency and update package/Cargo lock files accordingly.
This commit is contained in:
Generated
+74
@@ -8,6 +8,18 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -866,6 +878,18 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
@@ -1369,6 +1393,15 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -1384,6 +1417,15 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
@@ -1886,6 +1928,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
@@ -2047,6 +2100,7 @@ dependencies = [
|
||||
name = "nebula-os"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -2940,6 +2994,20 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -4212,6 +4280,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.1"
|
||||
|
||||
@@ -22,4 +22,5 @@ tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
|
||||
|
||||
+201
-4
@@ -1,14 +1,211 @@
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::Manager;
|
||||
|
||||
struct DbState {
|
||||
db_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UserRecord {
|
||||
id: i64,
|
||||
name: String,
|
||||
first_name: String,
|
||||
last_name: Option<String>,
|
||||
created_at_unix_ms: i64,
|
||||
}
|
||||
|
||||
impl DbState {
|
||||
fn connect(&self) -> Result<Connection, String> {
|
||||
Connection::open(&self.db_path).map_err(|err| format!("Failed to open database: {err}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn now_unix_ms() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis() as i64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn sanitize_name(name: &str) -> Result<String, String> {
|
||||
let trimmed = name.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("User name cannot be empty.".to_string());
|
||||
}
|
||||
if trimmed.chars().count() > 64 {
|
||||
return Err("User name is too long (max 64 chars).".to_string());
|
||||
}
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn sanitize_optional_name(name: Option<String>) -> Result<Option<String>, String> {
|
||||
let Some(value) = name else {
|
||||
return Ok(None);
|
||||
};
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if trimmed.chars().count() > 64 {
|
||||
return Err("Name is too long (max 64 chars).".to_string());
|
||||
}
|
||||
Ok(Some(trimmed.to_string()))
|
||||
}
|
||||
|
||||
fn build_display_name(first_name: &str, last_name: Option<&str>) -> String {
|
||||
let last = last_name.map(str::trim).unwrap_or("");
|
||||
if last.is_empty() {
|
||||
first_name.to_string()
|
||||
} else {
|
||||
format!("{first_name} {last}")
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_users_schema(conn: &Connection) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
created_at_unix_ms INTEGER NOT NULL
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|err| format!("Failed to prepare users table: {err}"))?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("PRAGMA table_info(users)")
|
||||
.map_err(|err| format!("Failed to inspect users schema: {err}"))?;
|
||||
let mut rows = stmt
|
||||
.query([])
|
||||
.map_err(|err| format!("Failed to read users schema: {err}"))?;
|
||||
|
||||
let mut has_first_name = false;
|
||||
let mut has_last_name = false;
|
||||
while let Some(row) = rows
|
||||
.next()
|
||||
.map_err(|err| format!("Failed to iterate users schema: {err}"))?
|
||||
{
|
||||
let column_name: String = row
|
||||
.get(1)
|
||||
.map_err(|err| format!("Failed to parse users schema: {err}"))?;
|
||||
if column_name == "first_name" {
|
||||
has_first_name = true;
|
||||
}
|
||||
if column_name == "last_name" {
|
||||
has_last_name = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !has_first_name {
|
||||
conn.execute("ALTER TABLE users ADD COLUMN first_name TEXT", [])
|
||||
.map_err(|err| format!("Failed to add first_name column: {err}"))?;
|
||||
conn.execute(
|
||||
"UPDATE users SET first_name = TRIM(name) WHERE first_name IS NULL OR TRIM(first_name) = ''",
|
||||
[],
|
||||
)
|
||||
.map_err(|err| format!("Failed to backfill first_name: {err}"))?;
|
||||
}
|
||||
|
||||
if !has_last_name {
|
||||
conn.execute("ALTER TABLE users ADD COLUMN last_name TEXT", [])
|
||||
.map_err(|err| format!("Failed to add last_name column: {err}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
fn get_first_user(state: tauri::State<'_, DbState>) -> Result<Option<UserRecord>, String> {
|
||||
let conn = state.connect()?;
|
||||
ensure_users_schema(&conn)?;
|
||||
conn.query_row(
|
||||
"SELECT
|
||||
id,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(COALESCE(first_name, '') || ' ' || COALESCE(last_name, '')), ''),
|
||||
name
|
||||
) AS display_name,
|
||||
COALESCE(NULLIF(TRIM(first_name), ''), name) AS first_name,
|
||||
NULLIF(TRIM(COALESCE(last_name, '')), '') AS last_name,
|
||||
created_at_unix_ms
|
||||
FROM users
|
||||
ORDER BY id ASC
|
||||
LIMIT 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(UserRecord {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
first_name: row.get(2)?,
|
||||
last_name: row.get(3)?,
|
||||
created_at_unix_ms: row.get(4)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(|err| format!("Failed to load user: {err}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn create_user(
|
||||
first_name: String,
|
||||
last_name: Option<String>,
|
||||
state: tauri::State<'_, DbState>,
|
||||
) -> Result<UserRecord, String> {
|
||||
let safe_first_name = sanitize_name(&first_name)?;
|
||||
let safe_last_name = sanitize_optional_name(last_name)?;
|
||||
let display_name = build_display_name(&safe_first_name, safe_last_name.as_deref());
|
||||
let created_at_unix_ms = now_unix_ms();
|
||||
let conn = state.connect()?;
|
||||
ensure_users_schema(&conn)?;
|
||||
conn.execute(
|
||||
"INSERT INTO users (name, first_name, last_name, created_at_unix_ms) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
display_name,
|
||||
safe_first_name,
|
||||
safe_last_name,
|
||||
created_at_unix_ms
|
||||
],
|
||||
)
|
||||
.map_err(|err| format!("Failed to create user: {err}"))?;
|
||||
|
||||
let id = conn.last_insert_rowid();
|
||||
Ok(UserRecord {
|
||||
id,
|
||||
name: build_display_name(&safe_first_name, safe_last_name.as_deref()),
|
||||
first_name: safe_first_name,
|
||||
last_name: safe_last_name,
|
||||
created_at_unix_ms,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.setup(|app| {
|
||||
let app_data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|err| format!("Failed to resolve app data dir: {err}"))?;
|
||||
fs::create_dir_all(&app_data_dir)
|
||||
.map_err(|err| format!("Failed to create app data dir: {err}"))?;
|
||||
|
||||
let db_path = app_data_dir.join("nebula.db");
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|err| format!("Failed to initialize database: {err}"))?;
|
||||
ensure_users_schema(&conn)?;
|
||||
drop(conn);
|
||||
|
||||
app.manage(DbState { db_path });
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![get_first_user, create_user])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user