feat(oxibadge): add onboarding flow with i18n and reader preferences
Check, format and test / build (push) Successful in 9m59s

Adds the first real screen: a setup form for the device's host/port,
tested against the real device (3 retries) and persisted to
preferences.toml before showing a placeholder "done" screen.

- Rust: test_connection/save_preferences/load_preferences commands;
  preferences stored as [[reader]] in TOML so more readers can be
  added later without a format change.
- Svelte: split the monolithic page into reusable components
  (Screen, Column, Field, Button, ErrorBanner, Hint) under
  src/lib/components, sharing design tokens from app.css.
- Automatic it/en i18n via svelte-i18n, locale detected from
  navigator.language.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 11:08:02 +02:00
parent 268a917a69
commit 99772b9e4a
19 changed files with 821 additions and 29 deletions
+3
View File
@@ -17,3 +17,6 @@ tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
toml = "0.9"
r701 = { path = "../../r701" }
+45
View File
@@ -1,6 +1,51 @@
mod preferences;
use preferences::Reader;
use tauri::AppHandle;
#[tauri::command]
fn load_preferences(app: AppHandle) -> Option<Reader> {
preferences::load(&app)
}
#[tauri::command]
fn save_preferences(app: AppHandle, host: String, port: u16) -> Result<(), String> {
preferences::save(&app, Reader { host, port })
}
/// Connects to the device up to 3 times, returning a stable error code (not
/// a user-facing message) on failure so the frontend can translate it.
#[tauri::command]
async fn test_connection(host: String, port: u16) -> Result<(), String> {
let mut last_error = String::new();
for _ in 0..3 {
let err = match r701::R701::connect((host.as_str(), port)).await {
Ok(_) => return Ok(()),
Err(err) => err,
};
last_error = match err.kind() {
std::io::ErrorKind::ConnectionRefused => "connection-refused".to_string(),
std::io::ErrorKind::TimedOut => "timed-out".to_string(),
_ if err
.to_string()
.contains("failed to lookup address information") =>
{
"dns-failure".to_string()
}
_ => "unknown".to_string(),
};
}
Err(last_error)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
load_preferences,
save_preferences,
test_connection
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+31
View File
@@ -0,0 +1,31 @@
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reader {
pub host: String,
pub port: u16,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct Preferences {
#[serde(default)]
reader: Vec<Reader>,
}
pub fn load(app: &AppHandle) -> Option<Reader> {
let path = app.path().app_data_dir().ok()?.join("preferences.toml");
let contents = std::fs::read_to_string(path).ok()?;
let preferences: Preferences = toml::from_str(&contents).ok()?;
preferences.reader.into_iter().next()
}
pub fn save(app: &AppHandle, reader: Reader) -> Result<(), String> {
let dir = app.path().app_data_dir().map_err(|err| err.to_string())?;
std::fs::create_dir_all(&dir).map_err(|err| err.to_string())?;
let preferences = Preferences {
reader: vec![reader],
};
let contents = toml::to_string_pretty(&preferences).map_err(|err| err.to_string())?;
std::fs::write(dir.join("preferences.toml"), contents).map_err(|err| err.to_string())
}