The Rust core was already portable (caller-provided db paths, rustls rather than openssl) and the shell already had the mobile entry point, so this is mostly toolchain, provisioning, and platform-gating. Toolchain: - scripts/android-setup.sh installs what Android Studio doesn't: SDK cmdline-tools, a pinned stable NDK, and the Rust Android targets. The NDK is pinned rather than "latest" because sdkmanager's newest is often a beta (30.0.15729638 is r30-beta2) -- not something to compile SQLite with. Prefers Android Studio's bundled JDK 21, since Gradle rejects newer JDKs. - Track src-tauri/gen/android (excluding build output): AndroidManifest.xml is hand-edited and Android permissions have no tauri.conf.json equivalent. - vite: serve on TAURI_DEV_HOST so a device/emulator can reach the dev server. Card data: downloading streams ~740MB to build a ~73MB db, which is rough on a phone. Add import_reference_db to adopt a desktop-built file instead, validated by db::inspect_reference_db first so picking the wrong file is a clean error rather than a broken install. Installs via copy-then-rename so an interrupted copy can't leave a partial db. Ingest hardening (helps desktop too): - Give ureq connect/read timeouts. Cancellation is only polled per parsed record, so a stalled socket previously hung forever with no way to cancel. - Drop the remove_file before rename; POSIX rename already replaces atomically, and the delete-first window could leave no reference.db at all if the process was killed -- much likelier on Android. UI: hide Open/New Library on mobile (scoped storage has no arbitrary-path equivalent, and SQLite can't open a content:// URI), reported via a cfg!(mobile) flag on Status rather than user-agent sniffing. Use 100dvh so the soft keyboard doesn't break the shell. Verified: 70/70 core tests, desktop build clean, and the aarch64 APK builds (bundled SQLite C and ring both cross-compile under NDK 28). Not yet run on a device.
115 lines
3.8 KiB
Rust
115 lines
3.8 KiB
Rust
//! App state: the open [`Catalog`] and fuzzy [`NameIndex`], plus the db paths.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use mtgc_core::{Catalog, NameIndex};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Small app-shell preferences persisted across restarts (distinct from the
|
|
/// user's library data) — currently just which library file was last open.
|
|
#[derive(Default, Serialize, Deserialize)]
|
|
struct PersistedConfig {
|
|
library_path: Option<PathBuf>,
|
|
}
|
|
|
|
fn config_path(data_dir: &Path) -> PathBuf {
|
|
data_dir.join("config.json")
|
|
}
|
|
|
|
fn load_config(data_dir: &Path) -> PersistedConfig {
|
|
std::fs::read_to_string(config_path(data_dir))
|
|
.ok()
|
|
.and_then(|s| serde_json::from_str(&s).ok())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn save_config(data_dir: &Path, cfg: &PersistedConfig) {
|
|
if let Ok(s) = serde_json::to_string_pretty(cfg) {
|
|
let _ = std::fs::write(config_path(data_dir), s);
|
|
}
|
|
}
|
|
|
|
/// Managed application state, guarded by a `Mutex` in Tauri's state store.
|
|
pub struct AppState {
|
|
data_dir: PathBuf,
|
|
pub library_path: PathBuf,
|
|
pub reference_path: PathBuf,
|
|
pub catalog: Option<Catalog>,
|
|
pub index: Option<NameIndex>,
|
|
/// Set while `update_card_data` is running; `cancel_update_card_data`
|
|
/// flips it to abort the in-progress ingest.
|
|
pub ingest_cancel: Option<mtgc_core::ingest::CancelToken>,
|
|
}
|
|
|
|
impl AppState {
|
|
/// Initialize with the last-opened library path (or the default under the
|
|
/// app data dir, on first run) and open the catalog if a reference db
|
|
/// already exists.
|
|
pub fn new(data_dir: PathBuf) -> Self {
|
|
let cfg = load_config(&data_dir);
|
|
let library_path = cfg
|
|
.library_path
|
|
.unwrap_or_else(|| data_dir.join("library.db"));
|
|
let mut s = Self {
|
|
reference_path: data_dir.join("reference.db"),
|
|
library_path,
|
|
data_dir,
|
|
catalog: None,
|
|
index: None,
|
|
ingest_cancel: None,
|
|
};
|
|
// Best-effort open; failures just leave the catalog unloaded.
|
|
let _ = s.reopen();
|
|
s
|
|
}
|
|
|
|
/// Point at a different library file and persist it so the next launch
|
|
/// reopens the same one.
|
|
pub fn set_library_path(&mut self, path: PathBuf) {
|
|
self.library_path = path.clone();
|
|
save_config(
|
|
&self.data_dir,
|
|
&PersistedConfig {
|
|
library_path: Some(path),
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Drop the open catalog (and its name index), releasing the handles on both
|
|
/// db files. Call before replacing `reference.db` on disk so nothing is
|
|
/// still reading the file being swapped out.
|
|
pub fn close(&mut self) {
|
|
self.catalog = None;
|
|
self.index = None;
|
|
}
|
|
|
|
/// (Re)open the catalog + rebuild the name index. Only opens when a reference
|
|
/// db exists — attaching a non-existent file would create an empty (tableless)
|
|
/// db and make every query fail.
|
|
pub fn reopen(&mut self) -> Result<(), String> {
|
|
if self.reference_path.exists() {
|
|
let catalog = Catalog::open(&self.library_path, &self.reference_path)
|
|
.map_err(|e| e.to_string())?;
|
|
let index = catalog.build_name_index().map_err(|e| e.to_string())?;
|
|
self.index = Some(index);
|
|
self.catalog = Some(catalog);
|
|
} else {
|
|
self.catalog = None;
|
|
self.index = None;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn catalog(&self) -> Result<&Catalog, String> {
|
|
self.catalog
|
|
.as_ref()
|
|
.ok_or_else(|| "Card data not loaded yet — click 'Download card data'.".to_string())
|
|
}
|
|
|
|
pub fn index(&self) -> Result<&NameIndex, String> {
|
|
self.index
|
|
.as_ref()
|
|
.ok_or_else(|| "Card data not loaded yet — click 'Download card data'.".to_string())
|
|
}
|
|
}
|