diff --git a/.gitignore b/.gitignore index 0956075..94459d4 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ captures node_modules/ target/ .junie + +# Local design export scratch +output/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c210366 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,308 @@ +# AGENTS.md + +Operational instructions for coding agents working in this repository. +Humans: see `README.md` for product overview and run configs. +Agents: read this file (and the nearest nested `AGENTS.md`) before editing. + +Nested guides take precedence when editing under those trees: + +- [`crates/vnidrop/AGENTS.md`](crates/vnidrop/AGENTS.md) — Rust core +- [`shared/AGENTS.md`](shared/AGENTS.md) — Compose Multiplatform UI / KMP + +--- + +## Project overview + +VniDrop is a cross-platform **local P2P file transfer** app (Android, iOS, Desktop). + +| Layer | Path | Responsibility | +|-------|------|----------------| +| Rust core | `crates/vnidrop/` | Iroh endpoint, blobs, SQLite, tickets, approval, streaming | +| Shared KMP | `shared/` | Compose UI, ViewModels, expect/actual platform bridges | +| Hosts | `androidApp/`, `iosApp/`, `desktopApp/` | Thin app shells | + +**Invariant:** UI/platform opens files and handles pickers; **Rust streams bytes**. +Do not design features that move transfer payloads through Kotlin heap by default. + +Domain docs (reference, do not paste into PRs): + +- [`crates/vnidrop/CORE_FLOW.md`](crates/vnidrop/CORE_FLOW.md) +- [`crates/vnidrop/tests/README.md`](crates/vnidrop/tests/README.md) + +--- + +## Absolute rules + +1. Prefer PRs into `master`. Do not merge to `master` locally unless the user asks. +2. Do not `git push`, force-push, or open a PR unless the user asks. +3. If `commit.gpgsign` is enabled, create **signed** commits only. If signing fails + (empty `ssh-add -l`), stop and tell the user to unlock the key. Never switch to + unsigned commits to “unblock” yourself. +4. Change only files required for the task. No drive-by refactors, dependency bumps, + or repo-wide formatting. +5. Do not force architecture migrations (MVI, Hilt, Nav3, etc.) unless requested. +6. Never commit secrets, key material, or passphrases. +7. Destructive git (`reset --hard`, `push --force`, dropping DBs) only with explicit + user approval. +8. **Every bug fix includes a regression test** at the lowest layer that catches it. +9. After code changes, run the **relevant** checks in [Build and test](#build-and-test) + and fix failures before finishing. + +--- + +## Build and test + +Install prerequisites when missing: Rust stable + rustfmt + clippy, JDK 17, +Android NDK/SDK only if building Android, Xcode only for iOS. + +### Rust core (`crates/vnidrop` or workspace root) + +Run from the **repo root** (Cargo workspace): + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace --all-targets +``` + +Focused: + +```bash +cargo test -p vnidrop +cargo test -p vnidrop --test output_sink +cargo test -p vnidrop --test transfer +cargo test -p vnidrop --test approval +cargo test -p vnidrop --test lifecycle +``` + +After finishing Rust edits, format: + +```bash +cargo fmt --all +``` + +CI also runs `cargo doc --workspace --no-deps` with `RUSTDOCFLAGS=-D warnings` +(see `.github/workflows/rust-core.yml`). Run it before large Rust public-API changes. + +### Shared KMP / Compose (`shared/`) + +```bash +./gradlew :shared:jvmTest +./gradlew :shared:compileKotlinJvm +``` + +Other targets (slower / machine-dependent): + +```bash +./gradlew :shared:testAndroidHostTest +./gradlew :shared:iosSimulatorArm64Test # macOS + Xcode +./gradlew :androidApp:assembleDebug +./gradlew :desktopApp:run +``` + +**Note:** `jvmTest` CI runs on **macOS** because Gobley host cargo is enabled for +the current Gobley host; Linux JVM cargo may be disabled in +`shared/build.gradle.kts`. Prefer macOS for local parity with CI. + +### What to run before finishing + +| You changed… | Minimum verification | +|--------------|----------------------| +| `crates/vnidrop/**` only | `cargo fmt`, `cargo clippy … -D warnings`, `cargo test -p vnidrop` | +| Cancel / export / sinks | Above + `cargo test -p vnidrop --test output_sink` | +| `shared/**` only | `./gradlew :shared:jvmTest` | +| Both | Rust suite + `:shared:jvmTest` | +| Docs only | No suite required; verify links/paths | + +Do not kill long `cargo` / Gradle runs mid-flight unless they hang past several +minutes with no output; first builds are slow. + +--- + +## Repository map (edit here) + +### Rust runtime (keep split; do not re-merge into one file) + +``` +crates/vnidrop/src/runtime/ + mod.rs # CoreInner, startup recovery, emit helpers + facade.rs # UniFFI VnidropCore + block_on + share.rs # import / share + receive.rs # receive, download, export, output sinks + lifecycle.rs # cancel, delete, status, access mode, shutdown + provider.rs # provider events, per-connection send progress +``` + +Other core modules: `filesystem.rs`, `repository.rs`, `approval.rs`, +`handshake.rs`, `ticket.rs`, `access_policy.rs`, `event_hub.rs`, `api.rs`. + +### Shared app + +``` +shared/src/commonMain/kotlin/com/vnidrop/app/ + core/ # CoreGateway, models, pickers interfaces + feature/send|receive|approvals|settings|app/ + ui/theme|components|navigation|feedback|state/ +androidMain|iosMain|jvmMain/ # expect/actual implementations +``` + +### Platform file rules (do not violate) + +- Desktop / path-based iOS: paths; directory walk in Rust when `is_directory`. +- Android **share**: ParcelFileDescriptor **file** FDs only — never a directory FD. + Folder share expands SAF trees in Kotlin to per-file FDs + relative names. +- Android **receive** default: MediaStore Downloads sink; custom trees via SAF write. +- Receive publish: no-overwrite temp + hard link / exclusive rename + (see `CORE_FLOW.md`). + +--- + +## Code style + +### General + +- Match surrounding code (naming, imports, error handling). +- Prefer small, reviewable diffs. Avoid files growing past ~800 LoC without + splitting when adding substantial logic. +- Do not add one-off helpers used only once if an inline block is clearer. +- Prefer exhaustive `when` / `match`; avoid wildcards that hide new cases. + +### Comments (strict) + +Comment **why**, invariants, and platform/concurrency traps only. + +- Do comment: cancel-before-await ordering, SAF/FD limits, security-scoped + leases, “exactly one finish/abort after start_file”, durability rules. +- Do **not** comment: restating the next line, tutorial narration, section + banners that repeat the function name, pasted docs from this file. + +### Rust + +- Follow Clippy with `-D warnings` (CI fails otherwise). +- Do not hold `std::sync::MutexGuard` or other guards across `.await`. +- Prefer `Handle::block_on` via existing `VnidropCore::block_on` for concurrent + API entry; cancel signals active transfers **synchronously** before async work. +- Prefer private modules; export only what UniFFI / other crates need. +- New public traits/types: short docs when the role is non-obvious. + +### Kotlin / Compose + +For UI and presentation work, **load and follow** the in-repo skill: + +```text +.codex/skills/compose-skill/SKILL.md +``` + +- Open at most one `references/*.md` file when the skill’s Quick Routing requires it. +- Do not invent a second Compose style guide. +- VniDrop uses **MVVM-style** ViewModels (`*State` + `StateFlow` + named methods), + not a forced MVI `onEvent` base — adapt, do not rewrite. +- Theme via `LocalVniDropColors` / `VniDropThemeTokens` only. + Brand primary (light): HSL `271, 91%, 65%` ≈ `#A855F7`. +- Strings: CMP `Res.string.*` / composeResources — not Android `R` in `commonMain`. +- Verify multiplatform target support before adding AndroidX/Jetpack deps to + `commonMain`. + +Details: [`shared/AGENTS.md`](shared/AGENTS.md). + +--- + +## Testing instructions + +- Prefer deterministic tests (gates, fixed sizes, public API fixtures). +- Avoid long sleeps; if polling is required: short interval + hard timeout + + clear assertion message. +- Rust integration tests use **public** UniFFI API + `tests/support/` only. +- Failure paths: assert durable status and/or events when applicable, not only + the error string. +- Do not add tests for pure static constants. +- Do not add negative tests for code you deleted. +- Prefer comparing whole objects when equality is meaningful. + +Layout: + +| Layer | Location | +|-------|----------| +| Rust unit / private | `crates/vnidrop/src/tests/` | +| Rust integration | `crates/vnidrop/tests/` | +| Shared logic | `shared/src/commonTest/` | +| Shared Compose/JVM | `shared/src/jvmTest/` | + +--- + +## Git and PR instructions + +### Branches + +Name the change, not a roadmap step: + +- Good: `feat/folder-share`, `fix/cancel-export-hang`, `docs/agents-md` +- Bad: `feat/step3-remaining`, `wip`, `temp` + +After a PR merges: delete the feature branch **locally and on `origin`**, then +branch from updated `master`. + +### Commits + +- Style in history: `feat(scope):`, `fix(scope):`, `refactor(scope):`, `docs:`, `ci:`. +- Subject = outcome; body only when needed. +- Signed when repo requires it. + +### Pull requests + +- Title matches the main change. +- Summary: short bullets of what/why. +- **Test plan must be executable for this PR**: + - exact commands, and/or + - 1–2 concrete scenarios that would catch a regression. +- No filler plans (“everything works”, “CI green”) without commands or scenarios. + +--- + +## Security considerations + +- Treat tickets and endpoint IDs as sensitive enough not to log full blobs in + production paths. +- Do not weaken approval/access checks for convenience. +- Do not store secrets in the repo; app data dirs and key files stay out of git. +- Be careful with file publish races (no-clobber rename/link policy exists for a reason). + +--- + +## Common tasks → start files + +| Task | Start here | +|------|------------| +| Share / multi-file / folders | `runtime/share.rs`, `filesystem.rs`, platform `FileSystemService.*` | +| Receive / export / sinks | `runtime/receive.rs` | +| Cancel / delete / stop share | `runtime/lifecycle.rs`, `facade.rs` | +| Per-receiver send progress | `runtime/provider.rs`, `ui/state/AppUiModels.kt` | +| Approvals | `feature/approvals/`, `approval.rs` | +| QR / NFC invitations | `TransferShareActions.*`, `ReceiveInvitationActions.*` | +| Theme / brand | `ui/theme/VniDropTheme.kt` | +| Compose skill | `.codex/skills/compose-skill/SKILL.md` | + +--- + +## Anti-patterns (never) + +- Streaming multi-MB transfer data through Kotlin as the primary design +- Passing Android **directory** FDs into Rust +- Nested exclusive `Runtime::block_on` that deadlocks cancel during receive +- Holding locks across `.await` +- Rebuilding a monolithic `runtime.rs` +- Flaky multi-minute sleeps in tests +- Unsigned commits when signing is required +- Force-push or secret commits without explicit user direction + +--- + +## Implementation checklist + +1. Read this file + nearest nested `AGENTS.md`. +2. For Compose/UI: load `compose-skill`. +3. Smallest correct change; tests for bugs/behavior changes. +4. Run relevant build/test commands; fix failures. +5. Sparse comments only where non-obvious. +6. Commit (signed) / push / PR only as the user requests. +7. Summarize what changed and what you ran. diff --git a/androidApp/src/main/ic_launcher-playstore.png b/androidApp/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..f8f0179 Binary files /dev/null and b/androidApp/src/main/ic_launcher-playstore.png differ diff --git a/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml b/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml index b4e3546..a8e49b1 100644 --- a/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -1,31 +1,35 @@ + - - - - - - - - - + xmlns:aapt="http://schemas.android.com/aapt" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + + + + + + + + + + + + + + + + + + + + + diff --git a/androidApp/src/main/res/drawable/ic_launcher_background.xml b/androidApp/src/main/res/drawable/ic_launcher_background.xml index 869e6b7..a723447 100644 --- a/androidApp/src/main/res/drawable/ic_launcher_background.xml +++ b/androidApp/src/main/res/drawable/ic_launcher_background.xml @@ -1,170 +1,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + diff --git a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index dbde2b7..65291b9 100644 --- a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,6 @@ - - - + + + + \ No newline at end of file diff --git a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index dbde2b7..65291b9 100644 --- a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,6 @@ - - - + + + + \ No newline at end of file diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index a571e60..0000000 Binary files a/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher.webp b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..503daac Binary files /dev/null and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..3acda44 Binary files /dev/null and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 61da551..0000000 Binary files a/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..7b68f67 Binary files /dev/null and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index c41dd28..0000000 Binary files a/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher.webp b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..db442ba Binary files /dev/null and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..b8f2bc7 Binary files /dev/null and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index db5080a..0000000 Binary files a/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..5fb172b Binary files /dev/null and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 6dba46d..0000000 Binary files a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.webp b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..4e8b941 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..4515eb3 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index da31a87..0000000 Binary files a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..c6ad6a0 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 15ac681..0000000 Binary files a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..2c7315d Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..e3df4ec Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index b216f2d..0000000 Binary files a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..f0012f2 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index f25a419..0000000 Binary files a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..94d681c Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..122ac21 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index e96783c..0000000 Binary files a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..c2fc611 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/androidApp/src/main/res/values/ic_launcher_background.xml b/androidApp/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/androidApp/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/assets/1024x1024.png b/assets/1024x1024.png new file mode 100644 index 0000000..b2540f9 Binary files /dev/null and b/assets/1024x1024.png differ diff --git a/assets/1024x1024.svg b/assets/1024x1024.svg new file mode 100644 index 0000000..5598da1 --- /dev/null +++ b/assets/1024x1024.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/108x108.svg b/assets/108x108.svg new file mode 100644 index 0000000..3be9180 --- /dev/null +++ b/assets/108x108.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/android/app-icon.png b/assets/android/app-icon.png new file mode 100644 index 0000000..7ac58fb Binary files /dev/null and b/assets/android/app-icon.png differ diff --git a/assets/android/app-icon.svg b/assets/android/app-icon.svg new file mode 100644 index 0000000..818ff1e --- /dev/null +++ b/assets/android/app-icon.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/desktop/app-icon.png b/assets/desktop/app-icon.png new file mode 100644 index 0000000..9a21223 Binary files /dev/null and b/assets/desktop/app-icon.png differ diff --git a/assets/desktop/app-icon.svg b/assets/desktop/app-icon.svg new file mode 100644 index 0000000..f8eb118 --- /dev/null +++ b/assets/desktop/app-icon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/desktop/foreground.png b/assets/desktop/foreground.png new file mode 100644 index 0000000..b2540f9 Binary files /dev/null and b/assets/desktop/foreground.png differ diff --git a/assets/desktop/foreground.svg b/assets/desktop/foreground.svg new file mode 100644 index 0000000..5598da1 --- /dev/null +++ b/assets/desktop/foreground.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/ios/app-icon.png b/assets/ios/app-icon.png new file mode 100644 index 0000000..91e521f Binary files /dev/null and b/assets/ios/app-icon.png differ diff --git a/assets/ios/app-icon.svg b/assets/ios/app-icon.svg new file mode 100644 index 0000000..b2a33e3 --- /dev/null +++ b/assets/ios/app-icon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/ios/foreground.png b/assets/ios/foreground.png new file mode 100644 index 0000000..b2540f9 Binary files /dev/null and b/assets/ios/foreground.png differ diff --git a/assets/ios/foreground.svg b/assets/ios/foreground.svg new file mode 100644 index 0000000..5598da1 --- /dev/null +++ b/assets/ios/foreground.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/linux/app-icon.png b/assets/linux/app-icon.png new file mode 100644 index 0000000..9a21223 Binary files /dev/null and b/assets/linux/app-icon.png differ diff --git a/assets/linux/app-icon.svg b/assets/linux/app-icon.svg new file mode 100644 index 0000000..f8eb118 --- /dev/null +++ b/assets/linux/app-icon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/linux/foreground.png b/assets/linux/foreground.png new file mode 100644 index 0000000..b2540f9 Binary files /dev/null and b/assets/linux/foreground.png differ diff --git a/assets/linux/foreground.svg b/assets/linux/foreground.svg new file mode 100644 index 0000000..5598da1 --- /dev/null +++ b/assets/linux/foreground.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/macos/app-icon.icns b/assets/macos/app-icon.icns new file mode 100644 index 0000000..5d93c92 Binary files /dev/null and b/assets/macos/app-icon.icns differ diff --git a/assets/macos/app-icon.png b/assets/macos/app-icon.png new file mode 100644 index 0000000..9a21223 Binary files /dev/null and b/assets/macos/app-icon.png differ diff --git a/assets/macos/app-icon.svg b/assets/macos/app-icon.svg new file mode 100644 index 0000000..f8eb118 --- /dev/null +++ b/assets/macos/app-icon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/macos/foreground.png b/assets/macos/foreground.png new file mode 100644 index 0000000..b2540f9 Binary files /dev/null and b/assets/macos/foreground.png differ diff --git a/assets/macos/foreground.svg b/assets/macos/foreground.svg new file mode 100644 index 0000000..5598da1 --- /dev/null +++ b/assets/macos/foreground.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/windows/app-icon.ico b/assets/windows/app-icon.ico new file mode 100644 index 0000000..451dbbf Binary files /dev/null and b/assets/windows/app-icon.ico differ diff --git a/assets/windows/app-icon.png b/assets/windows/app-icon.png new file mode 100644 index 0000000..9a21223 Binary files /dev/null and b/assets/windows/app-icon.png differ diff --git a/assets/windows/app-icon.svg b/assets/windows/app-icon.svg new file mode 100644 index 0000000..f8eb118 --- /dev/null +++ b/assets/windows/app-icon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/windows/foreground.png b/assets/windows/foreground.png new file mode 100644 index 0000000..b2540f9 Binary files /dev/null and b/assets/windows/foreground.png differ diff --git a/assets/windows/foreground.svg b/assets/windows/foreground.svg new file mode 100644 index 0000000..5598da1 --- /dev/null +++ b/assets/windows/foreground.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/vnidrop/AGENTS.md b/crates/vnidrop/AGENTS.md new file mode 100644 index 0000000..ec71e19 --- /dev/null +++ b/crates/vnidrop/AGENTS.md @@ -0,0 +1,128 @@ +# AGENTS.md — `crates/vnidrop` (Rust core) + +Nearest guide when editing under `crates/vnidrop/`. Root [`AGENTS.md`](../../AGENTS.md) +still applies; this file wins for Rust-specific commands and conventions. + +--- + +## Purpose + +This crate is the transfer backend exposed to Kotlin via UniFFI (`VnidropCore`). +It owns Iroh, blobs, SQLite history, tickets, approval handshake, and file streaming. + +Read [`CORE_FLOW.md`](CORE_FLOW.md) before changing send/receive/export/cancel. + +--- + +## Commands (run from repo root) + +Always prefer workspace commands so lockfile/fmt stay consistent: + +```bash +cargo fmt --all +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test -p vnidrop +cargo test --workspace --all-targets +``` + +Focused integration suites: + +```bash +cargo test -p vnidrop --test transfer +cargo test -p vnidrop --test approval +cargo test -p vnidrop --test lifecycle +cargo test -p vnidrop --test output_sink +``` + +Docs (CI uses `-D warnings`): + +```bash +RUSTDOCFLAGS='-D warnings' cargo doc -p vnidrop --no-deps +``` + +Run `cargo fmt --all` after finishing Rust edits without asking. + +--- + +## Module layout + +``` +src/ + runtime/ + mod.rs # CoreInner, startup recovery, emit helpers + facade.rs # UniFFI surface, block_on, cancel entry + share.rs # share / import + receive.rs # receive, download, export, OutputSinkFile + lifecycle.rs # cancel share, delete, status, access mode, shutdown + provider.rs # provider messages, per-peer transfer progress + filesystem.rs # collect sources, atomic publish, path rules + repository.rs # SQLite + approval.rs / handshake.rs / ticket.rs / access_policy.rs / event_hub.rs + api.rs # UniFFI records/enums + tests/ # crate-private unit tests +tests/ # public-API integration tests + support/ +``` + +**Do not** reassemble a single huge `runtime.rs`. Prefer new focused modules if a +file approaches ~800 LoC of non-test code. + +--- + +## Hard constraints + +1. **Public API stability:** UniFFI surface changes break Kotlin. Prefer additive + changes; update shared Kotlin call sites in the same change when required. +2. **Streaming stays in Rust.** Platform passes paths or FDs; core does not pull + whole files into Kotlin. +3. **Android FDs are files only.** `SourceKind::FileDescriptor` with + `is_directory=true` must fail; directories are expanded on the platform side. +4. **Cancel:** signal active-transfer oneshot **synchronously** before async DB + work. Use existing `take_active_transfer` / facade cancel path. Do not reintroduce + nested exclusive `Runtime::block_on` deadlocks. +5. **No lock across await:** Clippy `await_holding_lock` fails CI. +6. **ReceiveOutputSink:** after successful `start_file`, exactly one of + `finish_file` or `abort_file` (see `OutputSinkFile` Drop). +7. **No-overwrite publish** for path receives (temp + hard link / exclusive rename). +8. Integration tests must use the **public** API + `tests/support/` only. + +--- + +## Code style (Rust) + +- Clippy clean with `-D warnings`. +- Prefer exhaustive `match`; avoid catch-all arms that hide new enum variants. +- Prefer comparing whole objects in tests when practical. +- Comment only non-obvious why (concurrency, durability, platform FS quirks). +- Do not add one-off private helpers used once if inline is clearer. +- Prefer private modules; export deliberately via `lib.rs` / UniFFI. + +--- + +## Testing + +- Unit / private: `src/tests/` (see crate `tests.rs` paths). +- Integration: `tests/*.rs` + `tests/support/mod.rs` (`TestNode`, `MemoryOutputSink`, + `CoreGuard`, etc.). +- Bug fixes need a regression test. +- Prefer gates/latches over multi-second sleeps (see output_sink cancel test). +- Failure tests: durable status and/or events when applicable. +- Recovery tests: shut down core, reopen same data dir. + +Details: [`tests/README.md`](tests/README.md). + +--- + +## PR / verify checklist for this crate + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test -p vnidrop +``` + +If you touched cancel, export, or sinks, also: + +```bash +cargo test -p vnidrop --test output_sink +``` diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index a0a7f62..deeb310 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -25,6 +25,15 @@ compose.desktop { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "com.vnidrop.app" packageVersion = "1.0.0" + macOS { + iconFile.set(project.file("../assets/macos/app-icon.icns")) + } + windows { + iconFile.set(project.file("../assets/windows/app-icon.ico")) + } + linux { + iconFile.set(project.file("../assets/linux/app-icon.png")) + } fileAssociation( mimeType = "application/vnd.vnidrop.transfer", extension = "vnd", diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json index 6dc06c4..91523fc 100644 --- a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,36 +1,36 @@ { - "images": [ - { - "filename": "app-icon-1024.png", - "idiom": "universal", - "platform": "ios", - "size": "1024x1024" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "idiom": "universal", - "platform": "ios", - "size": "1024x1024" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "tinted" - } - ], - "idiom": "universal", - "platform": "ios", - "size": "1024x1024" - } - ], - "info": { - "author": "xcode", - "version": 1 - } + "images" : [ + { + "filename" : "app-icon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } } diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png deleted file mode 100644 index 53fc536..0000000 Binary files a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png and /dev/null differ diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon.png new file mode 100644 index 0000000..91e521f Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon.png differ diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json index d458f1c..73c0059 100644 --- a/iosApp/iosApp/Assets.xcassets/Contents.json +++ b/iosApp/iosApp/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { - "info": { - "author": "xcode", - "version": 1 - } + "info" : { + "author" : "xcode", + "version" : 1 + } } diff --git a/shared/AGENTS.md b/shared/AGENTS.md new file mode 100644 index 0000000..26ecea6 --- /dev/null +++ b/shared/AGENTS.md @@ -0,0 +1,126 @@ +# AGENTS.md — `shared/` (Compose Multiplatform + KMP) + +Nearest guide when editing under `shared/`. Root [`AGENTS.md`](../AGENTS.md) +still applies; this file wins for UI/KMP work. + +--- + +## Purpose + +`shared` is the multiplatform app layer: Compose UI, feature ViewModels, and +`expect`/`actual` bridges into Android, iOS, and desktop. Native transfer work +goes through UniFFI `VnidropCore` (see `crates/vnidrop`). + +--- + +## Compose skill (required for UI work) + +For screens, components, theme, navigation, resources, ViewModel↔UI wiring, +lists, animation, accessibility: + +1. Load [`.codex/skills/compose-skill/SKILL.md`](../.codex/skills/compose-skill/SKILL.md). +2. Follow its workflow and defaults. +3. Open **at most one** file under `.codex/skills/compose-skill/references/` when + the skill’s Quick Routing table says you need deeper guidance. +4. Do **not** invent a parallel Compose style guide. + +### Project policy (overrides generic skill defaults) + +| Topic | Do this | +|-------|---------| +| Architecture | Keep **MVVM-style** ViewModels: immutable `*State`, `StateFlow`, **named methods**. Do not force MVI `onEvent` sealed hierarchies unless asked. | +| Structure | Feature packages under `com.vnidrop.app.feature.*`; thin route/wiring + screen/composables. | +| Theme | Only `LocalVniDropColors` / `VniDropThemeTokens` (`ui/theme/VniDropTheme.kt`). Brand primary light ≈ `#A855F7` (HSL 271, 91%, 65%). | +| Strings | CMP composeResources / `Res.string.*` — not Android `R` in `commonMain`. | +| DI | Follow existing `AppGraph` construction; no unprompted Hilt/Koin migration. | +| Platform | `androidMain` / `iosMain` / `jvmMain` for pickers, SAF, security-scoped URLs, NFC/QR. | +| Dependencies | Before adding Jetpack/AndroidX to `commonMain`, verify multiplatform artifacts for all targets. | + +compose-skill “Existing Project Policy”: adapt to this repo; do not force-migrate. + +--- + +## Commands + +From repo root: + +```bash +./gradlew :shared:jvmTest +./gradlew :shared:compileKotlinJvm +``` + +Optional: + +```bash +./gradlew :shared:testAndroidHostTest +./gradlew :shared:iosSimulatorArm64Test +./gradlew :desktopApp:run +./gradlew :androidApp:assembleDebug +``` + +CI `:shared:jvmTest` runs on **macOS** (Gobley host cargo). Prefer macOS for +local parity. + +When Kotlin changes touch UniFFI-generated APIs, rebuild/test with a full +`jvmTest` so Gobley/native pieces stay aligned. + +--- + +## Layout + +``` +src/ + commonMain/kotlin/com/vnidrop/app/ + core/ # models, CoreGateway, FilePicker interfaces + feature/send|receive|approvals|settings|app/ + ui/ # theme, components, navigation, feedback, state helpers + commonMain/composeResources/ + androidMain|iosMain|jvmMain/ + commonTest|jvmTest|... +``` + +### Platform file bridging (must preserve) + +- **Android share:** open content URIs as FDs; expand **folder trees** to per-file + documents with relative `displayName` paths before calling Rust + (`FileSystemService.android.kt` / `expandShareDirectory`). +- **Android receive:** MediaStore Downloads sink and/or SAF tree write sink. +- **iOS:** keep security-scoped leases alive while Rust reads paths. +- **Desktop:** filesystem paths; directories may be marked `isDirectory` for Rust walk. + +Never pass a directory as a single Android FD into `SourceKind.FILE_DESCRIPTOR`. + +--- + +## Code style (Kotlin) + +- Match existing feature style (imports, naming, state updates via `update { }`). +- Composables render state and invoke callbacks; no business rules in `@Composable` + bodies (network, share creation, ticket parse — ViewModel/core). +- Prefer stable list keys from domain IDs. +- Comments only for non-obvious platform or concurrency reasons. +- Do not hard-code brand colors; use theme tokens. + +--- + +## Testing + +- Logic: `src/commonTest/` (e.g. ViewModel fakes, `AppUiModels` progress helpers). +- Compose/JVM: `src/jvmTest/`. +- Add/adjust tests when changing state machines, progress aggregation, or + share/receive eligibility. +- Prefer fakes in `commonTest` support over real UniFFI in pure unit tests. + +```bash +./gradlew :shared:jvmTest +``` + +--- + +## Anti-patterns + +- Streaming transfer bytes through Kotlin as the main design +- Directory FDs on Android +- New DI framework “because best practice” +- Loading every compose-skill reference file for a small UI tweak +- Android `R.string` in `commonMain`