Merge pull request #17 from SteelDynamite/tauri-android-gating
tauri-android-gating
This commit is contained in:
commit
aa61f85d5f
|
|
@ -29,9 +29,9 @@ The Tauri dev server runs on port 1422 (`vite.config.ts` and `tauri.conf.json`).
|
||||||
|
|
||||||
Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
|
Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
|
||||||
|
|
||||||
- **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies.
|
- **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies. `keyring` feature-gated behind `keyring-storage` (default on) for Android compatibility.
|
||||||
- **onyx-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`.
|
- **onyx-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`.
|
||||||
- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`.
|
- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`. `notify` crate feature-gated for Android.
|
||||||
- **apps/flutter/** — Flutter GUI. Dart frontend in `lib/src/`, Rust backend in `rust/` via flutter_rust_bridge FFI into `onyx-core`.
|
- **apps/flutter/** — Flutter GUI. Dart frontend in `lib/src/`, Rust backend in `rust/` via flutter_rust_bridge FFI into `onyx-core`.
|
||||||
|
|
||||||
### Key patterns
|
### Key patterns
|
||||||
|
|
@ -61,6 +61,7 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
|
||||||
- **Phase 1** (Core + CLI): Complete
|
- **Phase 1** (Core + CLI): Complete
|
||||||
- **Phase 2** (WebDAV sync): Backend done, CLI done, GUI wired (settings auto-populates credentials)
|
- **Phase 2** (WebDAV sync): Backend done, CLI done, GUI wired (settings auto-populates credentials)
|
||||||
- **Phase 3** (GUI MVP): Complete — both Tauri and Flutter GUIs at feature parity
|
- **Phase 3** (GUI MVP): Complete — both Tauri and Flutter GUIs at feature parity
|
||||||
|
- **Phase 4** (Mobile): Tauri Android cfg-gated, needs `tauri android init` + build
|
||||||
|
|
||||||
### GUI features done
|
### GUI features done
|
||||||
|
|
||||||
|
|
@ -85,6 +86,7 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
|
||||||
- Push/pull/full sync mode selection (session-only, in settings)
|
- Push/pull/full sync mode selection (session-only, in settings)
|
||||||
- Desktop packaging (Linux: AppImage + .deb)
|
- Desktop packaging (Linux: AppImage + .deb)
|
||||||
- Flutter GUI at full parity with Tauri (WebDAV UI, has_time, sync status, sync mode)
|
- Flutter GUI at full parity with Tauri (WebDAV UI, has_time, sync status, sync mode)
|
||||||
|
- Tauri desktop-only deps (notify, keyring) feature-gated for Android compilation
|
||||||
|
|
||||||
### GUI features NOT yet done
|
### GUI features NOT yet done
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,16 @@ tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-os = "2"
|
tauri-plugin-os = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
onyx-core = { path = "../../../crates/onyx-core" }
|
onyx-core = { path = "../../../crates/onyx-core", default-features = false }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
notify = "7"
|
notify = { version = "7", optional = true }
|
||||||
notify-debouncer-mini = "0.5"
|
notify-debouncer-mini = { version = "0.5", optional = true }
|
||||||
|
|
||||||
[package.metadata.tauri]
|
[package.metadata.tauri]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["desktop"]
|
||||||
|
desktop = ["notify", "notify-debouncer-mini", "onyx-core/keyring-storage"]
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use std::time::Instant;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{Emitter, Manager, State};
|
use tauri::{Emitter, Manager, State};
|
||||||
|
|
@ -17,10 +18,12 @@ use onyx_core::{
|
||||||
webdav,
|
webdav,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
/// Active file watcher stored globally so it lives for the app lifetime.
|
/// Active file watcher stored globally so it lives for the app lifetime.
|
||||||
static WATCHER: Mutex<Option<notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>>> =
|
static WATCHER: Mutex<Option<notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>>> =
|
||||||
Mutex::new(None);
|
Mutex::new(None);
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
/// Shared mute timestamp — set before writes, checked by the watcher.
|
/// Shared mute timestamp — set before writes, checked by the watcher.
|
||||||
static LAST_WRITE: Mutex<Option<Instant>> = Mutex::new(None);
|
static LAST_WRITE: Mutex<Option<Instant>> = Mutex::new(None);
|
||||||
|
|
||||||
|
|
@ -55,10 +58,14 @@ impl From<CoreSyncResult> for SyncResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Suppress file watcher events for the next second (call before writes).
|
/// Suppress file watcher events for the next second (call before writes).
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
fn mute_watcher(_state: &mut AppState) {
|
fn mute_watcher(_state: &mut AppState) {
|
||||||
*LAST_WRITE.lock().unwrap() = Some(Instant::now());
|
*LAST_WRITE.lock().unwrap() = Some(Instant::now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
fn mute_watcher(_state: &mut AppState) {}
|
||||||
|
|
||||||
/// Helper: get or open a TaskRepository for the current workspace.
|
/// Helper: get or open a TaskRepository for the current workspace.
|
||||||
fn ensure_repo(state: &mut AppState) -> Result<(), String> {
|
fn ensure_repo(state: &mut AppState) -> Result<(), String> {
|
||||||
if state.repo.is_some() {
|
if state.repo.is_some() {
|
||||||
|
|
@ -463,6 +470,7 @@ async fn sync_workspace(
|
||||||
|
|
||||||
// ── File watcher ────────────────────────────────────────────────────
|
// ── File watcher ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
fn start_watcher(handle: tauri::AppHandle, path: PathBuf) {
|
fn start_watcher(handle: tauri::AppHandle, path: PathBuf) {
|
||||||
let handle = handle.clone();
|
let handle = handle.clone();
|
||||||
let debouncer = new_debouncer(
|
let debouncer = new_debouncer(
|
||||||
|
|
@ -492,12 +500,19 @@ fn start_watcher(handle: tauri::AppHandle, path: PathBuf) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn watch_workspace(path: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
fn watch_workspace(path: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||||
start_watcher(app_handle, PathBuf::from(path));
|
start_watcher(app_handle, PathBuf::from(path));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[tauri::command]
|
||||||
|
fn watch_workspace(_path: String, _app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// ── App entry ────────────────────────────────────────────────────────
|
// ── App entry ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
|
@ -517,6 +532,7 @@ pub fn run() {
|
||||||
let s = state.lock().unwrap();
|
let s = state.lock().unwrap();
|
||||||
s.config.get_current_workspace().ok().map(|(_, ws)| ws.path.clone())
|
s.config.get_current_workspace().ok().map(|(_, ws)| ws.path.clone())
|
||||||
};
|
};
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
if let Some(path) = workspace_path {
|
if let Some(path) = workspace_path {
|
||||||
start_watcher(handle, path);
|
start_watcher(handle, path);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ description = "Core library for local-first task management with markdown storag
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
repository = "https://github.com/SteelDynamite/onyx"
|
repository = "https://github.com/SteelDynamite/onyx"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["keyring-storage"]
|
||||||
|
keyring-storage = ["keyring"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
@ -17,7 +21,7 @@ reqwest = { workspace = true }
|
||||||
sha2 = { workspace = true }
|
sha2 = { workspace = true }
|
||||||
quick-xml = { workspace = true }
|
quick-xml = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] }
|
keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
|
|
|
||||||
|
|
@ -379,6 +379,7 @@ fn extract_relative_path(href: &str, base_url: &str, request_path: &str) -> Stri
|
||||||
|
|
||||||
// --- Credential Storage ---
|
// --- Credential Storage ---
|
||||||
|
|
||||||
|
#[cfg(feature = "keyring-storage")]
|
||||||
/// Store WebDAV credentials in the platform keychain.
|
/// Store WebDAV credentials in the platform keychain.
|
||||||
pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()> {
|
pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()> {
|
||||||
let service = format!("com.onyx.webdav.{}", domain);
|
let service = format!("com.onyx.webdav.{}", domain);
|
||||||
|
|
@ -396,6 +397,13 @@ pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "keyring-storage"))]
|
||||||
|
/// Store WebDAV credentials (not available without keyring-storage feature).
|
||||||
|
pub fn store_credentials(_domain: &str, _username: &str, _password: &str) -> Result<()> {
|
||||||
|
Err(Error::Credential("Credential storage not available on this platform".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "keyring-storage")]
|
||||||
/// Load WebDAV credentials from the platform keychain, falling back to env vars.
|
/// Load WebDAV credentials from the platform keychain, falling back to env vars.
|
||||||
pub fn load_credentials(domain: &str) -> Result<(String, String)> {
|
pub fn load_credentials(domain: &str) -> Result<(String, String)> {
|
||||||
let service = format!("com.onyx.webdav.{}", domain);
|
let service = format!("com.onyx.webdav.{}", domain);
|
||||||
|
|
@ -423,6 +431,23 @@ pub fn load_credentials(domain: &str) -> Result<(String, String)> {
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "keyring-storage"))]
|
||||||
|
/// Load WebDAV credentials from env vars only (keyring not available).
|
||||||
|
pub fn load_credentials(domain: &str) -> Result<(String, String)> {
|
||||||
|
if let (Ok(user), Ok(pass)) = (
|
||||||
|
std::env::var("ONYX_WEBDAV_USER"),
|
||||||
|
std::env::var("ONYX_WEBDAV_PASS"),
|
||||||
|
) {
|
||||||
|
return Ok((user, pass));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::Credential(format!(
|
||||||
|
"No credentials found for '{}'. Set ONYX_WEBDAV_USER and ONYX_WEBDAV_PASS.",
|
||||||
|
domain
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "keyring-storage")]
|
||||||
/// Delete WebDAV credentials from the platform keychain.
|
/// Delete WebDAV credentials from the platform keychain.
|
||||||
pub fn delete_credentials(domain: &str) -> Result<()> {
|
pub fn delete_credentials(domain: &str) -> Result<()> {
|
||||||
let service = format!("com.onyx.webdav.{}", domain);
|
let service = format!("com.onyx.webdav.{}", domain);
|
||||||
|
|
@ -437,6 +462,12 @@ pub fn delete_credentials(domain: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "keyring-storage"))]
|
||||||
|
/// Delete WebDAV credentials (no-op without keyring-storage feature).
|
||||||
|
pub fn delete_credentials(_domain: &str) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue