Docs: update all markdown files to reflect workspace-settings-overhaul changes
Update CLAUDE.md, PLAN.md, README.md, docs/API.md, and docs/DEVELOPMENT.md to reflect UUID-keyed workspaces, checksum-based conflict resolution, auto-sync lifecycle, tauri-plugin-credentials, .onyx-workspace.json rename, and new GUI features (remote folder browsing, workspace rename, sync interval, loading screen, safe area insets, task dedup).
This commit is contained in:
parent
7cc899470f
commit
eaab66609c
33
CLAUDE.md
33
CLAUDE.md
|
|
@ -31,19 +31,19 @@ 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. `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`.
|
||||
- **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/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. `tauri-plugin-credentials/` provides cross-platform credential storage (Android Keystore via EncryptedSharedPreferences, desktop via keyring crate).
|
||||
|
||||
### Key patterns
|
||||
|
||||
- **Storage trait** (`storage.rs`): Strategy pattern for task persistence. `FileSystemStorage` reads/writes markdown files with YAML frontmatter and JSON metadata files.
|
||||
- **Repository** (`repository.rs`): `TaskRepository` wraps a `Storage` impl and provides the public API for task/list CRUD, ordering, and grouping. Tests live here.
|
||||
- **Config** (`config.rs`): `AppConfig` manages named workspaces with paths, mode (local/webdav), theme, and WebDAV URL. Stored in platform-specific config dirs via the `directories` crate.
|
||||
- **Sync** (`sync.rs`): Three-way diff sync with offline queue. Auto-appends `Onyx/` to WebDAV URL. Wrapped in `tokio::time::timeout` (60s) to handle unreachable servers on Windows.
|
||||
- **WebDAV** (`webdav.rs`): reqwest client with rustls-tls, 30s request timeout, 10s connect timeout. Credentials stored via `keyring` crate (feature-gated). `Zeroizing<String>` for credential fields. Scoped keyring keys (`com.onyx.webdav.<domain>::<username>`); auto-migrates legacy dot-separated format on load. 10MB PROPFIND response cap.
|
||||
- **Config** (`config.rs`): `AppConfig` manages workspaces keyed by UUID string. `WorkspaceConfig` stores `name`, `path`, `mode` (Local/Webdav), `webdav_url`, `webdav_path` (user-selected remote folder), `theme`, and `sync_interval_secs`. `add_workspace` returns a generated UUID. Stored in platform-specific config dirs via the `directories` crate.
|
||||
- **Sync** (`sync.rs`): Three-way diff sync with offline queue. Checksum-based conflict resolution: downloads remote, compares SHA-256 — identical content is a false conflict (skipped); when different, remote wins and local is recovered as a duplicate with a new UUID and `[RECOVERED FROM CONFLICT]` prefix. Auto-sync lifecycle: periodic polling (configurable interval, default 60s), debounced file-change (5s), window-focus (30s stale threshold). Wrapped in `tokio::time::timeout` (60s) to handle unreachable servers on Windows.
|
||||
- **WebDAV** (`webdav.rs`): reqwest client with rustls-tls, 30s request timeout, 10s connect timeout. Rejects non-HTTPS URLs. `Zeroizing<String>` for credential fields. `move_resource` method for WebDAV MOVE (workspace rename). 10MB PROPFIND response cap. Desktop credentials via `keyring` crate (feature-gated); Tauri GUI uses `tauri-plugin-credentials` for cross-platform support (Android Keystore + desktop keychain).
|
||||
|
||||
### On-disk format
|
||||
|
||||
Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.onyx-workspace.json` for list ordering and workspace detection.
|
||||
Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.onyx-workspace.json` for list ordering and workspace detection. Sync only processes files at expected depths: `.onyx-workspace.json` at root, `.listdata.json` and `*.md` inside list directories.
|
||||
|
||||
### Tauri GUI structure
|
||||
|
||||
|
|
@ -66,9 +66,9 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to
|
|||
### Current state (2026-04-05)
|
||||
|
||||
- **Phase 1** (Core + CLI): Complete
|
||||
- **Phase 2** (WebDAV sync): In progress — reworking to let users browse and pick a remote folder instead of hardcoding `Onyx/` subfolder
|
||||
- **Phase 2** (WebDAV sync): Complete — remote folder browsing, checksum-based conflict resolution, auto-sync lifecycle, per-workspace sync interval
|
||||
- **Phase 3** (GUI MVP): Complete
|
||||
- **Phase 4** (Mobile): Tauri Android cfg-gated, needs `tauri android init` + build
|
||||
- **Phase 4** (Mobile): Tauri Android cfg-gated with tauri-plugin-credentials and safe area insets; needs `tauri android init` + build
|
||||
|
||||
### GUI features done
|
||||
|
||||
|
|
@ -87,21 +87,26 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to
|
|||
- Group-by-due-date toggle per list (main panel kebab)
|
||||
- Delete completed tasks (main panel kebab + subtask kebab, with confirmation dialogs)
|
||||
- Keyboard shortcuts (Escape priority chain: settings → detail → list menu → drawer → menus)
|
||||
- Setup screen with 2-step mode selection (Local Folder vs WebDAV Server), window dragging, "Open Existing Folder" option
|
||||
- WebDAV setup flow with connection test, credential storage in system keychain
|
||||
- WebDAV sync: auto-creates `Onyx/` subfolder on remote, 60s hard timeout, sync error display in settings
|
||||
- Setup screen with 2-step mode selection (Local Folder vs WebDAV Server), window dragging, "Open Existing Folder" option, remote folder browsing
|
||||
- WebDAV setup flow with connection test, credential storage via tauri-plugin-credentials (Android Keystore + desktop keychain)
|
||||
- WebDAV sync: user-selected remote folder, 60s hard timeout, checksum-based conflict resolution (remote wins, local recovered as duplicate)
|
||||
- Auto-sync lifecycle: periodic polling (configurable interval), debounced file-change (5s), window-focus (30s stale threshold); sync status dot (idle/synced/error/offline) and manual sync button in drawer
|
||||
- Initial sync loading screen for new WebDAV workspaces
|
||||
- Per-workspace sync interval (configurable in settings)
|
||||
- Upload/download counts in drawer footer (hidden when zero)
|
||||
- Transient sync error suppression (connectivity issues update status dot only, no error banner)
|
||||
- File watcher (notify crate, 500ms debounce, auto-reloads on external changes)
|
||||
- Sync status indicators (last-sync time + upload/download counts chip)
|
||||
- Push/pull/full sync mode selection (session-only, in settings)
|
||||
- WorkspaceMode enum (local/webdav) with per-workspace config
|
||||
- WorkspaceMode enum (local/webdav) with per-workspace config, UUID-keyed workspaces
|
||||
- Workspace rename (local folder rename + WebDAV MOVE for remote folders, with confirmation dialog)
|
||||
- Desktop packaging (Linux: AppImage + .deb; Windows: MSI)
|
||||
- Tauri desktop-only deps (notify, keyring) feature-gated for Android compilation
|
||||
- Safe area insets for mobile (CSS variables --safe-top/--safe-bottom, viewport-fit=cover)
|
||||
- Task deduplication on load (handles sync conflict duplicates)
|
||||
- Subtask hierarchy: subtask count shown on parent tasks in list, subtask detail via three-panel slide navigation, inline add at top of subtask list (new subtasks prepend), collapsible completed subtasks section, cascade delete (parent deletion removes all subtasks with confirmation warning)
|
||||
- Custom confirmation dialogs (ConfirmDialog component replaces native confirm())
|
||||
|
||||
### GUI features NOT yet done
|
||||
|
||||
- Workspace retarget/migrate
|
||||
- Search/filter tasks
|
||||
- Desktop packaging for macOS
|
||||
|
||||
|
|
|
|||
63
PLAN.md
63
PLAN.md
|
|
@ -106,12 +106,18 @@ TaskList {
|
|||
}
|
||||
|
||||
AppConfig {
|
||||
workspaces: HashMap<String, WorkspaceConfig>,
|
||||
current_workspace: Option<String>,
|
||||
workspaces: HashMap<String, WorkspaceConfig>, // UUID keys
|
||||
current_workspace: Option<String>, // UUID
|
||||
}
|
||||
|
||||
WorkspaceConfig {
|
||||
name: String, // Display name
|
||||
path: PathBuf,
|
||||
mode: WorkspaceMode, // Local or Webdav
|
||||
webdav_url: Option<String>,
|
||||
webdav_path: Option<String>, // User-selected remote folder
|
||||
theme: Option<String>,
|
||||
sync_interval_secs: Option<u64>,
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -119,7 +125,7 @@ WorkspaceConfig {
|
|||
|
||||
```
|
||||
~/Documents/Tasks/ # User-selected folder
|
||||
├── .metadata.json # Global: list ordering, last opened list
|
||||
├── .onyx-workspace.json # Global: list ordering, last opened list
|
||||
├── My Tasks/ # Task list folder
|
||||
│ ├── .listdata.json # List metadata: task order, id, timestamps
|
||||
│ ├── Buy groceries.md # Title: "Buy groceries" (without .md)
|
||||
|
|
@ -132,7 +138,7 @@ WorkspaceConfig {
|
|||
|
||||
**Note**: Task titles are derived from filenames by removing the `.md` extension.
|
||||
|
||||
**`.metadata.json` (root level)**:
|
||||
**`.onyx-workspace.json` (root level)**:
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
|
|
@ -169,14 +175,20 @@ WorkspaceConfig {
|
|||
```json
|
||||
{
|
||||
"workspaces": {
|
||||
"personal": {
|
||||
"path": "/home/user/Documents/Tasks"
|
||||
"a1b2c3d4-...": {
|
||||
"name": "personal",
|
||||
"path": "/home/user/Documents/Tasks",
|
||||
"mode": "local"
|
||||
},
|
||||
"shared": {
|
||||
"path": "/home/user/Dropbox/TeamTasks"
|
||||
"e5f6g7h8-...": {
|
||||
"name": "shared",
|
||||
"path": "/home/user/Dropbox/TeamTasks",
|
||||
"mode": "webdav",
|
||||
"webdav_url": "https://nextcloud.example.com/remote.php/dav/files/user/",
|
||||
"webdav_path": "TeamTasks"
|
||||
}
|
||||
},
|
||||
"current_workspace": "personal"
|
||||
"current_workspace": "a1b2c3d4-..."
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -394,7 +406,7 @@ $ onyx workspace migrate personal ~/Dropbox/Tasks
|
|||
⚠ This will move all files from ~/Documents/Tasks to ~/Dropbox/Tasks
|
||||
Continue? (y/n): y
|
||||
Moving files...
|
||||
Moved .metadata.json
|
||||
Moved .onyx-workspace.json
|
||||
Moved My Tasks/ (15 files)
|
||||
Moved Work/ (8 files)
|
||||
✓ Migrated 23 files to ~/Dropbox/Tasks
|
||||
|
|
@ -453,11 +465,16 @@ cargo run -p onyx-cli -- workspace list
|
|||
Add WebDAV support to `onyx-core`:
|
||||
|
||||
```rust
|
||||
// Update WorkspaceConfig to include WebDAV
|
||||
// WorkspaceConfig with WebDAV support (UUID-keyed in AppConfig)
|
||||
WorkspaceConfig {
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
mode: WorkspaceMode, // Local or Webdav
|
||||
webdav_url: Option<String>,
|
||||
webdav_path: Option<String>, // User-selected remote folder
|
||||
last_sync: Option<DateTime>,
|
||||
theme: Option<String>,
|
||||
sync_interval_secs: Option<u64>,
|
||||
}
|
||||
|
||||
// AppConfig remains the same (workspaces + current_workspace)
|
||||
|
|
@ -484,8 +501,8 @@ pub fn delete_credentials(domain: &str) -> Result<()>;
|
|||
```
|
||||
|
||||
#### Sync Strategy
|
||||
- **Trigger**: On app start (if connected), background timer (every 5 min), on modification (debounced)
|
||||
- **Conflict Resolution**: Last-write-wins with timestamp
|
||||
- **Trigger**: Auto-sync lifecycle — periodic polling (configurable interval, default 60s), debounced file-change (5s), window-focus (30s stale threshold)
|
||||
- **Conflict Resolution**: Checksum-based — downloads remote, compares SHA-256. Identical = false conflict (skipped). Different = remote wins, local recovered as duplicate with new UUID and `[RECOVERED FROM CONFLICT]` prefix
|
||||
- **Offline Support**: Queue operations when offline, sync when online
|
||||
|
||||
#### Authentication
|
||||
|
|
@ -664,20 +681,22 @@ apps/tauri/
|
|||
|
||||
#### App Configuration (Phase 3+)
|
||||
|
||||
**Update AppConfig** to include UI preferences:
|
||||
**AppConfig** with UI preferences (theme is per-workspace):
|
||||
```rust
|
||||
AppConfig {
|
||||
workspaces: HashMap<String, WorkspaceConfig>, // From Phase 1
|
||||
current_workspace: Option<String>,
|
||||
theme: Theme, // NEW: light/dark mode
|
||||
window_size: Option<(u32, u32)>, // NEW: remember window size
|
||||
last_opened_list_per_workspace: HashMap<String, Uuid>, // NEW: per-workspace last view
|
||||
workspaces: HashMap<String, WorkspaceConfig>, // UUID keys
|
||||
current_workspace: Option<String>, // UUID
|
||||
}
|
||||
|
||||
WorkspaceConfig {
|
||||
name: String, // Display name
|
||||
path: PathBuf,
|
||||
webdav_url: Option<String>, // From Phase 2
|
||||
mode: WorkspaceMode, // Local or Webdav
|
||||
webdav_url: Option<String>,
|
||||
webdav_path: Option<String>, // User-selected remote folder
|
||||
last_sync: Option<DateTime>,
|
||||
theme: Option<String>, // Per-workspace theme
|
||||
sync_interval_secs: Option<u64>, // Auto-sync interval
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -1011,6 +1030,6 @@ This project is free and open-source software licensed under GPL v3.
|
|||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-04-03
|
||||
**Document Version**: 4.2
|
||||
**Last Updated**: 2026-04-05
|
||||
**Document Version**: 4.3
|
||||
**Status**: Ready to Implement - Milestone-Driven Plan
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ onyx/
|
|||
- Subtask hierarchy (parent_id)
|
||||
- WebDAV sync with three-way diff and offline queue
|
||||
- Platform keychain credential storage (feature-gated for Android)
|
||||
- Checksum-based conflict resolution (remote wins, local recovered as duplicate)
|
||||
|
||||
### CLI (`onyx-cli`)
|
||||
- Workspace management (init, add, list, switch, remove, retarget, migrate)
|
||||
|
|
@ -62,7 +63,7 @@ onyx/
|
|||
- Keyboard shortcuts (Escape priority chain)
|
||||
- WebDAV setup flow with credential auto-population
|
||||
- File watcher (auto-reloads on external changes)
|
||||
- Sync status indicators and push/pull/full mode selection
|
||||
- Auto-sync with configurable interval, status indicators
|
||||
- Custom confirmation dialogs
|
||||
- Desktop packaging (Linux: AppImage + .deb; Windows: MSI)
|
||||
|
||||
|
|
@ -182,7 +183,7 @@ Task description and notes go here in **markdown** format.
|
|||
|
||||
```
|
||||
~/Documents/Tasks/ # User-selected folder
|
||||
├── .metadata.json # Global: list ordering, last opened list
|
||||
├── .onyx-workspace.json # Workspace metadata: list ordering, detection
|
||||
├── My Tasks/ # Task list folder
|
||||
│ ├── .listdata.json # List metadata: task order, id, timestamps
|
||||
│ ├── Buy groceries.md # Individual task files
|
||||
|
|
|
|||
45
docs/API.md
45
docs/API.md
|
|
@ -64,12 +64,12 @@ pub struct TaskList {
|
|||
|
||||
#### AppConfig
|
||||
|
||||
Global application configuration supporting multiple workspaces.
|
||||
Global application configuration supporting multiple workspaces. Workspaces are keyed by UUID string.
|
||||
|
||||
```rust
|
||||
pub struct AppConfig {
|
||||
pub workspaces: HashMap<String, WorkspaceConfig>,
|
||||
pub current_workspace: Option<String>,
|
||||
pub workspaces: HashMap<String, WorkspaceConfig>, // UUID keys
|
||||
pub current_workspace: Option<String>, // UUID of active workspace
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -87,14 +87,18 @@ use onyx_core::AppConfig;
|
|||
let config_path = AppConfig::get_config_path();
|
||||
let mut config = AppConfig::load_from_file(&config_path)?;
|
||||
|
||||
// Add workspace
|
||||
config.add_workspace(
|
||||
"personal".to_string(),
|
||||
WorkspaceConfig::new(PathBuf::from("/home/user/tasks"))
|
||||
// Add workspace (returns generated UUID)
|
||||
let id = config.add_workspace(
|
||||
WorkspaceConfig::new("personal".to_string(), PathBuf::from("/home/user/tasks"))
|
||||
);
|
||||
|
||||
// Set current workspace
|
||||
config.set_current_workspace("personal".to_string())?;
|
||||
// Set current workspace by ID
|
||||
config.set_current_workspace(id)?;
|
||||
|
||||
// Find workspace by display name
|
||||
if let Some((id, ws)) = config.find_by_name("personal") {
|
||||
println!("Found: {} at {:?}", id, ws.path);
|
||||
}
|
||||
|
||||
// Save config
|
||||
config.save_to_file(&config_path)?;
|
||||
|
|
@ -106,9 +110,14 @@ Configuration for a single workspace.
|
|||
|
||||
```rust
|
||||
pub struct WorkspaceConfig {
|
||||
pub name: String, // Display name
|
||||
pub path: PathBuf,
|
||||
pub mode: WorkspaceMode, // Local or Webdav
|
||||
pub webdav_url: Option<String>,
|
||||
pub webdav_path: Option<String>, // User-selected remote folder path
|
||||
pub last_sync: Option<DateTime<Utc>>,
|
||||
pub theme: Option<String>,
|
||||
pub sync_interval_secs: Option<u64>, // Auto-sync polling interval
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -267,7 +276,7 @@ Each list folder contains a `.listdata.json` file:
|
|||
|
||||
### Root Metadata
|
||||
|
||||
The root folder contains a `.metadata.json` file:
|
||||
The root folder contains a `.onyx-workspace.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -318,7 +327,7 @@ let status = get_sync_status(Path::new("/home/user/tasks"))?;
|
|||
|
||||
Credentials are stored in the platform keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service).
|
||||
|
||||
Keyring service keys use the format `com.onyx.webdav.<domain>::<username>` — the `::` separator prevents key collisions when usernames contain dots. On first load, credentials stored in the legacy `.`-separated format (`com.onyx.webdav.<domain>.<username>`) are automatically migrated to the scoped format and the old entries are removed.
|
||||
**Core library** (`onyx-core::webdav`): Keyring service keys use the format `com.onyx.webdav.<domain>::<username>` — the `::` separator prevents key collisions when usernames contain dots. On first load, credentials stored in the legacy `.`-separated format are automatically migrated.
|
||||
|
||||
```rust
|
||||
use onyx_core::webdav::{store_credentials, load_credentials, delete_credentials};
|
||||
|
|
@ -333,6 +342,8 @@ let (username, password) = load_credentials("nextcloud.example.com")?;
|
|||
delete_credentials("nextcloud.example.com")?;
|
||||
```
|
||||
|
||||
**Tauri GUI**: Uses `tauri-plugin-credentials` instead of direct keyring calls. This plugin provides cross-platform support: EncryptedSharedPreferences (Android Keystore) on Android, keyring crate on desktop. Plugin crate at `apps/tauri/tauri-plugin-credentials/`.
|
||||
|
||||
### WebDAV Client
|
||||
|
||||
```rust
|
||||
|
|
@ -342,7 +353,7 @@ let client = WebDavClient::new(
|
|||
"https://nextcloud.example.com/remote.php/dav/files/user/Tasks",
|
||||
"username",
|
||||
"password",
|
||||
);
|
||||
)?; // Returns Result — rejects non-HTTPS URLs
|
||||
|
||||
// Test connection
|
||||
client.test_connection().await?;
|
||||
|
|
@ -361,11 +372,13 @@ client.delete_file("old-task.md").await?;
|
|||
|
||||
### Sync Strategy
|
||||
|
||||
- **Three-way diff**: Compares local state, remote state, and last-known baseline to determine actions (upload, download, delete local/remote)
|
||||
- **Conflict resolution**: Last-write-wins using file timestamps
|
||||
- **Three-way diff**: Compares local state, remote state, and last-known baseline to determine actions (upload, download, delete local/remote, conflict)
|
||||
- **Conflict resolution**: Checksum-based — downloads remote file and compares SHA-256 checksums. Identical content is a false conflict (skipped). When different, remote wins and the local version is recovered as a duplicate task with a new UUID and `[RECOVERED FROM CONFLICT]` prefix, inserted adjacent to the original in `.listdata.json`
|
||||
- **Offline queue**: Pending operations are queued and replayed when connectivity returns
|
||||
- **Sync state**: Stored in `.syncstate.json` within the workspace directory
|
||||
- **Auto-sync**: Periodic polling (configurable `sync_interval_secs`), debounced file-change trigger (5s), window-focus trigger (30s stale threshold)
|
||||
- **Response size cap**: PROPFIND responses are limited to 10 MB (checked via `Content-Length` header and actual body size) to prevent memory exhaustion from malicious servers
|
||||
- **Syncable files**: Only processes files at expected depths — `.onyx-workspace.json` at root (depth 1), `.listdata.json` and `*.md` inside list directories (depth 2)
|
||||
|
||||
## Error Handling
|
||||
|
||||
|
|
@ -424,8 +437,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
// Configure workspace
|
||||
let mut config = AppConfig::new();
|
||||
config.add_workspace("personal".to_string(), WorkspaceConfig::new(path));
|
||||
config.set_current_workspace("personal".to_string())?;
|
||||
let ws_id = config.add_workspace(WorkspaceConfig::new("personal".to_string(), path));
|
||||
config.set_current_workspace(ws_id)?;
|
||||
config.save_to_file(&AppConfig::get_config_path())?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -76,6 +76,11 @@ onyx/
|
|||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ ├── stores/ # Svelte state (app.svelte.ts)
|
||||
│ │ └── types.ts # TypeScript type definitions
|
||||
│ ├── tauri-plugin-credentials/ # Cross-platform credential storage plugin
|
||||
│ │ ├── Cargo.toml
|
||||
│ │ ├── src/
|
||||
│ │ │ └── lib.rs # Desktop (keyring) + plugin API
|
||||
│ │ └── android/ # Android (EncryptedSharedPreferences)
|
||||
│ └── src-tauri/ # Rust backend (Tauri commands)
|
||||
│ ├── Cargo.toml
|
||||
│ ├── tauri.conf.json
|
||||
|
|
|
|||
Loading…
Reference in a new issue