feat(oxibadge): add onboarding flow with i18n and reader preferences
Check, format and test / build (push) Successful in 9m59s
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:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user