19 Commits

Author SHA1 Message Date
Hammed Abass
4feff29c74 Merge pull request #23 from sudosylabs/feat/remove-apple-kmp-targets
refactor(platform): move Apple apps out of KMP
2026-07-21 18:46:21 +02:00
873ec6fd94 fix(diagnostics): stabilize pending crash ordering 2026-07-21 18:23:50 +02:00
a8a873c83d build: unify project development commands 2026-07-21 18:07:06 +02:00
9e8564e013 refactor(platform): remove Apple targets from KMP 2026-07-21 17:09:59 +02:00
Hammed Abass
d8eaf78997 Merge pull request #21 from sudosylabs/feat/localize
Single-source localization + 8 new languages
2026-07-21 16:40:11 +02:00
8c1ce16c0e Merge branch 'master' into feat/localize
merge(master): Fix for fileImporter before iOS/macOS 27
2026-07-20 18:25:06 +02:00
276ee9f974 i18n(ru): avoid seeding terminology
Replace "раздача"/"раздаваться" (literally "seeding") with neutral
"общий доступ"/"отправка" phrasing across the Russian strings, to avoid
BitTorrent connotations for App Store review.
2026-07-20 12:40:57 +02:00
8178296d92 feat(l10n): add Russian translations
Translate all 253 user-facing strings to Russian (professional UI register;
count strings use number-neutral "label: {count}" form). transfer_file_count
uses full CLDR plural categories (one/few/many/other). Generates
values-ru/strings.xml for KMP, ru localizations in the Apple catalog, and adds
ru to CFBundleLocalizations. Completes the initial 8-language set.
2026-07-20 11:38:12 +02:00
0efde72414 feat(l10n): add Dutch translations
Translate all 253 user-facing strings to Dutch (formal "u"), including the
transfer_file_count plural. Generates values-nl/strings.xml for KMP, nl
localizations in the Apple catalog, and adds nl to CFBundleLocalizations.
2026-07-20 11:34:21 +02:00
48a523371c feat(l10n): add Polish translations
Translate all 253 user-facing strings to Polish (standard professional UI
register; gender-neutral phrasing; count strings use number-neutral "label:
{count}" form). transfer_file_count uses full CLDR plural categories
(one/few/many/other). Generates values-pl/strings.xml for KMP, pl localizations
in the Apple catalog, and adds pl to CFBundleLocalizations.
2026-07-20 11:29:58 +02:00
674e59f8cc feat(l10n): add European Portuguese translations
Translate all 253 user-facing strings to European Portuguese (pt-PT, formal;
EP vocabulary — ficheiro, guardar, Definições, partilhar). Generates
values-pt/strings.xml for KMP, pt localizations in the Apple catalog, and adds
pt to CFBundleLocalizations.
2026-07-20 11:25:11 +02:00
0309cfd649 feat(l10n): add German translations
Translate all 253 user-facing strings to German (formal "Sie"; standard iOS
term conventions). Generates values-de/strings.xml for KMP, de localizations in
the Apple catalog, and adds de to CFBundleLocalizations.
2026-07-20 11:20:32 +02:00
ea07f5661d feat(l10n): add Italian translations
Translate all 253 user-facing strings to Italian (formal "Lei"; standard iOS
button terms follow Apple conventions). Generates values-it/strings.xml for
KMP, it localizations in the Apple catalog, and adds it to CFBundleLocalizations.
2026-07-20 11:15:54 +02:00
141be2e55a feat(l10n): add Spanish translations
Translate all 253 user-facing strings to Spanish (formal "usted"), including
the transfer_file_count plural. Generates values-es/strings.xml for KMP, es
localizations in the Apple catalog, and adds es to CFBundleLocalizations.
2026-07-20 11:11:12 +02:00
c71e1b97d9 feat(l10n): advertise app localizations to iOS
The per-app language picker in iOS Settings only appears when the built app
declares multiple localizations. Add the planned languages to the Xcode
project's knownRegions (so Xcode compiles their .lproj from the String
Catalog), and have `generate` keep CFBundleLocalizations in Info.plist in sync
with supportedLanguages (currently en, fr).
2026-07-20 10:41:41 +02:00
91573f381a feat(l10n): add French translations
Translate all 253 user-facing strings to French (formal register), including
the transfer_file_count plural. Generates values-fr/strings.xml for KMP and
fr localizations in the Apple catalog (marked needs_review).
2026-07-20 10:34:52 +02:00
4448822bfa refactor(l10n): default strings to all targets
Most strings were imported with a platform-specific `targets` (usually
apple-only) just because that's where they happen to be used today. Drop the
restriction so they default to all targets and are available to KMP too; only
the four literal `%@…` format-composition keys stay apple-only.

Also fix the placeholder parser to tolerate C length modifiers (`%lld`), and
give progress_sending_to_count a proper int arg so it emits `%1$d` on both
platforms instead of a literal `%lld`.
2026-07-20 10:24:54 +02:00
3c0f29adb6 docs(l10n): describe context for every string key
Replace the placeholder `context` on all 255 keys in strings.json with a
description of where each string appears and its purpose, and regenerate the
xcstrings so the comments flow through to the Apple catalog.
2026-07-20 10:20:34 +02:00
cd6a2d66aa feat(l10n): single-source localization pipeline
Add localization/ — one strings.json is the source of truth for every
user-facing string, and a Bun CLI generates the platform files:

  loc migrate   rebuild strings.json from existing platform files (one-time)
  loc validate  structural checks (plural `other`, arg refs, coverage)
  loc generate  emit .xcstrings (Apple) + per-language strings.xml (KMP)

Canonical `{name}` placeholders are converted to each platform's positional
printf tokens using declared arg types. Plurals use CLDR categories and emit
native forms: xcstrings plural variations and Compose <plurals> blocks.

Migration folds the KMP `transfer_file_count_one`/`_other` pair into a single
plural key, so `transferFileCountResource` now returns the plural resource and
callers resolve it with pluralStringResource.
2026-07-20 10:15:28 +02:00
120 changed files with 22806 additions and 5946 deletions

View File

@@ -8,6 +8,9 @@ on:
- "crates/uniffi-bindgen/**"
- "Cargo.toml"
- "Cargo.lock"
- "Makefile"
- "config.mk"
- "make/**"
- ".github/workflows/apple.yml"
push:
branches:
@@ -18,6 +21,9 @@ on:
- "crates/uniffi-bindgen/**"
- "Cargo.toml"
- "Cargo.lock"
- "Makefile"
- "config.mk"
- "make/**"
- ".github/workflows/apple.yml"
permissions:
@@ -57,23 +63,5 @@ jobs:
- name: Install XcodeGen
run: brew install xcodegen
- name: Build Rust core (xcframework + Swift bindings)
working-directory: apple
run: ./scripts/build-core.sh debug
- name: Generate Xcode project
working-directory: apple
run: xcodegen generate
- name: Run unit tests (iOS Simulator)
working-directory: apple
run: |
set -euo pipefail
DEVICE=$(xcrun simctl list devices available \
| grep -oE 'iPhone [0-9]+( Pro)?' | head -1)
echo "Testing on: ${DEVICE:-iPhone 16}"
xcodebuild test \
-project VniDrop.xcodeproj \
-scheme VniDrop \
-destination "platform=iOS Simulator,name=${DEVICE:-iPhone 16}" \
CODE_SIGNING_ALLOWED=NO
- name: Build and test Apple app
run: make check-apple

View File

@@ -4,12 +4,18 @@ on:
pull_request:
paths:
- "services/diagnostics-api/**"
- "Makefile"
- "config.mk"
- "make/**"
- ".github/workflows/diagnostics-api.yml"
push:
branches:
- master
paths:
- "services/diagnostics-api/**"
- "Makefile"
- "config.mk"
- "make/**"
- ".github/workflows/diagnostics-api.yml"
permissions:
@@ -23,9 +29,6 @@ jobs:
quality:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: services/diagnostics-api
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -37,17 +40,5 @@ jobs:
cache: npm
cache-dependency-path: services/diagnostics-api/package-lock.json
- name: Install dependencies
run: npm ci
- name: Verify generated Worker types
run: npm run types:check
- name: Type-check
run: npm run typecheck
- name: Test in the Workers runtime
run: npm test
- name: Validate the deployment bundle
run: npm run deploy:dry-run
- name: Check diagnostics API
run: make check-diagnostics

View File

@@ -4,12 +4,18 @@ on:
pull_request:
paths:
- "docs/**"
- "Makefile"
- "config.mk"
- "make/**"
- ".github/workflows/docs.yml"
push:
branches:
- master
paths:
- "docs/**"
- "Makefile"
- "config.mk"
- "make/**"
- ".github/workflows/docs.yml"
permissions:
@@ -23,9 +29,6 @@ jobs:
quality:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: docs
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -37,11 +40,5 @@ jobs:
cache: npm
cache-dependency-path: docs/package-lock.json
- name: Install dependencies
run: npm ci
- name: Type-check
run: npm run typecheck
- name: Build
run: npm run build
- name: Check documentation website
run: make check-docs

View File

@@ -17,6 +17,9 @@ on:
- "gradle.properties"
- "gradle/**"
- "gradlew"
- "Makefile"
- "config.mk"
- "make/**"
push:
tags:
- "v*.*.*"
@@ -92,37 +95,7 @@ jobs:
echo "app=$version" >> "$GITHUB_OUTPUT"
- name: Test and build Debian package
run: |
./gradlew \
:shared:jvmTest \
:desktopApp:packageReleaseDeb \
-Pvnidrop.version=${{ steps.version.outputs.app }} \
-Pvnidrop.desktop.rustVariant=release \
-Pvnidrop.diagnostics.included=false \
--no-daemon \
--no-configuration-cache \
--stacktrace
- name: Validate Debian package
id: package
env:
VERSION: ${{ steps.version.outputs.app }}
run: |
mapfile -t packages < <(find desktopApp/build/compose/binaries/main-release/deb -maxdepth 1 -type f -name '*.deb')
if (( ${#packages[@]} != 1 )); then
echo "Expected exactly one Debian package, found ${#packages[@]}" >&2
exit 1
fi
output_directory=build/release/linux/deb
output_name="vnidrop_${VERSION}-1_amd64.deb"
mkdir -p "$output_directory"
cp "${packages[0]}" "$output_directory/$output_name"
packaging/linux/verify-package.sh deb "$VERSION" "$output_directory/$output_name"
(
cd "$output_directory"
sha256sum "$output_name" > "$output_name.sha256"
)
run: make package-deb VERSION=${{ steps.version.outputs.app }}
- name: Upload Debian artifact
if: github.event_name != 'pull_request'
@@ -225,35 +198,7 @@ jobs:
echo "app=$version" >> "$GITHUB_OUTPUT"
- name: Build RPM package
run: |
./gradlew \
:desktopApp:packageReleaseRpm \
-Pvnidrop.version=${{ steps.version.outputs.app }} \
-Pvnidrop.desktop.rustVariant=release \
-Pvnidrop.diagnostics.included=false \
--no-daemon \
--no-configuration-cache \
--stacktrace
- name: Validate RPM package
env:
VERSION: ${{ steps.version.outputs.app }}
run: |
mapfile -t packages < <(find desktopApp/build/compose/binaries/main-release/rpm -maxdepth 1 -type f -name '*.rpm')
if (( ${#packages[@]} != 1 )); then
echo "Expected exactly one RPM package, found ${#packages[@]}" >&2
exit 1
fi
output_directory=build/release/linux/rpm
output_name="vnidrop-${VERSION}-1.x86_64.rpm"
mkdir -p "$output_directory"
cp "${packages[0]}" "$output_directory/$output_name"
packaging/linux/verify-package.sh rpm "$VERSION" "$output_directory/$output_name"
(
cd "$output_directory"
sha256sum "$output_name" > "$output_name.sha256"
)
run: make package-rpm VERSION=${{ steps.version.outputs.app }}
- name: Upload RPM artifact
if: github.event_name != 'pull_request'

View File

@@ -6,6 +6,9 @@ on:
- "Cargo.toml"
- "Cargo.lock"
- "crates/vnidrop/**"
- "Makefile"
- "config.mk"
- "make/**"
- ".github/workflows/rust-core.yml"
push:
# Only after merge (or direct master pushes). Feature-branch work is covered
@@ -16,6 +19,9 @@ on:
- "Cargo.toml"
- "Cargo.lock"
- "crates/vnidrop/**"
- "Makefile"
- "config.mk"
- "make/**"
- ".github/workflows/rust-core.yml"
permissions:
@@ -33,18 +39,10 @@ jobs:
- uses: actions/checkout@v4
- name: Install Rust quality components
run: rustup component add clippy rustfmt
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run strict Clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Run unit and integration tests
run: cargo test --workspace --all-targets
- name: Check documentation
env:
RUSTDOCFLAGS: -D warnings
run: cargo doc --workspace --no-deps
- name: Check Rust core
run: make check-rust
- name: Install cargo-audit
run: cargo install cargo-audit --locked
- name: Audit Rust dependencies
# Ignores are listed in .cargo/audit.toml for known transitive issues.
run: cargo audit
run: make audit-rust

View File

@@ -15,6 +15,9 @@ on:
- "gradle.properties"
- "androidApp/**"
- "desktopApp/**"
- "Makefile"
- "config.mk"
- "make/**"
- ".github/workflows/shared-kmp.yml"
push:
branches:
@@ -32,6 +35,9 @@ on:
- "gradle.properties"
- "androidApp/**"
- "desktopApp/**"
- "Makefile"
- "config.mk"
- "make/**"
- ".github/workflows/shared-kmp.yml"
permissions:
@@ -44,8 +50,7 @@ concurrency:
jobs:
jvm-test:
# Host Rust embedding is enabled only for the current Gobley host target.
# This job stays on macOS to cover the Apple targets as well as JVM tests.
runs-on: macos-latest
runs-on: ubuntu-latest
timeout-minutes: 75
steps:
- name: Checkout
@@ -63,7 +68,7 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,aarch64-linux-android,x86_64-linux-android
targets: aarch64-linux-android,x86_64-linux-android
- name: Cache Cargo
uses: actions/cache@v4
@@ -95,7 +100,7 @@ jobs:
echo "ANDROID_NDK_ROOT=${{ steps.setup-ndk.outputs.ndk-path }}" >> "$GITHUB_ENV"
- name: Run shared JVM tests
run: ./gradlew :shared:jvmTest --no-daemon --stacktrace
run: make check-shared
- name: Verify Android native libraries
run: ./gradlew :androidApp:verifyDebugVnidropLibraries --no-daemon --stacktrace
run: make verify-android-libs

1
.gitignore vendored
View File

@@ -19,6 +19,7 @@ captures
node_modules/
target/
.junie
config.override.mk
# Local design export scratch
output/

View File

@@ -13,13 +13,14 @@ Nested guides take precedence when editing under those trees:
## Project overview
VniDrop is a cross-platform **local P2P file transfer** app (Android, iOS, Desktop).
VniDrop is a cross-platform **local P2P file transfer** app.
| 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 |
| Shared KMP | `shared/` | Compose UI and platform bridges for Android, Windows, and Linux |
| Compose hosts | `androidApp/`, `desktopApp/` | Thin Android and Windows/Linux app shells |
| Apple app | `apple/` | Native SwiftUI UI using generated Rust/UniFFI Swift bindings |
**Invariant:** UI/platform opens files and handles pickers; **Rust streams bytes**.
Do not design features that move transfer payloads through Kotlin heap by default.
@@ -52,67 +53,62 @@ Domain docs (reference, do not paste into PRs):
## Build and test
Install prerequisites when missing: Rust stable + rustfmt + clippy, JDK 17,
Android NDK/SDK only if building Android, Xcode only for iOS.
Install prerequisites when missing: GNU Make + Bash, Rust stable + rustfmt + clippy, JDK 17,
Android NDK/SDK only if building Android, Xcode only for the native Apple app.
### 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
make check-rust
```
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
make test-rust
make test-rust-output-sink
make test-rust-transfer
make test-rust-approval
make test-rust-lifecycle
```
After finishing Rust edits, format:
```bash
cargo fmt --all
make format
```
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.
`make check-rust` includes documentation with warnings denied, matching
`.github/workflows/rust-core.yml`.
### Shared KMP / Compose (`shared/`)
```bash
./gradlew :shared:jvmTest
./gradlew :shared:compileKotlinJvm
make check-shared
```
Other targets (slower / machine-dependent):
```bash
./gradlew :shared:testAndroidHostTest
./gradlew :shared:iosSimulatorArm64Test # macOS + Xcode
./gradlew :androidApp:assembleDebug
./gradlew :desktopApp:run
make test-android-host
make check-android
make run-desktop
```
**Note:** `jvmTest` CI currently runs on **macOS**. Gobley host cargo is enabled
for the current host and architecture, so local Linux and Windows builds embed
their matching desktop Rust library. Prefer macOS only when exact CI parity is
required.
**Note:** `jvmTest` CI runs on **Linux**. Gobley host cargo is enabled for the
current host and architecture, so local desktop builds embed their matching
Rust library.
### 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` |
| `crates/vnidrop/**` only | `make check-rust` |
| Cancel / export / sinks | Above + `make test-rust-output-sink` |
| `shared/**` only | `make test-shared` |
| Both | `make test-rust test-shared` |
| Docs only | No suite required; verify links/paths |
Do not kill long `cargo` / Gradle runs mid-flight unless they hang past several
@@ -144,12 +140,12 @@ 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
androidMain|jvmMain/ # expect/actual implementations
```
### Platform file rules (do not violate)
- Desktop / path-based iOS: paths; directory walk in Rust when `is_directory`.
- Windows/Linux desktop: 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.

View File

@@ -35,24 +35,44 @@ Use a branch name that describes the outcome, such as
Install the tools needed for the area you plan to change:
- GNU Make and Bash for the root command interface
- JDK 17 or newer for Gradle and application builds
- Rust stable with `rustfmt` and Clippy for the transfer core
- Android SDK and NDK for Android builds
- Xcode on macOS for iOS builds and simulator tests
- Xcode and XcodeGen on macOS for native Apple builds and simulator tests
- Node.js 22.12 or newer for the optional diagnostics service
The first Rust and Gradle builds may take several minutes while dependencies are
downloaded and native components are compiled.
## Command Interface
Run development commands through the root `Makefile`. It keeps local and CI
commands aligned while continuing to delegate builds to Cargo, Gradle, Xcode,
Bun, and npm:
```bash
make help # list commands
make doctor # report missing host tools
make setup # install repository-local JavaScript dependencies
make check # portable Rust, shared, localization, docs, and service checks
```
Configuration can be passed on the command line, for example
`make package-deb VERSION=1.2.0`, or placed in an ignored
`config.override.mk`. Windows use requires GNU Make in a Bash environment; the
underlying Gradle and PowerShell entry points remain available when Make is not
installed.
## Repository Structure
| Path | Purpose |
|------|---------|
| `crates/vnidrop/` | Rust transfer core, persistence, approval, and streaming |
| `shared/` | Shared Kotlin Multiplatform UI and platform bridges |
| `shared/` | Compose Multiplatform UI and bridges for Android, Windows, and Linux |
| `androidApp/` | Android application shell |
| `iosApp/` | iOS application shell |
| `desktopApp/` | Desktop JVM application shell |
| `desktopApp/` | Windows/Linux JVM application shell |
| `apple/` | Native SwiftUI application and Rust/UniFFI integration for Apple platforms |
| `services/diagnostics-api/` | Optional Cloudflare diagnostics service |
Read the nearest contributor guidance before editing:
@@ -87,43 +107,48 @@ Run checks from the repository root. Choose the suite for the files you changed.
### Rust Core
```bash
cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p vnidrop
make format
make test-rust
```
For cancel, export, or output-sink changes, also run:
```bash
cargo test -p vnidrop --test output_sink
make test-rust-output-sink
```
For broader core changes, run the complete workspace suite:
```bash
cargo test --workspace --all-targets
make check-rust
```
### Shared Kotlin and Compose
```bash
./gradlew :shared:jvmTest
make test-shared
```
Platform-specific checks may also be appropriate:
```bash
./gradlew :shared:testAndroidHostTest
./gradlew :shared:iosSimulatorArm64Test
./gradlew :androidApp:assembleDebug
make test-android-host
make check-android
```
### Native Apple App
```bash
make check-apple
```
Override the selected simulator when needed with
`make check-apple APPLE_DESTINATION='platform=iOS Simulator,name=iPhone 16'`.
### Diagnostics Service
```bash
cd services/diagnostics-api
npm ci
npm run check
make check-diagnostics
```
If a required check cannot run on your machine, explain why in the pull request

181
Makefile Normal file
View File

@@ -0,0 +1,181 @@
ROOT := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help
include $(ROOT)/config.mk
-include $(ROOT)/config.override.mk
include $(ROOT)/make/release.mk
.PHONY: help doctor setup setup-localization setup-docs setup-diagnostics
.PHONY: format test check check-rust audit-rust test-rust test-rust-all
.PHONY: test-rust-transfer test-rust-approval test-rust-lifecycle test-rust-output-sink
.PHONY: check-shared test-shared test-android-host check-android verify-android-libs build-android run-desktop
.PHONY: apple-core apple-project open-apple-project open-apple build-apple-macos build-apple-ios check-apple
.PHONY: check-localization localization localization-migrate
.PHONY: check-docs run-docs check-diagnostics run-diagnostics diagnostics-db-local diagnostics-db-remote diagnostics-typegen deploy-diagnostics
help: ## Show available commands and common configuration variables.
@grep -hE '^[A-Za-z0-9_.-]+:.*## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "} {printf " %-28s %s\n", $$1, $$2}'
@printf '\nCommon variables:\n'
@printf ' %-28s %s\n' 'VERSION=x.y.z' 'Package version (default: $(VERSION))'
@printf ' %-28s %s\n' 'APPLE_PROFILE=debug|release' 'Rust profile for the Apple XCFramework'
@printf ' %-28s %s\n' 'APPLE_CONFIGURATION=...' 'Xcode configuration (default: $(APPLE_CONFIGURATION))'
@printf ' %-28s %s\n' 'APPLE_DESTINATION=...' 'Optional xcodebuild destination override'
@printf ' %-28s %s\n' 'APPLE_CODE_SIGNING=NO|YES' 'Enable Apple code signing (default: $(APPLE_CODE_SIGNING))'
doctor: ## Check that tools required by the current host are available.
@missing=0; \
for tool in "$(firstword $(CARGO))" java "$(firstword $(NPM))" "$(firstword $(BUN))"; do \
if command -v "$$tool" >/dev/null 2>&1; then \
printf 'ok %s\n' "$$tool"; \
else \
printf 'missing %s\n' "$$tool"; \
missing=1; \
fi; \
done; \
if [[ ! -f "$(GRADLE)" ]]; then printf 'missing %s\n' "$(GRADLE)"; missing=1; else printf 'ok %s\n' "$(GRADLE)"; fi; \
if [[ "$(HOST_OS)" == macos ]]; then \
for tool in "$(firstword $(XCODEBUILD))" "$(firstword $(XCODEGEN))"; do \
if command -v "$$tool" >/dev/null 2>&1; then printf 'ok %s\n' "$$tool"; else printf 'missing %s\n' "$$tool"; missing=1; fi; \
done; \
fi; \
exit $$missing
setup: setup-localization setup-docs setup-diagnostics ## Install repository-local JavaScript dependencies.
setup-localization: ## Install localization CLI dependencies with Bun.
cd $(ROOT)/localization && $(BUN) install --frozen-lockfile
setup-docs: ## Install documentation website dependencies.
cd $(ROOT)/docs && $(NPM) ci
setup-diagnostics: ## Install diagnostics Worker dependencies.
cd $(ROOT)/services/diagnostics-api && $(NPM) ci
format: ## Format Rust sources.
cd $(ROOT) && $(CARGO) fmt --all
test: test-rust test-shared ## Run the main Rust and shared JVM test suites.
check: check-rust check-shared check-localization check-docs check-diagnostics ## Run portable pre-PR verification.
check-rust: ## Run Rust formatting, lint, tests, and documentation checks.
cd $(ROOT) && $(CARGO) fmt --all -- --check
cd $(ROOT) && $(CARGO) clippy --workspace --all-targets -- -D warnings
cd $(ROOT) && $(CARGO) test --workspace --all-targets
cd $(ROOT) && RUSTDOCFLAGS='-D warnings' $(CARGO) doc --workspace --no-deps
audit-rust: ## Audit Rust dependencies (requires cargo-audit).
cd $(ROOT) && $(CARGO) audit
test-rust: ## Run the focused Rust core suite.
cd $(ROOT) && $(CARGO) test -p vnidrop
test-rust-all: ## Run every Rust workspace test target.
cd $(ROOT) && $(CARGO) test --workspace --all-targets
test-rust-transfer: ## Run Rust transfer integration tests.
cd $(ROOT) && $(CARGO) test -p vnidrop --test transfer
test-rust-approval: ## Run Rust approval integration tests.
cd $(ROOT) && $(CARGO) test -p vnidrop --test approval
test-rust-lifecycle: ## Run Rust lifecycle integration tests.
cd $(ROOT) && $(CARGO) test -p vnidrop --test lifecycle
test-rust-output-sink: ## Run Rust output-sink integration tests.
cd $(ROOT) && $(CARGO) test -p vnidrop --test output_sink
check-shared: ## Test and compile the shared Android/JVM module.
cd $(ROOT) && $(GRADLE) :shared:jvmTest :shared:compileKotlinJvm $(GRADLE_FLAGS)
test-shared: ## Run shared JVM tests.
cd $(ROOT) && $(GRADLE) :shared:jvmTest $(GRADLE_FLAGS)
test-android-host: ## Run Android host-side shared tests.
cd $(ROOT) && $(GRADLE) :shared:testAndroidHostTest $(GRADLE_FLAGS)
check-android: ## Build Android debug and verify packaged Rust libraries.
cd $(ROOT) && $(GRADLE) :androidApp:assembleDebug :androidApp:verifyDebugVnidropLibraries $(GRADLE_FLAGS)
verify-android-libs: ## Verify the Rust libraries packaged in the Android debug app.
cd $(ROOT) && $(GRADLE) :androidApp:verifyDebugVnidropLibraries $(GRADLE_FLAGS)
build-android: ## Build the Android debug APK.
cd $(ROOT) && $(GRADLE) :androidApp:assembleDebug $(GRADLE_FLAGS)
run-desktop: ## Run the Windows/Linux Compose desktop app.
cd $(ROOT) && $(GRADLE) :desktopApp:run $(GRADLE_FLAGS)
apple-core: ## Build the Rust XCFramework and generated Swift bindings.
@test "$(HOST_OS)" = macos || { printf 'Apple builds require macOS.\n' >&2; exit 1; }
cd $(ROOT) && apple/scripts/build-core.sh $(APPLE_PROFILE)
apple-project: apple-core ## Generate the native Apple Xcode project.
cd $(ROOT)/apple && $(XCODEGEN) generate
open-apple-project: apple-project ## Generate and open the native Apple Xcode project.
cd $(ROOT)/apple && $(OPEN) VniDrop.xcodeproj
build-apple-macos: apple-project ## Build the native macOS app (unsigned by default).
cd $(ROOT)/apple && $(XCODEBUILD) -project VniDrop.xcodeproj -scheme VniDrop -configuration $(APPLE_CONFIGURATION) -derivedDataPath "$(APPLE_DERIVED_DATA)" -destination 'platform=macOS' CODE_SIGNING_ALLOWED=$(APPLE_CODE_SIGNING) CODE_SIGNING_REQUIRED=$(APPLE_CODE_SIGNING) build
open-apple: build-apple-macos ## Build and launch the native macOS app.
@test -d "$(APPLE_DERIVED_DATA)/Build/Products/$(APPLE_CONFIGURATION)/VniDrop.app" || { printf 'Built macOS app was not found.\n' >&2; exit 1; }
$(OPEN) "$(APPLE_DERIVED_DATA)/Build/Products/$(APPLE_CONFIGURATION)/VniDrop.app"
build-apple-ios: apple-project ## Build the native iOS simulator app (unsigned by default).
@destination="$(APPLE_DESTINATION)"; \
if [[ -z "$$destination" ]]; then \
device_id="$$(xcrun simctl list devices available | sed -nE '/iPhone/ s/.*\(([0-9A-F-]{36})\) \((Booted|Shutdown)\).*/\1/p' | head -1 || true)"; \
[[ -n "$$device_id" ]] || { printf 'No available iPhone simulator found. Set APPLE_DESTINATION explicitly.\n' >&2; exit 1; }; \
destination="platform=iOS Simulator,id=$$device_id"; \
fi; \
cd $(ROOT)/apple && $(XCODEBUILD) -project VniDrop.xcodeproj -scheme VniDrop -configuration $(APPLE_CONFIGURATION) -derivedDataPath "$(APPLE_DERIVED_DATA)" -destination "$$destination" CODE_SIGNING_ALLOWED=$(APPLE_CODE_SIGNING) CODE_SIGNING_REQUIRED=$(APPLE_CODE_SIGNING) build
check-apple: apple-project ## Build the Apple core and run iOS simulator tests.
@destination="$(APPLE_DESTINATION)"; \
if [[ -z "$$destination" ]]; then \
device_id="$$(xcrun simctl list devices available | sed -nE '/iPhone/ s/.*\(([0-9A-F-]{36})\) \((Booted|Shutdown)\).*/\1/p' | head -1 || true)"; \
[[ -n "$$device_id" ]] || { printf 'No available iPhone simulator found. Set APPLE_DESTINATION explicitly.\n' >&2; exit 1; }; \
destination="platform=iOS Simulator,id=$$device_id"; \
fi; \
printf 'Testing on: %s\n' "$$destination"; \
cd $(ROOT)/apple && $(XCODEBUILD) test -project VniDrop.xcodeproj -scheme VniDrop -configuration $(APPLE_CONFIGURATION) -derivedDataPath "$(APPLE_DERIVED_DATA)" -destination "$$destination" CODE_SIGNING_ALLOWED=$(APPLE_CODE_SIGNING) CODE_SIGNING_REQUIRED=$(APPLE_CODE_SIGNING)
check-localization: setup-localization ## Validate the localization source catalog.
cd $(ROOT)/localization && $(BUN) run validate
localization: setup-localization ## Regenerate Apple and KMP localization resources.
cd $(ROOT)/localization && $(BUN) run generate
localization-migrate: setup-localization ## Rebuild strings.json from platform resources.
cd $(ROOT)/localization && $(BUN) run migrate
check-docs: setup-docs ## Lint, type-check, and build the documentation website.
cd $(ROOT)/docs && $(NPM) run lint
cd $(ROOT)/docs && $(NPM) run typecheck
cd $(ROOT)/docs && $(NPM) run build
run-docs: setup-docs ## Run the documentation development server.
cd $(ROOT)/docs && $(NPM) run dev
check-diagnostics: setup-diagnostics ## Run diagnostics types, tests, and deployment dry-run.
cd $(ROOT)/services/diagnostics-api && $(NPM) run check
run-diagnostics: setup-diagnostics ## Run the diagnostics Worker locally.
cd $(ROOT)/services/diagnostics-api && $(NPM) run dev
diagnostics-db-local: setup-diagnostics ## Apply diagnostics database migrations locally.
cd $(ROOT)/services/diagnostics-api && $(NPM) run db:migrate:local
diagnostics-db-remote: setup-diagnostics ## Apply diagnostics database migrations to the configured remote D1 database.
cd $(ROOT)/services/diagnostics-api && $(NPM) run db:migrate:remote
diagnostics-typegen: setup-diagnostics ## Regenerate diagnostics Worker binding types.
cd $(ROOT)/services/diagnostics-api && $(NPM) run typegen
deploy-diagnostics: setup-diagnostics ## Check and deploy the diagnostics Worker to Cloudflare.
cd $(ROOT)/services/diagnostics-api && $(NPM) run deploy

View File

@@ -88,7 +88,8 @@ people, especially when using **Anyone with this transfer**.
- Per-receiver requests, approvals, progress, and delivery status
- Cancel, stop sharing, and local transfer history
- Safe receive destinations that do not silently overwrite existing files
- Android, iOS, and desktop apps built from a shared Compose Multiplatform UI
- Native SwiftUI apps on iOS, iPadOS, and macOS; Compose apps on Android,
Windows, and Linux
- Opt-in diagnostics with transfer contents, invitations, and file paths
excluded
@@ -117,14 +118,21 @@ if you want to try the current version.
git clone https://github.com/vnidrop/vnidrop.git
cd vnidrop
# Desktop
./gradlew :desktopApp:run
# List the supported development commands and check prerequisites
make help
make doctor
# Windows/Linux desktop
make run-desktop
# Android debug build
./gradlew :androidApp:assembleDebug
make build-android
# iOS
open iosApp/iosApp.xcodeproj
# Build and launch the macOS app
make open-apple
# Open the native project for iOS, iPadOS, or Xcode development
make open-apple-project
```
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for prerequisites, development setup,

View File

@@ -3,7 +3,7 @@
A native SwiftUI app for Apple platforms, sharing the existing Rust transfer core
(`crates/vnidrop`) through UniFFI-generated Swift bindings. The Rust crate is not
modified; the Kotlin/Compose app layer is ported to Swift and mirrors the Compose
UI screen-for-screen. Android and desktop JVM continue to use `shared/` + Compose.
UI screen-for-screen. Android, Windows, and Linux continue to use `shared/` + Compose.
## Layout
@@ -30,21 +30,24 @@ Prerequisites: Xcode, Rust with the Apple targets
`aarch64-apple-darwin`), and `xcodegen` (`brew install xcodegen`).
```bash
# 1. Build the Rust core and generate the Swift bindings + xcframework.
apple/scripts/build-core.sh debug # or: release (see note below)
# 2. Generate the Xcode project.
cd apple && xcodegen generate
# 3. Open and run, or build from the CLI:
open VniDrop.xcodeproj
# macOS:
xcodebuild -project VniDrop.xcodeproj -scheme VniDrop -destination 'platform=macOS' build
# iOS simulator:
xcodebuild -project VniDrop.xcodeproj -scheme VniDrop \
-destination 'platform=iOS Simulator,name=iPhone 15' build
# From the repository root:
make apple-core # Rust core, Swift bindings, and XCFramework
make apple-project # generate apple/VniDrop.xcodeproj
make open-apple-project # generate and open the project in Xcode
make build-apple-macos # unsigned macOS build
make open-apple # build and launch the macOS app
make build-apple-ios # unsigned iOS simulator build
make check-apple # iOS simulator tests
```
Use `APPLE_PROFILE=release` to request a release Rust core, or set
`APPLE_DESTINATION` to override the automatically selected iOS simulator.
Code signing is disabled for the app and test targets; local and CI builds do
not require an Apple Development team or provisioning profile. Make builds can
opt in with `APPLE_CODE_SIGNING=YES`. For signed builds from Xcode, create the
ignored `apple/Local.xcconfig` and override the signing settings there, including
the development team.
## Command-line typecheck & tests
`Package.swift` builds the same sources as a library (minus the `@main` entry),

View File

@@ -1,9 +1,6 @@
// Committed signing config. Contains no secrets.
//
// Per-developer signing (e.g. DEVELOPMENT_TEAM) goes in Local.xcconfig, which is
// gitignored. The optional include below means the build still works for anyone
// who doesn't have a Local.xcconfig — Xcode automatic signing fills in their team.
//
// To persist your team across `xcodegen generate`, create apple/Local.xcconfig:
// DEVELOPMENT_TEAM = XXXXXXXXXX
// VniDrop development and CI builds are intentionally unsigned.
CODE_SIGNING_ALLOWED = NO
CODE_SIGNING_REQUIRED = NO
// Signed local builds can opt in through this ignored file.
#include? "Local.xcconfig"

View File

@@ -1,6 +1,6 @@
import SwiftUI
/// App entry point for iOS/iPadOS/macOS, ported from `iOSApp.swift` + `App.kt`.
/// Native app entry point for iOS, iPadOS, and macOS.
/// Opens `.vnd` invitations via `onOpenURL` and routes them to the receive flow.
@main
struct VniDropApp: App {

View File

@@ -16,8 +16,7 @@ struct LocalNotification {
let body: String
}
/// Local notification service, ported from `LocalNotificationService.kt` /
/// `.ios.kt`, backed by `UNUserNotificationCenter`.
/// Local notification service backed by `UNUserNotificationCenter`.
@MainActor
final class LocalNotificationService: ObservableObject {
@Published private(set) var permission: NotificationPermission = .notDetermined

View File

@@ -2,7 +2,7 @@ import SwiftUI
enum ReceiveMethodAvailability { case available, unavailable, hidden }
/// Invitation acquisition actions, ported from `ReceiveInvitationActions` (iosMain).
/// Invitation acquisition actions shared by the native Apple feature models.
@MainActor
protocol ReceiveInvitationActions: AnyObject {
var fileAvailability: ReceiveMethodAvailability { get }

View File

@@ -1,8 +1,7 @@
import Foundation
import Combine
/// Persisted per-transfer thumbnail store, ported from
/// `feature/send/FilePreviewRepository.kt` + `PlatformPreviewStore.ios.kt`.
/// Native Apple preview cache and thumbnail loader.
/// Only small PNG/JPEG/WEBP previews are retained, under a total quota.
struct PreviewStoragePolicy {
var maxEntryBytes: Int = 512 * 1024

View File

@@ -2,7 +2,7 @@ import SwiftUI
enum NfcShareAvailability { case available, unavailable, hidden }
/// Invitation delivery actions, ported from `TransferShareActions` (iosMain).
/// Invitation delivery actions shared by the native Apple feature models.
/// Platform implementations perform export, native share, and NFC write.
@MainActor
protocol TransferShareActions: AnyObject {

View File

@@ -3,7 +3,7 @@ import Foundation
import UIKit
import VnidropCore
/// iOS file system service, ported from `FileSystemService.ios.kt`.
/// Native iOS file system service.
/// App-owned Documents is the fixed receive folder; custom folders are not
/// supported because raw external picker URLs do not survive relaunch.
struct IosFileSystemService: FileSystemService {

View File

@@ -1,7 +1,6 @@
import Foundation
/// Builds a `.vnd` invitation filename from a transfer name, mirroring the iOS
/// helper in `TransferShareActions.ios.kt`.
/// Builds a `.vnd` invitation filename from a transfer name.
func invitationFileName(_ transferName: String) -> String {
let trimmed = transferName.trimmingCharacters(in: .whitespacesAndNewlines)
let base = trimmed.isEmpty ? "invitation" : trimmed

View File

@@ -7,7 +7,7 @@ import UniformTypeIdentifiers
@MainActor
func makeReceiveInvitationActions() -> ReceiveInvitationActions { IosReceiveInvitationActions() }
/// iOS invitation acquisition, ported from `ReceiveInvitationActions.ios.kt`:
/// iOS invitation acquisition:
/// document picker, camera QR scanner, and NFC read.
final class IosReceiveInvitationActions: NSObject, ReceiveInvitationActions, UIDocumentPickerDelegate {
private var documentResult: ((Result<String, Error>) -> Void)?

View File

@@ -5,7 +5,7 @@ import UIKit
@MainActor
func makePlatformShareActions() -> TransferShareActions { IosTransferShareActions() }
/// iOS invitation delivery, ported from `TransferShareActions.ios.kt`: export via
/// iOS invitation delivery: export via
/// document picker, native share via `UIActivityViewController`, and NFC write.
final class IosTransferShareActions: NSObject, TransferShareActions {
private var nfcWriter: InvitationNfcWriter?
@@ -61,8 +61,7 @@ final class IosTransferShareActions: NSObject, TransferShareActions {
}
}
/// Writes a VniDrop invitation to a writable NDEF tag, ported from
/// `InvitationNfcWriter` in `TransferShareActions.ios.kt`.
/// Writes a VniDrop invitation to a writable NDEF tag.
// Runs entirely on the NFC session's `.main` delegate queue.
final class InvitationNfcWriter: NSObject, NFCNDEFReaderSessionDelegate, @unchecked Sendable {
private let ticket: String

View File

@@ -2,6 +2,18 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>fr</string>
<string>es</string>
<string>it</string>
<string>de</string>
<string>pt</string>
<string>pl</string>
<string>nl</string>
<string>ru</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- iOS: NFC NDEF reading (matches the original iosApp entitlements). -->
<!-- iOS: NFC NDEF reading. -->
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>

View File

@@ -41,7 +41,6 @@ targets:
SWIFT_STRICT_CONCURRENCY: complete
ENABLE_USER_SCRIPT_SANDBOXING: NO
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
CODE_SIGN_STYLE: Automatic
configs:
debug:
CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements
@@ -56,6 +55,9 @@ targets:
VniDropTests:
type: bundle.unit-test
supportedDestinations: [iOS, macOS]
configFiles:
Debug: Signing.xcconfig
Release: Signing.xcconfig
sources:
- path: Tests
settings:
@@ -63,7 +65,6 @@ targets:
GENERATE_INFOPLIST_FILE: YES
SWIFT_VERSION: "6.0"
SWIFT_STRICT_CONCURRENCY: complete
CODE_SIGN_STYLE: Automatic
dependencies:
- target: VniDrop

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,29 +0,0 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1024" height="1024" fill="#FFFFFF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="url(#paint0_linear_11_12)"/>
<path d="M520.4 431.72C495.8 464.52 443.32 530.12 420.36 577.68C390.84 636.72 403.96 699.04 446.6 731.84C487.6 758.08 549.92 758.08 592.56 730.2C633.56 700.68 646.68 636.72 620.44 577.68C597.48 530.12 546.64 464.52 520.4 431.72Z" fill="url(#paint1_linear_11_12)"/>
<mask id="mask0_11_12" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="176" y="148" width="671" height="732">
<path d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="white"/>
</mask>
<g mask="url(#mask0_11_12)">
<path d="M688 148H782C832 148 842 171.8 847.48 210V303.68L688 148Z" fill="url(#paint2_linear_11_12)"/>
</g>
<defs>
<linearGradient id="paint0_linear_11_12" x1="176" y1="148" x2="904.706" y2="816.252" gradientUnits="userSpaceOnUse">
<stop stop-color="#A855F7"/>
<stop offset="0.48" stop-color="#9D4DF4"/>
<stop offset="1" stop-color="#7C2AEF"/>
</linearGradient>
<linearGradient id="paint1_linear_11_12" x1="404.439" y1="431.72" x2="707.314" y2="649.286" gradientUnits="userSpaceOnUse">
<stop stop-color="#A855F7"/>
<stop offset="0.48" stop-color="#9D4DF4"/>
<stop offset="1" stop-color="#7C2AEF"/>
</linearGradient>
<linearGradient id="paint2_linear_11_12" x1="683.48" y1="144.6" x2="793.636" y2="306.833" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2DDFF"/>
<stop offset="1" stop-color="#C084FC"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

View File

@@ -1,26 +0,0 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="url(#paint0_linear_11_12)"/>
<path d="M520.4 431.72C495.8 464.52 443.32 530.12 420.36 577.68C390.84 636.72 403.96 699.04 446.6 731.84C487.6 758.08 549.92 758.08 592.56 730.2C633.56 700.68 646.68 636.72 620.44 577.68C597.48 530.12 546.64 464.52 520.4 431.72Z" fill="url(#paint1_linear_11_12)"/>
<mask id="mask0_11_12" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="176" y="148" width="671" height="732">
<path d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="white"/>
</mask>
<g mask="url(#mask0_11_12)">
<path d="M688 148H782C832 148 842 171.8 847.48 210V303.68L688 148Z" fill="url(#paint2_linear_11_12)"/>
</g>
<defs>
<linearGradient id="paint0_linear_11_12" x1="176" y1="148" x2="904.706" y2="816.252" gradientUnits="userSpaceOnUse">
<stop stop-color="#A855F7"/>
<stop offset="0.48" stop-color="#9D4DF4"/>
<stop offset="1" stop-color="#7C2AEF"/>
</linearGradient>
<linearGradient id="paint1_linear_11_12" x1="404.439" y1="431.72" x2="707.314" y2="649.286" gradientUnits="userSpaceOnUse">
<stop stop-color="#A855F7"/>
<stop offset="0.48" stop-color="#9D4DF4"/>
<stop offset="1" stop-color="#7C2AEF"/>
</linearGradient>
<linearGradient id="paint2_linear_11_12" x1="683.48" y1="144.6" x2="793.636" y2="306.833" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2DDFF"/>
<stop offset="1" stop-color="#C084FC"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -1,29 +0,0 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="40" y="40" width="944" height="944" rx="210" fill="#FFFFFF" stroke="#E9E7F0" stroke-width="8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="url(#paint0_linear_11_12)"/>
<path d="M520.4 431.72C495.8 464.52 443.32 530.12 420.36 577.68C390.84 636.72 403.96 699.04 446.6 731.84C487.6 758.08 549.92 758.08 592.56 730.2C633.56 700.68 646.68 636.72 620.44 577.68C597.48 530.12 546.64 464.52 520.4 431.72Z" fill="url(#paint1_linear_11_12)"/>
<mask id="mask0_11_12" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="176" y="148" width="671" height="732">
<path d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="white"/>
</mask>
<g mask="url(#mask0_11_12)">
<path d="M688 148H782C832 148 842 171.8 847.48 210V303.68L688 148Z" fill="url(#paint2_linear_11_12)"/>
</g>
<defs>
<linearGradient id="paint0_linear_11_12" x1="176" y1="148" x2="904.706" y2="816.252" gradientUnits="userSpaceOnUse">
<stop stop-color="#A855F7"/>
<stop offset="0.48" stop-color="#9D4DF4"/>
<stop offset="1" stop-color="#7C2AEF"/>
</linearGradient>
<linearGradient id="paint1_linear_11_12" x1="404.439" y1="431.72" x2="707.314" y2="649.286" gradientUnits="userSpaceOnUse">
<stop stop-color="#A855F7"/>
<stop offset="0.48" stop-color="#9D4DF4"/>
<stop offset="1" stop-color="#7C2AEF"/>
</linearGradient>
<linearGradient id="paint2_linear_11_12" x1="683.48" y1="144.6" x2="793.636" y2="306.833" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2DDFF"/>
<stop offset="1" stop-color="#C084FC"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

View File

@@ -1,26 +0,0 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="url(#paint0_linear_11_12)"/>
<path d="M520.4 431.72C495.8 464.52 443.32 530.12 420.36 577.68C390.84 636.72 403.96 699.04 446.6 731.84C487.6 758.08 549.92 758.08 592.56 730.2C633.56 700.68 646.68 636.72 620.44 577.68C597.48 530.12 546.64 464.52 520.4 431.72Z" fill="url(#paint1_linear_11_12)"/>
<mask id="mask0_11_12" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="176" y="148" width="671" height="732">
<path d="M236.68 148H338.36C366.24 148 387.56 170.96 387.56 198.84V564.56C387.56 597.36 372.8 620.32 372.8 646.56C372.8 725.28 436.76 787.6 522.04 787.6C607.32 787.6 668 725.28 668 646.56C668 620.32 656.52 597.36 656.52 564.56V198.84C656.52 170.96 677.84 148 705.72 148H781.16C817.24 148 846.76 177.52 846.76 213.6V564.56C846.76 738.4 704.08 879.44 522.04 879.44C340 879.44 194.04 738.4 194.04 564.56V374.32H220.28V305.44C195.68 305.44 176 297.24 176 280.84V246.4C176 231.64 187.48 220.16 202.24 220.16H236.68V148ZM256.36 239.84C251.44 239.84 248.16 244.76 248.16 249.68V275.92C248.16 282.48 253.08 285.76 259.64 285.76H282.6C289.16 285.76 292.44 280.84 292.44 274.28V251.32C292.44 244.76 287.52 239.84 280.96 239.84H256.36Z" fill="white"/>
</mask>
<g mask="url(#mask0_11_12)">
<path d="M688 148H782C832 148 842 171.8 847.48 210V303.68L688 148Z" fill="url(#paint2_linear_11_12)"/>
</g>
<defs>
<linearGradient id="paint0_linear_11_12" x1="176" y1="148" x2="904.706" y2="816.252" gradientUnits="userSpaceOnUse">
<stop stop-color="#A855F7"/>
<stop offset="0.48" stop-color="#9D4DF4"/>
<stop offset="1" stop-color="#7C2AEF"/>
</linearGradient>
<linearGradient id="paint1_linear_11_12" x1="404.439" y1="431.72" x2="707.314" y2="649.286" gradientUnits="userSpaceOnUse">
<stop stop-color="#A855F7"/>
<stop offset="0.48" stop-color="#9D4DF4"/>
<stop offset="1" stop-color="#7C2AEF"/>
</linearGradient>
<linearGradient id="paint2_linear_11_12" x1="683.48" y1="144.6" x2="793.636" y2="306.833" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2DDFF"/>
<stop offset="1" stop-color="#C084FC"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

35
config.mk Normal file
View File

@@ -0,0 +1,35 @@
# Default command configuration. Override locally in the ignored
# config.override.mk or on the command line (for example: make package-deb VERSION=1.2.0).
ifeq ($(OS),Windows_NT)
HOST_OS := windows
GRADLE ?= ./gradlew.bat
else
HOST_UNAME := $(shell uname -s)
ifeq ($(HOST_UNAME),Darwin)
HOST_OS := macos
else ifeq ($(HOST_UNAME),Linux)
HOST_OS := linux
else
HOST_OS := unknown
endif
GRADLE ?= ./gradlew
endif
CARGO ?= cargo
NPM ?= npm
BUN ?= bun
XCODEBUILD ?= xcodebuild
XCODEGEN ?= xcodegen
OPEN ?= open
POWERSHELL ?= pwsh
VERSION ?= $(shell sed -n 's/^vnidrop.version=//p' $(ROOT)/gradle.properties)
APPLE_PROFILE ?= debug
APPLE_CONFIGURATION ?= Debug
APPLE_DESTINATION ?=
APPLE_CODE_SIGNING ?= NO
APPLE_DERIVED_DATA ?= $(ROOT)/apple/DerivedData
GRADLE_FLAGS ?= --no-daemon --stacktrace
GRADLE_RELEASE_FLAGS ?= --no-daemon --no-configuration-cache --stacktrace

View File

@@ -19,20 +19,18 @@ Read [`CORE_FLOW.md`](CORE_FLOW.md) before changing send/receive/export/cancel.
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
make format
make test-rust
make check-rust
```
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
make test-rust-transfer
make test-rust-approval
make test-rust-lifecycle
make test-rust-output-sink
```
Docs (CI uses `-D warnings`):
@@ -41,7 +39,7 @@ Docs (CI uses `-D warnings`):
RUSTDOCFLAGS='-D warnings' cargo doc -p vnidrop --no-deps
```
Run `cargo fmt --all` after finishing Rust edits without asking.
Run `make format` after finishing Rust edits without asking.
---
@@ -116,13 +114,11 @@ 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
make check-rust
```
If you touched cancel, export, or sinks, also:
```bash
cargo test -p vnidrop --test output_sink
make test-rust-output-sink
```

View File

@@ -51,7 +51,7 @@ bytes through Kotlin memory.
- Desktop uses normal filesystem paths.
- Android opens SAF/content URIs in Kotlin and passes a borrowed file
descriptor; Rust duplicates the descriptor before streaming.
- iOS starts the security-scoped URL lease in Kotlin and keeps it alive while
- iOS starts the security-scoped URL lease in Swift and keeps it alive while
Rust streams from the accessible file URL/path.
## Durability And Filesystem Policy

View File

@@ -25,7 +25,6 @@ dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutinesSwing)
implementation(libs.jna)
implementation(libs.compose.uiToolingPreview)
testImplementation(libs.kotlin.testJunit)
@@ -37,16 +36,12 @@ compose.desktop {
buildTypes.release.proguard.isEnabled.set(false)
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Deb, TargetFormat.Rpm)
targetFormats(TargetFormat.Deb, TargetFormat.Rpm)
packageName = "VniDrop"
packageVersion = appVersion
description = "Send files directly across your devices"
vendor = "Sudosy Labs"
licenseFile.set(project.file("../LICENSE"))
macOS {
bundleID = "com.vnidrop.app"
iconFile.set(project.file("../assets/macos/app-icon.icns"))
}
windows {
iconFile.set(project.file("../assets/windows/app-icon.ico"))
}

View File

@@ -1,156 +0,0 @@
package com.vnidrop.app
import com.sun.jna.Callback
import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.NativeLibrary
import com.sun.jna.Pointer
import com.sun.jna.Structure
import java.io.File
internal object MacOsAppKitAppearance {
private val objc: ObjCRuntime? by lazy {
runCatching {
NativeLibrary.getInstance("AppKit")
Native.load("objc", ObjCRuntime::class.java)
}.getOrNull()
}
fun apply(isDarkTheme: Boolean) {
if (!isMacOs()) return
runCatching {
val runtime = objc ?: return
val applicationClass = runtime.objc_getClass("NSApplication") ?: return
val appearanceClass = runtime.objc_getClass("NSAppearance") ?: return
val application = runtime.objc_msgSend(applicationClass, runtime.sel_registerName("sharedApplication")) ?: return
val appearanceName = nsString(runtime, macOsAppearanceName(isDarkTheme)) ?: return
val appearance = runtime.objc_msgSend(
appearanceClass,
runtime.sel_registerName("appearanceNamed:"),
appearanceName,
) ?: return
runtime.objc_msgSend(application, runtime.sel_registerName("setAppearance:"), appearance)
}
}
private fun macOsAppearanceName(isDarkTheme: Boolean): String =
if (isDarkTheme) "NSAppearanceNameDarkAqua" else "NSAppearanceNameAqua"
private fun nsString(runtime: ObjCRuntime, value: String): Pointer? {
val stringClass = runtime.objc_getClass("NSString") ?: return null
return runtime.objc_msgSend(stringClass, runtime.sel_registerName("stringWithUTF8String:"), value)
}
private fun isMacOs(): Boolean =
System.getProperty("os.name").startsWith("Mac", ignoreCase = true)
}
internal object MacOsShareSheet {
private val objc: ObjCRuntime? by lazy {
runCatching {
NativeLibrary.getInstance("AppKit")
Native.load("objc", ObjCRuntime::class.java)
}.getOrNull()
}
private var retainedPicker: Pointer? = null
private val systemLibrary: NativeLibrary? by lazy {
runCatching { NativeLibrary.getInstance("System") }.getOrNull()
}
private val dispatch: DispatchRuntime? by lazy {
runCatching { Native.load("System", DispatchRuntime::class.java) }.getOrNull()
}
fun share(file: File): Result<Unit> = runCatching {
require(file.isFile) { "The invitation file could not be created" }
var failure: Throwable? = null
val runtime = dispatch ?: error("The macOS main queue is unavailable")
// dispatch_get_main_queue() is a C macro on Darwin, so there is no
// function for dlsym/JNA to resolve. The macro returns this exported
// queue object directly.
val queue = systemLibrary?.getGlobalVariableAddress("_dispatch_main_q")
?: error("The macOS main queue is unavailable")
runtime.dispatch_sync_f(queue, null, DispatchWork { failure = runCatching { show(file) }.exceptionOrNull() })
failure?.let { throw it }
}
private fun show(file: File) {
val runtime = objc ?: error("AppKit is unavailable")
val applicationClass = runtime.objc_getClass("NSApplication") ?: error("NSApplication is unavailable")
val application = runtime.objc_msgSend(applicationClass, runtime.sel_registerName("sharedApplication"))
?: error("NSApplication could not be opened")
val window = runtime.objc_msgSend(application, runtime.sel_registerName("keyWindow"))
?: runtime.objc_msgSend(application, runtime.sel_registerName("mainWindow"))
?: error("No active macOS window")
val contentView = runtime.objc_msgSend(window, runtime.sel_registerName("contentView"))
?: error("The active window has no content view")
val path = nsString(runtime, file.absolutePath) ?: error("The invitation path is invalid")
val urlClass = runtime.objc_getClass("NSURL") ?: error("NSURL is unavailable")
val url = runtime.objc_msgSend(urlClass, runtime.sel_registerName("fileURLWithPath:"), path)
?: error("The invitation URL could not be created")
val arrayClass = runtime.objc_getClass("NSArray") ?: error("NSArray is unavailable")
val items = runtime.objc_msgSend(arrayClass, runtime.sel_registerName("arrayWithObject:"), url)
?: error("The share item could not be created")
val pickerClass = runtime.objc_getClass("NSSharingServicePicker") ?: error("The macOS share sheet is unavailable")
val allocated = runtime.objc_msgSend(pickerClass, runtime.sel_registerName("alloc"))
?: error("The macOS share sheet could not be allocated")
val picker = runtime.objc_msgSend(allocated, runtime.sel_registerName("initWithItems:"), items)
?: error("The macOS share sheet could not be created")
retainedPicker?.let { runtime.objc_msgSend(it, runtime.sel_registerName("release")) }
retainedPicker = picker
runtime.objc_msgSend(
picker,
runtime.sel_registerName("showRelativeToRect:ofView:preferredEdge:"),
anchorRect(),
contentView,
3L,
)
}
private fun nsString(runtime: ObjCRuntime, value: String): Pointer? {
val stringClass = runtime.objc_getClass("NSString") ?: return null
return runtime.objc_msgSend(stringClass, runtime.sel_registerName("stringWithUTF8String:"), value)
}
internal fun validateNativeRectMapping(): Int = anchorRect().size()
internal fun hasNativeMainQueue(): Boolean =
runCatching { systemLibrary?.getGlobalVariableAddress("_dispatch_main_q") != null }.getOrDefault(false)
private fun anchorRect() = NSRectByValue().apply {
x = 0.0
y = 0.0
width = 1.0
height = 1.0
write()
}
}
@Structure.FieldOrder("x", "y", "width", "height")
internal class NSRectByValue : Structure(), Structure.ByValue {
@JvmField var x: Double = 0.0
@JvmField var y: Double = 0.0
@JvmField var width: Double = 0.0
@JvmField var height: Double = 0.0
}
private interface ObjCRuntime : Library {
fun objc_getClass(name: String): Pointer?
fun sel_registerName(name: String): Pointer
fun objc_msgSend(receiver: Pointer?, selector: Pointer?): Pointer?
fun objc_msgSend(receiver: Pointer?, selector: Pointer?, argument: Pointer?): Pointer?
fun objc_msgSend(receiver: Pointer?, selector: Pointer?, argument: String): Pointer?
fun objc_msgSend(
receiver: Pointer?,
selector: Pointer?,
rect: NSRectByValue,
view: Pointer?,
edge: Long,
): Pointer?
}
private fun interface DispatchWork : Callback {
fun invoke(context: Pointer?)
}
private interface DispatchRuntime : Library {
fun dispatch_sync_f(queue: Pointer?, context: Pointer?, work: DispatchWork)
}

View File

@@ -47,31 +47,23 @@ import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.WindowScope
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import com.vnidrop.app.platform.DesktopAppearanceBridge
import com.vnidrop.app.feature.send.DesktopShareBridge
import com.vnidrop.app.feature.receive.ExternalInvitationController
import com.vnidrop.app.feature.receive.MaxVniDropInvitationBytes
import com.vnidrop.app.feature.receive.VniDropInvitationExtension
import com.vnidrop.app.feature.receive.decodeInvitationBytes
import com.vnidrop.app.platform.DesktopAppearanceBridge
import com.vnidrop.app.ui.theme.LocalVniDropColors
import java.awt.Desktop
import java.io.File
fun main(args: Array<String>) {
val externalInvitations = ExternalInvitationController()
val macOs = DesktopAppearanceBridge.isMacOs()
val linux = DesktopAppearanceBridge.isLinux()
val customWindowChrome = macOs || linux
configureMacOsNativeAppearance()
configureInvitationOpenHandler(externalInvitations)
args.asSequence()
.map(::File)
.filter { it.extension.equals(VniDropInvitationExtension, ignoreCase = true) }
.forEach { externalInvitations.openFile(it) }
DesktopAppearanceBridge.applyNativeAppearance = MacOsAppKitAppearance::apply
if (macOs) {
DesktopShareBridge.shareFile = MacOsShareSheet::share
}
application {
val windowState = rememberWindowState()
Window(
@@ -83,29 +75,21 @@ fun main(args: Array<String>) {
) {
App(
dependencies = rememberJvmAppDependencies(externalInvitations),
windowChromeTopInset = when {
macOs -> MacOsTitleBarHeight
linux -> LinuxTitleBarHeight
else -> 0.dp
},
windowContentTopStartRadius = if (customWindowChrome) DesktopContentCornerRadius else 0.dp,
windowChrome = when {
macOs -> {
{ MacOsTitleBar() }
windowChromeTopInset = if (linux) LinuxTitleBarHeight else 0.dp,
windowContentTopStartRadius = if (linux) DesktopContentCornerRadius else 0.dp,
windowChrome = if (linux) {
{
LinuxTitleBar(
isMaximized = windowState.placement == WindowPlacement.Maximized,
onMinimize = { windowState.isMinimized = true },
onToggleMaximize = {
windowState.placement = toggledWindowPlacement(windowState.placement)
},
onClose = ::exitApplication,
)
}
linux -> {
{
LinuxTitleBar(
isMaximized = windowState.placement == WindowPlacement.Maximized,
onMinimize = { windowState.isMinimized = true },
onToggleMaximize = {
windowState.placement = toggledWindowPlacement(windowState.placement)
},
onClose = ::exitApplication,
)
}
}
else -> null
} else {
null
},
)
}
@@ -130,55 +114,11 @@ private fun ExternalInvitationController.openFile(file: File) {
}
}
private fun configureMacOsNativeAppearance() {
if (!DesktopAppearanceBridge.isMacOs()) return
// AWT reads this before creating the first native window. Runtime theme
// changes are handled in the JVM platform appearance hook.
System.setProperty("apple.awt.application.appearance", "system")
}
private val MacOsTitleBarHeight = 28.dp
private val MacOsTrafficLightsWidth = 76.dp
private val LinuxTitleBarHeight = 40.dp
private val LinuxWindowControlWidth = 46.dp
private val LinuxWindowControlsWidth = 138.dp
private val DesktopContentCornerRadius = 20.dp
@Composable
@OptIn(ExperimentalComposeUiApi::class)
private fun WindowScope.MacOsTitleBar() {
val colors = LocalVniDropColors.current
Box(
modifier = Modifier
.fillMaxWidth()
.height(MacOsTitleBarHeight)
.background(colors.backgroundSurface200),
) {
WindowDraggableArea(
modifier = Modifier
.fillMaxSize()
.padding(start = MacOsTrafficLightsWidth)
.onPointerEvent(PointerEventType.Press) { event ->
if (event.awtEventOrNull?.clickCount == 2) {
DesktopAppearanceBridge.toggleMaximized(window)
}
},
) {
Box(modifier = Modifier.fillMaxSize().padding(end = MacOsTrafficLightsWidth)) {
BasicText(
text = "VniDrop",
modifier = Modifier.align(Alignment.Center),
style = TextStyle(
color = colors.foregroundDefault,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
),
)
}
}
}
}
@Composable
@OptIn(ExperimentalComposeUiApi::class)
private fun WindowScope.LinuxTitleBar(

View File

@@ -1,20 +0,0 @@
package com.vnidrop.app
import com.vnidrop.app.platform.DesktopAppearanceBridge
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.junit.Assume.assumeTrue
class MacOsShareSheetTest {
@Test
fun nativeAnchorRectHasTheCocoaLayout() {
assertEquals(32, MacOsShareSheet.validateNativeRectMapping())
}
@Test
fun nativeMainDispatchQueueCanBeResolved() {
assumeTrue(DesktopAppearanceBridge.isMacOs())
assertTrue(MacOsShareSheet.hasNativeMainQueue())
}
}

View File

@@ -5,8 +5,8 @@ The product website for VniDrop, built with Next.js and exported as a static sit
## Local development
```bash
npm install
npm run dev
# From the repository root:
make run-docs
```
Open [http://localhost:3000](http://localhost:3000).
@@ -14,9 +14,7 @@ Open [http://localhost:3000](http://localhost:3000).
## Checks
```bash
npm run lint
npm run typecheck
npm run build
make check-docs
```
The production build is written to `out/` and can be hosted by any static web server.

View File

@@ -1,7 +1,6 @@
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx3072M
kotlin.mpp.enableCInteropCommonization=true
#Gradle
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true

View File

@@ -18,7 +18,6 @@ kotlin = "2.4.0"
kotlinx-coroutines = "1.11.0"
material3 = "1.11.0-alpha07"
qrcode = "4.5.0"
jna = "5.17.0"
google-code-scanner = "16.1.0"
[libraries]
@@ -47,7 +46,6 @@ kotlinx-coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-co
kotlinx-coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
qrcode-kotlin = { module = "io.github.g0dkar:qrcode-kotlin", version.ref = "qrcode" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
google-code-scanner = { module = "com.google.android.gms:play-services-code-scanner", version.ref = "google-code-scanner" }
[plugins]

View File

@@ -1,7 +0,0 @@
TEAM_ID=
PRODUCT_NAME=VniDrop
PRODUCT_BUNDLE_IDENTIFIER=com.vnidrop.app.vnidrop$(TEAM_ID)
CURRENT_PROJECT_VERSION=1
MARKETING_VERSION=1.0

View File

@@ -1,403 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXFileReference section */
FA325F1B4E7D8FFDF19A5C4A /* VniDrop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VniDrop.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
7582E3F916810EEF251FA8C4 /* Exceptions for "iosApp" folder in "iosApp" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = B83017A9EC036A038DC1B430 /* iosApp */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
244B04A7F91FA6EE0623BFF1 /* iosApp */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
7582E3F916810EEF251FA8C4 /* Exceptions for "iosApp" folder in "iosApp" target */,
);
path = iosApp;
sourceTree = "<group>";
};
B93EE283488EDD1107331E67 /* Configuration */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Configuration;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
2BC80A7FB47F5EF23FB83738 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
CBA2EB46BC3B70D303C46327 = {
isa = PBXGroup;
children = (
B93EE283488EDD1107331E67 /* Configuration */,
244B04A7F91FA6EE0623BFF1 /* iosApp */,
C13A067056BA9F38FD87A539 /* Products */,
);
sourceTree = "<group>";
};
C13A067056BA9F38FD87A539 /* Products */ = {
isa = PBXGroup;
children = (
FA325F1B4E7D8FFDF19A5C4A /* VniDrop.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
B83017A9EC036A038DC1B430 /* iosApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = 94A2E337CA60E8CC4A507E7E /* Build configuration list for PBXNativeTarget "iosApp" */;
buildPhases = (
091163AA7720AC82BC13CE03 /* Compile Kotlin Framework */,
C8C88FCA7F60AE3F54E85A8C /* Sources */,
2BC80A7FB47F5EF23FB83738 /* Frameworks */,
8E81423A1A7357695DF65754 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
244B04A7F91FA6EE0623BFF1 /* iosApp */,
);
name = iosApp;
packageProductDependencies = (
);
productName = iosApp;
productReference = FA325F1B4E7D8FFDF19A5C4A /* VniDrop.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
F60687F0AEE04DF31E2599D0 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1620;
TargetAttributes = {
B83017A9EC036A038DC1B430 = {
CreatedOnToolsVersion = 16.2;
};
};
};
buildConfigurationList = FE990A962A8E8AC95D51FA0E /* Build configuration list for PBXProject "iosApp" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = CBA2EB46BC3B70D303C46327;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = C13A067056BA9F38FD87A539 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
B83017A9EC036A038DC1B430 /* iosApp */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
8E81423A1A7357695DF65754 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
091163AA7720AC82BC13CE03 /* Compile Kotlin Framework */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Compile Kotlin Framework";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export PATH=\"$HOME/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:$PATH\"\nexport CARGO_PROFILE_DEV_STRIP=none\nexport JAVA_HOME=$(/usr/libexec/java_home)\n\nif [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C8C88FCA7F60AE3F54E85A8C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
5EDFED06EDA142275594F3F7 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = B93EE283488EDD1107331E67 /* Configuration */;
baseConfigurationReferenceRelativePath = Config.xcconfig;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
6A37768B9DA3138100D19D47 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = B93EE283488EDD1107331E67 /* Configuration */;
baseConfigurationReferenceRelativePath = Config.xcconfig;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
87E567D3634C99D02D035476 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = iosApp/vnidrop.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = A8A4JSMV5D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NFCReaderUsageDescription = "VniDrop uses NFC to read transfer invitation tags.";
INFOPLIST_KEY_NSCameraUsageDescription = "VniDrop uses the camera to scan transfer QR codes.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-framework",
SystemConfiguration,
"-framework",
Network,
"-framework",
CoreNFC,
"-framework",
AVFoundation,
);
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
A9DAF5A8F7787C0F3BAEC312 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = iosApp/vnidrop.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = A8A4JSMV5D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NFCReaderUsageDescription = "VniDrop uses NFC to read transfer invitation tags.";
INFOPLIST_KEY_NSCameraUsageDescription = "VniDrop uses the camera to scan transfer QR codes.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-framework",
SystemConfiguration,
"-framework",
Network,
"-framework",
CoreNFC,
"-framework",
AVFoundation,
);
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
94A2E337CA60E8CC4A507E7E /* Build configuration list for PBXNativeTarget "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
87E567D3634C99D02D035476 /* Debug */,
A9DAF5A8F7787C0F3BAEC312 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
FE990A962A8E8AC95D51FA0E /* Build configuration list for PBXProject "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
6A37768B9DA3138100D19D47 /* Debug */,
5EDFED06EDA142275594F3F7 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = F60687F0AEE04DF31E2599D0 /* Project object */;
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -1,11 +0,0 @@
{
"colors": [
{
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View File

@@ -1,36 +0,0 @@
{
"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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,125 +0,0 @@
import Shared
import SwiftUI
import UIKit
// Compose renders the app content, but UIKit owns the status bar style. This
// host listens for theme changes from shared Kotlin code and asks iOS to
// recompute the status bar contrast.
final class VniDropHostViewController: UIViewController {
private let composeController: UIViewController
private var usesDarkTheme: Bool
init(composeController: UIViewController) {
self.composeController = composeController
self.usesDarkTheme = UITraitCollection.current.userInterfaceStyle == .dark
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var preferredStatusBarStyle: UIStatusBarStyle {
usesDarkTheme ? .lightContent : .darkContent
}
override func viewDidLoad() {
super.viewDidLoad()
addChild(composeController)
view.addSubview(composeController.view)
composeController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
composeController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
composeController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
composeController.view.topAnchor.constraint(equalTo: view.topAnchor),
composeController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
composeController.didMove(toParent: self)
NotificationCenter.default.addObserver(
self,
selector: #selector(themeDidChange(_:)),
name: Notification.Name("VniDropThemeChanged"),
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc private func themeDidChange(_ notification: Notification) {
guard let isDark = notification.userInfo?["isDark"] as? String else { return }
usesDarkTheme = isDark == "true"
setNeedsStatusBarAppearanceUpdate()
}
}
struct ComposeView: UIViewControllerRepresentable {
let externalInvitations: ExternalInvitationController
func makeUIViewController(context: Self.Context) -> UIViewController {
VniDropHostViewController(
composeController: MainViewControllerKt.MainViewController(
externalInvitations: externalInvitations
)
)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Self.Context) {}
}
struct ContentView: View {
let externalInvitations: ExternalInvitationController
var body: some View {
ComposeView(externalInvitations: externalInvitations)
.ignoresSafeArea()
.onOpenURL(perform: openInvitation)
}
private func openInvitation(_ url: URL) {
guard url.pathExtension.caseInsensitiveCompare("vnd") == .orderedSame else {
externalInvitations.reportOpenFailure(message: "This is not a VniDrop invitation")
return
}
let hasSecurityAccess = url.startAccessingSecurityScopedResource()
defer {
if hasSecurityAccess {
url.stopAccessingSecurityScopedResource()
}
}
do {
let values = try url.resourceValues(forKeys: [.fileSizeKey])
if let fileSize = values.fileSize, fileSize > 65_536 {
throw InvitationOpenError.tooLarge
}
let data = try Data(contentsOf: url, options: .mappedIfSafe)
guard data.count <= 65_536 else { throw InvitationOpenError.tooLarge }
guard let raw = String(data: data, encoding: .utf8) else {
throw InvitationOpenError.invalidEncoding
}
externalInvitations.openInvitation(raw: raw)
} catch {
externalInvitations.reportOpenFailure(
message: (error as? LocalizedError)?.errorDescription ?? "The invitation could not be opened"
)
}
}
}
private enum InvitationOpenError: LocalizedError {
case tooLarge
case invalidEncoding
var errorDescription: String? {
switch self {
case .tooLarge: "The invitation is too large"
case .invalidEncoding: "The invitation is not valid text"
}
}
}

View File

@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>VniDrop Invitation</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.vnidrop.app.invitation</string>
</array>
</dict>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
<string>remote-notification</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>VniDrop Invitation</string>
<key>UTTypeIdentifier</key>
<string>com.vnidrop.app.invitation</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>vnd</string>
</array>
<key>public.mime-type</key>
<string>application/vnd.vnidrop.transfer</string>
</dict>
</dict>
</array>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>VniDrop uses the camera to scan transfer QR codes.</string>
<key>NFCReaderUsageDescription</key>
<string>VniDrop uses NFC to read transfer invitation tags.</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>NSLocalNetworkUsageDescription</key>
<string>VniDrop needs local network access to send to other local devices if needed.</string>
<key>NSBonjourServices</key>
<array>
<string></string>
</array>
</dict>
</plist>

View File

@@ -1,6 +0,0 @@
{
"info": {
"author": "xcode",
"version": 1
}
}

View File

@@ -1,13 +0,0 @@
import Shared
import SwiftUI
@main
struct iOSApp: App {
private let externalInvitations = ExternalInvitationController()
var body: some Scene {
WindowGroup {
ContentView(externalInvitations: externalInvitations)
}
}
}

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
</array>
</dict>
</plist>

87
localization/README.md Normal file
View File

@@ -0,0 +1,87 @@
# Localization
Single source of truth for every user-facing string, in [`strings.json`](strings.json).
A Bun CLI generates the platform-native files from it:
| Target | Output | Notes |
| --- | --- | --- |
| `apple` | `apple/VniDrop/Resources/Localizable.xcstrings` | one catalog, all languages nested |
| `kmp` | `shared/src/commonMain/composeResources/values[-lang]/strings.xml` | one file per language |
## Workflow
```bash
# From the repository root:
make check-localization # structural checks (run before committing)
make localization # regenerate .xcstrings + strings.xml from strings.json
make localization-migrate # one-time: rebuild strings.json from platform files
```
**Never edit the generated `.xcstrings` / `strings.xml` by hand** — edit `strings.json` and
regenerate. Regenerated output is deterministic (sorted keys), so diffs stay small.
## `strings.json` format
```jsonc
{
"sourceLanguage": "en",
"supportedLanguages": ["en", "fr"],
"strings": {
"send_title": {
"context": "Send tab — screen title.",
"translations": { "en": "Send", "fr": "Envoyer" }
},
"send_selected_files_count": {
"context": "Send flow — number of files chosen before creating a transfer.",
"targets": ["kmp", "apple"],
"args": [{ "name": "count", "type": "int" }],
"plural": {
"en": { "one": "{count} file selected", "other": "{count} files selected" },
"fr": { "one": "{count} fichier sélectionné", "other": "{count} fichiers sélectionnés" }
}
}
}
}
```
### Fields
- **`context`** *(required)* — where the string appears and its purpose. Emitted as the
`.xcstrings` comment and an XML comment; also the note translators see.
- **`targets`** *(optional)*`["kmp", "apple"]`. Omit to mean **all** targets.
- **`args`** *(optional)* — ordered list of `{ name, type }`, `type``string | int | double`.
Referenced in text as `{name}`.
- **`translations`** — flat text per language. **Mutually exclusive with `plural`.**
- **`plural`** — per language, per [CLDR category](https://cldr.unicode.org/index/cldr-spec/plural-rules)
(`zero`, `one`, `two`, `few`, `many`, `other`). `other` is always required.
### Placeholders
Write named tokens `{count}`, `{name}` in text. The generator converts them to the right
positional token per platform, using the declared `type`:
| type | Apple | Android/KMP |
| --- | --- | --- |
| `string` | `%N$@` | `%N$s` |
| `int` | `%N$d` | `%N$d` |
| `double` | `%N$f` | `%N$f` |
A literal `%` in text is emitted as `%%` whenever the string has args.
## Adding a language
Add its code to `supportedLanguages`, fill in `translations` / `plural` for each key, then
`generate`. KMP gets a new `values-<lang>/strings.xml`; Apple gets the language inside the
single catalog. `validate` warns about any key still missing that language.
## Migration notes (from the initial import)
- Apple keys that were literal English strings (`"%@ · %@"`) were imported verbatim — rename
them to semantic keys and update the Swift call sites.
- Arg names default to `arg1`, `arg2`… (a lone int arg becomes `count`). Rename for clarity;
keep the `{token}` in text in sync.
- Folding `transfer_file_count_one` / `_other` into the plural key `transfer_file_count`
requires switching the KMP call site from `Res.string.transfer_file_count_one` to the
Compose plural API (`pluralStringResource(Res.plurals.transfer_file_count, count, count)`),
and the Apple side to automatic plural inflection.

19
localization/bun.lock Normal file
View File

@@ -0,0 +1,19 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "vnidrop-localization",
"devDependencies": {
"bun-types": "^1.3.14",
},
},
},
"packages": {
"@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
}
}

18
localization/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "vnidrop-localization",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Single-source localization for VniDrop: strings.json -> .xcstrings (Apple) + per-language strings.xml (KMP/Compose).",
"bin": {
"loc": "./src/cli.ts"
},
"scripts": {
"migrate": "bun run src/cli.ts migrate",
"generate": "bun run src/cli.ts generate",
"validate": "bun run src/cli.ts validate"
},
"devDependencies": {
"bun-types": "^1.3.14"
}
}

72
localization/schema.json Normal file
View File

@@ -0,0 +1,72 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "VniDrop localization source",
"type": "object",
"required": ["sourceLanguage", "supportedLanguages", "strings"],
"additionalProperties": false,
"properties": {
"sourceLanguage": { "type": "string", "description": "Default/base language code, e.g. \"en\"." },
"supportedLanguages": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
},
"strings": {
"type": "object",
"additionalProperties": { "$ref": "#/definitions/entry" }
}
},
"definitions": {
"entry": {
"type": "object",
"required": ["context"],
"additionalProperties": false,
"oneOf": [
{ "required": ["translations"] },
{ "required": ["plural"] },
{ "not": { "anyOf": [{ "required": ["translations"] }, { "required": ["plural"] }] } }
],
"properties": {
"context": { "type": "string" },
"targets": {
"type": "array",
"items": { "enum": ["kmp", "apple"] },
"uniqueItems": true
},
"args": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "type"],
"additionalProperties": false,
"properties": {
"name": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
"type": { "enum": ["string", "int", "double"] }
}
}
},
"translations": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"plural": {
"type": "object",
"additionalProperties": {
"type": "object",
"required": ["other"],
"additionalProperties": false,
"properties": {
"zero": { "type": "string" },
"one": { "type": "string" },
"two": { "type": "string" },
"few": { "type": "string" },
"many": { "type": "string" },
"other": { "type": "string" }
}
}
}
}
}
}
}

21
localization/src/cli.ts Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bun
import { migrate } from "./commands/migrate";
import { generate } from "./commands/generate";
import { validate } from "./commands/validate";
const [cmd] = process.argv.slice(2);
switch (cmd) {
case "migrate":
await migrate();
break;
case "generate":
await generate();
break;
case "validate":
await validate();
break;
default:
console.error(`Unknown command: ${cmd ?? "(none)"}\nUsage: loc <migrate|generate|validate>`);
process.exit(1);
}

View File

@@ -0,0 +1,123 @@
/**
* Generate platform files from strings.json:
* - Apple: one Localizable.xcstrings (all languages nested).
* - KMP: one values[-lang]/strings.xml per supported language.
*/
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import {
APPLE_INFO_PLIST,
APPLE_XCSTRINGS,
kmpValuesDir,
KMP_RESOURCES,
STRINGS_JSON,
} from "../config";
import { renderAndroid, type ParsedAndroid } from "../lib/android-xml";
import { fromCanonical, type Flavor } from "../lib/placeholders";
import { renderXcstrings, type XcCatalog, type XcEntry } from "../lib/xcstrings";
import { targetsOf, type StringEntry, type StringsFile } from "../types";
function render(entry: StringEntry, text: string, flavor: Flavor): string {
return fromCanonical(text, entry.args ?? [], flavor);
}
// ---- Apple ------------------------------------------------------------------
function buildXcstrings(doc: StringsFile): XcCatalog {
const strings: Record<string, XcEntry> = {};
for (const [key, entry] of Object.entries(doc.strings)) {
if (!targetsOf(entry).includes("apple")) continue;
const xc: XcEntry = { extractionState: "manual" };
if (entry.context) xc.comment = entry.context;
const localizations: XcEntry["localizations"] = {};
for (const lang of doc.supportedLanguages) {
const state = lang === doc.sourceLanguage ? "translated" : "needs_review";
if (entry.plural) {
const forms = entry.plural[lang];
if (!forms) continue;
const plural: Record<string, { stringUnit: { state: any; value: string } }> = {};
for (const [cat, value] of Object.entries(forms)) {
plural[cat] = { stringUnit: { state, value: render(entry, value, "apple") } };
}
localizations[lang] = { variations: { plural } };
} else {
const value = entry.translations?.[lang];
if (value === undefined) continue;
localizations[lang] = { stringUnit: { state, value: render(entry, value, "apple") } };
}
}
if (Object.keys(localizations).length) xc.localizations = localizations;
strings[key] = xc;
}
return { sourceLanguage: doc.sourceLanguage, strings, version: "1.0" };
}
// ---- KMP --------------------------------------------------------------------
function buildAndroid(doc: StringsFile, lang: string): ParsedAndroid {
const out: ParsedAndroid = { strings: [], plurals: [] };
for (const [key, entry] of Object.entries(doc.strings)) {
if (!targetsOf(entry).includes("kmp")) continue;
if (entry.plural) {
const forms = entry.plural[lang];
if (!forms) continue;
const items: Record<string, string> = {};
for (const [cat, value] of Object.entries(forms)) {
items[cat] = render(entry, value, "android");
}
out.plurals.push({ name: key, items });
} else {
const value = entry.translations?.[lang];
if (value === undefined) continue;
out.strings.push({ name: key, value: render(entry, value, "android") });
}
}
return out;
}
// ---- Apple Info.plist CFBundleLocalizations ---------------------------------
/**
* Keep the app's CFBundleLocalizations in sync with supportedLanguages so iOS
* advertises the languages (and shows the per-app language picker in Settings).
* Replaces an existing array or inserts one right after the root <dict>.
*/
async function syncInfoPlistLocalizations(langs: string[]) {
const plist = await Bun.file(APPLE_INFO_PLIST).text();
const items = langs.map((l) => `\t\t<string>${l}</string>`).join("\n");
const block = `\t<key>CFBundleLocalizations</key>\n\t<array>\n${items}\n\t</array>\n`;
const existing = /\t*<key>CFBundleLocalizations<\/key>\s*<array>[\s\S]*?<\/array>\n/;
const next = existing.test(plist)
? plist.replace(existing, block)
: plist.replace(/(<dict>\n)/, `$1${block}`);
if (next !== plist) {
await Bun.write(APPLE_INFO_PLIST, next);
console.log(`Updated CFBundleLocalizations in ${APPLE_INFO_PLIST}`);
}
}
// ---- entry ------------------------------------------------------------------
export async function generate() {
const doc = JSON.parse(await Bun.file(STRINGS_JSON).text()) as StringsFile;
await Bun.write(APPLE_XCSTRINGS, renderXcstrings(buildXcstrings(doc)));
console.log(`Wrote ${APPLE_XCSTRINGS}`);
await syncInfoPlistLocalizations(doc.supportedLanguages);
for (const lang of doc.supportedLanguages) {
const dir = join(KMP_RESOURCES, kmpValuesDir(lang, doc.sourceLanguage));
mkdirSync(dir, { recursive: true });
const file = join(dir, "strings.xml");
await Bun.write(file, renderAndroid(buildAndroid(doc, lang)));
console.log(`Wrote ${file}`);
}
}

View File

@@ -0,0 +1,195 @@
/**
* One-time migration: fold the existing .xcstrings (Apple) and Compose
* strings.xml (KMP) files into a single strings.json.
*
* - Placeholders are normalized to canonical `{argN}` tokens.
* - KMP `<base>_one` / `<base>_other` key pairs are folded into one plural entry.
* - `targets` is set from which platform(s) a key was found in.
* Arg names (`arg1`, `count`, …) and `context` are best-effort; curate by hand after.
*/
import { readdirSync } from "node:fs";
import { join } from "node:path";
import { APPLE_XCSTRINGS, KMP_RESOURCES, STRINGS_JSON } from "../config";
import { parseAndroid } from "../lib/android-xml";
import { parseXcstrings, type XcLocalization } from "../lib/xcstrings";
import { toCanonical, type ParsedString } from "../lib/placeholders";
import type { Arg, PluralForms, StringEntry, StringsFile, Target } from "../types";
const PLURAL_SUFFIXES = ["zero", "one", "two", "few", "many", "other"] as const;
interface Draft {
targets: Set<Target>;
args?: Arg[];
translations?: Record<string, string>;
plural?: Record<string, PluralForms>;
comment?: string;
}
/** Give a friendlier name to a lone integer arg (common for counts). */
function prettify(parsed: ParsedString): ParsedString {
if (parsed.args.length === 1 && parsed.args[0]!.type === "int") {
return {
text: parsed.text.replaceAll("{arg1}", "{count}"),
args: [{ name: "count", type: "int" }],
};
}
return parsed;
}
function ingest(draft: Draft, lang: string, raw: string) {
const p = prettify(toCanonical(raw));
if (p.args.length) draft.args = p.args;
(draft.translations ??= {})[lang] = p.text;
}
// ---- Apple (.xcstrings) -----------------------------------------------------
function unitValue(loc: XcLocalization | undefined): string | undefined {
return loc?.stringUnit?.value;
}
async function readApple(drafts: Map<string, Draft>) {
const cat = parseXcstrings(await Bun.file(APPLE_XCSTRINGS).text());
for (const [key, entry] of Object.entries(cat.strings)) {
const draft = get(drafts, key);
draft.targets.add("apple");
if (entry.comment) draft.comment = entry.comment;
for (const [lang, loc] of Object.entries(entry.localizations ?? {})) {
const plural = loc.variations?.plural;
if (plural) {
for (const [cat, unit] of Object.entries(plural)) {
const p = prettify(toCanonical(unit.stringUnit.value));
if (p.args.length) draft.args = p.args;
((draft.plural ??= {})[lang] ??= { other: "" } as PluralForms)[
cat as keyof PluralForms
] = p.text;
}
} else {
const v = unitValue(loc);
if (v !== undefined) ingest(draft, lang, v);
}
}
}
}
// ---- KMP (compose strings.xml) ---------------------------------------------
function pluralBaseOf(name: string): { base: string; cat: string } | null {
for (const suf of PLURAL_SUFFIXES) {
if (name.endsWith(`_${suf}`)) return { base: name.slice(0, -(suf.length + 1)), cat: suf };
}
return null;
}
async function readKmp(drafts: Map<string, Draft>, sourceLanguage: string) {
const dirs = readdirSync(KMP_RESOURCES, { withFileTypes: true })
.filter((d) => d.isDirectory() && (d.name === "values" || d.name.startsWith("values-")))
.map((d) => d.name);
for (const dir of dirs) {
const lang = dir === "values" ? sourceLanguage : dir.slice("values-".length);
const file = join(KMP_RESOURCES, dir, "strings.xml");
if (!(await Bun.file(file).exists())) continue;
const { strings, plurals } = parseAndroid(await Bun.file(file).text());
for (const s of strings) {
const draft = get(drafts, s.name);
draft.targets.add("kmp");
ingest(draft, lang, s.value);
}
// Real <plurals> blocks (if any) fold directly into the base key.
for (const p of plurals) {
const draft = get(drafts, p.name);
draft.targets.add("kmp");
for (const [cat, raw] of Object.entries(p.items)) {
const parsed = prettify(toCanonical(raw));
if (parsed.args.length) draft.args = parsed.args;
((draft.plural ??= {})[lang] ??= { other: "" } as PluralForms)[
cat as keyof PluralForms
] = parsed.text;
}
}
}
}
/**
* Global fold: collapse flat `<base>_<cat>` sibling keys (e.g. `_one`/`_other`)
* into one plural entry on `<base>`, regardless of which platform they came from.
* Requires `other` plus at least one more category to avoid folding non-plurals.
*/
function foldPlurals(drafts: Map<string, Draft>) {
const groups = new Map<string, string[]>();
for (const key of drafts.keys()) {
const pl = pluralBaseOf(key);
if (pl && drafts.get(key)!.translations) {
(groups.get(pl.base) ?? groups.set(pl.base, []).get(pl.base)!).push(key);
}
}
for (const [base, keys] of groups) {
const cats = new Set(keys.map((k) => pluralBaseOf(k)!.cat));
if (!cats.has("other") || cats.size < 2) continue;
const target = get(drafts, base);
for (const key of keys) {
const cat = pluralBaseOf(key)!.cat as keyof PluralForms;
const src = drafts.get(key)!;
src.targets.forEach((t) => target.targets.add(t));
if (src.args?.length) target.args = src.args;
target.comment ??= src.comment;
for (const [lang, text] of Object.entries(src.translations ?? {})) {
((target.plural ??= {})[lang] ??= { other: "" } as PluralForms)[cat] = text;
}
drafts.delete(key);
}
}
}
// ---- assembly ---------------------------------------------------------------
function get(drafts: Map<string, Draft>, key: string): Draft {
let d = drafts.get(key);
if (!d) drafts.set(key, (d = { targets: new Set() }));
return d;
}
export async function migrate(sourceLanguage = "en") {
const drafts = new Map<string, Draft>();
await readApple(drafts);
await readKmp(drafts, sourceLanguage);
foldPlurals(drafts);
const langs = new Set<string>([sourceLanguage]);
const strings: Record<string, StringEntry> = {};
for (const key of [...drafts.keys()].sort()) {
const d = drafts.get(key)!;
const entry: StringEntry = { context: d.comment ?? "TODO: describe where this appears." };
const targets = [...d.targets].sort() as Target[];
if (targets.length && targets.length < 2) entry.targets = targets;
if (d.args?.length) entry.args = d.args;
if (d.plural) {
entry.plural = d.plural;
Object.keys(d.plural).forEach((l) => langs.add(l));
} else if (d.translations) {
entry.translations = d.translations;
Object.keys(d.translations).forEach((l) => langs.add(l));
}
strings[key] = entry;
}
const out: StringsFile = {
sourceLanguage,
supportedLanguages: [...langs].sort(),
strings,
};
await Bun.write(
STRINGS_JSON,
JSON.stringify({ $schema: "./schema.json", ...out }, null, 2) + "\n",
);
console.log(
`Wrote ${STRINGS_JSON}\n ${Object.keys(strings).length} keys, languages: ${[...langs].sort().join(", ")}`,
);
}

View File

@@ -0,0 +1,55 @@
/** Structural checks on strings.json. Exits non-zero on errors (warnings don't fail). */
import { STRINGS_JSON } from "../config";
import type { StringsFile } from "../types";
const TOKEN = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
export async function validate() {
const doc = JSON.parse(await Bun.file(STRINGS_JSON).text()) as StringsFile;
const errors: string[] = [];
const warnings: string[] = [];
for (const [key, entry] of Object.entries(doc.strings)) {
const where = `"${key}"`;
const argNames = new Set((entry.args ?? []).map((a) => a.name));
if (entry.translations && entry.plural) {
errors.push(`${where}: has both "translations" and "plural" (pick one).`);
}
if (!entry.translations && !entry.plural) {
warnings.push(`${where}: no translations yet.`);
}
if (!entry.context?.trim() || entry.context.startsWith("TODO")) {
warnings.push(`${where}: missing/placeholder context.`);
}
const texts: string[] = [];
if (entry.translations) texts.push(...Object.values(entry.translations));
if (entry.plural) {
for (const [lang, forms] of Object.entries(entry.plural)) {
if (!forms.other?.trim()) errors.push(`${where} [${lang}]: plural missing required "other".`);
texts.push(...Object.values(forms));
}
}
for (const text of texts) {
for (const m of text.matchAll(TOKEN)) {
if (!argNames.has(m[1]!)) {
errors.push(`${where}: uses {${m[1]}} but no matching arg is declared.`);
}
}
}
// Missing-language coverage (warn only).
for (const lang of doc.supportedLanguages) {
const present = entry.plural ? !!entry.plural[lang] : entry.translations?.[lang] !== undefined;
const anyTranslations = entry.translations || entry.plural;
if (anyTranslations && !present) warnings.push(`${where}: missing "${lang}".`);
}
}
for (const w of warnings) console.warn(`warn ${w}`);
for (const e of errors) console.error(`error ${e}`);
console.log(`\n${Object.keys(doc.strings).length} keys — ${errors.length} error(s), ${warnings.length} warning(s).`);
if (errors.length) process.exit(1);
}

View File

@@ -0,0 +1,30 @@
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
/** Repo root = parent of the localization/ directory. */
export const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
export const LOC_DIR = join(REPO_ROOT, "localization");
export const STRINGS_JSON = join(LOC_DIR, "strings.json");
/** Apple String Catalog — one file, all languages nested. */
export const APPLE_XCSTRINGS = join(
REPO_ROOT,
"apple/VniDrop/Resources/Localizable.xcstrings",
);
/** Apple app Info.plist — its CFBundleLocalizations is kept in sync with supportedLanguages. */
export const APPLE_INFO_PLIST = join(
REPO_ROOT,
"apple/VniDrop/Resources/Info.plist",
);
/** KMP / Compose Multiplatform resources root; one values[-lang]/strings.xml per language. */
export const KMP_RESOURCES = join(
REPO_ROOT,
"shared/src/commonMain/composeResources",
);
/** Directory holding a language's strings.xml under the compose resources root. */
export function kmpValuesDir(lang: string, sourceLanguage: string): string {
return lang === sourceLanguage ? "values" : `values-${lang}`;
}

View File

@@ -0,0 +1,96 @@
/**
* Read/write Compose Multiplatform (Android-style) strings.xml.
* The existing file is flat and machine-friendly, so we use a targeted parser
* rather than a full DOM. Handles <string> and <plurals> with XML entities and
* Android backslash escapes.
*/
export interface AndroidString {
name: string;
value: string;
}
export interface AndroidPlural {
name: string;
items: Record<string, string>; // quantity -> value
}
export interface ParsedAndroid {
strings: AndroidString[];
plurals: AndroidPlural[];
}
function decodeEntities(s: string): string {
return s
.replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => String.fromCodePoint(parseInt(h, 16)))
.replace(/&#(\d+);/g, (_m, d) => String.fromCodePoint(parseInt(d, 10)))
.replaceAll("&lt;", "<")
.replaceAll("&gt;", ">")
.replaceAll("&quot;", '"')
.replaceAll("&apos;", "'")
.replaceAll("&amp;", "&");
}
/** Reverse Android's leading/trailing-quote + backslash escapes into plain text. */
function unescapeAndroid(raw: string): string {
let s = raw.trim();
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
s = s.slice(1, -1);
}
s = s
.replace(/\\n/g, "\n")
.replace(/\\t/g, "\t")
.replace(/\\'/g, "'")
.replace(/\\"/g, '"')
.replace(/\\@/g, "@")
.replace(/\\\\/g, "\\");
return decodeEntities(s);
}
export function parseAndroid(xml: string): ParsedAndroid {
const strings: AndroidString[] = [];
const plurals: AndroidPlural[] = [];
const stringRe = /<string\s+name="([^"]+)"\s*>([\s\S]*?)<\/string>/g;
for (const m of xml.matchAll(stringRe)) {
strings.push({ name: m[1]!, value: unescapeAndroid(m[2]!) });
}
const pluralRe = /<plurals\s+name="([^"]+)"\s*>([\s\S]*?)<\/plurals>/g;
const itemRe = /<item\s+quantity="([^"]+)"\s*>([\s\S]*?)<\/item>/g;
for (const m of xml.matchAll(pluralRe)) {
const items: Record<string, string> = {};
for (const it of m[2]!.matchAll(itemRe)) {
items[it[1]!] = unescapeAndroid(it[2]!);
}
plurals.push({ name: m[1]!, items });
}
return { strings, plurals };
}
function escapeXmlText(s: string): string {
return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}
/** Android needs apostrophes and quotes escaped, and quotes on leading/trailing whitespace. */
function escapeAndroidValue(s: string): string {
let out = escapeXmlText(s).replace(/'/g, "\\'").replace(/\n/g, "\\n");
if (out !== out.trim()) out = `"${out}"`;
return out;
}
export function renderAndroid(parsed: ParsedAndroid): string {
const lines: string[] = ['<?xml version="1.0" encoding="utf-8"?>', "<resources>"];
for (const s of parsed.strings) {
lines.push(` <string name="${s.name}">${escapeAndroidValue(s.value)}</string>`);
}
for (const p of parsed.plurals) {
lines.push(` <plurals name="${p.name}">`);
for (const [q, v] of Object.entries(p.items)) {
lines.push(` <item quantity="${q}">${escapeAndroidValue(v)}</item>`);
}
lines.push(" </plurals>");
}
lines.push("</resources>", "");
return lines.join("\n");
}

View File

@@ -0,0 +1,99 @@
/**
* Canonical placeholder handling.
*
* Canonical form (in strings.json) uses named tokens: `{count}`, `{name}`.
* Platform output uses positional printf tokens, resolved from the entry's
* ordered `args`:
* - Apple (.xcstrings): string -> %N$@ int -> %N$d double -> %N$f
* - Android (.xml): string -> %N$s int -> %N$d double -> %N$f
* A literal percent is written `%%` on both platforms whenever the string
* carries args (printf-active); canonical text stores it as a bare `%`.
*/
import type { Arg, ArgType } from "../types";
export type Flavor = "apple" | "android";
function stringToken(flavor: Flavor): string {
return flavor === "apple" ? "@" : "s";
}
function tokenForType(type: ArgType, flavor: Flavor): string {
switch (type) {
case "string":
return stringToken(flavor);
case "int":
return "d";
case "double":
return "f";
}
}
/** Render canonical `{name}` text into a platform printf string. */
export function fromCanonical(
text: string,
args: Arg[],
flavor: Flavor,
): string {
const hasArgs = args.length > 0;
const index = new Map(args.map((a, i) => [a.name, { i: i + 1, type: a.type }]));
// Escape literal % first (only meaningful when printf-active).
let out = hasArgs ? text.replaceAll("%", "%%") : text;
out = out.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_whole, name: string) => {
const found = index.get(name);
if (!found) {
throw new Error(
`placeholder {${name}} has no matching arg (${args.map((a) => a.name).join(", ") || "none"})`,
);
}
return `%${found.i}$${tokenForType(found.type, flavor)}`;
});
return out;
}
export interface ParsedString {
/** Canonical text with `{argN}` tokens. */
text: string;
/** Args discovered, ordered by first positional index. */
args: Arg[];
}
// Matches an escaped literal `%%` or a printf specifier (positional or bare),
// tolerating C length modifiers like `%lld` / `%ld` on the integer forms.
const CONV = /%%|%(?:(\d+)\$)?l{0,2}([@sdif])/g;
function typeForConv(conv: string): ArgType {
if (conv === "@" || conv === "s") return "string";
if (conv === "d" || conv === "i") return "int";
return "double"; // f
}
/**
* Parse a platform printf string back into canonical `{argN}` form, inferring
* arg names (`arg1`, `arg2`, ...) and types from the conversion specifiers.
* Handles positional (`%1$@`), bare (`%@`), and escaped literals (`%%`).
*/
export function toCanonical(raw: string): ParsedString {
const byIndex = new Map<number, ArgType>();
let auto = 0;
const text = raw.replace(CONV, (whole, pos: string | undefined, conv: string) => {
if (whole === "%%") return "%"; // literal percent, not a specifier
const idx = pos ? Number(pos) : ++auto;
const type = typeForConv(conv);
const existing = byIndex.get(idx);
if (existing && existing !== type) {
throw new Error(`arg ${idx} used with conflicting types in "${raw}"`);
}
byIndex.set(idx, type);
return `{arg${idx}}`;
});
const args: Arg[] = [...byIndex.keys()]
.sort((a, b) => a - b)
.map((idx) => ({ name: `arg${idx}`, type: byIndex.get(idx)! }));
return { text, args };
}

View File

@@ -0,0 +1,45 @@
/** Read/write Apple String Catalog (.xcstrings) — a single JSON file, all languages nested. */
export type XcState = "new" | "translated" | "needs_review" | "stale";
export interface XcStringUnit {
state: XcState;
value: string;
}
export interface XcVariations {
plural?: Record<string, { stringUnit: XcStringUnit }>;
}
export interface XcLocalization {
stringUnit?: XcStringUnit;
variations?: XcVariations;
}
export interface XcEntry {
comment?: string;
extractionState?: string;
localizations?: Record<string, XcLocalization>;
}
export interface XcCatalog {
sourceLanguage: string;
strings: Record<string, XcEntry>;
version: string;
}
export function parseXcstrings(json: string): XcCatalog {
return JSON.parse(json) as XcCatalog;
}
/** Stable, Xcode-compatible serialization: 2-space indent, keys sorted, trailing newline. */
export function renderXcstrings(cat: XcCatalog): string {
const sortDeep = (v: unknown): unknown => {
if (Array.isArray(v)) return v.map(sortDeep);
if (v && typeof v === "object") {
const out: Record<string, unknown> = {};
for (const k of Object.keys(v as Record<string, unknown>).sort()) {
out[k] = sortDeep((v as Record<string, unknown>)[k]);
}
return out;
}
return v;
};
return JSON.stringify(sortDeep(cat), null, 2) + "\n";
}

43
localization/src/types.ts Normal file
View File

@@ -0,0 +1,43 @@
/** Canonical localization data model. See ../README.md for the full spec. */
/** Output targets a string can be emitted to. */
export type Target = "kmp" | "apple";
export const ALL_TARGETS: Target[] = ["kmp", "apple"];
/** Argument types, used to pick the right platform placeholder token. */
export type ArgType = "string" | "int" | "double";
export interface Arg {
/** Name referenced inside translations as `{name}`. */
name: string;
type: ArgType;
}
/** CLDR plural categories. `other` is always required when `plural` is used. */
export type PluralCategory = "zero" | "one" | "two" | "few" | "many" | "other";
export type PluralForms = Partial<Record<PluralCategory, string>> & {
other: string;
};
export interface StringEntry {
/** Where the string appears / its purpose. Emitted as xcstrings comment + xml comment. */
context: string;
/** Defaults to all targets when omitted. */
targets?: Target[];
/** Ordered positional args referenced as `{name}` in translations. */
args?: Arg[];
/** Flat (non-plural) translations, keyed by language. Mutually exclusive with `plural`. */
translations?: Record<string, string>;
/** Plural translations, keyed by language then CLDR category. Mutually exclusive with `translations`. */
plural?: Record<string, PluralForms>;
}
export interface StringsFile {
sourceLanguage: string;
supportedLanguages: string[];
strings: Record<string, StringEntry>;
}
export function targetsOf(entry: StringEntry): Target[] {
return entry.targets ?? ALL_TARGETS;
}

3696
localization/strings.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["bun-types"],
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"verbatimModuleSyntax": true
},
"include": ["src"]
}

51
make/release.mk Normal file
View File

@@ -0,0 +1,51 @@
.PHONY: package-deb package-rpm package-msix
package-deb: ## Build and verify a Debian x64 package (VERSION=x.y.z).
@test "$(HOST_OS)" = linux || { printf 'Debian packaging requires Linux.\n' >&2; exit 1; }
@cd $(ROOT); \
version="$$(packaging/linux/resolve-version.sh "$(VERSION)")"; \
$(GRADLE) :shared:jvmTest :desktopApp:packageReleaseDeb \
-Pvnidrop.version="$$version" \
-Pvnidrop.desktop.rustVariant=release \
-Pvnidrop.diagnostics.included=false \
$(GRADLE_RELEASE_FLAGS); \
mapfile -t packages < <(find desktopApp/build/compose/binaries/main-release/deb -maxdepth 1 -type f -name '*.deb'); \
(( $${#packages[@]} == 1 )) || { printf 'Expected exactly one Debian package, found %s\n' "$${#packages[@]}" >&2; exit 1; }; \
output_directory=build/release/linux/deb; \
output_name="vnidrop_$${version}-1_amd64.deb"; \
mkdir -p "$$output_directory"; \
cp "$${packages[0]}" "$$output_directory/$$output_name"; \
packaging/linux/verify-package.sh deb "$$version" "$$output_directory/$$output_name"; \
( cd "$$output_directory" && sha256sum "$$output_name" > "$$output_name.sha256" ); \
printf 'Package: %s/%s\n' "$$output_directory" "$$output_name"
package-rpm: ## Build and verify an RPM x64 package (VERSION=x.y.z).
@test "$(HOST_OS)" = linux || { printf 'RPM packaging requires Linux.\n' >&2; exit 1; }
@cd $(ROOT); \
version="$$(packaging/linux/resolve-version.sh "$(VERSION)")"; \
$(GRADLE) :desktopApp:packageReleaseRpm \
-Pvnidrop.version="$$version" \
-Pvnidrop.desktop.rustVariant=release \
-Pvnidrop.diagnostics.included=false \
$(GRADLE_RELEASE_FLAGS); \
mapfile -t packages < <(find desktopApp/build/compose/binaries/main-release/rpm -maxdepth 1 -type f -name '*.rpm'); \
(( $${#packages[@]} == 1 )) || { printf 'Expected exactly one RPM package, found %s\n' "$${#packages[@]}" >&2; exit 1; }; \
output_directory=build/release/linux/rpm; \
output_name="vnidrop-$${version}-1.x86_64.rpm"; \
mkdir -p "$$output_directory"; \
cp "$${packages[0]}" "$$output_directory/$$output_name"; \
packaging/linux/verify-package.sh rpm "$$version" "$$output_directory/$$output_name"; \
( cd "$$output_directory" && sha256sum "$$output_name" > "$$output_name.sha256" ); \
printf 'Package: %s/%s\n' "$$output_directory" "$$output_name"
package-msix: ## Build and verify an unsigned Windows Store MSIX (VERSION=x.y.z).
@test "$(HOST_OS)" = windows || { printf 'MSIX packaging requires Windows.\n' >&2; exit 1; }
cd $(ROOT) && $(GRADLE) :shared:jvmTest :desktopApp:createReleaseDistributable \
-Pvnidrop.version="$(VERSION)" \
-Pvnidrop.desktop.rustVariant=release \
-Pvnidrop.diagnostics.included=false \
$(GRADLE_RELEASE_FLAGS)
cd $(ROOT) && $(POWERSHELL) -NoProfile -File packaging/windows/build-msix.ps1 \
-Version "$(VERSION)" \
-AppImage desktopApp/build/compose/binaries/main-release/app/VniDrop \
-OutputDirectory build/release/windows

View File

@@ -64,21 +64,10 @@ Ubuntu prevents `jpackage` from discovering normal RPM dependencies.
From the repository root on the matching Linux family, run one of:
```bash
./gradlew :shared:jvmTest :desktopApp:packageReleaseDeb \
-Pvnidrop.version=1.0.0 \
-Pvnidrop.desktop.rustVariant=release \
-Pvnidrop.diagnostics.included=false \
--no-daemon --no-configuration-cache --stacktrace
./gradlew :shared:jvmTest :desktopApp:packageReleaseRpm \
-Pvnidrop.version=1.0.0 \
-Pvnidrop.desktop.rustVariant=release \
-Pvnidrop.diagnostics.included=false \
--no-daemon --no-configuration-cache --stacktrace
make package-deb VERSION=1.0.0
make package-rpm VERSION=1.0.0
```
Compose writes the packages under
`desktopApp/build/compose/binaries/main-release/deb/` and
`desktopApp/build/compose/binaries/main-release/rpm/`. The workflow then
validates package identity, version, architecture, dependencies, bundled JVM,
and release Rust payload before publishing anything.
The Make targets collect the Compose output under `build/release/linux/`, then
validate package identity, version, architecture, dependencies, bundled JVM,
and release Rust payload before generating a SHA-256 checksum.

View File

@@ -85,19 +85,23 @@ npx wrangler r2 bucket create vnidrop-diagnostics
```
Replace the placeholder `database_id` in `wrangler.jsonc` with the UUID returned
by `wrangler d1 create`. Set the ingest key interactively, apply the tracked D1
migrations, and configure the R2 retention rule once:
by `wrangler d1 create`. Set the ingest key interactively and configure the R2
retention rule once:
```bash
npx wrangler secret put INGEST_KEY
npm run db:migrate:remote
npx wrangler r2 bucket lifecycle add vnidrop-diagnostics diagnostics-retention --expire-days 90
npm run check
npm run deploy
```
`npm run deploy` also runs the complete `check` script automatically before
Wrangler changes the remote Worker.
Then apply migrations and deploy from the repository root:
```bash
make diagnostics-db-remote
make deploy-diagnostics
```
`make deploy-diagnostics` runs the complete check before Wrangler changes the
remote Worker.
The lifecycle command changes the remote bucket. Before adding or changing a
rule, inspect the current state with:
@@ -117,8 +121,9 @@ INGEST_KEY=local-development-only
Then initialize the local D1 database and run the Worker:
```bash
npm run db:migrate:local
npm run dev
# From the repository root:
make diagnostics-db-local
make run-diagnostics
```
Wrangler keeps local D1 and R2 state under the ignored `.wrangler/` directory.
@@ -133,7 +138,7 @@ Never edit an applied migration; add the next numbered SQL file instead.
bindings cannot silently drift from the Worker code:
```bash
npm run typegen # regenerate after changing bindings or vars
make diagnostics-typegen # from the repository root
npm run types:check # verify the committed file is current
```

View File

@@ -7,9 +7,10 @@ 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`).
`shared` is the Compose Multiplatform app layer for Android, Windows, and Linux:
Compose UI, feature ViewModels, and `expect`/`actual` platform bridges. Native
transfer work goes through UniFFI `VnidropCore` (see `crates/vnidrop`). Apple
platforms use the native SwiftUI app under `apple/`.
---
@@ -33,7 +34,7 @@ lists, animation, accessibility:
| 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. |
| Platform | `androidMain` / `jvmMain` for pickers, SAF, NFC/QR, and desktop integration. |
| 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.
@@ -45,22 +46,19 @@ compose-skill “Existing Project Policy”: adapt to this repo; do not force-mi
From repo root:
```bash
./gradlew :shared:jvmTest
./gradlew :shared:compileKotlinJvm
make check-shared
```
Optional:
```bash
./gradlew :shared:testAndroidHostTest
./gradlew :shared:iosSimulatorArm64Test
./gradlew :desktopApp:run
./gradlew :androidApp:assembleDebug
make test-android-host
make run-desktop
make check-android
```
CI `:shared:jvmTest` currently runs on **macOS**. Gobley host cargo follows the
current host and architecture, including Linux and Windows; use macOS only when
exact CI parity is required.
CI `:shared:jvmTest` runs on **Linux**. Gobley host cargo follows the current
host and architecture so local desktop builds embed their matching Rust library.
When Kotlin changes touch UniFFI-generated APIs, rebuild/test with a full
`jvmTest` so Gobley/native pieces stay aligned.
@@ -76,7 +74,7 @@ src/
feature/send|receive|approvals|settings|app/
ui/ # theme, components, navigation, feedback, state helpers
commonMain/composeResources/
androidMain|iosMain|jvmMain/
androidMain|jvmMain/
commonTest|jvmTest|...
```
@@ -86,7 +84,8 @@ src/
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.
- **Apple:** lives outside this module under `apple/`; do not add Apple platform
behavior back to KMP.
- **Desktop:** filesystem paths; directories may be marked `isDirectory` for Rust walk.
Never pass a directory as a single Android FD into `SourceKind.FILE_DESCRIPTOR`.
@@ -113,7 +112,7 @@ Never pass a directory as a single Android FD into `SourceKind.FILE_DESCRIPTOR`.
- Prefer fakes in `commonTest` support over real UniFFI in pure unit tests.
```bash
./gradlew :shared:jvmTest
make test-shared
```
---

View File

@@ -1,12 +1,10 @@
@file:OptIn(gobley.gradle.InternalGobleyGradleApi::class)
import gobley.gradle.cargo.dsl.appleMobile
import gobley.gradle.cargo.dsl.jvm
import gobley.gradle.cargo.tasks.CargoBuildTask
import gobley.gradle.cargo.tasks.CargoCheckTask
import gobley.gradle.GobleyHost
import gobley.gradle.rust.targets.RustAndroidTarget
import gobley.gradle.rust.targets.RustAppleMobileTarget
import gobley.gradle.rust.targets.RustTarget
import gobley.gradle.Variant
import org.gradle.api.DefaultTask
@@ -113,18 +111,6 @@ val generateDiagnosticsBuildConfig by tasks.registering {
}
kotlin {
if (GobleyHost.current.platform == GobleyHost.Platform.MacOS) {
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "Shared"
isStatic = true
}
}
}
androidTarget {
compilerOptions {
jvmTarget = JvmTarget.JVM_11
@@ -190,9 +176,6 @@ android {
val hostCargoTargets = buildSet<RustTarget> {
add(GobleyHost.current.rustTarget)
addAll(RustAndroidTarget.entries)
if (GobleyHost.current.platform == GobleyHost.Platform.MacOS) {
addAll(RustAppleMobileTarget.entries)
}
}
cargo {
@@ -207,15 +190,6 @@ cargo {
embedRustLibrary.set(rustTarget == GobleyHost.current.rustTarget)
}
}
builds.appleMobile {
variants {
buildTaskProvider.configure {
if (rustTarget.cinteropName == "ios") {
additionalEnvironment.put("IPHONEOS_DEPLOYMENT_TARGET", "16.0.0")
}
}
}
}
builds.configureEach {
val buildOnCurrentHost = rustTarget in hostCargoTargets
installTargetBeforeBuild.set(buildOnCurrentHost)

View File

@@ -52,15 +52,13 @@ private class AndroidFileSystemService(
ReceiveFolderKind.FileSystemPath -> validatePath(folder.value)
ReceiveFolderKind.AndroidPublicDownloads -> validatePublicDownloads()
ReceiveFolderKind.AndroidTreeUri -> validateTreeUri(folder.value)
ReceiveFolderKind.IosSecurityScopedUrl -> FolderAccessStatus.Unavailable
}
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSink? =
when (folder.kind) {
ReceiveFolderKind.AndroidPublicDownloads -> AndroidMediaStoreDownloadsSink(context)
ReceiveFolderKind.AndroidTreeUri -> AndroidTreeReceiveOutputSink(context, folder.value.toUri())
ReceiveFolderKind.FileSystemPath,
ReceiveFolderKind.IosSecurityScopedUrl -> null
ReceiveFolderKind.FileSystemPath -> null
}
override suspend fun sharePickedFiles(

View File

@@ -0,0 +1,257 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_bug_report">Fehler melden</string>
<string name="about_description">VniDrop überträgt Dateien und Ordner direkt von einem Gerät auf ein anderes über das Netzwerk, ohne sie auf einen Hosting-Dienst hochzuladen. Es muss kein Konto erstellt werden, und nach Abschluss verbleibt keine Kopie Ihrer Übertragung in der Cloud.</string>
<string name="about_is_direct">Eine direkte Gerät-zu-Gerät-Übertragung Ihre Dateien gehen direkt an den Empfänger.</string>
<string name="about_is_encrypted">Verbindungen sind authentifiziert und Ende-zu-Ende-verschlüsselt (über Iroh), und eingehende Dateien werden anhand ihres Inhalts-Hashs überprüft.</string>
<string name="about_is_in_control">Sie entscheiden, wer empfängt: Genehmigen Sie jede Anfrage oder öffnen Sie eine Übertragung für alle, die die Einladung besitzen.</string>
<string name="about_is_no_account">Ohne Konto es gibt nichts zu registrieren.</string>
<string name="about_is_open">Open Source, veröffentlicht unter der Apache-2.0-Lizenz.</string>
<string name="about_is_title">Was VniDrop ist</string>
<string name="about_isnt_cloud">Kein Cloud-Speicher kein Server hält Ihre Dateien, und nach einer Übertragung wartet nichts in der Cloud.</string>
<string name="about_isnt_public">Keine öffentliche Übertragung eine Einladung ist ein privater Zugangslink, keine Ankündigung an alle in der Nähe.</string>
<string name="about_isnt_sync">Kein Synchronisierungs- oder Backup-Dienst.</string>
<string name="about_isnt_title">Was VniDrop nicht ist</string>
<string name="about_license_label">Lizenz</string>
<string name="about_privacy">Datenschutzrichtlinie</string>
<string name="about_privacy_capability">Einladungen sind Zugangsschlüssel. Behandeln Sie einen QR-Code, ein NFC-Tag oder eine .vnd-Datei wie einen privaten Zugangslink und teilen Sie ihn nur mit den vorgesehenen Personen insbesondere bei „Jeder mit dieser Übertragung“.</string>
<string name="about_privacy_deny">Standardmäßig ablehnen VniDrop stellt nur den Inhalt einer aktiven Freigabe bereit und weist unbekannte Anfragen ab.</string>
<string name="about_privacy_local">Empfangene Dateien werden auf Ihrem Gerät gespeichert und überschreiben niemals eine vorhandene Datei.</string>
<string name="about_privacy_policy_label">Datenschutzrichtlinie</string>
<string name="about_privacy_relay">Wenn zwei Geräte sich nicht direkt verbinden können, kann die verschlüsselte Verbindung weitergeleitet werden. Relays leiten nur verschlüsselte Pakete weiter; sie speichern niemals Ihre Dateien.</string>
<string name="about_privacy_title">Datenschutz &amp; Sicherheit</string>
<string name="about_tagline">Senden Sie Dateien direkt. Behalten Sie die Kontrolle darüber, wer sie empfängt.</string>
<string name="about_title">Über</string>
<string name="appearance_auto_description">Der hellen oder dunklen Darstellung dieses Geräts folgen.</string>
<string name="appearance_dark_mode">Dunkelmodus</string>
<string name="appearance_light_mode">Hellmodus</string>
<string name="appearance_system_mode">System</string>
<string name="appearance_title">Darstellung</string>
<string name="approval_connection_request">Empfangsanfrage</string>
<string name="approval_endpoint_id">Geräte-ID: %1$s</string>
<string name="approval_nearby_device">Ein Gerät in der Nähe</string>
<string name="approval_pending_count">%1$d Anfragen warten</string>
<string name="approval_request_body">%1$s möchte „%2$s“ empfangen.</string>
<string name="battery_level_title">Batteriestand</string>
<string name="bug_report_contact_hint">name@beispiel.com</string>
<string name="bug_report_contact_label">Kontakt-E-Mail (optional)</string>
<string name="bug_report_description">Sagen Sie uns, was schiefgelaufen ist. Wir hängen Geräteinformationen und optional aktuelle Protokolle an (sensible Werte werden geschwärzt).</string>
<string name="bug_report_device_section">Geräteinformationen</string>
<string name="bug_report_expected_hint">Beschreiben Sie, was Sie erwartet haben</string>
<string name="bug_report_expected_label">Was haben Sie erwartet?</string>
<string name="bug_report_include_logs">Aktuelle Protokolle einschließen</string>
<string name="bug_report_include_logs_description">Hilft uns, das Problem zu diagnostizieren. Sensible Werte werden vor dem Senden geschwärzt.</string>
<string name="bug_report_logs_size">Größe des Protokollanhangs</string>
<string name="bug_report_missing_expected">Bitte beschreiben Sie, was Sie erwartet haben.</string>
<string name="bug_report_missing_what">Bitte beschreiben Sie, was passiert ist.</string>
<string name="bug_report_steps_hint">Listen Sie die Schritte zum Reproduzieren des Problems auf</string>
<string name="bug_report_steps_label">Schritte zum Reproduzieren (optional)</string>
<string name="bug_report_submit">Bericht senden</string>
<string name="bug_report_submit_failed">Der Fehlerbericht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut.</string>
<string name="bug_report_submitted">Danke Ihr Fehlerbericht wurde erfasst.</string>
<string name="bug_report_submitting">Wird gesendet…</string>
<string name="bug_report_what_hint">Beschreiben Sie, was passiert ist</string>
<string name="bug_report_what_label">Was ist passiert?</string>
<string name="button_approve">Genehmigen</string>
<string name="button_back">Zurück</string>
<string name="button_cancel">Abbrechen</string>
<string name="button_cancel_receive">Abbrechen</string>
<string name="button_change_files">Dateien ändern</string>
<string name="button_choose_files">Dateien auswählen</string>
<string name="button_choose_folder">Ordner auswählen</string>
<string name="button_clear">Löschen</string>
<string name="button_close">Schließen</string>
<string name="button_create_new_transfer">Neue Übertragung</string>
<string name="button_delete_transfer">Übertragung löschen</string>
<string name="button_download_invitation">.vnd-Datei sichern</string>
<string name="button_native_share">Einladung teilen</string>
<string name="button_open_settings">Einstellungen öffnen</string>
<string name="button_receive">Empfangen</string>
<string name="button_receive_files">Empfang starten</string>
<string name="button_refuse">Ablehnen</string>
<string name="button_remove_file">Datei entfernen</string>
<string name="button_reset_default">Standard verwenden</string>
<string name="button_retry">Wiederholen</string>
<string name="button_share_file">Freigabe starten</string>
<string name="button_sharing_file">Übertragung wird vorbereitet…</string>
<string name="button_show_in_files">In „Dateien“ anzeigen</string>
<string name="button_write_nfc">Auf NFC-Tag schreiben</string>
<string name="device_model_title">Gerätemodell</string>
<string name="device_name_title">Gerätename</string>
<string name="diagnostics_description">Anonyme Absturzberichte und Nutzungsereignisse senden, damit wir VniDrop verbessern können. Sie können dies jederzeit deaktivieren. Einladungen, Dateipfade und Übertragungsinhalte werden niemals einbezogen.</string>
<string name="diagnostics_disabled_message">Die Freigabe von Diagnosedaten ist deaktiviert.</string>
<string name="diagnostics_enabled_message">Die Freigabe von Diagnosedaten ist aktiviert.</string>
<string name="diagnostics_title">Diagnosedaten teilen</string>
<string name="error_camera">Für das Scannen eines QR-Codes ist Kamerazugriff erforderlich.</string>
<string name="error_device_info">Geräteinformationen konnten nicht geladen werden.</string>
<string name="error_filesystem">VniDrop konnte nicht auf die ausgewählten Dateien oder den Ordner zugreifen. Überprüfen Sie die Berechtigungen und versuchen Sie es erneut.</string>
<string name="error_generic">Etwas ist schiefgelaufen. Versuchen Sie es erneut.</string>
<string name="error_initialization">VniDrop konnte den Start nicht abschließen. Schließen Sie die App und versuchen Sie es erneut.</string>
<string name="error_invalid_ticket">Diese Einladung konnte nicht gelesen werden. Bitten Sie den Absender um eine neue.</string>
<string name="error_invitation_empty">Diese Einladung ist leer. Versuchen Sie, sie erneut zu öffnen.</string>
<string name="error_missing_native_library">Die native VniDrop-Bibliothek fehlt in diesem Build.</string>
<string name="error_nfc">Dieses NFC-Tag konnte nicht verwendet werden. Versuchen Sie ein anderes.</string>
<string name="error_permission">Der Absender hat diese Übertragung nicht genehmigt oder sie wurde abgelehnt.</string>
<string name="error_repository">VniDrop konnte die Übertragungsdaten auf diesem Gerät nicht speichern.</string>
<string name="error_selection_failed">Das ausgewählte Objekt konnte nicht geöffnet werden. Versuchen Sie, es erneut auszuwählen.</string>
<string name="error_share_empty">Wählen Sie mindestens ein Objekt zum Teilen aus.</string>
<string name="error_socket_bind">VniDrop konnte seine Netzwerk-Sockets auf diesem Gerät nicht öffnen.</string>
<string name="error_starting_up">VniDrop startet noch. Öffnen Sie die Einladung gleich erneut.</string>
<string name="error_transfer">Die Übertragung konnte nicht abgeschlossen werden. Überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.</string>
<string name="field_receiver_name">Empfängername</string>
<string name="field_sender_name">Absendername</string>
<string name="field_transfer_name">Übertragungsname</string>
<string name="field_username">Anzeigename</string>
<string name="folder_status_permission_required">Berechtigung erforderlich</string>
<string name="folder_status_unavailable">Nicht verfügbar</string>
<string name="folder_status_validating">Ordner wird geprüft…</string>
<string name="folder_status_writable">Bereit</string>
<string name="metadata_files">Dateien</string>
<string name="metadata_size">Größe</string>
<string name="metadata_status">Status</string>
<string name="nav_receive">Empfangen</string>
<string name="nav_send">Senden</string>
<string name="nav_settings">Einstellungen</string>
<string name="network_title">Netzwerk</string>
<string name="notifications_description">Werden Sie über neue Empfangsanfragen benachrichtigt, während VniDrop im Hintergrund läuft.</string>
<string name="notifications_enabled_message">Mitteilungen aktiviert.</string>
<string name="notifications_local_title">Mitteilungen erlauben</string>
<string name="notifications_permission_denied">Mitteilungen sind für VniDrop deaktiviert. Sie können sie in den Einstellungen aktivieren.</string>
<string name="notifications_settings_open_failed">Die Mitteilungseinstellungen konnten nicht geöffnet werden.</string>
<string name="notifications_title">Mitteilungen</string>
<string name="notifications_unsupported">Mitteilungen sind auf diesem Gerät nicht verfügbar.</string>
<string name="os_version_title">Betriebssystem</string>
<string name="preferences_receive_folder_title">Empfangene Übertragungen sichern in</string>
<string name="preferences_title">Voreinstellungen</string>
<string name="progress_cancelled">Abgebrochen</string>
<string name="progress_completed">Abgeschlossen</string>
<string name="progress_connected">Verbunden</string>
<string name="progress_connecting">Verbinden</string>
<string name="progress_downloading">Wird geladen</string>
<string name="progress_failed">Fehlgeschlagen</string>
<string name="progress_getting_ready">Wird bereitgemacht</string>
<string name="progress_interrupted">Übertragung unterbrochen</string>
<string name="progress_preparing">Wird vorbereitet</string>
<string name="progress_ready">Bereit</string>
<string name="progress_receiving">Wird empfangen</string>
<string name="progress_requesting_access">Zugriff wird angefragt</string>
<string name="progress_saving">Wird gesichert</string>
<string name="progress_sending">Wird gesendet</string>
<string name="progress_sending_to_count">Senden an %1$d</string>
<string name="progress_share_ready">Bereit zum Teilen</string>
<string name="progress_working">Wird bearbeitet…</string>
<string name="receive_choose_method_body">Wählen Sie die Ihnen zur Verfügung stehende Einladungsmethode.</string>
<string name="receive_choose_method_title">Wie möchten Sie sich verbinden?</string>
<string name="receive_clear_history">Verlauf löschen</string>
<string name="receive_clear_history_description">Alle abgeschlossenen, fehlgeschlagenen und abgebrochenen Empfänge werden aus dem Verlauf von VniDrop entfernt. Heruntergeladene Dateien verbleiben auf diesem Gerät.</string>
<string name="receive_clear_history_title">Empfangsverlauf löschen?</string>
<string name="receive_completed">Übertragung empfangen.</string>
<string name="receive_delete_history_description">„%1$s“ wird aus dem Verlauf von VniDrop entfernt. Die heruntergeladene Datei verbleibt auf diesem Gerät.</string>
<string name="receive_delete_history_item">Aus Empfangsverlauf löschen</string>
<string name="receive_delete_history_title">Aus dem Verlauf entfernen?</string>
<string name="receive_empty_body">Öffnen Sie eine VniDrop-Einladung, scannen Sie einen QR-Code oder halten Sie das Gerät an ein NFC-Tag.</string>
<string name="receive_empty_title">Noch nichts empfangen</string>
<string name="receive_history_cleared">Empfangsverlauf gelöscht.</string>
<string name="receive_history_title">Verlauf</string>
<string name="receive_method_file">Eine .vnd-Einladung öffnen</string>
<string name="receive_method_file_description">Wählen Sie eine auf diesem Gerät gespeicherte oder geteilte Einladung.</string>
<string name="receive_method_nfc">NFC-Tag lesen</string>
<string name="receive_method_nfc_description">Halten Sie dieses Gerät an das Einladungs-Tag des Absenders.</string>
<string name="receive_method_scan">QR-Code scannen</string>
<string name="receive_method_scan_description">Verwenden Sie die Kamera, um den VniDrop-Code des Absenders zu scannen.</string>
<string name="receive_new_subtitle">Übertragungen, die Sie auf diesem Gerät empfangen haben.</string>
<string name="receive_nfc_waiting">Halten Sie das Gerät an das NFC-Tag…</string>
<string name="receive_open_files_failed">VniDrop konnte in „Dateien“ nicht geöffnet werden.</string>
<string name="receive_review_title">Übertragung prüfen</string>
<string name="receive_title">Empfangen</string>
<string name="receive_unknown_transfer">VniDrop-Übertragung</string>
<string name="send_access_anyone">Jeder mit dieser Übertragung</string>
<string name="send_access_anyone_description">Keine Genehmigung erforderlich. Verwenden Sie dies nur für Objekte, die Sie unbedenklich teilen können.</string>
<string name="send_access_anyone_warning">Jeder mit der Einladung kann herunterladen, bis Sie die Freigabe beenden. Verwenden Sie dies nicht für private oder sensible Objekte.</string>
<string name="send_access_approval">Vor jedem Download fragen</string>
<string name="send_access_approval_description">Sie genehmigen oder lehnen jeden neuen Empfänger ab.</string>
<string name="send_access_title">Wer kann sie empfangen?</string>
<string name="send_choose_file_body">Wählen Sie Dateien oder einen Ordner auf diesem Gerät aus. Sie können die Auswahl überprüfen, bevor Sie die Übertragung erstellen.</string>
<string name="send_choose_file_title">Wählen Sie, was Sie teilen möchten</string>
<string name="send_empty_body">Erstellen Sie eine Übertragung, entscheiden Sie, wer sie empfangen darf, und laden Sie diese Personen dann mit einem QR-Code, einem NFC-Tag oder einer Einladungsdatei ein.</string>
<string name="send_empty_title">Noch nichts geteilt</string>
<string name="send_file_size_unknown">Größe nicht verfügbar</string>
<string name="send_folder_label">Ordner</string>
<string name="send_new_transfer_description">Eine neue Übertragung erstellen</string>
<string name="send_new_transfer_title">Neue Übertragung</string>
<string name="send_review_title">Übertragung prüfen</string>
<string name="send_selected_files_count">%1$d Dateien ausgewählt</string>
<string name="send_stop_sharing">Freigabe beenden</string>
<string name="send_stop_sharing_description">Dies beendet die Übertragung und unterbricht alle, die sie gerade herunterladen. Sie verbleibt in Ihrem Verlauf als „Beendet“.</string>
<string name="send_subtitle">Übertragungen, die Sie von diesem Gerät aus teilen.</string>
<string name="send_title">Senden</string>
<string name="send_transfer_created">Übertragung erstellt.</string>
<string name="send_transfer_details_title">Übertragungsdetails</string>
<string name="send_transfers_title">Ihre Übertragungen</string>
<string name="settings_subtitle">Ihr Name, wo Übertragungen gesichert werden, Darstellung und Mitteilungen.</string>
<string name="settings_title">Einstellungen</string>
<string name="snackbar_dismiss">Ausblenden</string>
<string name="status_available">Verfügbar</string>
<string name="status_cancelled">Abgebrochen</string>
<string name="status_completed">Abgeschlossen</string>
<string name="status_failed">Fehlgeschlagen</string>
<string name="status_preparing">Wird vorbereitet</string>
<string name="status_receiving">Wird empfangen</string>
<string name="status_stopped">Beendet</string>
<string name="storage_calculating">Wird berechnet…</string>
<string name="storage_delete_transfers">Alle Übertragungen löschen</string>
<string name="storage_delete_transfers_description">Dies löscht alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf. Ihre empfangenen Dateien werden nicht gelöscht. Hinweis: Die Übertragungs-Engine behält ihre gespeicherten Inhalte, sodass die Übertragungsdaten möglicherweise nicht abnehmen. Dies kann nicht rückgängig gemacht werden.</string>
<string name="storage_deleting">Wird gelöscht…</string>
<string name="storage_footer">Übertragungsdaten werden von der Übertragungs-Engine verwaltet Ihr Verlauf sowie der Inhalt der von Ihnen geteilten Dateien. Die Schaltfläche unten löscht Ihre Übertragungsdatensätze; die Engine behält ihre gespeicherten Inhalte, sodass dieser Wert möglicherweise nicht sinkt. Empfangene Dateien sind Ihre heruntergeladenen Dateien und werden hier niemals gelöscht.</string>
<string name="storage_received_files">Empfangene Dateien</string>
<string name="storage_temporary">Temporäre Dateien</string>
<string name="storage_title">Speicher</string>
<string name="storage_total">Gesamt</string>
<string name="storage_transfer_data">Übertragungsdaten</string>
<string name="storage_transfers_deleted">Alle Übertragungen gelöscht</string>
<string name="transfer_activity_description">Wichtige Aktualisierungen zu dieser Übertragung ansehen</string>
<string name="transfer_activity_title">Aktivität</string>
<string name="transfer_delete_description">„%1$s“ wird nicht mehr geteilt und sein Übertragungsverlauf wird von diesem Gerät entfernt.</string>
<string name="transfer_delete_title">Übertragung löschen?</string>
<string name="transfer_deleted">Übertragung gelöscht.</string>
<string name="transfer_deleting">Wird gelöscht…</string>
<string name="transfer_details_title">Übertragungsdetails</string>
<string name="transfer_event_approved">Empfängerzugriff genehmigt</string>
<string name="transfer_event_completed">Ein Empfänger hat die Übertragung abgeschlossen</string>
<string name="transfer_event_connecting">Verbindung zum Absender</string>
<string name="transfer_event_downloading">Wird geladen</string>
<string name="transfer_event_failed">Bei der Übertragung ist ein Problem aufgetreten</string>
<string name="transfer_event_preparing">Ihre Übertragung wird vorbereitet</string>
<string name="transfer_event_ready">Bereit zum Teilen</string>
<string name="transfer_event_refused">Empfängerzugriff abgelehnt</string>
<string name="transfer_event_requested">Ein Empfänger hat Zugriff angefragt</string>
<string name="transfer_event_saving">Wird gesichert</string>
<string name="transfer_event_stopped">Freigabe beendet</string>
<string name="transfer_event_updated">Übertragung aktualisiert</string>
<string name="transfer_invitation_saved">Einladung gesichert.</string>
<string name="transfer_nearby_device">Gerät in der Nähe</string>
<string name="transfer_nfc_unavailable">Das Beschreiben von NFC-Tags ist auf diesem Gerät nicht verfügbar.</string>
<string name="transfer_nfc_waiting">Halten Sie Ihr Gerät an ein beschreibbares NFC-Tag.</string>
<string name="transfer_nfc_written">Einladung auf das NFC-Tag geschrieben.</string>
<string name="transfer_no_activity">Es gibt noch keine Aktivität anzuzeigen.</string>
<string name="transfer_no_receivers">Noch niemand hat diese Übertragung angefragt.</string>
<string name="transfer_receiver_accepted">Genehmigt wartet auf Abschluss</string>
<string name="transfer_receiver_completed">Erfolgreich empfangen</string>
<string name="transfer_receiver_expired">Anfrage abgelaufen</string>
<string name="transfer_receiver_refused">Anfrage abgelehnt</string>
<string name="transfer_receiver_requested">Wartet auf Ihre Genehmigung</string>
<string name="transfer_receiver_unknown">Status nicht verfügbar</string>
<string name="transfer_receivers_completed_count">%1$d abgeschlossen</string>
<string name="transfer_receivers_description">Anfragen, Genehmigungen und abgeschlossene Zustellungen</string>
<string name="transfer_receivers_pending">%1$d warten</string>
<string name="transfer_receivers_title">Empfänger</string>
<string name="transfer_scan_qr">Mit VniDrop scannen, um diese Übertragung zu empfangen</string>
<string name="transfer_share_description">QR-Code, Einladungsdatei und Optionen in der Nähe</string>
<string name="transfer_share_title">Teilen</string>
<string name="value_unavailable">Nicht verfügbar</string>
<string name="version_title">App-Version</string>
<plurals name="transfer_file_count">
<item quantity="one">%1$d Datei</item>
<item quantity="other">%1$d Dateien</item>
</plurals>
</resources>

View File

@@ -0,0 +1,257 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_bug_report">Informar de un error</string>
<string name="about_description">VniDrop transfiere archivos y carpetas directamente de un dispositivo a otro a través de la red, sin subirlos a ningún servicio de alojamiento. No hay que crear ninguna cuenta y no queda ninguna copia de su transferencia en la nube una vez que termina.</string>
<string name="about_is_direct">Una transferencia directa entre dispositivos: sus archivos van directamente al destinatario.</string>
<string name="about_is_encrypted">Las conexiones están autenticadas y cifradas de extremo a extremo (mediante Iroh), y los archivos entrantes se verifican por su huella de contenido.</string>
<string name="about_is_in_control">Usted decide quién recibe: apruebe cada solicitud o abra una transferencia a cualquiera que tenga la invitación.</string>
<string name="about_is_no_account">Sin cuenta: no hay nada que registrar.</string>
<string name="about_is_open">Código abierto, publicado bajo la licencia Apache 2.0.</string>
<string name="about_is_title">Qué es VniDrop</string>
<string name="about_isnt_cloud">No es almacenamiento en la nube: ningún servidor guarda sus archivos y nada queda en la nube después de una transferencia.</string>
<string name="about_isnt_public">No es difusión pública: una invitación es un enlace de acceso privado, no un anuncio para todos los que estén cerca.</string>
<string name="about_isnt_sync">No es un servicio de sincronización ni de copia de seguridad.</string>
<string name="about_isnt_title">Qué no es VniDrop</string>
<string name="about_license_label">Licencia</string>
<string name="about_privacy">Política de privacidad</string>
<string name="about_privacy_capability">Las invitaciones son claves de acceso. Trate un código QR, una etiqueta NFC o un archivo .vnd como un enlace de acceso privado y compártalo solo con las personas previstas, especialmente con «Cualquiera que tenga esta transferencia».</string>
<string name="about_privacy_deny">Denegación por defecto: VniDrop solo sirve el contenido de un recurso compartido activo y rechaza las solicitudes desconocidas.</string>
<string name="about_privacy_local">Los archivos recibidos se guardan en su dispositivo y nunca sobrescriben un archivo existente.</string>
<string name="about_privacy_policy_label">Política de privacidad</string>
<string name="about_privacy_relay">Si dos dispositivos no pueden conectarse directamente, la conexión cifrada puede retransmitirse. Los relés solo reenvían paquetes cifrados; nunca almacenan sus archivos.</string>
<string name="about_privacy_title">Privacidad y seguridad</string>
<string name="about_tagline">Envíe archivos directamente. Mantenga el control de quién los recibe.</string>
<string name="about_title">Acerca de</string>
<string name="appearance_auto_description">Seguir la apariencia clara u oscura de este dispositivo.</string>
<string name="appearance_dark_mode">Modo oscuro</string>
<string name="appearance_light_mode">Modo claro</string>
<string name="appearance_system_mode">Sistema</string>
<string name="appearance_title">Apariencia</string>
<string name="approval_connection_request">Solicitud de recepción</string>
<string name="approval_endpoint_id">ID del dispositivo: %1$s</string>
<string name="approval_nearby_device">Un dispositivo cercano</string>
<string name="approval_pending_count">%1$d solicitudes en espera</string>
<string name="approval_request_body">%1$s quiere recibir «%2$s».</string>
<string name="battery_level_title">Nivel de batería</string>
<string name="bug_report_contact_hint">nombre@ejemplo.com</string>
<string name="bug_report_contact_label">Correo de contacto (opcional)</string>
<string name="bug_report_description">Cuéntenos qué salió mal. Adjuntamos información del dispositivo y, opcionalmente, registros recientes (con los valores sensibles ocultos).</string>
<string name="bug_report_device_section">Información del dispositivo</string>
<string name="bug_report_expected_hint">Describa lo que esperaba que ocurriera</string>
<string name="bug_report_expected_label">¿Qué esperaba?</string>
<string name="bug_report_include_logs">Incluir registros recientes</string>
<string name="bug_report_include_logs_description">Nos ayuda a diagnosticar el problema. Los valores sensibles se ocultan antes de enviarlos.</string>
<string name="bug_report_logs_size">Tamaño del archivo de registros</string>
<string name="bug_report_missing_expected">Describa lo que esperaba.</string>
<string name="bug_report_missing_what">Describa lo que ocurrió.</string>
<string name="bug_report_steps_hint">Enumere los pasos para reproducir el problema</string>
<string name="bug_report_steps_label">Pasos para reproducir (opcional)</string>
<string name="bug_report_submit">Enviar informe</string>
<string name="bug_report_submit_failed">No se pudo enviar el informe de error. Inténtelo de nuevo más tarde.</string>
<string name="bug_report_submitted">Gracias: su informe de error se ha registrado.</string>
<string name="bug_report_submitting">Enviando…</string>
<string name="bug_report_what_hint">Describa lo que ocurrió</string>
<string name="bug_report_what_label">¿Qué ocurrió?</string>
<string name="button_approve">Aprobar</string>
<string name="button_back">Atrás</string>
<string name="button_cancel">Cancelar</string>
<string name="button_cancel_receive">Cancelar</string>
<string name="button_change_files">Cambiar archivos</string>
<string name="button_choose_files">Elegir archivos</string>
<string name="button_choose_folder">Elegir carpeta</string>
<string name="button_clear">Borrar</string>
<string name="button_close">Cerrar</string>
<string name="button_create_new_transfer">Nueva transferencia</string>
<string name="button_delete_transfer">Eliminar transferencia</string>
<string name="button_download_invitation">Guardar archivo .vnd</string>
<string name="button_native_share">Compartir invitación</string>
<string name="button_open_settings">Abrir Ajustes</string>
<string name="button_receive">Recibir</string>
<string name="button_receive_files">Empezar a recibir</string>
<string name="button_refuse">Rechazar</string>
<string name="button_remove_file">Quitar archivo</string>
<string name="button_reset_default">Usar predeterminado</string>
<string name="button_retry">Reintentar</string>
<string name="button_share_file">Empezar a compartir</string>
<string name="button_sharing_file">Preparando la transferencia…</string>
<string name="button_show_in_files">Mostrar en Archivos</string>
<string name="button_write_nfc">Escribir en etiqueta NFC</string>
<string name="device_model_title">Modelo del dispositivo</string>
<string name="device_name_title">Nombre del dispositivo</string>
<string name="diagnostics_description">Enviar informes de fallos y eventos de uso anónimos para ayudarnos a mejorar VniDrop. Puede desactivarlo en cualquier momento. Las invitaciones, las rutas de archivos y el contenido de las transferencias nunca se incluyen.</string>
<string name="diagnostics_disabled_message">El uso compartido de diagnósticos está desactivado.</string>
<string name="diagnostics_enabled_message">El uso compartido de diagnósticos está activado.</string>
<string name="diagnostics_title">Compartir diagnósticos</string>
<string name="error_camera">Se necesita acceso a la cámara para escanear un código QR.</string>
<string name="error_device_info">No se pudo cargar la información del dispositivo.</string>
<string name="error_filesystem">VniDrop no pudo acceder a los archivos o la carpeta seleccionados. Compruebe los permisos e inténtelo de nuevo.</string>
<string name="error_generic">Algo salió mal. Inténtelo de nuevo.</string>
<string name="error_initialization">VniDrop no pudo terminar de iniciarse. Cierre la app e inténtelo de nuevo.</string>
<string name="error_invalid_ticket">No se pudo leer esta invitación. Pida al remitente una nueva.</string>
<string name="error_invitation_empty">Esa invitación está vacía. Intente abrirla de nuevo.</string>
<string name="error_missing_native_library">Falta la biblioteca nativa de VniDrop en esta versión.</string>
<string name="error_nfc">No se pudo usar esta etiqueta NFC. Pruebe con otra.</string>
<string name="error_permission">El remitente no ha aprobado esta transferencia, o fue rechazada.</string>
<string name="error_repository">VniDrop no pudo guardar los datos de la transferencia en este dispositivo.</string>
<string name="error_selection_failed">No se pudo abrir el elemento seleccionado. Intente elegirlo de nuevo.</string>
<string name="error_share_empty">Seleccione al menos un elemento para compartir.</string>
<string name="error_socket_bind">VniDrop no pudo abrir sus sockets de red en este dispositivo.</string>
<string name="error_starting_up">VniDrop todavía se está iniciando. Vuelva a abrir la invitación en un momento.</string>
<string name="error_transfer">No se pudo completar la transferencia. Compruebe su conexión e inténtelo de nuevo.</string>
<string name="field_receiver_name">Nombre del destinatario</string>
<string name="field_sender_name">Nombre del remitente</string>
<string name="field_transfer_name">Nombre de la transferencia</string>
<string name="field_username">Nombre visible</string>
<string name="folder_status_permission_required">Permiso necesario</string>
<string name="folder_status_unavailable">No disponible</string>
<string name="folder_status_validating">Comprobando carpeta…</string>
<string name="folder_status_writable">Listo</string>
<string name="metadata_files">Archivos</string>
<string name="metadata_size">Tamaño</string>
<string name="metadata_status">Estado</string>
<string name="nav_receive">Recibir</string>
<string name="nav_send">Enviar</string>
<string name="nav_settings">Ajustes</string>
<string name="network_title">Red</string>
<string name="notifications_description">Reciba avisos sobre nuevas solicitudes de recepción cuando VniDrop está en segundo plano.</string>
<string name="notifications_enabled_message">Notificaciones activadas.</string>
<string name="notifications_local_title">Permitir notificaciones</string>
<string name="notifications_permission_denied">Las notificaciones están desactivadas para VniDrop. Puede activarlas en Ajustes.</string>
<string name="notifications_settings_open_failed">No se pudieron abrir los ajustes de notificaciones.</string>
<string name="notifications_title">Notificaciones</string>
<string name="notifications_unsupported">Las notificaciones no están disponibles en este dispositivo.</string>
<string name="os_version_title">Sistema operativo</string>
<string name="preferences_receive_folder_title">Guardar las transferencias recibidas en</string>
<string name="preferences_title">Preferencias</string>
<string name="progress_cancelled">Cancelado</string>
<string name="progress_completed">Completado</string>
<string name="progress_connected">Conectado</string>
<string name="progress_connecting">Conectando</string>
<string name="progress_downloading">Descargando</string>
<string name="progress_failed">Fallido</string>
<string name="progress_getting_ready">Preparándose</string>
<string name="progress_interrupted">Transferencia interrumpida</string>
<string name="progress_preparing">Preparando</string>
<string name="progress_ready">Listo</string>
<string name="progress_receiving">Recibiendo</string>
<string name="progress_requesting_access">Solicitando acceso</string>
<string name="progress_saving">Guardando</string>
<string name="progress_sending">Enviando</string>
<string name="progress_sending_to_count">Enviando a %1$d</string>
<string name="progress_share_ready">Listo para compartir</string>
<string name="progress_working">Trabajando…</string>
<string name="receive_choose_method_body">Elija el método de invitación de que disponga.</string>
<string name="receive_choose_method_title">¿Cómo quiere conectarse?</string>
<string name="receive_clear_history">Borrar historial</string>
<string name="receive_clear_history_description">Todas las recepciones completadas, fallidas y canceladas se eliminarán del historial de VniDrop. Los archivos descargados permanecerán en este dispositivo.</string>
<string name="receive_clear_history_title">¿Borrar el historial de recepción?</string>
<string name="receive_completed">Transferencia recibida.</string>
<string name="receive_delete_history_description">«%1$s» se eliminará del historial de VniDrop. El archivo descargado permanecerá en este dispositivo.</string>
<string name="receive_delete_history_item">Eliminar del historial de recepción</string>
<string name="receive_delete_history_title">¿Quitar del historial?</string>
<string name="receive_empty_body">Abra una invitación de VniDrop, escanee un código QR o acerque una etiqueta NFC.</string>
<string name="receive_empty_title">Aún no se ha recibido nada</string>
<string name="receive_history_cleared">Historial de recepción borrado.</string>
<string name="receive_history_title">Historial</string>
<string name="receive_method_file">Abrir una invitación .vnd</string>
<string name="receive_method_file_description">Elija una invitación guardada o compartida en este dispositivo.</string>
<string name="receive_method_nfc">Leer etiqueta NFC</string>
<string name="receive_method_nfc_description">Acerque este dispositivo a la etiqueta de invitación del remitente.</string>
<string name="receive_method_scan">Escanear código QR</string>
<string name="receive_method_scan_description">Use la cámara para escanear el código de VniDrop del remitente.</string>
<string name="receive_new_subtitle">Transferencias que ha recibido en este dispositivo.</string>
<string name="receive_nfc_waiting">Acerque a la etiqueta NFC…</string>
<string name="receive_open_files_failed">No se pudo abrir VniDrop en Archivos.</string>
<string name="receive_review_title">Revisar transferencia</string>
<string name="receive_title">Recibir</string>
<string name="receive_unknown_transfer">Transferencia de VniDrop</string>
<string name="send_access_anyone">Cualquiera que tenga esta transferencia</string>
<string name="send_access_anyone_description">No se requiere aprobación. Úselo solo para elementos que no le importe compartir.</string>
<string name="send_access_anyone_warning">Cualquiera que tenga la invitación puede descargar hasta que deje de compartir. No lo use para elementos privados o sensibles.</string>
<string name="send_access_approval">Preguntar antes de cada descarga</string>
<string name="send_access_approval_description">Usted aprueba o rechaza a cada nuevo destinatario.</string>
<string name="send_access_title">¿Quién puede recibirla?</string>
<string name="send_choose_file_body">Seleccione archivos o una carpeta de este dispositivo. Podrá revisar la selección antes de crear la transferencia.</string>
<string name="send_choose_file_title">Elija qué compartir</string>
<string name="send_empty_body">Cree una transferencia, decida quién puede recibirla y luego invite a esas personas con un código QR, una etiqueta NFC o un archivo de invitación.</string>
<string name="send_empty_title">Aún no se ha compartido nada</string>
<string name="send_file_size_unknown">Tamaño no disponible</string>
<string name="send_folder_label">Carpeta</string>
<string name="send_new_transfer_description">Crear una nueva transferencia</string>
<string name="send_new_transfer_title">Nueva transferencia</string>
<string name="send_review_title">Revisar transferencia</string>
<string name="send_selected_files_count">%1$d archivos seleccionados</string>
<string name="send_stop_sharing">Dejar de compartir</string>
<string name="send_stop_sharing_description">Esto detiene la transferencia e interrumpe a quien esté descargándola. Permanece en su historial como Detenida.</string>
<string name="send_subtitle">Transferencias que está compartiendo desde este dispositivo.</string>
<string name="send_title">Enviar</string>
<string name="send_transfer_created">Transferencia creada.</string>
<string name="send_transfer_details_title">Detalles de la transferencia</string>
<string name="send_transfers_title">Sus transferencias</string>
<string name="settings_subtitle">Su nombre, dónde se guardan las transferencias, la apariencia y las notificaciones.</string>
<string name="settings_title">Ajustes</string>
<string name="snackbar_dismiss">Descartar</string>
<string name="status_available">Disponible</string>
<string name="status_cancelled">Cancelado</string>
<string name="status_completed">Completado</string>
<string name="status_failed">Fallido</string>
<string name="status_preparing">Preparando</string>
<string name="status_receiving">Recibiendo</string>
<string name="status_stopped">Detenido</string>
<string name="storage_calculating">Calculando…</string>
<string name="storage_delete_transfers">Eliminar todas las transferencias</string>
<string name="storage_delete_transfers_description">Esto borra todos los registros de transferencias enviadas y recibidas de su historial. Sus archivos recibidos no se eliminan. Nota: el motor de transferencia conserva su contenido almacenado, por lo que los datos de transferencia pueden no disminuir. Esto no se puede deshacer.</string>
<string name="storage_deleting">Eliminando…</string>
<string name="storage_footer">Los datos de transferencia los gestiona el motor de transferencia: su historial más el contenido de los archivos que ha compartido. El botón de abajo borra sus registros de transferencias; el motor conserva su contenido almacenado, por lo que este valor puede no bajar. Los archivos recibidos son los archivos que ha descargado y nunca se eliminan aquí.</string>
<string name="storage_received_files">Archivos recibidos</string>
<string name="storage_temporary">Archivos temporales</string>
<string name="storage_title">Almacenamiento</string>
<string name="storage_total">Total</string>
<string name="storage_transfer_data">Datos de transferencia</string>
<string name="storage_transfers_deleted">Todas las transferencias eliminadas</string>
<string name="transfer_activity_description">Vea las actualizaciones importantes de esta transferencia</string>
<string name="transfer_activity_title">Actividad</string>
<string name="transfer_delete_description">«%1$s» dejará de compartirse y su historial de transferencia se eliminará de este dispositivo.</string>
<string name="transfer_delete_title">¿Eliminar la transferencia?</string>
<string name="transfer_deleted">Transferencia eliminada.</string>
<string name="transfer_deleting">Eliminando…</string>
<string name="transfer_details_title">Detalles de la transferencia</string>
<string name="transfer_event_approved">Acceso del destinatario aprobado</string>
<string name="transfer_event_completed">Un destinatario completó la transferencia</string>
<string name="transfer_event_connecting">Conectando con el remitente</string>
<string name="transfer_event_downloading">Descargando</string>
<string name="transfer_event_failed">La transferencia tuvo un problema</string>
<string name="transfer_event_preparing">Preparando su transferencia</string>
<string name="transfer_event_ready">Listo para compartir</string>
<string name="transfer_event_refused">Acceso del destinatario rechazado</string>
<string name="transfer_event_requested">Un destinatario solicitó acceso</string>
<string name="transfer_event_saving">Guardando</string>
<string name="transfer_event_stopped">Se dejó de compartir</string>
<string name="transfer_event_updated">Transferencia actualizada</string>
<string name="transfer_invitation_saved">Invitación guardada.</string>
<string name="transfer_nearby_device">Dispositivo cercano</string>
<string name="transfer_nfc_unavailable">La escritura de etiquetas NFC no está disponible en este dispositivo.</string>
<string name="transfer_nfc_waiting">Acerque su dispositivo a una etiqueta NFC grabable.</string>
<string name="transfer_nfc_written">Invitación escrita en la etiqueta NFC.</string>
<string name="transfer_no_activity">Todavía no hay actividad que mostrar.</string>
<string name="transfer_no_receivers">Nadie ha solicitado aún esta transferencia.</string>
<string name="transfer_receiver_accepted">Aprobado: esperando a que se complete</string>
<string name="transfer_receiver_completed">Recibido correctamente</string>
<string name="transfer_receiver_expired">Solicitud caducada</string>
<string name="transfer_receiver_refused">Solicitud rechazada</string>
<string name="transfer_receiver_requested">Esperando su aprobación</string>
<string name="transfer_receiver_unknown">Estado no disponible</string>
<string name="transfer_receivers_completed_count">%1$d completados</string>
<string name="transfer_receivers_description">Solicitudes, aprobaciones y entregas completadas</string>
<string name="transfer_receivers_pending">%1$d en espera</string>
<string name="transfer_receivers_title">Destinatarios</string>
<string name="transfer_scan_qr">Escanee con VniDrop para recibir esta transferencia</string>
<string name="transfer_share_description">Código QR, archivo de invitación y opciones cercanas</string>
<string name="transfer_share_title">Compartir</string>
<string name="value_unavailable">No disponible</string>
<string name="version_title">Versión de la app</string>
<plurals name="transfer_file_count">
<item quantity="one">%1$d archivo</item>
<item quantity="other">%1$d archivos</item>
</plurals>
</resources>

View File

@@ -0,0 +1,257 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_bug_report">Signaler un bug</string>
<string name="about_description">VniDrop transfère fichiers et dossiers directement dun appareil à un autre sur le réseau, sans les téléverser vers un service dhébergement. Aucun compte à créer, et aucune copie de votre transfert ne reste dans le cloud une fois terminé.</string>
<string name="about_is_direct">Un transfert direct dappareil à appareil — vos fichiers vont droit au destinataire.</string>
<string name="about_is_encrypted">Les connexions sont authentifiées et chiffrées de bout en bout (via Iroh), et les fichiers entrants sont vérifiés par leur empreinte de contenu.</string>
<string name="about_is_in_control">Vous décidez qui reçoit : approuvez chaque demande, ou ouvrez un transfert à toute personne disposant de linvitation.</string>
<string name="about_is_no_account">Sans compte — il ny a rien à créer.</string>
<string name="about_is_open">Open source, publié sous licence Apache 2.0.</string>
<string name="about_is_title">Ce quest VniDrop</string>
<string name="about_isnt_cloud">Pas un stockage cloud — aucun serveur ne détient vos fichiers, et rien nattend dans le cloud après un transfert.</string>
<string name="about_isnt_public">Pas une diffusion publique — une invitation est un lien daccès privé, pas une annonce à tout le voisinage.</string>
<string name="about_isnt_sync">Pas un service de synchronisation ou de sauvegarde.</string>
<string name="about_isnt_title">Ce que VniDrop nest pas</string>
<string name="about_license_label">Licence</string>
<string name="about_privacy">Politique de confidentialité</string>
<string name="about_privacy_capability">Les invitations sont des clés daccès. Traitez un QR code, un tag NFC ou un fichier .vnd comme un lien daccès privé et ne le partagez quavec les personnes visées — en particulier avec « Toute personne disposant de ce transfert ».</string>
<string name="about_privacy_deny">Refus par défaut — VniDrop ne sert que le contenu dun partage actif et rejette les demandes inconnues.</string>
<string name="about_privacy_local">Les fichiers reçus sont enregistrés sur votre appareil et nécrasent jamais un fichier existant.</string>
<string name="about_privacy_policy_label">Politique de confidentialité</string>
<string name="about_privacy_relay">Si deux appareils ne peuvent pas se connecter directement, la connexion chiffrée peut être relayée. Les relais ne transmettent que des paquets chiffrés ; ils ne stockent jamais vos fichiers.</string>
<string name="about_privacy_title">Confidentialité et sécurité</string>
<string name="about_tagline">Envoyez des fichiers directement. Gardez le contrôle de qui les reçoit.</string>
<string name="about_title">À propos</string>
<string name="appearance_auto_description">Suivre lapparence claire ou sombre de cet appareil.</string>
<string name="appearance_dark_mode">Mode sombre</string>
<string name="appearance_light_mode">Mode clair</string>
<string name="appearance_system_mode">Système</string>
<string name="appearance_title">Apparence</string>
<string name="approval_connection_request">Demande de réception</string>
<string name="approval_endpoint_id">Identifiant de lappareil : %1$s</string>
<string name="approval_nearby_device">Un appareil à proximité</string>
<string name="approval_pending_count">%1$d demandes en attente</string>
<string name="approval_request_body">%1$s souhaite recevoir « %2$s ».</string>
<string name="battery_level_title">Niveau de batterie</string>
<string name="bug_report_contact_hint">nom@exemple.com</string>
<string name="bug_report_contact_label">E-mail de contact (facultatif)</string>
<string name="bug_report_description">Dites-nous ce qui sest passé. Nous joignons les infos de lappareil et, en option, les journaux récents (valeurs sensibles masquées).</string>
<string name="bug_report_device_section">Informations sur lappareil</string>
<string name="bug_report_expected_hint">Décrivez ce à quoi vous vous attendiez</string>
<string name="bug_report_expected_label">À quoi vous attendiez-vous ?</string>
<string name="bug_report_include_logs">Inclure les journaux récents</string>
<string name="bug_report_include_logs_description">Nous aide à diagnostiquer le problème. Les valeurs sensibles sont masquées avant lenvoi.</string>
<string name="bug_report_logs_size">Taille de la pièce jointe des journaux</string>
<string name="bug_report_missing_expected">Veuillez décrire ce à quoi vous vous attendiez.</string>
<string name="bug_report_missing_what">Veuillez décrire ce qui sest passé.</string>
<string name="bug_report_steps_hint">Listez les étapes pour reproduire le problème</string>
<string name="bug_report_steps_label">Étapes pour reproduire (facultatif)</string>
<string name="bug_report_submit">Envoyer le rapport</string>
<string name="bug_report_submit_failed">Impossible denvoyer le rapport de bug. Réessayez plus tard.</string>
<string name="bug_report_submitted">Merci — votre rapport de bug a été enregistré.</string>
<string name="bug_report_submitting">Envoi…</string>
<string name="bug_report_what_hint">Décrivez ce qui sest passé</string>
<string name="bug_report_what_label">Que sest-il passé ?</string>
<string name="button_approve">Approuver</string>
<string name="button_back">Retour</string>
<string name="button_cancel">Annuler</string>
<string name="button_cancel_receive">Annuler</string>
<string name="button_change_files">Modifier les fichiers</string>
<string name="button_choose_files">Choisir des fichiers</string>
<string name="button_choose_folder">Choisir un dossier</string>
<string name="button_clear">Effacer</string>
<string name="button_close">Fermer</string>
<string name="button_create_new_transfer">Nouveau transfert</string>
<string name="button_delete_transfer">Supprimer le transfert</string>
<string name="button_download_invitation">Enregistrer le fichier .vnd</string>
<string name="button_native_share">Partager linvitation</string>
<string name="button_open_settings">Ouvrir les Réglages</string>
<string name="button_receive">Recevoir</string>
<string name="button_receive_files">Commencer à recevoir</string>
<string name="button_refuse">Refuser</string>
<string name="button_remove_file">Retirer le fichier</string>
<string name="button_reset_default">Valeur par défaut</string>
<string name="button_retry">Réessayer</string>
<string name="button_share_file">Commencer le partage</string>
<string name="button_sharing_file">Préparation du transfert…</string>
<string name="button_show_in_files">Afficher dans Fichiers</string>
<string name="button_write_nfc">Écrire sur un tag NFC</string>
<string name="device_model_title">Modèle de lappareil</string>
<string name="device_name_title">Nom de lappareil</string>
<string name="diagnostics_description">Envoyer des rapports de plantage et des événements dutilisation anonymes pour nous aider à améliorer VniDrop. Vous pouvez désactiver cela à tout moment. Les invitations, chemins de fichiers et contenus de transfert ne sont jamais inclus.</string>
<string name="diagnostics_disabled_message">Le partage des diagnostics est désactivé.</string>
<string name="diagnostics_enabled_message">Le partage des diagnostics est activé.</string>
<string name="diagnostics_title">Partager les diagnostics</string>
<string name="error_camera">Laccès à la caméra est nécessaire pour scanner un QR code.</string>
<string name="error_device_info">Impossible de charger les informations de lappareil.</string>
<string name="error_filesystem">VniDrop na pas pu accéder aux fichiers ou au dossier sélectionnés. Vérifiez les autorisations et réessayez.</string>
<string name="error_generic">Une erreur est survenue. Réessayez.</string>
<string name="error_initialization">VniDrop na pas pu terminer son démarrage. Fermez lapp et réessayez.</string>
<string name="error_invalid_ticket">Cette invitation na pas pu être lue. Demandez-en une nouvelle à lexpéditeur.</string>
<string name="error_invitation_empty">Cette invitation est vide. Essayez de louvrir à nouveau.</string>
<string name="error_missing_native_library">La bibliothèque native de VniDrop est absente de cette version.</string>
<string name="error_nfc">Ce tag NFC na pas pu être utilisé. Essayez-en un autre.</string>
<string name="error_permission">Lexpéditeur na pas approuvé ce transfert, ou il a été refusé.</string>
<string name="error_repository">VniDrop na pas pu enregistrer les données de transfert sur cet appareil.</string>
<string name="error_selection_failed">Impossible douvrir lélément sélectionné. Essayez de le choisir à nouveau.</string>
<string name="error_share_empty">Sélectionnez au moins un élément à partager.</string>
<string name="error_socket_bind">VniDrop na pas pu ouvrir ses sockets réseau sur cet appareil.</string>
<string name="error_starting_up">VniDrop démarre encore. Rouvrez linvitation dans un instant.</string>
<string name="error_transfer">Le transfert na pas pu être terminé. Vérifiez votre connexion et réessayez.</string>
<string name="field_receiver_name">Nom du destinataire</string>
<string name="field_sender_name">Nom de lexpéditeur</string>
<string name="field_transfer_name">Nom du transfert</string>
<string name="field_username">Nom daffichage</string>
<string name="folder_status_permission_required">Autorisation requise</string>
<string name="folder_status_unavailable">Indisponible</string>
<string name="folder_status_validating">Vérification du dossier…</string>
<string name="folder_status_writable">Prêt</string>
<string name="metadata_files">Fichiers</string>
<string name="metadata_size">Taille</string>
<string name="metadata_status">Statut</string>
<string name="nav_receive">Recevoir</string>
<string name="nav_send">Envoyer</string>
<string name="nav_settings">Réglages</string>
<string name="network_title">Réseau</string>
<string name="notifications_description">Soyez averti des nouvelles demandes de réception lorsque VniDrop est en arrière-plan.</string>
<string name="notifications_enabled_message">Notifications activées.</string>
<string name="notifications_local_title">Autoriser les notifications</string>
<string name="notifications_permission_denied">Les notifications sont désactivées pour VniDrop. Vous pouvez les activer dans les Réglages.</string>
<string name="notifications_settings_open_failed">Impossible douvrir les réglages de notifications.</string>
<string name="notifications_title">Notifications</string>
<string name="notifications_unsupported">Les notifications ne sont pas disponibles sur cet appareil.</string>
<string name="os_version_title">Système dexploitation</string>
<string name="preferences_receive_folder_title">Enregistrer les transferts reçus dans</string>
<string name="preferences_title">Préférences</string>
<string name="progress_cancelled">Annulé</string>
<string name="progress_completed">Terminé</string>
<string name="progress_connected">Connecté</string>
<string name="progress_connecting">Connexion</string>
<string name="progress_downloading">Téléchargement</string>
<string name="progress_failed">Échec</string>
<string name="progress_getting_ready">Préparation</string>
<string name="progress_interrupted">Transfert interrompu</string>
<string name="progress_preparing">Préparation</string>
<string name="progress_ready">Prêt</string>
<string name="progress_receiving">Réception</string>
<string name="progress_requesting_access">Demande daccès</string>
<string name="progress_saving">Enregistrement</string>
<string name="progress_sending">Envoi</string>
<string name="progress_sending_to_count">Envoi à %1$d</string>
<string name="progress_share_ready">Prêt à partager</string>
<string name="progress_working">En cours…</string>
<string name="receive_choose_method_body">Choisissez la méthode dinvitation à votre disposition.</string>
<string name="receive_choose_method_title">Comment souhaitez-vous vous connecter ?</string>
<string name="receive_clear_history">Effacer lhistorique</string>
<string name="receive_clear_history_description">Toutes les réceptions terminées, échouées et annulées seront retirées de lhistorique de VniDrop. Les fichiers téléchargés resteront sur cet appareil.</string>
<string name="receive_clear_history_title">Effacer lhistorique de réception ?</string>
<string name="receive_completed">Transfert reçu.</string>
<string name="receive_delete_history_description">« %1$s » sera retiré de lhistorique de VniDrop. Le fichier téléchargé restera sur cet appareil.</string>
<string name="receive_delete_history_item">Supprimer de lhistorique de réception</string>
<string name="receive_delete_history_title">Retirer de lhistorique ?</string>
<string name="receive_empty_body">Ouvrez une invitation VniDrop, scannez un QR code ou approchez un tag NFC.</string>
<string name="receive_empty_title">Rien reçu pour linstant</string>
<string name="receive_history_cleared">Historique de réception effacé.</string>
<string name="receive_history_title">Historique</string>
<string name="receive_method_file">Ouvrir une invitation .vnd</string>
<string name="receive_method_file_description">Choisissez une invitation enregistrée ou partagée sur cet appareil.</string>
<string name="receive_method_nfc">Lire un tag NFC</string>
<string name="receive_method_nfc_description">Approchez cet appareil du tag dinvitation de lexpéditeur.</string>
<string name="receive_method_scan">Scanner un QR code</string>
<string name="receive_method_scan_description">Utilisez la caméra pour scanner le code VniDrop de lexpéditeur.</string>
<string name="receive_new_subtitle">Transferts que vous avez reçus sur cet appareil.</string>
<string name="receive_nfc_waiting">Approchez du tag NFC…</string>
<string name="receive_open_files_failed">Impossible douvrir VniDrop dans Fichiers.</string>
<string name="receive_review_title">Vérifier le transfert</string>
<string name="receive_title">Recevoir</string>
<string name="receive_unknown_transfer">Transfert VniDrop</string>
<string name="send_access_anyone">Toute personne disposant de ce transfert</string>
<string name="send_access_anyone_description">Aucune approbation requise. À nutiliser que pour des éléments que vous êtes à laise de partager.</string>
<string name="send_access_anyone_warning">Toute personne disposant de linvitation peut télécharger jusquà ce que vous arrêtiez le partage. Ne lutilisez pas pour des éléments privés ou sensibles.</string>
<string name="send_access_approval">Demander avant chaque téléchargement</string>
<string name="send_access_approval_description">Vous approuvez ou refusez chaque nouveau destinataire.</string>
<string name="send_access_title">Qui peut le recevoir ?</string>
<string name="send_choose_file_body">Sélectionnez des fichiers ou un dossier sur cet appareil. Vous pourrez vérifier la sélection avant de créer le transfert.</string>
<string name="send_choose_file_title">Choisissez quoi partager</string>
<string name="send_empty_body">Créez un transfert, décidez qui peut le recevoir, puis invitez-les avec un QR code, un tag NFC ou un fichier dinvitation.</string>
<string name="send_empty_title">Rien partagé pour linstant</string>
<string name="send_file_size_unknown">Taille indisponible</string>
<string name="send_folder_label">Dossier</string>
<string name="send_new_transfer_description">Créer un nouveau transfert</string>
<string name="send_new_transfer_title">Nouveau transfert</string>
<string name="send_review_title">Vérifier le transfert</string>
<string name="send_selected_files_count">%1$d fichiers sélectionnés</string>
<string name="send_stop_sharing">Arrêter le partage</string>
<string name="send_stop_sharing_description">Cela arrête le transfert et interrompt toute personne en train de le télécharger. Il reste dans votre historique en tant quArrêté.</string>
<string name="send_subtitle">Transferts que vous partagez depuis cet appareil.</string>
<string name="send_title">Envoyer</string>
<string name="send_transfer_created">Transfert créé.</string>
<string name="send_transfer_details_title">Détails du transfert</string>
<string name="send_transfers_title">Vos transferts</string>
<string name="settings_subtitle">Votre nom, lemplacement denregistrement des transferts, lapparence et les notifications.</string>
<string name="settings_title">Réglages</string>
<string name="snackbar_dismiss">Ignorer</string>
<string name="status_available">Disponible</string>
<string name="status_cancelled">Annulé</string>
<string name="status_completed">Terminé</string>
<string name="status_failed">Échec</string>
<string name="status_preparing">Préparation</string>
<string name="status_receiving">Réception</string>
<string name="status_stopped">Arrêté</string>
<string name="storage_calculating">Calcul…</string>
<string name="storage_delete_transfers">Supprimer tous les transferts</string>
<string name="storage_delete_transfers_description">Cela efface tous les enregistrements de transferts envoyés et reçus de votre historique. Vos fichiers reçus ne sont pas supprimés. Remarque : le moteur de transfert conserve son contenu stocké, donc les données de transfert peuvent ne pas diminuer. Cette action est irréversible.</string>
<string name="storage_deleting">Suppression…</string>
<string name="storage_footer">Les données de transfert sont gérées par le moteur de transfert — votre historique ainsi que le contenu des fichiers que vous avez partagés. Le bouton ci-dessous efface vos enregistrements de transferts ; le moteur conserve son contenu stocké, donc cette valeur peut ne pas baisser. Les fichiers reçus sont vos fichiers téléchargés et ne sont jamais supprimés ici.</string>
<string name="storage_received_files">Fichiers reçus</string>
<string name="storage_temporary">Fichiers temporaires</string>
<string name="storage_title">Stockage</string>
<string name="storage_total">Total</string>
<string name="storage_transfer_data">Données de transfert</string>
<string name="storage_transfers_deleted">Tous les transferts supprimés</string>
<string name="transfer_activity_description">Consultez les mises à jour importantes de ce transfert</string>
<string name="transfer_activity_title">Activité</string>
<string name="transfer_delete_description">« %1$s » cessera dêtre partagé et son historique de transfert sera retiré de cet appareil.</string>
<string name="transfer_delete_title">Supprimer le transfert ?</string>
<string name="transfer_deleted">Transfert supprimé.</string>
<string name="transfer_deleting">Suppression…</string>
<string name="transfer_details_title">Détails du transfert</string>
<string name="transfer_event_approved">Accès du destinataire approuvé</string>
<string name="transfer_event_completed">Un destinataire a terminé le transfert</string>
<string name="transfer_event_connecting">Connexion à lexpéditeur</string>
<string name="transfer_event_downloading">Téléchargement</string>
<string name="transfer_event_failed">Le transfert a rencontré un problème</string>
<string name="transfer_event_preparing">Préparation de votre transfert</string>
<string name="transfer_event_ready">Prêt à partager</string>
<string name="transfer_event_refused">Accès du destinataire refusé</string>
<string name="transfer_event_requested">Un destinataire a demandé laccès</string>
<string name="transfer_event_saving">Enregistrement</string>
<string name="transfer_event_stopped">Partage arrêté</string>
<string name="transfer_event_updated">Transfert mis à jour</string>
<string name="transfer_invitation_saved">Invitation enregistrée.</string>
<string name="transfer_nearby_device">Appareil à proximité</string>
<string name="transfer_nfc_unavailable">Lécriture de tag NFC nest pas disponible sur cet appareil.</string>
<string name="transfer_nfc_waiting">Approchez votre appareil dun tag NFC inscriptible.</string>
<string name="transfer_nfc_written">Invitation écrite sur le tag NFC.</string>
<string name="transfer_no_activity">Aucune activité à afficher pour linstant.</string>
<string name="transfer_no_receivers">Personne na encore demandé ce transfert.</string>
<string name="transfer_receiver_accepted">Approuvé — en attente de la fin</string>
<string name="transfer_receiver_completed">Reçu avec succès</string>
<string name="transfer_receiver_expired">Demande expirée</string>
<string name="transfer_receiver_refused">Demande refusée</string>
<string name="transfer_receiver_requested">En attente de votre approbation</string>
<string name="transfer_receiver_unknown">Statut indisponible</string>
<string name="transfer_receivers_completed_count">%1$d terminés</string>
<string name="transfer_receivers_description">Demandes, approbations et livraisons terminées</string>
<string name="transfer_receivers_pending">%1$d en attente</string>
<string name="transfer_receivers_title">Destinataires</string>
<string name="transfer_scan_qr">Scannez avec VniDrop pour recevoir ce transfert</string>
<string name="transfer_share_description">QR code, fichier dinvitation et options à proximité</string>
<string name="transfer_share_title">Partager</string>
<string name="value_unavailable">Non disponible</string>
<string name="version_title">Version de lapp</string>
<plurals name="transfer_file_count">
<item quantity="one">%1$d fichier</item>
<item quantity="other">%1$d fichiers</item>
</plurals>
</resources>

View File

@@ -0,0 +1,257 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_bug_report">Segnala un bug</string>
<string name="about_description">VniDrop trasferisce file e cartelle direttamente da un dispositivo a un altro sulla rete, senza caricarli su alcun servizio di hosting. Non cè alcun account da creare e nessuna copia del suo trasferimento rimane nel cloud una volta terminato.</string>
<string name="about_is_direct">Un trasferimento diretto da dispositivo a dispositivo: i suoi file vanno direttamente al destinatario.</string>
<string name="about_is_encrypted">Le connessioni sono autenticate e cifrate end-to-end (tramite Iroh) e i file in arrivo vengono verificati tramite limpronta del loro contenuto.</string>
<string name="about_is_in_control">Lei decide chi riceve: approvi ogni richiesta oppure apra un trasferimento a chiunque disponga dellinvito.</string>
<string name="about_is_no_account">Senza account: non cè nulla da registrare.</string>
<string name="about_is_open">Open source, rilasciato con licenza Apache 2.0.</string>
<string name="about_is_title">Cosè VniDrop</string>
<string name="about_isnt_cloud">Non è unarchiviazione cloud: nessun server conserva i suoi file e nulla resta nel cloud dopo un trasferimento.</string>
<string name="about_isnt_public">Non è una diffusione pubblica: un invito è un link di accesso privato, non un annuncio a tutti nelle vicinanze.</string>
<string name="about_isnt_sync">Non è un servizio di sincronizzazione o di backup.</string>
<string name="about_isnt_title">Cosa non è VniDrop</string>
<string name="about_license_label">Licenza</string>
<string name="about_privacy">Informativa sulla privacy</string>
<string name="about_privacy_capability">Gli inviti sono chiavi di accesso. Tratti un codice QR, un tag NFC o un file .vnd come un link di accesso privato e lo condivida solo con le persone previste, soprattutto con «Chiunque abbia questo trasferimento».</string>
<string name="about_privacy_deny">Rifiuto predefinito: VniDrop serve solo il contenuto di una condivisione attiva e rifiuta le richieste sconosciute.</string>
<string name="about_privacy_local">I file ricevuti vengono salvati sul suo dispositivo e non sovrascrivono mai un file esistente.</string>
<string name="about_privacy_policy_label">Informativa sulla privacy</string>
<string name="about_privacy_relay">Se due dispositivi non riescono a connettersi direttamente, la connessione cifrata può essere instradata tramite relay. I relay inoltrano solo pacchetti cifrati; non memorizzano mai i suoi file.</string>
<string name="about_privacy_title">Privacy e sicurezza</string>
<string name="about_tagline">Invii file direttamente. Mantenga il controllo su chi li riceve.</string>
<string name="about_title">Informazioni</string>
<string name="appearance_auto_description">Segue laspetto chiaro o scuro di questo dispositivo.</string>
<string name="appearance_dark_mode">Modalità scura</string>
<string name="appearance_light_mode">Modalità chiara</string>
<string name="appearance_system_mode">Sistema</string>
<string name="appearance_title">Aspetto</string>
<string name="approval_connection_request">Richiesta di ricezione</string>
<string name="approval_endpoint_id">ID dispositivo: %1$s</string>
<string name="approval_nearby_device">Un dispositivo nelle vicinanze</string>
<string name="approval_pending_count">%1$d richieste in attesa</string>
<string name="approval_request_body">%1$s vuole ricevere «%2$s».</string>
<string name="battery_level_title">Livello batteria</string>
<string name="bug_report_contact_hint">nome@esempio.com</string>
<string name="bug_report_contact_label">Email di contatto (facoltativa)</string>
<string name="bug_report_description">Ci dica cosa è andato storto. Alleghiamo le informazioni sul dispositivo e, facoltativamente, i log recenti (con i valori sensibili oscurati).</string>
<string name="bug_report_device_section">Informazioni sul dispositivo</string>
<string name="bug_report_expected_hint">Descriva cosa si aspettava che accadesse</string>
<string name="bug_report_expected_label">Cosa si aspettava?</string>
<string name="bug_report_include_logs">Includi i log recenti</string>
<string name="bug_report_include_logs_description">Ci aiuta a diagnosticare il problema. I valori sensibili vengono oscurati prima dellinvio.</string>
<string name="bug_report_logs_size">Dimensione dellallegato dei log</string>
<string name="bug_report_missing_expected">Descriva cosa si aspettava.</string>
<string name="bug_report_missing_what">Descriva cosa è accaduto.</string>
<string name="bug_report_steps_hint">Elenchi i passaggi per riprodurre il problema</string>
<string name="bug_report_steps_label">Passaggi per riprodurre (facoltativi)</string>
<string name="bug_report_submit">Invia segnalazione</string>
<string name="bug_report_submit_failed">Impossibile inviare la segnalazione. Riprovi più tardi.</string>
<string name="bug_report_submitted">Grazie: la sua segnalazione è stata registrata.</string>
<string name="bug_report_submitting">Invio in corso…</string>
<string name="bug_report_what_hint">Descriva cosa è accaduto</string>
<string name="bug_report_what_label">Cosa è accaduto?</string>
<string name="button_approve">Approva</string>
<string name="button_back">Indietro</string>
<string name="button_cancel">Annulla</string>
<string name="button_cancel_receive">Annulla</string>
<string name="button_change_files">Cambia file</string>
<string name="button_choose_files">Scegli file</string>
<string name="button_choose_folder">Scegli cartella</string>
<string name="button_clear">Cancella</string>
<string name="button_close">Chiudi</string>
<string name="button_create_new_transfer">Nuovo trasferimento</string>
<string name="button_delete_transfer">Elimina trasferimento</string>
<string name="button_download_invitation">Salva file .vnd</string>
<string name="button_native_share">Condividi invito</string>
<string name="button_open_settings">Apri Impostazioni</string>
<string name="button_receive">Ricevi</string>
<string name="button_receive_files">Inizia a ricevere</string>
<string name="button_refuse">Rifiuta</string>
<string name="button_remove_file">Rimuovi file</string>
<string name="button_reset_default">Usa predefinito</string>
<string name="button_retry">Riprova</string>
<string name="button_share_file">Inizia a condividere</string>
<string name="button_sharing_file">Preparazione del trasferimento…</string>
<string name="button_show_in_files">Mostra in File</string>
<string name="button_write_nfc">Scrivi su tag NFC</string>
<string name="device_model_title">Modello del dispositivo</string>
<string name="device_name_title">Nome del dispositivo</string>
<string name="diagnostics_description">Invia report di arresto anomalo ed eventi duso anonimi per aiutarci a migliorare VniDrop. Può disattivarlo in qualsiasi momento. Inviti, percorsi dei file e contenuti dei trasferimenti non vengono mai inclusi.</string>
<string name="diagnostics_disabled_message">La condivisione dei dati diagnostici è disattivata.</string>
<string name="diagnostics_enabled_message">La condivisione dei dati diagnostici è attivata.</string>
<string name="diagnostics_title">Condividi dati diagnostici</string>
<string name="error_camera">Per scansionare un codice QR è necessario laccesso alla fotocamera.</string>
<string name="error_device_info">Impossibile caricare le informazioni sul dispositivo.</string>
<string name="error_filesystem">VniDrop non ha potuto accedere ai file o alla cartella selezionati. Controlli le autorizzazioni e riprovi.</string>
<string name="error_generic">Qualcosa è andato storto. Riprovi.</string>
<string name="error_initialization">VniDrop non ha potuto completare lavvio. Chiuda lapp e riprovi.</string>
<string name="error_invalid_ticket">Impossibile leggere questo invito. Ne chieda uno nuovo al mittente.</string>
<string name="error_invitation_empty">Questo invito è vuoto. Provi ad aprirlo di nuovo.</string>
<string name="error_missing_native_library">La libreria nativa di VniDrop non è presente in questa build.</string>
<string name="error_nfc">Impossibile usare questo tag NFC. Ne provi un altro.</string>
<string name="error_permission">Il mittente non ha approvato questo trasferimento, oppure è stato rifiutato.</string>
<string name="error_repository">VniDrop non ha potuto salvare i dati del trasferimento su questo dispositivo.</string>
<string name="error_selection_failed">Impossibile aprire lelemento selezionato. Provi a sceglierlo di nuovo.</string>
<string name="error_share_empty">Selezioni almeno un elemento da condividere.</string>
<string name="error_socket_bind">VniDrop non ha potuto aprire i suoi socket di rete su questo dispositivo.</string>
<string name="error_starting_up">VniDrop è ancora in fase di avvio. Riapra linvito tra un momento.</string>
<string name="error_transfer">Impossibile completare il trasferimento. Controlli la connessione e riprovi.</string>
<string name="field_receiver_name">Nome del destinatario</string>
<string name="field_sender_name">Nome del mittente</string>
<string name="field_transfer_name">Nome del trasferimento</string>
<string name="field_username">Nome visualizzato</string>
<string name="folder_status_permission_required">Autorizzazione necessaria</string>
<string name="folder_status_unavailable">Non disponibile</string>
<string name="folder_status_validating">Controllo della cartella…</string>
<string name="folder_status_writable">Pronta</string>
<string name="metadata_files">File</string>
<string name="metadata_size">Dimensione</string>
<string name="metadata_status">Stato</string>
<string name="nav_receive">Ricevi</string>
<string name="nav_send">Invia</string>
<string name="nav_settings">Impostazioni</string>
<string name="network_title">Rete</string>
<string name="notifications_description">Ricevi avvisi sulle nuove richieste di ricezione quando VniDrop è in background.</string>
<string name="notifications_enabled_message">Notifiche attivate.</string>
<string name="notifications_local_title">Consenti le notifiche</string>
<string name="notifications_permission_denied">Le notifiche sono disattivate per VniDrop. Può attivarle in Impostazioni.</string>
<string name="notifications_settings_open_failed">Impossibile aprire le impostazioni delle notifiche.</string>
<string name="notifications_title">Notifiche</string>
<string name="notifications_unsupported">Le notifiche non sono disponibili su questo dispositivo.</string>
<string name="os_version_title">Sistema operativo</string>
<string name="preferences_receive_folder_title">Salva i trasferimenti ricevuti in</string>
<string name="preferences_title">Preferenze</string>
<string name="progress_cancelled">Annullato</string>
<string name="progress_completed">Completato</string>
<string name="progress_connected">Connesso</string>
<string name="progress_connecting">Connessione</string>
<string name="progress_downloading">Download</string>
<string name="progress_failed">Non riuscito</string>
<string name="progress_getting_ready">Preparazione</string>
<string name="progress_interrupted">Trasferimento interrotto</string>
<string name="progress_preparing">Preparazione</string>
<string name="progress_ready">Pronto</string>
<string name="progress_receiving">Ricezione</string>
<string name="progress_requesting_access">Richiesta di accesso</string>
<string name="progress_saving">Salvataggio</string>
<string name="progress_sending">Invio</string>
<string name="progress_sending_to_count">Invio a %1$d</string>
<string name="progress_share_ready">Pronto per la condivisione</string>
<string name="progress_working">Elaborazione…</string>
<string name="receive_choose_method_body">Scelga il metodo di invito a sua disposizione.</string>
<string name="receive_choose_method_title">Come vuole connettersi?</string>
<string name="receive_clear_history">Cancella cronologia</string>
<string name="receive_clear_history_description">Tutte le ricezioni completate, non riuscite e annullate verranno rimosse dalla cronologia di VniDrop. I file scaricati rimarranno su questo dispositivo.</string>
<string name="receive_clear_history_title">Cancellare la cronologia di ricezione?</string>
<string name="receive_completed">Trasferimento ricevuto.</string>
<string name="receive_delete_history_description">«%1$s» verrà rimosso dalla cronologia di VniDrop. Il file scaricato rimarrà su questo dispositivo.</string>
<string name="receive_delete_history_item">Elimina dalla cronologia di ricezione</string>
<string name="receive_delete_history_title">Rimuovere dalla cronologia?</string>
<string name="receive_empty_body">Apra un invito VniDrop, scansioni un codice QR o avvicini un tag NFC.</string>
<string name="receive_empty_title">Ancora nulla di ricevuto</string>
<string name="receive_history_cleared">Cronologia di ricezione cancellata.</string>
<string name="receive_history_title">Cronologia</string>
<string name="receive_method_file">Apri un invito .vnd</string>
<string name="receive_method_file_description">Scelga un invito salvato o condiviso su questo dispositivo.</string>
<string name="receive_method_nfc">Leggi tag NFC</string>
<string name="receive_method_nfc_description">Avvicini questo dispositivo al tag di invito del mittente.</string>
<string name="receive_method_scan">Scansiona codice QR</string>
<string name="receive_method_scan_description">Usi la fotocamera per scansionare il codice VniDrop del mittente.</string>
<string name="receive_new_subtitle">Trasferimenti che ha ricevuto su questo dispositivo.</string>
<string name="receive_nfc_waiting">Avvicini al tag NFC…</string>
<string name="receive_open_files_failed">Impossibile aprire VniDrop in File.</string>
<string name="receive_review_title">Rivedi trasferimento</string>
<string name="receive_title">Ricevi</string>
<string name="receive_unknown_transfer">Trasferimento VniDrop</string>
<string name="send_access_anyone">Chiunque abbia questo trasferimento</string>
<string name="send_access_anyone_description">Nessuna approvazione richiesta. Da usare solo per elementi che non ha problemi a condividere.</string>
<string name="send_access_anyone_warning">Chiunque abbia linvito può scaricare finché non interrompe la condivisione. Non lo usi per elementi privati o sensibili.</string>
<string name="send_access_approval">Chiedi prima di ogni download</string>
<string name="send_access_approval_description">Approva o rifiuta ogni nuovo destinatario.</string>
<string name="send_access_title">Chi può riceverlo?</string>
<string name="send_choose_file_body">Selezioni file o una cartella da questo dispositivo. Potrà rivedere la selezione prima di creare il trasferimento.</string>
<string name="send_choose_file_title">Scelga cosa condividere</string>
<string name="send_empty_body">Crei un trasferimento, decida chi può riceverlo, poi inviti queste persone con un codice QR, un tag NFC o un file di invito.</string>
<string name="send_empty_title">Ancora nulla di condiviso</string>
<string name="send_file_size_unknown">Dimensione non disponibile</string>
<string name="send_folder_label">Cartella</string>
<string name="send_new_transfer_description">Crea un nuovo trasferimento</string>
<string name="send_new_transfer_title">Nuovo trasferimento</string>
<string name="send_review_title">Rivedi trasferimento</string>
<string name="send_selected_files_count">%1$d file selezionati</string>
<string name="send_stop_sharing">Interrompi condivisione</string>
<string name="send_stop_sharing_description">Questo interrompe il trasferimento e blocca chiunque lo stia scaricando. Rimane nella sua cronologia come Interrotto.</string>
<string name="send_subtitle">Trasferimenti che sta condividendo da questo dispositivo.</string>
<string name="send_title">Invia</string>
<string name="send_transfer_created">Trasferimento creato.</string>
<string name="send_transfer_details_title">Dettagli del trasferimento</string>
<string name="send_transfers_title">I suoi trasferimenti</string>
<string name="settings_subtitle">Il suo nome, dove vengono salvati i trasferimenti, laspetto e le notifiche.</string>
<string name="settings_title">Impostazioni</string>
<string name="snackbar_dismiss">Ignora</string>
<string name="status_available">Disponibile</string>
<string name="status_cancelled">Annullato</string>
<string name="status_completed">Completato</string>
<string name="status_failed">Non riuscito</string>
<string name="status_preparing">Preparazione</string>
<string name="status_receiving">Ricezione</string>
<string name="status_stopped">Interrotto</string>
<string name="storage_calculating">Calcolo…</string>
<string name="storage_delete_transfers">Elimina tutti i trasferimenti</string>
<string name="storage_delete_transfers_description">Questo cancella dalla cronologia tutti i record dei trasferimenti inviati e ricevuti. I suoi file ricevuti non vengono eliminati. Nota: il motore di trasferimento conserva il contenuto memorizzato, quindi i dati di trasferimento potrebbero non diminuire. Questa operazione non può essere annullata.</string>
<string name="storage_deleting">Eliminazione…</string>
<string name="storage_footer">I dati di trasferimento sono gestiti dal motore di trasferimento: la sua cronologia più il contenuto dei file che ha condiviso. Il pulsante qui sotto cancella i record dei trasferimenti; il motore conserva il contenuto memorizzato, quindi questo valore potrebbe non diminuire. I file ricevuti sono i file che ha scaricato e non vengono mai eliminati qui.</string>
<string name="storage_received_files">File ricevuti</string>
<string name="storage_temporary">File temporanei</string>
<string name="storage_title">Archiviazione</string>
<string name="storage_total">Totale</string>
<string name="storage_transfer_data">Dati di trasferimento</string>
<string name="storage_transfers_deleted">Tutti i trasferimenti eliminati</string>
<string name="transfer_activity_description">Veda gli aggiornamenti importanti di questo trasferimento</string>
<string name="transfer_activity_title">Attività</string>
<string name="transfer_delete_description">«%1$s» non verrà più condiviso e la sua cronologia di trasferimento verrà rimossa da questo dispositivo.</string>
<string name="transfer_delete_title">Eliminare il trasferimento?</string>
<string name="transfer_deleted">Trasferimento eliminato.</string>
<string name="transfer_deleting">Eliminazione…</string>
<string name="transfer_details_title">Dettagli del trasferimento</string>
<string name="transfer_event_approved">Accesso del destinatario approvato</string>
<string name="transfer_event_completed">Un destinatario ha completato il trasferimento</string>
<string name="transfer_event_connecting">Connessione al mittente</string>
<string name="transfer_event_downloading">Download</string>
<string name="transfer_event_failed">Il trasferimento ha riscontrato un problema</string>
<string name="transfer_event_preparing">Preparazione del trasferimento</string>
<string name="transfer_event_ready">Pronto per la condivisione</string>
<string name="transfer_event_refused">Accesso del destinatario rifiutato</string>
<string name="transfer_event_requested">Un destinatario ha richiesto laccesso</string>
<string name="transfer_event_saving">Salvataggio</string>
<string name="transfer_event_stopped">Condivisione interrotta</string>
<string name="transfer_event_updated">Trasferimento aggiornato</string>
<string name="transfer_invitation_saved">Invito salvato.</string>
<string name="transfer_nearby_device">Dispositivo nelle vicinanze</string>
<string name="transfer_nfc_unavailable">La scrittura dei tag NFC non è disponibile su questo dispositivo.</string>
<string name="transfer_nfc_waiting">Avvicini il dispositivo a un tag NFC scrivibile.</string>
<string name="transfer_nfc_written">Invito scritto sul tag NFC.</string>
<string name="transfer_no_activity">Non cè ancora alcuna attività da mostrare.</string>
<string name="transfer_no_receivers">Nessuno ha ancora richiesto questo trasferimento.</string>
<string name="transfer_receiver_accepted">Approvato: in attesa del completamento</string>
<string name="transfer_receiver_completed">Ricevuto correttamente</string>
<string name="transfer_receiver_expired">Richiesta scaduta</string>
<string name="transfer_receiver_refused">Richiesta rifiutata</string>
<string name="transfer_receiver_requested">In attesa della sua approvazione</string>
<string name="transfer_receiver_unknown">Stato non disponibile</string>
<string name="transfer_receivers_completed_count">%1$d completati</string>
<string name="transfer_receivers_description">Richieste, approvazioni e consegne completate</string>
<string name="transfer_receivers_pending">%1$d in attesa</string>
<string name="transfer_receivers_title">Destinatari</string>
<string name="transfer_scan_qr">Scansioni con VniDrop per ricevere questo trasferimento</string>
<string name="transfer_share_description">Codice QR, file di invito e opzioni nelle vicinanze</string>
<string name="transfer_share_title">Condividi</string>
<string name="value_unavailable">Non disponibile</string>
<string name="version_title">Versione dellapp</string>
<plurals name="transfer_file_count">
<item quantity="one">%1$d file</item>
<item quantity="other">%1$d file</item>
</plurals>
</resources>

View File

@@ -0,0 +1,257 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_bug_report">Een fout melden</string>
<string name="about_description">VniDrop verplaatst bestanden en mappen rechtstreeks van het ene apparaat naar het andere via het netwerk, zonder ze naar een hostingdienst te uploaden. U hoeft geen account aan te maken en er blijft geen kopie van uw overdracht in de cloud achter zodra u klaar bent.</string>
<string name="about_is_direct">Een directe overdracht van apparaat naar apparaat — uw bestanden gaan rechtstreeks naar de ontvanger.</string>
<string name="about_is_encrypted">Verbindingen zijn geverifieerd en end-to-end versleuteld (via Iroh), en binnenkomende bestanden worden gecontroleerd aan de hand van hun inhouds-hash.</string>
<string name="about_is_in_control">U bepaalt wie ontvangt: keur elk verzoek goed, of stel een overdracht open voor iedereen met de uitnodiging.</string>
<string name="about_is_no_account">Zonder account — er is niets om aan te melden.</string>
<string name="about_is_open">Opensource, uitgebracht onder de Apache 2.0-licentie.</string>
<string name="about_is_title">Wat VniDrop is</string>
<string name="about_isnt_cloud">Geen cloudopslag — geen enkele server bewaart uw bestanden en er wacht niets in de cloud na een overdracht.</string>
<string name="about_isnt_public">Geen openbare uitzending — een uitnodiging is een privétoegangslink, geen aankondiging aan iedereen in de buurt.</string>
<string name="about_isnt_sync">Geen synchronisatie- of back-updienst.</string>
<string name="about_isnt_title">Wat VniDrop niet is</string>
<string name="about_license_label">Licentie</string>
<string name="about_privacy">Privacybeleid</string>
<string name="about_privacy_capability">Uitnodigingen zijn toegangssleutels. Behandel een QR-code, NFC-tag of .vnd-bestand als een privétoegangslink en deel deze alleen met de bedoelde personen — vooral bij Iedereen met deze overdracht.</string>
<string name="about_privacy_deny">Standaard weigeren — VniDrop levert alleen de inhoud van een actieve deling en wijst onbekende verzoeken af.</string>
<string name="about_privacy_local">Ontvangen bestanden worden op uw apparaat bewaard en overschrijven nooit een bestaand bestand.</string>
<string name="about_privacy_policy_label">Privacybeleid</string>
<string name="about_privacy_relay">Als twee apparaten niet rechtstreeks verbinding kunnen maken, kan de versleutelde verbinding via een relay worden doorgestuurd. Relays sturen alleen versleutelde pakketten door; ze slaan uw bestanden nooit op.</string>
<string name="about_privacy_title">Privacy en beveiliging</string>
<string name="about_tagline">Verstuur bestanden rechtstreeks. Houd controle over wie ze ontvangt.</string>
<string name="about_title">Over</string>
<string name="appearance_auto_description">De lichte of donkere weergave van dit apparaat volgen.</string>
<string name="appearance_dark_mode">Donkere modus</string>
<string name="appearance_light_mode">Lichte modus</string>
<string name="appearance_system_mode">Systeem</string>
<string name="appearance_title">Weergave</string>
<string name="approval_connection_request">Ontvangstverzoek</string>
<string name="approval_endpoint_id">Apparaat-ID: %1$s</string>
<string name="approval_nearby_device">Een apparaat in de buurt</string>
<string name="approval_pending_count">%1$d verzoeken in behandeling</string>
<string name="approval_request_body">%1$s wil %2$s ontvangen.</string>
<string name="battery_level_title">Batterijniveau</string>
<string name="bug_report_contact_hint">naam@voorbeeld.com</string>
<string name="bug_report_contact_label">Contact-e-mail (optioneel)</string>
<string name="bug_report_description">Vertel ons wat er misging. We voegen apparaatgegevens toe en optioneel recente logbestanden (met gevoelige waarden onleesbaar gemaakt).</string>
<string name="bug_report_device_section">Apparaatgegevens</string>
<string name="bug_report_expected_hint">Beschrijf wat u verwachtte</string>
<string name="bug_report_expected_label">Wat verwachtte u?</string>
<string name="bug_report_include_logs">Recente logbestanden meesturen</string>
<string name="bug_report_include_logs_description">Helpt ons het probleem te diagnosticeren. Gevoelige waarden worden vóór verzending onleesbaar gemaakt.</string>
<string name="bug_report_logs_size">Grootte van logbijlage</string>
<string name="bug_report_missing_expected">Beschrijf wat u verwachtte.</string>
<string name="bug_report_missing_what">Beschrijf wat er gebeurde.</string>
<string name="bug_report_steps_hint">Noem de stappen om het probleem te reproduceren</string>
<string name="bug_report_steps_label">Stappen om te reproduceren (optioneel)</string>
<string name="bug_report_submit">Rapport versturen</string>
<string name="bug_report_submit_failed">Het foutrapport kon niet worden verstuurd. Probeer het later opnieuw.</string>
<string name="bug_report_submitted">Bedankt — uw foutrapport is vastgelegd.</string>
<string name="bug_report_submitting">Bezig met versturen…</string>
<string name="bug_report_what_hint">Beschrijf wat er gebeurde</string>
<string name="bug_report_what_label">Wat is er gebeurd?</string>
<string name="button_approve">Goedkeuren</string>
<string name="button_back">Terug</string>
<string name="button_cancel">Annuleren</string>
<string name="button_cancel_receive">Annuleren</string>
<string name="button_change_files">Bestanden wijzigen</string>
<string name="button_choose_files">Bestanden kiezen</string>
<string name="button_choose_folder">Map kiezen</string>
<string name="button_clear">Wissen</string>
<string name="button_close">Sluiten</string>
<string name="button_create_new_transfer">Nieuwe overdracht</string>
<string name="button_delete_transfer">Overdracht verwijderen</string>
<string name="button_download_invitation">.vnd-bestand bewaren</string>
<string name="button_native_share">Uitnodiging delen</string>
<string name="button_open_settings">Instellingen openen</string>
<string name="button_receive">Ontvangen</string>
<string name="button_receive_files">Ontvangen starten</string>
<string name="button_refuse">Weigeren</string>
<string name="button_remove_file">Bestand verwijderen</string>
<string name="button_reset_default">Standaard gebruiken</string>
<string name="button_retry">Opnieuw proberen</string>
<string name="button_share_file">Delen starten</string>
<string name="button_sharing_file">Overdracht voorbereiden…</string>
<string name="button_show_in_files">Toon in Bestanden</string>
<string name="button_write_nfc">Naar NFC-tag schrijven</string>
<string name="device_model_title">Apparaatmodel</string>
<string name="device_name_title">Apparaatnaam</string>
<string name="diagnostics_description">Verstuur anonieme crashrapporten en gebruiksgebeurtenissen zodat we VniDrop kunnen verbeteren. U kunt dit op elk moment uitschakelen. Uitnodigingen, bestandspaden en overdrachtsinhoud worden nooit meegestuurd.</string>
<string name="diagnostics_disabled_message">Het delen van diagnostische gegevens is uitgeschakeld.</string>
<string name="diagnostics_enabled_message">Het delen van diagnostische gegevens is ingeschakeld.</string>
<string name="diagnostics_title">Diagnostische gegevens delen</string>
<string name="error_camera">Voor het scannen van een QR-code is toegang tot de camera vereist.</string>
<string name="error_device_info">Apparaatgegevens konden niet worden geladen.</string>
<string name="error_filesystem">VniDrop kon geen toegang krijgen tot de geselecteerde bestanden of map. Controleer de machtigingen en probeer het opnieuw.</string>
<string name="error_generic">Er is iets misgegaan. Probeer het opnieuw.</string>
<string name="error_initialization">VniDrop kon het opstarten niet voltooien. Sluit de app en probeer het opnieuw.</string>
<string name="error_invalid_ticket">Deze uitnodiging kon niet worden gelezen. Vraag de afzender om een nieuwe.</string>
<string name="error_invitation_empty">Die uitnodiging is leeg. Probeer deze opnieuw te openen.</string>
<string name="error_missing_native_library">De native VniDrop-bibliotheek ontbreekt in deze build.</string>
<string name="error_nfc">Deze NFC-tag kon niet worden gebruikt. Probeer een andere.</string>
<string name="error_permission">De afzender heeft deze overdracht niet goedgekeurd, of deze is geweigerd.</string>
<string name="error_repository">VniDrop kon de overdrachtsgegevens niet op dit apparaat bewaren.</string>
<string name="error_selection_failed">Het geselecteerde item kon niet worden geopend. Probeer het opnieuw te kiezen.</string>
<string name="error_share_empty">Selecteer minstens één item om te delen.</string>
<string name="error_socket_bind">VniDrop kon zijn netwerksockets niet openen op dit apparaat.</string>
<string name="error_starting_up">VniDrop is nog aan het opstarten. Open de uitnodiging zo meteen opnieuw.</string>
<string name="error_transfer">De overdracht kon niet worden voltooid. Controleer uw verbinding en probeer het opnieuw.</string>
<string name="field_receiver_name">Naam van ontvanger</string>
<string name="field_sender_name">Naam van afzender</string>
<string name="field_transfer_name">Naam van overdracht</string>
<string name="field_username">Weergavenaam</string>
<string name="folder_status_permission_required">Machtiging vereist</string>
<string name="folder_status_unavailable">Niet beschikbaar</string>
<string name="folder_status_validating">Map controleren…</string>
<string name="folder_status_writable">Gereed</string>
<string name="metadata_files">Bestanden</string>
<string name="metadata_size">Grootte</string>
<string name="metadata_status">Status</string>
<string name="nav_receive">Ontvangen</string>
<string name="nav_send">Versturen</string>
<string name="nav_settings">Instellingen</string>
<string name="network_title">Netwerk</string>
<string name="notifications_description">Ontvang meldingen over nieuwe ontvangstverzoeken terwijl VniDrop op de achtergrond draait.</string>
<string name="notifications_enabled_message">Meldingen ingeschakeld.</string>
<string name="notifications_local_title">Meldingen toestaan</string>
<string name="notifications_permission_denied">Meldingen zijn uitgeschakeld voor VniDrop. U kunt ze inschakelen in Instellingen.</string>
<string name="notifications_settings_open_failed">De meldingsinstellingen konden niet worden geopend.</string>
<string name="notifications_title">Meldingen</string>
<string name="notifications_unsupported">Meldingen zijn niet beschikbaar op dit apparaat.</string>
<string name="os_version_title">Besturingssysteem</string>
<string name="preferences_receive_folder_title">Ontvangen overdrachten bewaren in</string>
<string name="preferences_title">Voorkeuren</string>
<string name="progress_cancelled">Geannuleerd</string>
<string name="progress_completed">Voltooid</string>
<string name="progress_connected">Verbonden</string>
<string name="progress_connecting">Verbinden</string>
<string name="progress_downloading">Downloaden</string>
<string name="progress_failed">Mislukt</string>
<string name="progress_getting_ready">Klaarmaken</string>
<string name="progress_interrupted">Overdracht onderbroken</string>
<string name="progress_preparing">Voorbereiden</string>
<string name="progress_ready">Gereed</string>
<string name="progress_receiving">Ontvangen</string>
<string name="progress_requesting_access">Toegang aanvragen</string>
<string name="progress_saving">Bewaren</string>
<string name="progress_sending">Versturen</string>
<string name="progress_sending_to_count">Versturen naar %1$d</string>
<string name="progress_share_ready">Klaar om te delen</string>
<string name="progress_working">Bezig…</string>
<string name="receive_choose_method_body">Kies de uitnodigingsmethode die u ter beschikking staat.</string>
<string name="receive_choose_method_title">Hoe wilt u verbinding maken?</string>
<string name="receive_clear_history">Geschiedenis wissen</string>
<string name="receive_clear_history_description">Alle voltooide, mislukte en geannuleerde ontvangsten worden uit de geschiedenis van VniDrop verwijderd. Gedownloade bestanden blijven op dit apparaat.</string>
<string name="receive_clear_history_title">Ontvangstgeschiedenis wissen?</string>
<string name="receive_completed">Overdracht ontvangen.</string>
<string name="receive_delete_history_description">%1$s wordt uit de geschiedenis van VniDrop verwijderd. Het gedownloade bestand blijft op dit apparaat.</string>
<string name="receive_delete_history_item">Uit ontvangstgeschiedenis verwijderen</string>
<string name="receive_delete_history_title">Uit geschiedenis verwijderen?</string>
<string name="receive_empty_body">Open een VniDrop-uitnodiging, scan een QR-code of houd het apparaat bij een NFC-tag.</string>
<string name="receive_empty_title">Nog niets ontvangen</string>
<string name="receive_history_cleared">Ontvangstgeschiedenis gewist.</string>
<string name="receive_history_title">Geschiedenis</string>
<string name="receive_method_file">Een .vnd-uitnodiging openen</string>
<string name="receive_method_file_description">Kies een uitnodiging die op dit apparaat is bewaard of gedeeld.</string>
<string name="receive_method_nfc">NFC-tag lezen</string>
<string name="receive_method_nfc_description">Houd dit apparaat bij de uitnodigingstag van de afzender.</string>
<string name="receive_method_scan">QR-code scannen</string>
<string name="receive_method_scan_description">Gebruik de camera om de VniDrop-code van de afzender te scannen.</string>
<string name="receive_new_subtitle">Overdrachten die u op dit apparaat hebt ontvangen.</string>
<string name="receive_nfc_waiting">Houd bij de NFC-tag…</string>
<string name="receive_open_files_failed">VniDrop kon niet worden geopend in Bestanden.</string>
<string name="receive_review_title">Overdracht controleren</string>
<string name="receive_title">Ontvangen</string>
<string name="receive_unknown_transfer">VniDrop-overdracht</string>
<string name="send_access_anyone">Iedereen met deze overdracht</string>
<string name="send_access_anyone_description">Geen goedkeuring vereist. Gebruik dit alleen voor items die u gerust kunt delen.</string>
<string name="send_access_anyone_warning">Iedereen met de uitnodiging kan downloaden totdat u stopt met delen. Gebruik dit niet voor privé- of gevoelige items.</string>
<string name="send_access_approval">Vragen vóór elke download</string>
<string name="send_access_approval_description">U keurt elke nieuwe ontvanger goed of weigert deze.</string>
<string name="send_access_title">Wie kan het ontvangen?</string>
<string name="send_choose_file_body">Selecteer bestanden of een map op dit apparaat. U kunt de selectie controleren voordat u de overdracht aanmaakt.</string>
<string name="send_choose_file_title">Kies wat u wilt delen</string>
<string name="send_empty_body">Maak een overdracht aan, bepaal wie deze kan ontvangen en nodig die personen vervolgens uit met een QR-code, NFC-tag of uitnodigingsbestand.</string>
<string name="send_empty_title">Nog niets gedeeld</string>
<string name="send_file_size_unknown">Grootte niet beschikbaar</string>
<string name="send_folder_label">Map</string>
<string name="send_new_transfer_description">Een nieuwe overdracht aanmaken</string>
<string name="send_new_transfer_title">Nieuwe overdracht</string>
<string name="send_review_title">Overdracht controleren</string>
<string name="send_selected_files_count">%1$d bestanden geselecteerd</string>
<string name="send_stop_sharing">Stoppen met delen</string>
<string name="send_stop_sharing_description">Hiermee stopt de overdracht en wordt iedereen die deze op dit moment downloadt onderbroken. De overdracht blijft in uw geschiedenis staan als Gestopt.</string>
<string name="send_subtitle">Overdrachten die u vanaf dit apparaat deelt.</string>
<string name="send_title">Versturen</string>
<string name="send_transfer_created">Overdracht aangemaakt.</string>
<string name="send_transfer_details_title">Overdrachtsdetails</string>
<string name="send_transfers_title">Uw overdrachten</string>
<string name="settings_subtitle">Uw naam, waar overdrachten worden bewaard, weergave en meldingen.</string>
<string name="settings_title">Instellingen</string>
<string name="snackbar_dismiss">Sluiten</string>
<string name="status_available">Beschikbaar</string>
<string name="status_cancelled">Geannuleerd</string>
<string name="status_completed">Voltooid</string>
<string name="status_failed">Mislukt</string>
<string name="status_preparing">Voorbereiden</string>
<string name="status_receiving">Ontvangen</string>
<string name="status_stopped">Gestopt</string>
<string name="storage_calculating">Berekenen…</string>
<string name="storage_delete_transfers">Alle overdrachten verwijderen</string>
<string name="storage_delete_transfers_description">Hiermee worden alle records van verzonden en ontvangen overdrachten uit uw geschiedenis gewist. Uw ontvangen bestanden worden niet verwijderd. Let op: de overdrachtsengine bewaart de opgeslagen inhoud, dus de overdrachtsgegevens nemen mogelijk niet af. Dit kan niet ongedaan worden gemaakt.</string>
<string name="storage_deleting">Verwijderen…</string>
<string name="storage_footer">Overdrachtsgegevens worden beheerd door de overdrachtsengine — uw geschiedenis plus de inhoud van de bestanden die u hebt gedeeld. De knop hieronder wist uw overdrachtsrecords; de engine bewaart de opgeslagen inhoud, dus deze waarde daalt mogelijk niet. Ontvangen bestanden zijn uw gedownloade bestanden en worden hier nooit verwijderd.</string>
<string name="storage_received_files">Ontvangen bestanden</string>
<string name="storage_temporary">Tijdelijke bestanden</string>
<string name="storage_title">Opslag</string>
<string name="storage_total">Totaal</string>
<string name="storage_transfer_data">Overdrachtsgegevens</string>
<string name="storage_transfers_deleted">Alle overdrachten verwijderd</string>
<string name="transfer_activity_description">Bekijk belangrijke updates voor deze overdracht</string>
<string name="transfer_activity_title">Activiteit</string>
<string name="transfer_delete_description">%1$s wordt niet meer gedeeld en de overdrachtsgeschiedenis wordt van dit apparaat verwijderd.</string>
<string name="transfer_delete_title">Overdracht verwijderen?</string>
<string name="transfer_deleted">Overdracht verwijderd.</string>
<string name="transfer_deleting">Verwijderen…</string>
<string name="transfer_details_title">Overdrachtsdetails</string>
<string name="transfer_event_approved">Toegang ontvanger goedgekeurd</string>
<string name="transfer_event_completed">Een ontvanger heeft de overdracht voltooid</string>
<string name="transfer_event_connecting">Verbinden met afzender</string>
<string name="transfer_event_downloading">Downloaden</string>
<string name="transfer_event_failed">Er is een probleem opgetreden bij de overdracht</string>
<string name="transfer_event_preparing">Uw overdracht voorbereiden</string>
<string name="transfer_event_ready">Klaar om te delen</string>
<string name="transfer_event_refused">Toegang ontvanger geweigerd</string>
<string name="transfer_event_requested">Een ontvanger heeft toegang aangevraagd</string>
<string name="transfer_event_saving">Bewaren</string>
<string name="transfer_event_stopped">Delen gestopt</string>
<string name="transfer_event_updated">Overdracht bijgewerkt</string>
<string name="transfer_invitation_saved">Uitnodiging bewaard.</string>
<string name="transfer_nearby_device">Apparaat in de buurt</string>
<string name="transfer_nfc_unavailable">Het schrijven van NFC-tags is niet beschikbaar op dit apparaat.</string>
<string name="transfer_nfc_waiting">Houd uw apparaat bij een beschrijfbare NFC-tag.</string>
<string name="transfer_nfc_written">Uitnodiging naar de NFC-tag geschreven.</string>
<string name="transfer_no_activity">Er is nog geen activiteit om weer te geven.</string>
<string name="transfer_no_receivers">Nog niemand heeft deze overdracht aangevraagd.</string>
<string name="transfer_receiver_accepted">Goedgekeurd — wachten op voltooiing</string>
<string name="transfer_receiver_completed">Succesvol ontvangen</string>
<string name="transfer_receiver_expired">Verzoek verlopen</string>
<string name="transfer_receiver_refused">Verzoek geweigerd</string>
<string name="transfer_receiver_requested">Wachten op uw goedkeuring</string>
<string name="transfer_receiver_unknown">Status niet beschikbaar</string>
<string name="transfer_receivers_completed_count">%1$d voltooid</string>
<string name="transfer_receivers_description">Verzoeken, goedkeuringen en voltooide leveringen</string>
<string name="transfer_receivers_pending">%1$d in behandeling</string>
<string name="transfer_receivers_title">Ontvangers</string>
<string name="transfer_scan_qr">Scan met VniDrop om deze overdracht te ontvangen</string>
<string name="transfer_share_description">QR-code, uitnodigingsbestand en opties in de buurt</string>
<string name="transfer_share_title">Delen</string>
<string name="value_unavailable">Niet beschikbaar</string>
<string name="version_title">App-versie</string>
<plurals name="transfer_file_count">
<item quantity="one">%1$d bestand</item>
<item quantity="other">%1$d bestanden</item>
</plurals>
</resources>

View File

@@ -0,0 +1,259 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_bug_report">Zgłoś błąd</string>
<string name="about_description">VniDrop przesyła pliki i foldery bezpośrednio z jednego urządzenia na drugie przez sieć, bez przesyłania ich do jakiejkolwiek usługi hostingowej. Nie trzeba zakładać konta, a po zakończeniu żadna kopia transferu nie pozostaje w chmurze.</string>
<string name="about_is_direct">Bezpośredni transfer między urządzeniami — pliki trafiają prosto do odbiorcy.</string>
<string name="about_is_encrypted">Połączenia są uwierzytelniane i szyfrowane od końca do końca (przez Iroh), a przychodzące pliki są weryfikowane na podstawie skrótu ich zawartości.</string>
<string name="about_is_in_control">To Ty decydujesz, kto otrzymuje: zatwierdź każde żądanie lub udostępnij transfer każdemu, kto ma zaproszenie.</string>
<string name="about_is_no_account">Bez konta — nie trzeba się rejestrować.</string>
<string name="about_is_open">Otwarte oprogramowanie, wydane na licencji Apache 2.0.</string>
<string name="about_is_title">Czym jest VniDrop</string>
<string name="about_isnt_cloud">To nie magazyn w chmurze — żaden serwer nie przechowuje Twoich plików i nic nie pozostaje w chmurze po transferze.</string>
<string name="about_isnt_public">To nie publiczne rozgłaszanie — zaproszenie to prywatny link dostępu, a nie ogłoszenie dla wszystkich w pobliżu.</string>
<string name="about_isnt_sync">To nie usługa synchronizacji ani kopii zapasowej.</string>
<string name="about_isnt_title">Czym VniDrop nie jest</string>
<string name="about_license_label">Licencja</string>
<string name="about_privacy">Polityka prywatności</string>
<string name="about_privacy_capability">Zaproszenia są kluczami dostępu. Traktuj kod QR, tag NFC lub plik .vnd jak prywatny link dostępu i udostępniaj go tylko zamierzonym osobom — zwłaszcza przy opcji „Każdy, kto ma ten transfer”.</string>
<string name="about_privacy_deny">Domyślnie odmowa — VniDrop udostępnia tylko zawartość aktywnego udostępnienia i odrzuca nieznane żądania.</string>
<string name="about_privacy_local">Odebrane pliki są zapisywane na Twoim urządzeniu i nigdy nie nadpisują istniejącego pliku.</string>
<string name="about_privacy_policy_label">Polityka prywatności</string>
<string name="about_privacy_relay">Jeśli dwa urządzenia nie mogą połączyć się bezpośrednio, szyfrowane połączenie może być przekazywane przez przekaźnik. Przekaźniki przekazują tylko zaszyfrowane pakiety; nigdy nie przechowują Twoich plików.</string>
<string name="about_privacy_title">Prywatność i bezpieczeństwo</string>
<string name="about_tagline">Wysyłaj pliki bezpośrednio. Zachowaj kontrolę nad tym, kto je otrzymuje.</string>
<string name="about_title">Informacje</string>
<string name="appearance_auto_description">Dopasuj do jasnego lub ciemnego wyglądu tego urządzenia.</string>
<string name="appearance_dark_mode">Tryb ciemny</string>
<string name="appearance_light_mode">Tryb jasny</string>
<string name="appearance_system_mode">System</string>
<string name="appearance_title">Wygląd</string>
<string name="approval_connection_request">Prośba o odbiór</string>
<string name="approval_endpoint_id">Identyfikator urządzenia: %1$s</string>
<string name="approval_nearby_device">Urządzenie w pobliżu</string>
<string name="approval_pending_count">Oczekujące prośby: %1$d</string>
<string name="approval_request_body">%1$s chce odebrać „%2$s”.</string>
<string name="battery_level_title">Poziom baterii</string>
<string name="bug_report_contact_hint">nazwa@przyklad.com</string>
<string name="bug_report_contact_label">E-mail kontaktowy (opcjonalnie)</string>
<string name="bug_report_description">Napisz, co poszło nie tak. Dołączamy informacje o urządzeniu i opcjonalnie ostatnie dzienniki (z ukrytymi wrażliwymi wartościami).</string>
<string name="bug_report_device_section">Informacje o urządzeniu</string>
<string name="bug_report_expected_hint">Opisz oczekiwane zachowanie</string>
<string name="bug_report_expected_label">Czego oczekiwano?</string>
<string name="bug_report_include_logs">Dołącz ostatnie dzienniki</string>
<string name="bug_report_include_logs_description">Pomaga nam zdiagnozować problem. Wrażliwe wartości są ukrywane przed wysłaniem.</string>
<string name="bug_report_logs_size">Rozmiar załącznika z dziennikami</string>
<string name="bug_report_missing_expected">Opisz oczekiwane zachowanie.</string>
<string name="bug_report_missing_what">Opisz, co się stało.</string>
<string name="bug_report_steps_hint">Wymień kroki umożliwiające odtworzenie problemu</string>
<string name="bug_report_steps_label">Kroki do odtworzenia (opcjonalnie)</string>
<string name="bug_report_submit">Wyślij zgłoszenie</string>
<string name="bug_report_submit_failed">Nie udało się wysłać zgłoszenia błędu. Spróbuj ponownie później.</string>
<string name="bug_report_submitted">Dziękujemy — Twoje zgłoszenie błędu zostało zapisane.</string>
<string name="bug_report_submitting">Wysyłanie…</string>
<string name="bug_report_what_hint">Opisz, co się stało</string>
<string name="bug_report_what_label">Co się stało?</string>
<string name="button_approve">Zatwierdź</string>
<string name="button_back">Wstecz</string>
<string name="button_cancel">Anuluj</string>
<string name="button_cancel_receive">Anuluj</string>
<string name="button_change_files">Zmień pliki</string>
<string name="button_choose_files">Wybierz pliki</string>
<string name="button_choose_folder">Wybierz folder</string>
<string name="button_clear">Wyczyść</string>
<string name="button_close">Zamknij</string>
<string name="button_create_new_transfer">Nowy transfer</string>
<string name="button_delete_transfer">Usuń transfer</string>
<string name="button_download_invitation">Zapisz plik .vnd</string>
<string name="button_native_share">Udostępnij zaproszenie</string>
<string name="button_open_settings">Otwórz Ustawienia</string>
<string name="button_receive">Odbierz</string>
<string name="button_receive_files">Rozpocznij odbieranie</string>
<string name="button_refuse">Odrzuć</string>
<string name="button_remove_file">Usuń plik</string>
<string name="button_reset_default">Użyj domyślnego</string>
<string name="button_retry">Spróbuj ponownie</string>
<string name="button_share_file">Rozpocznij udostępnianie</string>
<string name="button_sharing_file">Przygotowywanie transferu…</string>
<string name="button_show_in_files">Pokaż w Plikach</string>
<string name="button_write_nfc">Zapisz na tagu NFC</string>
<string name="device_model_title">Model urządzenia</string>
<string name="device_name_title">Nazwa urządzenia</string>
<string name="diagnostics_description">Wysyłaj anonimowe raporty o awariach i zdarzenia użytkowania, aby pomóc nam ulepszać VniDrop. Możesz to wyłączyć w dowolnej chwili. Zaproszenia, ścieżki plików i zawartość transferów nigdy nie są dołączane.</string>
<string name="diagnostics_disabled_message">Udostępnianie diagnostyki jest wyłączone.</string>
<string name="diagnostics_enabled_message">Udostępnianie diagnostyki jest włączone.</string>
<string name="diagnostics_title">Udostępniaj diagnostykę</string>
<string name="error_camera">Do zeskanowania kodu QR wymagany jest dostęp do aparatu.</string>
<string name="error_device_info">Nie udało się wczytać informacji o urządzeniu.</string>
<string name="error_filesystem">VniDrop nie mógł uzyskać dostępu do wybranych plików lub folderu. Sprawdź uprawnienia i spróbuj ponownie.</string>
<string name="error_generic">Coś poszło nie tak. Spróbuj ponownie.</string>
<string name="error_initialization">VniDrop nie mógł dokończyć uruchamiania. Zamknij aplikację i spróbuj ponownie.</string>
<string name="error_invalid_ticket">Nie udało się odczytać tego zaproszenia. Poproś nadawcę o nowe.</string>
<string name="error_invitation_empty">To zaproszenie jest puste. Spróbuj otworzyć je ponownie.</string>
<string name="error_missing_native_library">W tej kompilacji brakuje natywnej biblioteki VniDrop.</string>
<string name="error_nfc">Nie udało się użyć tego tagu NFC. Spróbuj innego.</string>
<string name="error_permission">Nadawca nie zatwierdził tego transferu lub został on odrzucony.</string>
<string name="error_repository">VniDrop nie mógł zapisać danych transferu na tym urządzeniu.</string>
<string name="error_selection_failed">Nie udało się otworzyć wybranego elementu. Spróbuj wybrać go ponownie.</string>
<string name="error_share_empty">Wybierz co najmniej jeden element do udostępnienia.</string>
<string name="error_socket_bind">VniDrop nie mógł otworzyć gniazd sieciowych na tym urządzeniu.</string>
<string name="error_starting_up">VniDrop jeszcze się uruchamia. Otwórz zaproszenie ponownie za chwilę.</string>
<string name="error_transfer">Nie udało się ukończyć transferu. Sprawdź połączenie i spróbuj ponownie.</string>
<string name="field_receiver_name">Nazwa odbiorcy</string>
<string name="field_sender_name">Nazwa nadawcy</string>
<string name="field_transfer_name">Nazwa transferu</string>
<string name="field_username">Nazwa wyświetlana</string>
<string name="folder_status_permission_required">Wymagane uprawnienie</string>
<string name="folder_status_unavailable">Niedostępny</string>
<string name="folder_status_validating">Sprawdzanie folderu…</string>
<string name="folder_status_writable">Gotowy</string>
<string name="metadata_files">Pliki</string>
<string name="metadata_size">Rozmiar</string>
<string name="metadata_status">Status</string>
<string name="nav_receive">Odbierz</string>
<string name="nav_send">Wyślij</string>
<string name="nav_settings">Ustawienia</string>
<string name="network_title">Sieć</string>
<string name="notifications_description">Otrzymuj powiadomienia o nowych prośbach o odbiór, gdy VniDrop działa w tle.</string>
<string name="notifications_enabled_message">Powiadomienia włączone.</string>
<string name="notifications_local_title">Zezwól na powiadomienia</string>
<string name="notifications_permission_denied">Powiadomienia są wyłączone dla VniDrop. Możesz je włączyć w Ustawieniach.</string>
<string name="notifications_settings_open_failed">Nie udało się otworzyć ustawień powiadomień.</string>
<string name="notifications_title">Powiadomienia</string>
<string name="notifications_unsupported">Powiadomienia nie są dostępne na tym urządzeniu.</string>
<string name="os_version_title">System operacyjny</string>
<string name="preferences_receive_folder_title">Zapisuj odebrane transfery w</string>
<string name="preferences_title">Preferencje</string>
<string name="progress_cancelled">Anulowano</string>
<string name="progress_completed">Ukończono</string>
<string name="progress_connected">Połączono</string>
<string name="progress_connecting">Łączenie</string>
<string name="progress_downloading">Pobieranie</string>
<string name="progress_failed">Niepowodzenie</string>
<string name="progress_getting_ready">Przygotowywanie</string>
<string name="progress_interrupted">Transfer przerwany</string>
<string name="progress_preparing">Przygotowywanie</string>
<string name="progress_ready">Gotowe</string>
<string name="progress_receiving">Odbieranie</string>
<string name="progress_requesting_access">Prośba o dostęp</string>
<string name="progress_saving">Zapisywanie</string>
<string name="progress_sending">Wysyłanie</string>
<string name="progress_sending_to_count">Wysyłanie do %1$d</string>
<string name="progress_share_ready">Gotowe do udostępnienia</string>
<string name="progress_working">Przetwarzanie…</string>
<string name="receive_choose_method_body">Wybierz dostępną metodę zaproszenia.</string>
<string name="receive_choose_method_title">Jak chcesz się połączyć?</string>
<string name="receive_clear_history">Wyczyść historię</string>
<string name="receive_clear_history_description">Wszystkie ukończone, nieudane i anulowane odbiory zostaną usunięte z historii VniDrop. Pobrane pliki pozostaną na tym urządzeniu.</string>
<string name="receive_clear_history_title">Wyczyścić historię odbioru?</string>
<string name="receive_completed">Transfer odebrany.</string>
<string name="receive_delete_history_description">„%1$s” zostanie usunięty z historii VniDrop. Pobrany plik pozostanie na tym urządzeniu.</string>
<string name="receive_delete_history_item">Usuń z historii odbioru</string>
<string name="receive_delete_history_title">Usunąć z historii?</string>
<string name="receive_empty_body">Otwórz zaproszenie VniDrop, zeskanuj kod QR lub zbliż tag NFC.</string>
<string name="receive_empty_title">Nic jeszcze nie odebrano</string>
<string name="receive_history_cleared">Historia odbioru wyczyszczona.</string>
<string name="receive_history_title">Historia</string>
<string name="receive_method_file">Otwórz zaproszenie .vnd</string>
<string name="receive_method_file_description">Wybierz zaproszenie zapisane lub udostępnione na tym urządzeniu.</string>
<string name="receive_method_nfc">Odczytaj tag NFC</string>
<string name="receive_method_nfc_description">Zbliż to urządzenie do tagu zaproszenia nadawcy.</string>
<string name="receive_method_scan">Zeskanuj kod QR</string>
<string name="receive_method_scan_description">Użyj aparatu, aby zeskanować kod VniDrop nadawcy.</string>
<string name="receive_new_subtitle">Transfery odebrane na tym urządzeniu.</string>
<string name="receive_nfc_waiting">Zbliż do tagu NFC…</string>
<string name="receive_open_files_failed">Nie udało się otworzyć VniDrop w Plikach.</string>
<string name="receive_review_title">Przejrzyj transfer</string>
<string name="receive_title">Odbierz</string>
<string name="receive_unknown_transfer">Transfer VniDrop</string>
<string name="send_access_anyone">Każdy, kto ma ten transfer</string>
<string name="send_access_anyone_description">Nie jest wymagane zatwierdzenie. Używaj tylko dla elementów, które możesz swobodnie udostępniać.</string>
<string name="send_access_anyone_warning">Każdy, kto ma zaproszenie, może pobierać, dopóki nie zatrzymasz udostępniania. Nie używaj tego dla prywatnych ani wrażliwych elementów.</string>
<string name="send_access_approval">Pytaj przed każdym pobraniem</string>
<string name="send_access_approval_description">Zatwierdzasz lub odrzucasz każdego nowego odbiorcę.</string>
<string name="send_access_title">Kto może to odebrać?</string>
<string name="send_choose_file_body">Wybierz pliki lub folder z tego urządzenia. Przed utworzeniem transferu możesz przejrzeć wybór.</string>
<string name="send_choose_file_title">Wybierz, co udostępnić</string>
<string name="send_empty_body">Utwórz transfer, zdecyduj, kto może go odebrać, a następnie zaproś te osoby za pomocą kodu QR, tagu NFC lub pliku zaproszenia.</string>
<string name="send_empty_title">Nic jeszcze nie udostępniono</string>
<string name="send_file_size_unknown">Rozmiar niedostępny</string>
<string name="send_folder_label">Folder</string>
<string name="send_new_transfer_description">Utwórz nowy transfer</string>
<string name="send_new_transfer_title">Nowy transfer</string>
<string name="send_review_title">Przejrzyj transfer</string>
<string name="send_selected_files_count">Wybrane pliki: %1$d</string>
<string name="send_stop_sharing">Zatrzymaj udostępnianie</string>
<string name="send_stop_sharing_description">To zatrzymuje transfer i przerywa każdego, kto go właśnie pobiera. Pozostaje w historii jako Zatrzymany.</string>
<string name="send_subtitle">Transfery udostępniane z tego urządzenia.</string>
<string name="send_title">Wyślij</string>
<string name="send_transfer_created">Transfer utworzony.</string>
<string name="send_transfer_details_title">Szczegóły transferu</string>
<string name="send_transfers_title">Twoje transfery</string>
<string name="settings_subtitle">Twoja nazwa, miejsce zapisu transferów, wygląd i powiadomienia.</string>
<string name="settings_title">Ustawienia</string>
<string name="snackbar_dismiss">Zamknij</string>
<string name="status_available">Dostępny</string>
<string name="status_cancelled">Anulowany</string>
<string name="status_completed">Ukończony</string>
<string name="status_failed">Niepowodzenie</string>
<string name="status_preparing">Przygotowywanie</string>
<string name="status_receiving">Odbieranie</string>
<string name="status_stopped">Zatrzymany</string>
<string name="storage_calculating">Obliczanie…</string>
<string name="storage_delete_transfers">Usuń wszystkie transfery</string>
<string name="storage_delete_transfers_description">To usuwa z historii wszystkie rekordy wysłanych i odebranych transferów. Twoje odebrane pliki nie są usuwane. Uwaga: silnik transferu zachowuje przechowywaną zawartość, więc dane transferu mogą się nie zmniejszyć. Tej operacji nie można cofnąć.</string>
<string name="storage_deleting">Usuwanie…</string>
<string name="storage_footer">Danymi transferu zarządza silnik transferu — Twoja historia oraz zawartość udostępnionych plików. Przycisk poniżej usuwa rekordy transferów; silnik zachowuje przechowywaną zawartość, więc ta wartość może się nie zmniejszyć. Odebrane pliki to Twoje pobrane pliki i nigdy nie są tu usuwane.</string>
<string name="storage_received_files">Odebrane pliki</string>
<string name="storage_temporary">Pliki tymczasowe</string>
<string name="storage_title">Pamięć</string>
<string name="storage_total">Łącznie</string>
<string name="storage_transfer_data">Dane transferu</string>
<string name="storage_transfers_deleted">Usunięto wszystkie transfery</string>
<string name="transfer_activity_description">Zobacz ważne aktualizacje tego transferu</string>
<string name="transfer_activity_title">Aktywność</string>
<string name="transfer_delete_description">„%1$s” przestanie być udostępniany, a jego historia transferu zostanie usunięta z tego urządzenia.</string>
<string name="transfer_delete_title">Usunąć transfer?</string>
<string name="transfer_deleted">Transfer usunięty.</string>
<string name="transfer_deleting">Usuwanie…</string>
<string name="transfer_details_title">Szczegóły transferu</string>
<string name="transfer_event_approved">Zatwierdzono dostęp odbiorcy</string>
<string name="transfer_event_completed">Odbiorca ukończył transfer</string>
<string name="transfer_event_connecting">Łączenie z nadawcą</string>
<string name="transfer_event_downloading">Pobieranie</string>
<string name="transfer_event_failed">Podczas transferu wystąpił problem</string>
<string name="transfer_event_preparing">Przygotowywanie transferu</string>
<string name="transfer_event_ready">Gotowe do udostępnienia</string>
<string name="transfer_event_refused">Odrzucono dostęp odbiorcy</string>
<string name="transfer_event_requested">Odbiorca poprosił o dostęp</string>
<string name="transfer_event_saving">Zapisywanie</string>
<string name="transfer_event_stopped">Zatrzymano udostępnianie</string>
<string name="transfer_event_updated">Transfer zaktualizowany</string>
<string name="transfer_invitation_saved">Zaproszenie zapisane.</string>
<string name="transfer_nearby_device">Urządzenie w pobliżu</string>
<string name="transfer_nfc_unavailable">Zapis tagów NFC nie jest dostępny na tym urządzeniu.</string>
<string name="transfer_nfc_waiting">Zbliż urządzenie do zapisywalnego tagu NFC.</string>
<string name="transfer_nfc_written">Zaproszenie zapisane na tagu NFC.</string>
<string name="transfer_no_activity">Nie ma jeszcze aktywności do wyświetlenia.</string>
<string name="transfer_no_receivers">Nikt jeszcze nie poprosił o ten transfer.</string>
<string name="transfer_receiver_accepted">Zatwierdzono — oczekiwanie na ukończenie</string>
<string name="transfer_receiver_completed">Odebrano pomyślnie</string>
<string name="transfer_receiver_expired">Prośba wygasła</string>
<string name="transfer_receiver_refused">Prośba odrzucona</string>
<string name="transfer_receiver_requested">Oczekiwanie na Twoje zatwierdzenie</string>
<string name="transfer_receiver_unknown">Status niedostępny</string>
<string name="transfer_receivers_completed_count">Ukończone: %1$d</string>
<string name="transfer_receivers_description">Prośby, zatwierdzenia i ukończone dostawy</string>
<string name="transfer_receivers_pending">Oczekujące: %1$d</string>
<string name="transfer_receivers_title">Odbiorcy</string>
<string name="transfer_scan_qr">Zeskanuj za pomocą VniDrop, aby odebrać ten transfer</string>
<string name="transfer_share_description">Kod QR, plik zaproszenia i opcje w pobliżu</string>
<string name="transfer_share_title">Udostępnij</string>
<string name="value_unavailable">Niedostępne</string>
<string name="version_title">Wersja aplikacji</string>
<plurals name="transfer_file_count">
<item quantity="one">%1$d plik</item>
<item quantity="few">%1$d pliki</item>
<item quantity="many">%1$d plików</item>
<item quantity="other">%1$d pliku</item>
</plurals>
</resources>

View File

@@ -0,0 +1,257 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_bug_report">Comunicar um erro</string>
<string name="about_description">O VniDrop transfere ficheiros e pastas diretamente de um dispositivo para outro através da rede, sem os carregar para qualquer serviço de alojamento. Não é necessário criar qualquer conta e não fica nenhuma cópia da sua transferência na nuvem depois de terminar.</string>
<string name="about_is_direct">Uma transferência direta entre dispositivos — os seus ficheiros vão diretamente para o destinatário.</string>
<string name="about_is_encrypted">As ligações são autenticadas e cifradas ponto a ponto (através do Iroh), e os ficheiros recebidos são verificados pela impressão digital do seu conteúdo.</string>
<string name="about_is_in_control">É você quem decide quem recebe: aprove cada pedido ou abra uma transferência a qualquer pessoa que tenha o convite.</string>
<string name="about_is_no_account">Sem conta — não há nada para registar.</string>
<string name="about_is_open">Código aberto, publicado sob a licença Apache 2.0.</string>
<string name="about_is_title">O que o VniDrop é</string>
<string name="about_isnt_cloud">Não é armazenamento na nuvem — nenhum servidor guarda os seus ficheiros e nada fica na nuvem após uma transferência.</string>
<string name="about_isnt_public">Não é difusão pública — um convite é uma ligação de acesso privado, não um anúncio a toda a gente por perto.</string>
<string name="about_isnt_sync">Não é um serviço de sincronização ou de cópia de segurança.</string>
<string name="about_isnt_title">O que o VniDrop não é</string>
<string name="about_license_label">Licença</string>
<string name="about_privacy">Política de privacidade</string>
<string name="about_privacy_capability">Os convites são chaves de acesso. Trate um código QR, uma etiqueta NFC ou um ficheiro .vnd como uma ligação de acesso privado e partilhe-o apenas com as pessoas pretendidas — sobretudo com «Qualquer pessoa com esta transferência».</string>
<string name="about_privacy_deny">Recusa por predefinição — o VniDrop apenas fornece o conteúdo de uma partilha ativa e rejeita pedidos desconhecidos.</string>
<string name="about_privacy_local">Os ficheiros recebidos são guardados no seu dispositivo e nunca substituem um ficheiro existente.</string>
<string name="about_privacy_policy_label">Política de privacidade</string>
<string name="about_privacy_relay">Se dois dispositivos não conseguirem ligar-se diretamente, a ligação cifrada pode ser retransmitida. Os retransmissores apenas encaminham pacotes cifrados; nunca guardam os seus ficheiros.</string>
<string name="about_privacy_title">Privacidade e segurança</string>
<string name="about_tagline">Envie ficheiros diretamente. Mantenha o controlo sobre quem os recebe.</string>
<string name="about_title">Acerca de</string>
<string name="appearance_auto_description">Acompanhar o aspeto claro ou escuro deste dispositivo.</string>
<string name="appearance_dark_mode">Modo escuro</string>
<string name="appearance_light_mode">Modo claro</string>
<string name="appearance_system_mode">Sistema</string>
<string name="appearance_title">Aspeto</string>
<string name="approval_connection_request">Pedido de receção</string>
<string name="approval_endpoint_id">ID do dispositivo: %1$s</string>
<string name="approval_nearby_device">Um dispositivo próximo</string>
<string name="approval_pending_count">%1$d pedidos em espera</string>
<string name="approval_request_body">%1$s quer receber «%2$s».</string>
<string name="battery_level_title">Nível da bateria</string>
<string name="bug_report_contact_hint">nome@exemplo.com</string>
<string name="bug_report_contact_label">E-mail de contacto (opcional)</string>
<string name="bug_report_description">Diga-nos o que correu mal. Anexamos informações do dispositivo e, opcionalmente, registos recentes (com os valores sensíveis ocultados).</string>
<string name="bug_report_device_section">Informações do dispositivo</string>
<string name="bug_report_expected_hint">Descreva o que esperava que acontecesse</string>
<string name="bug_report_expected_label">O que esperava?</string>
<string name="bug_report_include_logs">Incluir registos recentes</string>
<string name="bug_report_include_logs_description">Ajuda-nos a diagnosticar o problema. Os valores sensíveis são ocultados antes do envio.</string>
<string name="bug_report_logs_size">Tamanho do anexo de registos</string>
<string name="bug_report_missing_expected">Descreva o que esperava.</string>
<string name="bug_report_missing_what">Descreva o que aconteceu.</string>
<string name="bug_report_steps_hint">Enumere os passos para reproduzir o problema</string>
<string name="bug_report_steps_label">Passos para reproduzir (opcional)</string>
<string name="bug_report_submit">Enviar relatório</string>
<string name="bug_report_submit_failed">Não foi possível enviar o relatório de erro. Tente novamente mais tarde.</string>
<string name="bug_report_submitted">Obrigado — o seu relatório de erro foi registado.</string>
<string name="bug_report_submitting">A enviar…</string>
<string name="bug_report_what_hint">Descreva o que aconteceu</string>
<string name="bug_report_what_label">O que aconteceu?</string>
<string name="button_approve">Aprovar</string>
<string name="button_back">Voltar</string>
<string name="button_cancel">Cancelar</string>
<string name="button_cancel_receive">Cancelar</string>
<string name="button_change_files">Alterar ficheiros</string>
<string name="button_choose_files">Escolher ficheiros</string>
<string name="button_choose_folder">Escolher pasta</string>
<string name="button_clear">Limpar</string>
<string name="button_close">Fechar</string>
<string name="button_create_new_transfer">Nova transferência</string>
<string name="button_delete_transfer">Eliminar transferência</string>
<string name="button_download_invitation">Guardar ficheiro .vnd</string>
<string name="button_native_share">Partilhar convite</string>
<string name="button_open_settings">Abrir Definições</string>
<string name="button_receive">Receber</string>
<string name="button_receive_files">Começar a receber</string>
<string name="button_refuse">Recusar</string>
<string name="button_remove_file">Remover ficheiro</string>
<string name="button_reset_default">Usar predefinição</string>
<string name="button_retry">Tentar novamente</string>
<string name="button_share_file">Começar a partilhar</string>
<string name="button_sharing_file">A preparar a transferência…</string>
<string name="button_show_in_files">Mostrar em Ficheiros</string>
<string name="button_write_nfc">Escrever em etiqueta NFC</string>
<string name="device_model_title">Modelo do dispositivo</string>
<string name="device_name_title">Nome do dispositivo</string>
<string name="diagnostics_description">Enviar relatórios de falhas e eventos de utilização anónimos para nos ajudar a melhorar o VniDrop. Pode desativar isto a qualquer momento. Convites, caminhos de ficheiros e conteúdos das transferências nunca são incluídos.</string>
<string name="diagnostics_disabled_message">A partilha de diagnósticos está desativada.</string>
<string name="diagnostics_enabled_message">A partilha de diagnósticos está ativada.</string>
<string name="diagnostics_title">Partilhar diagnósticos</string>
<string name="error_camera">É necessário acesso à câmara para ler um código QR.</string>
<string name="error_device_info">Não foi possível carregar as informações do dispositivo.</string>
<string name="error_filesystem">O VniDrop não conseguiu aceder aos ficheiros ou à pasta selecionados. Verifique as permissões e tente novamente.</string>
<string name="error_generic">Algo correu mal. Tente novamente.</string>
<string name="error_initialization">O VniDrop não conseguiu concluir o arranque. Feche a app e tente novamente.</string>
<string name="error_invalid_ticket">Não foi possível ler este convite. Peça um novo ao remetente.</string>
<string name="error_invitation_empty">Esse convite está vazio. Tente abri-lo novamente.</string>
<string name="error_missing_native_library">A biblioteca nativa do VniDrop está em falta nesta compilação.</string>
<string name="error_nfc">Não foi possível utilizar esta etiqueta NFC. Tente outra.</string>
<string name="error_permission">O remetente não aprovou esta transferência, ou foi recusada.</string>
<string name="error_repository">O VniDrop não conseguiu guardar os dados da transferência neste dispositivo.</string>
<string name="error_selection_failed">Não foi possível abrir o item selecionado. Tente escolhê-lo novamente.</string>
<string name="error_share_empty">Selecione pelo menos um item para partilhar.</string>
<string name="error_socket_bind">O VniDrop não conseguiu abrir os seus sockets de rede neste dispositivo.</string>
<string name="error_starting_up">O VniDrop ainda está a iniciar. Abra o convite novamente dentro de momentos.</string>
<string name="error_transfer">Não foi possível concluir a transferência. Verifique a sua ligação e tente novamente.</string>
<string name="field_receiver_name">Nome do destinatário</string>
<string name="field_sender_name">Nome do remetente</string>
<string name="field_transfer_name">Nome da transferência</string>
<string name="field_username">Nome a apresentar</string>
<string name="folder_status_permission_required">Permissão necessária</string>
<string name="folder_status_unavailable">Indisponível</string>
<string name="folder_status_validating">A verificar a pasta…</string>
<string name="folder_status_writable">Pronto</string>
<string name="metadata_files">Ficheiros</string>
<string name="metadata_size">Tamanho</string>
<string name="metadata_status">Estado</string>
<string name="nav_receive">Receber</string>
<string name="nav_send">Enviar</string>
<string name="nav_settings">Definições</string>
<string name="network_title">Rede</string>
<string name="notifications_description">Seja notificado sobre novos pedidos de receção enquanto o VniDrop está em segundo plano.</string>
<string name="notifications_enabled_message">Notificações ativadas.</string>
<string name="notifications_local_title">Permitir notificações</string>
<string name="notifications_permission_denied">As notificações estão desativadas para o VniDrop. Pode ativá-las nas Definições.</string>
<string name="notifications_settings_open_failed">Não foi possível abrir as definições de notificações.</string>
<string name="notifications_title">Notificações</string>
<string name="notifications_unsupported">As notificações não estão disponíveis neste dispositivo.</string>
<string name="os_version_title">Sistema operativo</string>
<string name="preferences_receive_folder_title">Guardar as transferências recebidas em</string>
<string name="preferences_title">Preferências</string>
<string name="progress_cancelled">Cancelado</string>
<string name="progress_completed">Concluído</string>
<string name="progress_connected">Ligado</string>
<string name="progress_connecting">A ligar</string>
<string name="progress_downloading">A descarregar</string>
<string name="progress_failed">Falhou</string>
<string name="progress_getting_ready">A preparar-se</string>
<string name="progress_interrupted">Transferência interrompida</string>
<string name="progress_preparing">A preparar</string>
<string name="progress_ready">Pronto</string>
<string name="progress_receiving">A receber</string>
<string name="progress_requesting_access">A pedir acesso</string>
<string name="progress_saving">A guardar</string>
<string name="progress_sending">A enviar</string>
<string name="progress_sending_to_count">A enviar para %1$d</string>
<string name="progress_share_ready">Pronto para partilhar</string>
<string name="progress_working">A processar…</string>
<string name="receive_choose_method_body">Escolha o método de convite ao seu dispor.</string>
<string name="receive_choose_method_title">Como pretende ligar-se?</string>
<string name="receive_clear_history">Limpar histórico</string>
<string name="receive_clear_history_description">Todas as receções concluídas, falhadas e canceladas serão removidas do histórico do VniDrop. Os ficheiros descarregados permanecerão neste dispositivo.</string>
<string name="receive_clear_history_title">Limpar o histórico de receção?</string>
<string name="receive_completed">Transferência recebida.</string>
<string name="receive_delete_history_description">«%1$s» será removido do histórico do VniDrop. O ficheiro descarregado permanecerá neste dispositivo.</string>
<string name="receive_delete_history_item">Eliminar do histórico de receção</string>
<string name="receive_delete_history_title">Remover do histórico?</string>
<string name="receive_empty_body">Abra um convite do VniDrop, leia um código QR ou aproxime uma etiqueta NFC.</string>
<string name="receive_empty_title">Ainda não recebeu nada</string>
<string name="receive_history_cleared">Histórico de receção limpo.</string>
<string name="receive_history_title">Histórico</string>
<string name="receive_method_file">Abrir um convite .vnd</string>
<string name="receive_method_file_description">Escolha um convite guardado ou partilhado neste dispositivo.</string>
<string name="receive_method_nfc">Ler etiqueta NFC</string>
<string name="receive_method_nfc_description">Aproxime este dispositivo da etiqueta de convite do remetente.</string>
<string name="receive_method_scan">Ler código QR</string>
<string name="receive_method_scan_description">Use a câmara para ler o código VniDrop do remetente.</string>
<string name="receive_new_subtitle">Transferências que recebeu neste dispositivo.</string>
<string name="receive_nfc_waiting">Aproxime da etiqueta NFC…</string>
<string name="receive_open_files_failed">Não foi possível abrir o VniDrop em Ficheiros.</string>
<string name="receive_review_title">Rever transferência</string>
<string name="receive_title">Receber</string>
<string name="receive_unknown_transfer">Transferência VniDrop</string>
<string name="send_access_anyone">Qualquer pessoa com esta transferência</string>
<string name="send_access_anyone_description">Não é necessária aprovação. Utilize apenas para itens que não se importe de partilhar.</string>
<string name="send_access_anyone_warning">Qualquer pessoa com o convite pode descarregar até parar de partilhar. Não utilize para itens privados ou sensíveis.</string>
<string name="send_access_approval">Perguntar antes de cada descarga</string>
<string name="send_access_approval_description">Aprova ou recusa cada novo destinatário.</string>
<string name="send_access_title">Quem a pode receber?</string>
<string name="send_choose_file_body">Selecione ficheiros ou uma pasta deste dispositivo. Poderá rever a seleção antes de criar a transferência.</string>
<string name="send_choose_file_title">Escolha o que partilhar</string>
<string name="send_empty_body">Crie uma transferência, decida quem a pode receber e depois convide essas pessoas com um código QR, uma etiqueta NFC ou um ficheiro de convite.</string>
<string name="send_empty_title">Ainda não partilhou nada</string>
<string name="send_file_size_unknown">Tamanho indisponível</string>
<string name="send_folder_label">Pasta</string>
<string name="send_new_transfer_description">Criar uma nova transferência</string>
<string name="send_new_transfer_title">Nova transferência</string>
<string name="send_review_title">Rever transferência</string>
<string name="send_selected_files_count">%1$d ficheiros selecionados</string>
<string name="send_stop_sharing">Parar de partilhar</string>
<string name="send_stop_sharing_description">Isto para a transferência e interrompe quem estiver a descarregá-la. Permanece no seu histórico como Parada.</string>
<string name="send_subtitle">Transferências que está a partilhar a partir deste dispositivo.</string>
<string name="send_title">Enviar</string>
<string name="send_transfer_created">Transferência criada.</string>
<string name="send_transfer_details_title">Detalhes da transferência</string>
<string name="send_transfers_title">As suas transferências</string>
<string name="settings_subtitle">O seu nome, onde as transferências são guardadas, o aspeto e as notificações.</string>
<string name="settings_title">Definições</string>
<string name="snackbar_dismiss">Ignorar</string>
<string name="status_available">Disponível</string>
<string name="status_cancelled">Cancelado</string>
<string name="status_completed">Concluído</string>
<string name="status_failed">Falhou</string>
<string name="status_preparing">A preparar</string>
<string name="status_receiving">A receber</string>
<string name="status_stopped">Parada</string>
<string name="storage_calculating">A calcular…</string>
<string name="storage_delete_transfers">Eliminar todas as transferências</string>
<string name="storage_delete_transfers_description">Isto elimina do seu histórico todos os registos de transferências enviadas e recebidas. Os seus ficheiros recebidos não são eliminados. Nota: o motor de transferência mantém o conteúdo armazenado, pelo que os dados de transferência podem não diminuir. Isto não pode ser anulado.</string>
<string name="storage_deleting">A eliminar…</string>
<string name="storage_footer">Os dados de transferência são geridos pelo motor de transferência — o seu histórico mais o conteúdo dos ficheiros que partilhou. O botão abaixo elimina os seus registos de transferências; o motor mantém o conteúdo armazenado, pelo que este valor pode não descer. Os ficheiros recebidos são os ficheiros que descarregou e nunca são eliminados aqui.</string>
<string name="storage_received_files">Ficheiros recebidos</string>
<string name="storage_temporary">Ficheiros temporários</string>
<string name="storage_title">Armazenamento</string>
<string name="storage_total">Total</string>
<string name="storage_transfer_data">Dados de transferência</string>
<string name="storage_transfers_deleted">Todas as transferências eliminadas</string>
<string name="transfer_activity_description">Ver as atualizações importantes desta transferência</string>
<string name="transfer_activity_title">Atividade</string>
<string name="transfer_delete_description">«%1$s» deixará de ser partilhado e o seu histórico de transferência será removido deste dispositivo.</string>
<string name="transfer_delete_title">Eliminar a transferência?</string>
<string name="transfer_deleted">Transferência eliminada.</string>
<string name="transfer_deleting">A eliminar…</string>
<string name="transfer_details_title">Detalhes da transferência</string>
<string name="transfer_event_approved">Acesso do destinatário aprovado</string>
<string name="transfer_event_completed">Um destinatário concluiu a transferência</string>
<string name="transfer_event_connecting">A ligar ao remetente</string>
<string name="transfer_event_downloading">A descarregar</string>
<string name="transfer_event_failed">A transferência teve um problema</string>
<string name="transfer_event_preparing">A preparar a sua transferência</string>
<string name="transfer_event_ready">Pronto para partilhar</string>
<string name="transfer_event_refused">Acesso do destinatário recusado</string>
<string name="transfer_event_requested">Um destinatário pediu acesso</string>
<string name="transfer_event_saving">A guardar</string>
<string name="transfer_event_stopped">Partilha parada</string>
<string name="transfer_event_updated">Transferência atualizada</string>
<string name="transfer_invitation_saved">Convite guardado.</string>
<string name="transfer_nearby_device">Dispositivo próximo</string>
<string name="transfer_nfc_unavailable">A escrita de etiquetas NFC não está disponível neste dispositivo.</string>
<string name="transfer_nfc_waiting">Aproxime o seu dispositivo de uma etiqueta NFC gravável.</string>
<string name="transfer_nfc_written">Convite escrito na etiqueta NFC.</string>
<string name="transfer_no_activity">Ainda não há atividade para mostrar.</string>
<string name="transfer_no_receivers">Ainda ninguém pediu esta transferência.</string>
<string name="transfer_receiver_accepted">Aprovado — a aguardar conclusão</string>
<string name="transfer_receiver_completed">Recebido com sucesso</string>
<string name="transfer_receiver_expired">Pedido expirado</string>
<string name="transfer_receiver_refused">Pedido recusado</string>
<string name="transfer_receiver_requested">A aguardar a sua aprovação</string>
<string name="transfer_receiver_unknown">Estado indisponível</string>
<string name="transfer_receivers_completed_count">%1$d concluídos</string>
<string name="transfer_receivers_description">Pedidos, aprovações e entregas concluídas</string>
<string name="transfer_receivers_pending">%1$d em espera</string>
<string name="transfer_receivers_title">Destinatários</string>
<string name="transfer_scan_qr">Leia com o VniDrop para receber esta transferência</string>
<string name="transfer_share_description">Código QR, ficheiro de convite e opções por perto</string>
<string name="transfer_share_title">Partilhar</string>
<string name="value_unavailable">Indisponível</string>
<string name="version_title">Versão da app</string>
<plurals name="transfer_file_count">
<item quantity="one">%1$d ficheiro</item>
<item quantity="other">%1$d ficheiros</item>
</plurals>
</resources>

View File

@@ -0,0 +1,259 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_bug_report">Сообщить об ошибке</string>
<string name="about_description">VniDrop передаёт файлы и папки напрямую с одного устройства на другое по сети, не загружая их в какой-либо хостинг-сервис. Не нужно создавать учётную запись, и после завершения ни одна копия вашей передачи не остаётся в облаке.</string>
<string name="about_is_direct">Прямая передача с устройства на устройство — ваши файлы попадают напрямую к получателю.</string>
<string name="about_is_encrypted">Соединения аутентифицированы и зашифрованы сквозным шифрованием (через Iroh), а входящие файлы проверяются по хешу их содержимого.</string>
<string name="about_is_in_control">Вы решаете, кто получает: одобряйте каждый запрос или откройте передачу всем, у кого есть приглашение.</string>
<string name="about_is_no_account">Без учётной записи — регистрироваться не нужно.</string>
<string name="about_is_open">Открытый исходный код, распространяется по лицензии Apache 2.0.</string>
<string name="about_is_title">Что такое VniDrop</string>
<string name="about_isnt_cloud">Это не облачное хранилище — ни один сервер не хранит ваши файлы, и после передачи в облаке ничего не остаётся.</string>
<string name="about_isnt_public">Это не публичная рассылка — приглашение является частной ссылкой доступа, а не объявлением для всех поблизости.</string>
<string name="about_isnt_sync">Это не служба синхронизации или резервного копирования.</string>
<string name="about_isnt_title">Чем VniDrop не является</string>
<string name="about_license_label">Лицензия</string>
<string name="about_privacy">Политика конфиденциальности</string>
<string name="about_privacy_capability">Приглашения — это ключи доступа. Относитесь к QR-коду, NFC-метке или файлу .vnd как к частной ссылке доступа и делитесь ими только с теми, для кого они предназначены, особенно при варианте «Любой, у кого есть эта передача».</string>
<string name="about_privacy_deny">Отказ по умолчанию — VniDrop предоставляет только содержимое активной передачи и отклоняет неизвестные запросы.</string>
<string name="about_privacy_local">Полученные файлы сохраняются на вашем устройстве и никогда не перезаписывают существующий файл.</string>
<string name="about_privacy_policy_label">Политика конфиденциальности</string>
<string name="about_privacy_relay">Если два устройства не могут соединиться напрямую, зашифрованное соединение может передаваться через ретранслятор. Ретрансляторы пересылают только зашифрованные пакеты; они никогда не хранят ваши файлы.</string>
<string name="about_privacy_title">Конфиденциальность и безопасность</string>
<string name="about_tagline">Отправляйте файлы напрямую. Сохраняйте контроль над тем, кто их получает.</string>
<string name="about_title">О приложении</string>
<string name="appearance_auto_description">Следовать светлому или тёмному оформлению этого устройства.</string>
<string name="appearance_dark_mode">Тёмный режим</string>
<string name="appearance_light_mode">Светлый режим</string>
<string name="appearance_system_mode">Системный</string>
<string name="appearance_title">Оформление</string>
<string name="approval_connection_request">Запрос на получение</string>
<string name="approval_endpoint_id">Идентификатор устройства: %1$s</string>
<string name="approval_nearby_device">Устройство поблизости</string>
<string name="approval_pending_count">Ожидающих запросов: %1$d</string>
<string name="approval_request_body">%1$s хочет получить «%2$s».</string>
<string name="battery_level_title">Уровень заряда</string>
<string name="bug_report_contact_hint">имя@пример.com</string>
<string name="bug_report_contact_label">Контактный e-mail (необязательно)</string>
<string name="bug_report_description">Расскажите, что пошло не так. Мы прикладываем сведения об устройстве и, при желании, недавние журналы (конфиденциальные значения скрыты).</string>
<string name="bug_report_device_section">Сведения об устройстве</string>
<string name="bug_report_expected_hint">Опишите, что вы ожидали</string>
<string name="bug_report_expected_label">Что вы ожидали?</string>
<string name="bug_report_include_logs">Включить недавние журналы</string>
<string name="bug_report_include_logs_description">Помогает нам диагностировать проблему. Конфиденциальные значения скрываются перед отправкой.</string>
<string name="bug_report_logs_size">Размер вложения с журналами</string>
<string name="bug_report_missing_expected">Опишите, что вы ожидали.</string>
<string name="bug_report_missing_what">Опишите, что произошло.</string>
<string name="bug_report_steps_hint">Перечислите шаги для воспроизведения проблемы</string>
<string name="bug_report_steps_label">Шаги для воспроизведения (необязательно)</string>
<string name="bug_report_submit">Отправить отчёт</string>
<string name="bug_report_submit_failed">Не удалось отправить отчёт об ошибке. Повторите попытку позже.</string>
<string name="bug_report_submitted">Спасибо — ваш отчёт об ошибке записан.</string>
<string name="bug_report_submitting">Отправка…</string>
<string name="bug_report_what_hint">Опишите, что произошло</string>
<string name="bug_report_what_label">Что произошло?</string>
<string name="button_approve">Одобрить</string>
<string name="button_back">Назад</string>
<string name="button_cancel">Отмена</string>
<string name="button_cancel_receive">Отмена</string>
<string name="button_change_files">Изменить файлы</string>
<string name="button_choose_files">Выбрать файлы</string>
<string name="button_choose_folder">Выбрать папку</string>
<string name="button_clear">Очистить</string>
<string name="button_close">Закрыть</string>
<string name="button_create_new_transfer">Новая передача</string>
<string name="button_delete_transfer">Удалить передачу</string>
<string name="button_download_invitation">Сохранить файл .vnd</string>
<string name="button_native_share">Поделиться приглашением</string>
<string name="button_open_settings">Открыть Настройки</string>
<string name="button_receive">Получить</string>
<string name="button_receive_files">Начать получение</string>
<string name="button_refuse">Отклонить</string>
<string name="button_remove_file">Удалить файл</string>
<string name="button_reset_default">По умолчанию</string>
<string name="button_retry">Повторить</string>
<string name="button_share_file">Начать общий доступ</string>
<string name="button_sharing_file">Подготовка передачи…</string>
<string name="button_show_in_files">Показать в Файлах</string>
<string name="button_write_nfc">Записать на NFC-метку</string>
<string name="device_model_title">Модель устройства</string>
<string name="device_name_title">Имя устройства</string>
<string name="diagnostics_description">Отправлять анонимные отчёты о сбоях и события использования, чтобы помочь нам улучшать VniDrop. Вы можете отключить это в любой момент. Приглашения, пути к файлам и содержимое передач никогда не включаются.</string>
<string name="diagnostics_disabled_message">Передача диагностики отключена.</string>
<string name="diagnostics_enabled_message">Передача диагностики включена.</string>
<string name="diagnostics_title">Делиться диагностикой</string>
<string name="error_camera">Для сканирования QR-кода требуется доступ к камере.</string>
<string name="error_device_info">Не удалось загрузить сведения об устройстве.</string>
<string name="error_filesystem">VniDrop не удалось получить доступ к выбранным файлам или папке. Проверьте разрешения и повторите попытку.</string>
<string name="error_generic">Что-то пошло не так. Повторите попытку.</string>
<string name="error_initialization">VniDrop не удалось завершить запуск. Закройте приложение и повторите попытку.</string>
<string name="error_invalid_ticket">Не удалось прочитать это приглашение. Попросите отправителя прислать новое.</string>
<string name="error_invitation_empty">Это приглашение пустое. Попробуйте открыть его снова.</string>
<string name="error_missing_native_library">В этой сборке отсутствует нативная библиотека VniDrop.</string>
<string name="error_nfc">Не удалось использовать эту NFC-метку. Попробуйте другую.</string>
<string name="error_permission">Отправитель не одобрил эту передачу, или она была отклонена.</string>
<string name="error_repository">VniDrop не удалось сохранить данные передачи на этом устройстве.</string>
<string name="error_selection_failed">Не удалось открыть выбранный объект. Попробуйте выбрать его снова.</string>
<string name="error_share_empty">Выберите хотя бы один объект для отправки.</string>
<string name="error_socket_bind">VniDrop не удалось открыть сетевые сокеты на этом устройстве.</string>
<string name="error_starting_up">VniDrop ещё запускается. Откройте приглашение снова через мгновение.</string>
<string name="error_transfer">Не удалось завершить передачу. Проверьте подключение и повторите попытку.</string>
<string name="field_receiver_name">Имя получателя</string>
<string name="field_sender_name">Имя отправителя</string>
<string name="field_transfer_name">Название передачи</string>
<string name="field_username">Отображаемое имя</string>
<string name="folder_status_permission_required">Требуется разрешение</string>
<string name="folder_status_unavailable">Недоступно</string>
<string name="folder_status_validating">Проверка папки…</string>
<string name="folder_status_writable">Готово</string>
<string name="metadata_files">Файлы</string>
<string name="metadata_size">Размер</string>
<string name="metadata_status">Статус</string>
<string name="nav_receive">Получить</string>
<string name="nav_send">Отправить</string>
<string name="nav_settings">Настройки</string>
<string name="network_title">Сеть</string>
<string name="notifications_description">Получайте уведомления о новых запросах на получение, пока VniDrop работает в фоне.</string>
<string name="notifications_enabled_message">Уведомления включены.</string>
<string name="notifications_local_title">Разрешить уведомления</string>
<string name="notifications_permission_denied">Уведомления отключены для VniDrop. Вы можете включить их в Настройках.</string>
<string name="notifications_settings_open_failed">Не удалось открыть настройки уведомлений.</string>
<string name="notifications_title">Уведомления</string>
<string name="notifications_unsupported">Уведомления недоступны на этом устройстве.</string>
<string name="os_version_title">Операционная система</string>
<string name="preferences_receive_folder_title">Сохранять полученные передачи в</string>
<string name="preferences_title">Параметры</string>
<string name="progress_cancelled">Отменено</string>
<string name="progress_completed">Завершено</string>
<string name="progress_connected">Подключено</string>
<string name="progress_connecting">Подключение</string>
<string name="progress_downloading">Загрузка</string>
<string name="progress_failed">Ошибка</string>
<string name="progress_getting_ready">Подготовка</string>
<string name="progress_interrupted">Передача прервана</string>
<string name="progress_preparing">Подготовка</string>
<string name="progress_ready">Готово</string>
<string name="progress_receiving">Получение</string>
<string name="progress_requesting_access">Запрос доступа</string>
<string name="progress_saving">Сохранение</string>
<string name="progress_sending">Отправка</string>
<string name="progress_sending_to_count">Отправка получателям: %1$d</string>
<string name="progress_share_ready">Готово к отправке</string>
<string name="progress_working">Обработка…</string>
<string name="receive_choose_method_body">Выберите доступный вам способ приглашения.</string>
<string name="receive_choose_method_title">Как вы хотите подключиться?</string>
<string name="receive_clear_history">Очистить историю</string>
<string name="receive_clear_history_description">Все завершённые, неудачные и отменённые получения будут удалены из истории VniDrop. Загруженные файлы останутся на этом устройстве.</string>
<string name="receive_clear_history_title">Очистить историю получения?</string>
<string name="receive_completed">Передача получена.</string>
<string name="receive_delete_history_description">«%1$s» будет удалён из истории VniDrop. Загруженный файл останется на этом устройстве.</string>
<string name="receive_delete_history_item">Удалить из истории получения</string>
<string name="receive_delete_history_title">Удалить из истории?</string>
<string name="receive_empty_body">Откройте приглашение VniDrop, отсканируйте QR-код или поднесите NFC-метку.</string>
<string name="receive_empty_title">Пока ничего не получено</string>
<string name="receive_history_cleared">История получения очищена.</string>
<string name="receive_history_title">История</string>
<string name="receive_method_file">Открыть приглашение .vnd</string>
<string name="receive_method_file_description">Выберите приглашение, сохранённое или отправленное на это устройство.</string>
<string name="receive_method_nfc">Прочитать NFC-метку</string>
<string name="receive_method_nfc_description">Поднесите это устройство к метке приглашения отправителя.</string>
<string name="receive_method_scan">Сканировать QR-код</string>
<string name="receive_method_scan_description">Используйте камеру, чтобы отсканировать код VniDrop отправителя.</string>
<string name="receive_new_subtitle">Передачи, полученные на этом устройстве.</string>
<string name="receive_nfc_waiting">Поднесите к NFC-метке…</string>
<string name="receive_open_files_failed">Не удалось открыть VniDrop в Файлах.</string>
<string name="receive_review_title">Проверить передачу</string>
<string name="receive_title">Получить</string>
<string name="receive_unknown_transfer">Передача VniDrop</string>
<string name="send_access_anyone">Любой, у кого есть эта передача</string>
<string name="send_access_anyone_description">Одобрение не требуется. Используйте только для объектов, которыми вы готовы поделиться.</string>
<string name="send_access_anyone_warning">Любой, у кого есть приглашение, может загружать, пока вы не остановите общий доступ. Не используйте это для личных или конфиденциальных объектов.</string>
<string name="send_access_approval">Спрашивать перед каждой загрузкой</string>
<string name="send_access_approval_description">Вы одобряете или отклоняете каждого нового получателя.</string>
<string name="send_access_title">Кто может это получить?</string>
<string name="send_choose_file_body">Выберите файлы или папку на этом устройстве. Вы сможете проверить выбор перед созданием передачи.</string>
<string name="send_choose_file_title">Выберите, чем поделиться</string>
<string name="send_empty_body">Создайте передачу, решите, кто может её получить, затем пригласите этих людей с помощью QR-кода, NFC-метки или файла приглашения.</string>
<string name="send_empty_title">Пока ничем не поделились</string>
<string name="send_file_size_unknown">Размер недоступен</string>
<string name="send_folder_label">Папка</string>
<string name="send_new_transfer_description">Создать новую передачу</string>
<string name="send_new_transfer_title">Новая передача</string>
<string name="send_review_title">Проверить передачу</string>
<string name="send_selected_files_count">Выбрано файлов: %1$d</string>
<string name="send_stop_sharing">Остановить общий доступ</string>
<string name="send_stop_sharing_description">Это остановит передачу и прервёт всех, кто сейчас её загружает. Она останется в вашей истории со статусом «Остановлена».</string>
<string name="send_subtitle">Передачи, которыми вы делитесь с этого устройства.</string>
<string name="send_title">Отправить</string>
<string name="send_transfer_created">Передача создана.</string>
<string name="send_transfer_details_title">Сведения о передаче</string>
<string name="send_transfers_title">Ваши передачи</string>
<string name="settings_subtitle">Ваше имя, место сохранения передач, оформление и уведомления.</string>
<string name="settings_title">Настройки</string>
<string name="snackbar_dismiss">Закрыть</string>
<string name="status_available">Доступно</string>
<string name="status_cancelled">Отменено</string>
<string name="status_completed">Завершено</string>
<string name="status_failed">Ошибка</string>
<string name="status_preparing">Подготовка</string>
<string name="status_receiving">Получение</string>
<string name="status_stopped">Остановлено</string>
<string name="storage_calculating">Вычисление…</string>
<string name="storage_delete_transfers">Удалить все передачи</string>
<string name="storage_delete_transfers_description">Это удалит из истории все записи об отправленных и полученных передачах. Ваши полученные файлы не удаляются. Примечание: механизм передачи сохраняет своё содержимое, поэтому объём данных передачи может не уменьшиться. Это действие нельзя отменить.</string>
<string name="storage_deleting">Удаление…</string>
<string name="storage_footer">Данными передачи управляет механизм передачи — ваша история плюс содержимое файлов, которыми вы поделились. Кнопка ниже удаляет записи о передачах; механизм сохраняет своё содержимое, поэтому это значение может не уменьшиться. Полученные файлы — это загруженные вами файлы, и они никогда не удаляются здесь.</string>
<string name="storage_received_files">Полученные файлы</string>
<string name="storage_temporary">Временные файлы</string>
<string name="storage_title">Хранилище</string>
<string name="storage_total">Всего</string>
<string name="storage_transfer_data">Данные передачи</string>
<string name="storage_transfers_deleted">Все передачи удалены</string>
<string name="transfer_activity_description">Просматривайте важные обновления этой передачи</string>
<string name="transfer_activity_title">Активность</string>
<string name="transfer_delete_description">Общий доступ к «%1$s» будет остановлен, а история передачи будет удалена с этого устройства.</string>
<string name="transfer_delete_title">Удалить передачу?</string>
<string name="transfer_deleted">Передача удалена.</string>
<string name="transfer_deleting">Удаление…</string>
<string name="transfer_details_title">Сведения о передаче</string>
<string name="transfer_event_approved">Доступ получателя одобрен</string>
<string name="transfer_event_completed">Получатель завершил передачу</string>
<string name="transfer_event_connecting">Подключение к отправителю</string>
<string name="transfer_event_downloading">Загрузка</string>
<string name="transfer_event_failed">При передаче возникла проблема</string>
<string name="transfer_event_preparing">Подготовка вашей передачи</string>
<string name="transfer_event_ready">Готово к отправке</string>
<string name="transfer_event_refused">Доступ получателя отклонён</string>
<string name="transfer_event_requested">Получатель запросил доступ</string>
<string name="transfer_event_saving">Сохранение</string>
<string name="transfer_event_stopped">Раздача остановлена</string>
<string name="transfer_event_updated">Передача обновлена</string>
<string name="transfer_invitation_saved">Приглашение сохранено.</string>
<string name="transfer_nearby_device">Устройство поблизости</string>
<string name="transfer_nfc_unavailable">Запись NFC-меток недоступна на этом устройстве.</string>
<string name="transfer_nfc_waiting">Поднесите устройство к записываемой NFC-метке.</string>
<string name="transfer_nfc_written">Приглашение записано на NFC-метку.</string>
<string name="transfer_no_activity">Пока нет активности для отображения.</string>
<string name="transfer_no_receivers">Никто ещё не запросил эту передачу.</string>
<string name="transfer_receiver_accepted">Одобрено — ожидание завершения</string>
<string name="transfer_receiver_completed">Успешно получено</string>
<string name="transfer_receiver_expired">Запрос истёк</string>
<string name="transfer_receiver_refused">Запрос отклонён</string>
<string name="transfer_receiver_requested">Ожидание вашего одобрения</string>
<string name="transfer_receiver_unknown">Статус недоступен</string>
<string name="transfer_receivers_completed_count">Завершено: %1$d</string>
<string name="transfer_receivers_description">Запросы, одобрения и завершённые доставки</string>
<string name="transfer_receivers_pending">Ожидают: %1$d</string>
<string name="transfer_receivers_title">Получатели</string>
<string name="transfer_scan_qr">Отсканируйте с помощью VniDrop, чтобы получить эту передачу</string>
<string name="transfer_share_description">QR-код, файл приглашения и варианты поблизости</string>
<string name="transfer_share_title">Поделиться</string>
<string name="value_unavailable">Недоступно</string>
<string name="version_title">Версия приложения</string>
<plurals name="transfer_file_count">
<item quantity="one">%1$d файл</item>
<item quantity="few">%1$d файла</item>
<item quantity="many">%1$d файлов</item>
<item quantity="other">%1$d файла</item>
</plurals>
</resources>

View File

@@ -1,218 +1,257 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="nav_send">Send</string>
<string name="nav_receive">Receive</string>
<string name="nav_settings">Settings</string>
<string name="send_title">Send</string>
<string name="send_subtitle">Transfers youre sharing from this device.</string>
<string name="send_empty_title">Nothing shared yet</string>
<string name="send_empty_body">Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file.</string>
<string name="button_create_new_transfer">New transfer</string>
<string name="send_transfers_title">Your transfers</string>
<string name="send_new_transfer_title">New transfer</string>
<string name="send_choose_file_title">Choose what to share</string>
<string name="send_choose_file_body">Select files or a folder from this device. You can review the selection before creating the transfer.</string>
<string name="send_selected_files_count">%1$d files selected</string>
<string name="send_folder_label">Folder</string>
<string name="button_remove_file">Remove file</string>
<string name="button_choose_files">Choose files</string>
<string name="button_change_files">Change files</string>
<string name="send_review_title">Review transfer</string>
<string name="send_access_title">Who can receive it?</string>
<string name="send_access_approval">Ask before each download</string>
<string name="send_access_approval_description">You approve or refuse every new receiver.</string>
<string name="send_access_anyone">Anyone with this transfer</string>
<string name="send_access_anyone_description">No approval is required. Only use this for items you are comfortable sharing.</string>
<string name="send_access_anyone_warning">Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items.</string>
<string name="send_file_size_unknown">Size unavailable</string>
<string name="send_transfer_created">Transfer created.</string>
<string name="send_transfer_details_title">Transfer details</string>
<string name="transfer_activity_title">Activity</string>
<string name="transfer_activity_description">See important updates for this transfer</string>
<string name="transfer_receivers_title">Receivers</string>
<string name="transfer_receivers_description">Requests, approvals, and completed deliveries</string>
<string name="transfer_share_title">Share</string>
<string name="transfer_share_description">QR code, invitation file, and nearby options</string>
<string name="transfer_delete_title">Delete transfer?</string>
<string name="transfer_delete_description">“%1$s” will stop being shared and its transfer history will be removed from this device.</string>
<string name="transfer_deleting">Deleting…</string>
<string name="transfer_deleted">Transfer deleted.</string>
<string name="transfer_no_activity">There is no activity to show yet.</string>
<string name="transfer_no_receivers">Nobody has requested this transfer yet.</string>
<string name="transfer_receiver_requested">Waiting for your approval</string>
<string name="transfer_receiver_accepted">Approved — waiting for completion</string>
<string name="transfer_receiver_refused">Request refused</string>
<string name="transfer_receiver_expired">Request expired</string>
<string name="transfer_receiver_completed">Received successfully</string>
<string name="transfer_receiver_unknown">Status unavailable</string>
<string name="transfer_nearby_device">Nearby device</string>
<string name="transfer_scan_qr">Scan with VniDrop to receive this transfer</string>
<string name="button_write_nfc">Write to NFC tag</string>
<string name="button_download_invitation">Save .vnd file</string>
<string name="button_native_share">Share invitation</string>
<string name="transfer_nfc_unavailable">NFC tag writing is not available on this device.</string>
<string name="transfer_nfc_waiting">Hold your device near a writable NFC tag.</string>
<string name="transfer_invitation_saved">Invitation saved.</string>
<string name="transfer_nfc_written">Invitation written to the NFC tag.</string>
<string name="transfer_event_preparing">Preparing your transfer</string>
<string name="transfer_event_ready">Ready to share</string>
<string name="transfer_event_requested">A receiver requested access</string>
<string name="transfer_event_approved">Receiver access approved</string>
<string name="transfer_event_refused">Receiver access refused</string>
<string name="transfer_event_completed">A receiver completed the transfer</string>
<string name="transfer_event_stopped">Sharing stopped</string>
<string name="transfer_event_failed">The transfer encountered a problem</string>
<string name="transfer_event_updated">Transfer updated</string>
<string name="button_share_file">Start sharing</string>
<string name="button_sharing_file">Preparing transfer…</string>
<string name="send_new_transfer_description">Create a new transfer</string>
<string name="button_clear">Clear</string>
<string name="transfer_details_title">Transfer details</string>
<string name="field_transfer_name">Transfer name</string>
<string name="field_sender_name">Sender name</string>
<string name="button_refuse">Refuse</string>
<string name="button_approve">Approve</string>
<string name="receive_title">Receive</string>
<string name="receive_new_subtitle">Transfers youve received on this device.</string>
<string name="receive_empty_title">Nothing received yet</string>
<string name="receive_empty_body">Open a VniDrop invitation, scan a QR code, or hold near an NFC tag.</string>
<string name="button_receive_files">Start receiving</string>
<string name="receive_history_title">History</string>
<string name="receive_clear_history">Clear history</string>
<string name="receive_delete_history_item">Delete from receive history</string>
<string name="receive_delete_history_title">Remove from history?</string>
<string name="receive_delete_history_description">“%1$s” will be removed from VniDrops history. The downloaded file will remain on this device.</string>
<string name="receive_clear_history_title">Clear receive history?</string>
<string name="receive_clear_history_description">All completed, failed, and cancelled receives will be removed from VniDrops history. Downloaded files will remain on this device.</string>
<string name="receive_history_cleared">Receive history cleared.</string>
<string name="receive_choose_method_title">How would you like to connect?</string>
<string name="receive_choose_method_body">Choose the invitation method available to you.</string>
<string name="receive_method_file">Open a .vnd invitation</string>
<string name="receive_method_file_description">Choose an invitation saved or shared to this device.</string>
<string name="receive_method_scan">Scan QR code</string>
<string name="receive_method_scan_description">Use the camera to scan the senders VniDrop code.</string>
<string name="receive_method_nfc">Read NFC tag</string>
<string name="receive_method_nfc_description">Hold this device near the senders invitation tag.</string>
<string name="receive_nfc_waiting">Hold near the NFC tag…</string>
<string name="receive_review_title">Review transfer</string>
<string name="receive_unknown_transfer">VniDrop transfer</string>
<string name="receive_completed">Transfer received.</string>
<string name="button_show_in_files">Show in Files</string>
<string name="receive_open_files_failed">Couldnt open VniDrop in Files.</string>
<string name="field_receiver_name">Receiver name</string>
<string name="button_receive">Receive</string>
<string name="button_retry">Retry</string>
<string name="button_cancel_receive">Cancel</string>
<string name="progress_receiving">Receiving</string>
<string name="progress_preparing">Preparing</string>
<string name="progress_ready">Ready</string>
<string name="progress_share_ready">Ready to share</string>
<string name="progress_connecting">Connecting</string>
<string name="progress_connected">Connected</string>
<string name="progress_requesting_access">Requesting access</string>
<string name="progress_getting_ready">Getting ready</string>
<string name="progress_downloading">Downloading</string>
<string name="progress_saving">Saving</string>
<string name="progress_sending">Sending</string>
<string name="progress_completed">Completed</string>
<string name="progress_cancelled">Cancelled</string>
<string name="progress_failed">Failed</string>
<string name="progress_interrupted">Transfer interrupted</string>
<string name="progress_working">Working…</string>
<string name="status_preparing">Preparing</string>
<string name="status_available">Available</string>
<string name="status_receiving">Receiving</string>
<string name="status_completed">Completed</string>
<string name="status_cancelled">Cancelled</string>
<string name="status_stopped">Stopped</string>
<string name="status_failed">Failed</string>
<string name="transfer_receivers_pending">%1$d waiting</string>
<string name="transfer_receivers_completed_count">%1$d completed</string>
<string name="transfer_event_downloading">Downloading</string>
<string name="transfer_event_saving">Saving</string>
<string name="transfer_event_connecting">Connecting to sender</string>
<string name="transfer_file_count_one">%1$d file</string>
<string name="transfer_file_count_other">%1$d files</string>
<string name="settings_title">Settings</string>
<string name="settings_subtitle">Your name, where transfers are saved, appearance, and notifications.</string>
<string name="appearance_title">Appearance</string>
<string name="preferences_title">Preferences</string>
<string name="field_username">Display name</string>
<string name="preferences_receive_folder_title">Save received transfers to</string>
<string name="button_choose_folder">Choose folder</string>
<string name="button_reset_default">Use default</string>
<string name="button_back">Back</string>
<string name="button_close">Close</string>
<string name="button_cancel">Cancel</string>
<string name="button_delete_transfer">Delete transfer</string>
<string name="folder_status_writable">Ready</string>
<string name="folder_status_permission_required">Permission needed</string>
<string name="folder_status_unavailable">Unavailable</string>
<string name="folder_status_validating">Checking folder…</string>
<string name="appearance_system_mode">System</string>
<string name="about_bug_report">Report a bug</string>
<string name="about_description">VniDrop moves files and folders straight from one device to another over the network, without uploading them to any file-hosting service. Theres no account to create, and no copy of your transfer is left in the cloud once youre done.</string>
<string name="about_is_direct">A direct device-to-device transfer — your files go straight to the receiver.</string>
<string name="about_is_encrypted">Connections are authenticated and end-to-end encrypted (via Iroh), and incoming files are verified by their content hash.</string>
<string name="about_is_in_control">You decide who receives: approve each request, or open a transfer to anyone holding the invitation.</string>
<string name="about_is_no_account">Account-free — theres nothing to sign up for.</string>
<string name="about_is_open">Open source, released under the Apache 2.0 license.</string>
<string name="about_is_title">What VniDrop is</string>
<string name="about_isnt_cloud">Not cloud storage — no server holds your files, and nothing waits in the cloud after a transfer.</string>
<string name="about_isnt_public">Not public broadcasting — an invitation is a private access link, not an announcement to everyone nearby.</string>
<string name="about_isnt_sync">Not a sync or backup service.</string>
<string name="about_isnt_title">What VniDrop isnt</string>
<string name="about_license_label">License</string>
<string name="about_privacy">Privacy policy</string>
<string name="about_privacy_capability">Invitations are capabilities. Treat a QR code, NFC tag, or .vnd file like a private access link and share it only with the people you intend — especially with “Anyone with this transfer.”</string>
<string name="about_privacy_deny">Deny by default — VniDrop serves only the content of an active share and rejects unknown requests.</string>
<string name="about_privacy_local">Received files are saved on your device and never silently overwrite an existing file.</string>
<string name="about_privacy_policy_label">Privacy policy</string>
<string name="about_privacy_relay">If two devices cant connect directly, the encrypted connection may be relayed. Relays forward encrypted packets only; they never store your files.</string>
<string name="about_privacy_title">Privacy &amp; security</string>
<string name="about_tagline">Send files directly. Stay in control of who receives them.</string>
<string name="about_title">About</string>
<string name="appearance_auto_description">Match this devices light or dark appearance.</string>
<string name="appearance_dark_mode">Dark mode</string>
<string name="appearance_light_mode">Light mode</string>
<string name="appearance_auto_description">Match this devices light or dark appearance.</string>
<string name="about_title">About</string>
<string name="about_privacy">Privacy policy</string>
<string name="about_bug_report">Report a bug</string>
<string name="diagnostics_title">Share diagnostics</string>
<string name="diagnostics_description">Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included.</string>
<string name="diagnostics_enabled_message">Diagnostics sharing is on.</string>
<string name="diagnostics_disabled_message">Diagnostics sharing is off.</string>
<string name="bug_report_description">Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted).</string>
<string name="bug_report_what_label">What happened?</string>
<string name="bug_report_expected_label">What did you expect?</string>
<string name="bug_report_steps_label">Steps to reproduce (optional)</string>
<string name="appearance_system_mode">System</string>
<string name="appearance_title">Appearance</string>
<string name="approval_connection_request">Receive request</string>
<string name="approval_endpoint_id">Device ID: %1$s</string>
<string name="approval_nearby_device">A nearby device</string>
<string name="approval_pending_count">%1$d requests waiting</string>
<string name="approval_request_body">%1$s wants to receive “%2$s”.</string>
<string name="battery_level_title">Battery level</string>
<string name="bug_report_contact_hint">name@example.com</string>
<string name="bug_report_contact_label">Contact email (optional)</string>
<string name="bug_report_description">Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted).</string>
<string name="bug_report_device_section">Device information</string>
<string name="bug_report_expected_hint">Describe what you expected to happen</string>
<string name="bug_report_expected_label">What did you expect?</string>
<string name="bug_report_include_logs">Include recent logs</string>
<string name="bug_report_include_logs_description">Helps us diagnose the issue. Sensitive values are redacted before sending.</string>
<string name="bug_report_logs_size">Log attachment size</string>
<string name="bug_report_device_section">Device information</string>
<string name="bug_report_submit">Submit report</string>
<string name="bug_report_submitting">Submitting…</string>
<string name="bug_report_submitted">Thanks — your bug report was recorded.</string>
<string name="bug_report_submit_failed">Could not submit the bug report. Try again later.</string>
<string name="bug_report_missing_what">Please describe what happened.</string>
<string name="bug_report_missing_expected">Please describe what you expected.</string>
<string name="version_title">App version</string>
<string name="device_name_title">Device name</string>
<string name="device_model_title">Device model</string>
<string name="os_version_title">Operating system</string>
<string name="network_title">Network</string>
<string name="battery_level_title">Battery level</string>
<string name="value_unavailable">Not available</string>
<string name="notifications_title">Notifications</string>
<string name="notifications_local_title">Allow notifications</string>
<string name="notifications_description">Get notified about new receive requests while VniDrop is in the background.</string>
<string name="notifications_permission_denied">Notifications are turned off for VniDrop. You can enable them in Settings.</string>
<string name="notifications_unsupported">Notifications are not available on this device.</string>
<string name="notifications_enabled_message">Notifications enabled.</string>
<string name="notifications_settings_open_failed">Could not open notification settings.</string>
<string name="bug_report_missing_what">Please describe what happened.</string>
<string name="bug_report_steps_hint">List the steps to reproduce the issue</string>
<string name="bug_report_steps_label">Steps to reproduce (optional)</string>
<string name="bug_report_submit">Submit report</string>
<string name="bug_report_submit_failed">Could not submit the bug report. Try again later.</string>
<string name="bug_report_submitted">Thanks — your bug report was recorded.</string>
<string name="bug_report_submitting">Submitting…</string>
<string name="bug_report_what_hint">Describe what happened</string>
<string name="bug_report_what_label">What happened?</string>
<string name="button_approve">Approve</string>
<string name="button_back">Back</string>
<string name="button_cancel">Cancel</string>
<string name="button_cancel_receive">Cancel</string>
<string name="button_change_files">Change files</string>
<string name="button_choose_files">Choose files</string>
<string name="button_choose_folder">Choose folder</string>
<string name="button_clear">Clear</string>
<string name="button_close">Close</string>
<string name="button_create_new_transfer">New transfer</string>
<string name="button_delete_transfer">Delete transfer</string>
<string name="button_download_invitation">Save .vnd file</string>
<string name="button_native_share">Share invitation</string>
<string name="button_open_settings">Open Settings</string>
<string name="snackbar_dismiss">Dismiss</string>
<string name="approval_connection_request">Receive request</string>
<string name="approval_request_body">%1$s wants to receive “%2$s”.</string>
<string name="approval_nearby_device">A nearby device</string>
<string name="approval_endpoint_id">Device ID: %1$s</string>
<string name="approval_pending_count">%1$d requests waiting</string>
<string name="metadata_size">Size</string>
<string name="metadata_files">Files</string>
<string name="metadata_status">Status</string>
<string name="button_receive">Receive</string>
<string name="button_receive_files">Start receiving</string>
<string name="button_refuse">Refuse</string>
<string name="button_remove_file">Remove file</string>
<string name="button_reset_default">Use default</string>
<string name="button_retry">Retry</string>
<string name="button_share_file">Start sharing</string>
<string name="button_sharing_file">Preparing transfer…</string>
<string name="button_show_in_files">Show in Files</string>
<string name="button_write_nfc">Write to NFC tag</string>
<string name="device_model_title">Device model</string>
<string name="device_name_title">Device name</string>
<string name="diagnostics_description">Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included.</string>
<string name="diagnostics_disabled_message">Diagnostics sharing is off.</string>
<string name="diagnostics_enabled_message">Diagnostics sharing is on.</string>
<string name="diagnostics_title">Share diagnostics</string>
<string name="error_camera">Camera access is required to scan a QR code.</string>
<string name="error_device_info">Could not load device information.</string>
<string name="error_filesystem">VniDrop could not access the selected files or folder. Check permissions and try again.</string>
<string name="error_generic">Something went wrong. Try again.</string>
<string name="error_starting_up">VniDrop is still starting. Open the invitation again in a moment.</string>
<string name="error_initialization">VniDrop could not finish starting up. Close the app and try again.</string>
<string name="error_invalid_ticket">This invitation could not be read. Ask the sender for a new one.</string>
<string name="error_invitation_empty">That invitation is empty. Try opening it again.</string>
<string name="error_permission">The sender has not approved this transfer, or it was refused.</string>
<string name="error_filesystem">VniDrop could not access the selected files or folder. Check permissions and try again.</string>
<string name="error_transfer">The transfer could not be completed. Check your connection and try again.</string>
<string name="error_repository">VniDrop could not save transfer data on this device.</string>
<string name="error_initialization">VniDrop could not finish starting up. Close the app and try again.</string>
<string name="error_socket_bind">VniDrop could not open its network sockets on this device.</string>
<string name="error_missing_native_library">The native VniDrop library is missing from this build.</string>
<string name="error_selection_failed">Could not open the selected item. Try choosing it again.</string>
<string name="error_device_info">Could not load device information.</string>
<string name="error_nfc">This NFC tag could not be used. Try another tag.</string>
<string name="error_camera">Camera access is required to scan a QR code.</string>
<string name="error_permission">The sender has not approved this transfer, or it was refused.</string>
<string name="error_repository">VniDrop could not save transfer data on this device.</string>
<string name="error_selection_failed">Could not open the selected item. Try choosing it again.</string>
<string name="error_share_empty">Select at least one item to share.</string>
<string name="error_socket_bind">VniDrop could not open its network sockets on this device.</string>
<string name="error_starting_up">VniDrop is still starting. Open the invitation again in a moment.</string>
<string name="error_transfer">The transfer could not be completed. Check your connection and try again.</string>
<string name="field_receiver_name">Receiver name</string>
<string name="field_sender_name">Sender name</string>
<string name="field_transfer_name">Transfer name</string>
<string name="field_username">Display name</string>
<string name="folder_status_permission_required">Permission needed</string>
<string name="folder_status_unavailable">Unavailable</string>
<string name="folder_status_validating">Checking folder…</string>
<string name="folder_status_writable">Ready</string>
<string name="metadata_files">Files</string>
<string name="metadata_size">Size</string>
<string name="metadata_status">Status</string>
<string name="nav_receive">Receive</string>
<string name="nav_send">Send</string>
<string name="nav_settings">Settings</string>
<string name="network_title">Network</string>
<string name="notifications_description">Get notified about new receive requests while VniDrop is in the background.</string>
<string name="notifications_enabled_message">Notifications enabled.</string>
<string name="notifications_local_title">Allow notifications</string>
<string name="notifications_permission_denied">Notifications are turned off for VniDrop. You can enable them in Settings.</string>
<string name="notifications_settings_open_failed">Could not open notification settings.</string>
<string name="notifications_title">Notifications</string>
<string name="notifications_unsupported">Notifications are not available on this device.</string>
<string name="os_version_title">Operating system</string>
<string name="preferences_receive_folder_title">Save received transfers to</string>
<string name="preferences_title">Preferences</string>
<string name="progress_cancelled">Cancelled</string>
<string name="progress_completed">Completed</string>
<string name="progress_connected">Connected</string>
<string name="progress_connecting">Connecting</string>
<string name="progress_downloading">Downloading</string>
<string name="progress_failed">Failed</string>
<string name="progress_getting_ready">Getting ready</string>
<string name="progress_interrupted">Transfer interrupted</string>
<string name="progress_preparing">Preparing</string>
<string name="progress_ready">Ready</string>
<string name="progress_receiving">Receiving</string>
<string name="progress_requesting_access">Requesting access</string>
<string name="progress_saving">Saving</string>
<string name="progress_sending">Sending</string>
<string name="progress_sending_to_count">Sending to %1$d</string>
<string name="progress_share_ready">Ready to share</string>
<string name="progress_working">Working…</string>
<string name="receive_choose_method_body">Choose the invitation method available to you.</string>
<string name="receive_choose_method_title">How would you like to connect?</string>
<string name="receive_clear_history">Clear history</string>
<string name="receive_clear_history_description">All completed, failed, and cancelled receives will be removed from VniDrops history. Downloaded files will remain on this device.</string>
<string name="receive_clear_history_title">Clear receive history?</string>
<string name="receive_completed">Transfer received.</string>
<string name="receive_delete_history_description">“%1$s” will be removed from VniDrops history. The downloaded file will remain on this device.</string>
<string name="receive_delete_history_item">Delete from receive history</string>
<string name="receive_delete_history_title">Remove from history?</string>
<string name="receive_empty_body">Open a VniDrop invitation, scan a QR code, or hold near an NFC tag.</string>
<string name="receive_empty_title">Nothing received yet</string>
<string name="receive_history_cleared">Receive history cleared.</string>
<string name="receive_history_title">History</string>
<string name="receive_method_file">Open a .vnd invitation</string>
<string name="receive_method_file_description">Choose an invitation saved or shared to this device.</string>
<string name="receive_method_nfc">Read NFC tag</string>
<string name="receive_method_nfc_description">Hold this device near the senders invitation tag.</string>
<string name="receive_method_scan">Scan QR code</string>
<string name="receive_method_scan_description">Use the camera to scan the senders VniDrop code.</string>
<string name="receive_new_subtitle">Transfers youve received on this device.</string>
<string name="receive_nfc_waiting">Hold near the NFC tag…</string>
<string name="receive_open_files_failed">Couldnt open VniDrop in Files.</string>
<string name="receive_review_title">Review transfer</string>
<string name="receive_title">Receive</string>
<string name="receive_unknown_transfer">VniDrop transfer</string>
<string name="send_access_anyone">Anyone with this transfer</string>
<string name="send_access_anyone_description">No approval is required. Only use this for items you are comfortable sharing.</string>
<string name="send_access_anyone_warning">Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items.</string>
<string name="send_access_approval">Ask before each download</string>
<string name="send_access_approval_description">You approve or refuse every new receiver.</string>
<string name="send_access_title">Who can receive it?</string>
<string name="send_choose_file_body">Select files or a folder from this device. You can review the selection before creating the transfer.</string>
<string name="send_choose_file_title">Choose what to share</string>
<string name="send_empty_body">Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file.</string>
<string name="send_empty_title">Nothing shared yet</string>
<string name="send_file_size_unknown">Size unavailable</string>
<string name="send_folder_label">Folder</string>
<string name="send_new_transfer_description">Create a new transfer</string>
<string name="send_new_transfer_title">New transfer</string>
<string name="send_review_title">Review transfer</string>
<string name="send_selected_files_count">%1$d files selected</string>
<string name="send_stop_sharing">Stop sharing</string>
<string name="send_stop_sharing_description">This stops the transfer and interrupts anyone currently downloading it. It stays in your history as Stopped.</string>
<string name="send_subtitle">Transfers youre sharing from this device.</string>
<string name="send_title">Send</string>
<string name="send_transfer_created">Transfer created.</string>
<string name="send_transfer_details_title">Transfer details</string>
<string name="send_transfers_title">Your transfers</string>
<string name="settings_subtitle">Your name, where transfers are saved, appearance, and notifications.</string>
<string name="settings_title">Settings</string>
<string name="snackbar_dismiss">Dismiss</string>
<string name="status_available">Available</string>
<string name="status_cancelled">Cancelled</string>
<string name="status_completed">Completed</string>
<string name="status_failed">Failed</string>
<string name="status_preparing">Preparing</string>
<string name="status_receiving">Receiving</string>
<string name="status_stopped">Stopped</string>
<string name="storage_calculating">Calculating…</string>
<string name="storage_delete_transfers">Delete all transfers</string>
<string name="storage_delete_transfers_description">This clears all sent and received transfer records from your history. Your received files are not deleted. Note: the transfer engine keeps its stored content, so Transfer data may not decrease. This cant be undone.</string>
<string name="storage_deleting">Deleting…</string>
<string name="storage_footer">Transfer data is managed by the transfer engine — your history plus the content of files youve shared. The button below clears your transfer records; the engine keeps its stored content, so this value may not drop. Received files are your downloaded files and are never deleted here.</string>
<string name="storage_received_files">Received files</string>
<string name="storage_temporary">Temporary files</string>
<string name="storage_title">Storage</string>
<string name="storage_total">Total</string>
<string name="storage_transfer_data">Transfer data</string>
<string name="storage_transfers_deleted">All transfers deleted</string>
<string name="transfer_activity_description">See important updates for this transfer</string>
<string name="transfer_activity_title">Activity</string>
<string name="transfer_delete_description">“%1$s” will stop being shared and its transfer history will be removed from this device.</string>
<string name="transfer_delete_title">Delete transfer?</string>
<string name="transfer_deleted">Transfer deleted.</string>
<string name="transfer_deleting">Deleting…</string>
<string name="transfer_details_title">Transfer details</string>
<string name="transfer_event_approved">Receiver access approved</string>
<string name="transfer_event_completed">A receiver completed the transfer</string>
<string name="transfer_event_connecting">Connecting to sender</string>
<string name="transfer_event_downloading">Downloading</string>
<string name="transfer_event_failed">The transfer encountered a problem</string>
<string name="transfer_event_preparing">Preparing your transfer</string>
<string name="transfer_event_ready">Ready to share</string>
<string name="transfer_event_refused">Receiver access refused</string>
<string name="transfer_event_requested">A receiver requested access</string>
<string name="transfer_event_saving">Saving</string>
<string name="transfer_event_stopped">Sharing stopped</string>
<string name="transfer_event_updated">Transfer updated</string>
<string name="transfer_invitation_saved">Invitation saved.</string>
<string name="transfer_nearby_device">Nearby device</string>
<string name="transfer_nfc_unavailable">NFC tag writing is not available on this device.</string>
<string name="transfer_nfc_waiting">Hold your device near a writable NFC tag.</string>
<string name="transfer_nfc_written">Invitation written to the NFC tag.</string>
<string name="transfer_no_activity">There is no activity to show yet.</string>
<string name="transfer_no_receivers">Nobody has requested this transfer yet.</string>
<string name="transfer_receiver_accepted">Approved — waiting for completion</string>
<string name="transfer_receiver_completed">Received successfully</string>
<string name="transfer_receiver_expired">Request expired</string>
<string name="transfer_receiver_refused">Request refused</string>
<string name="transfer_receiver_requested">Waiting for your approval</string>
<string name="transfer_receiver_unknown">Status unavailable</string>
<string name="transfer_receivers_completed_count">%1$d completed</string>
<string name="transfer_receivers_description">Requests, approvals, and completed deliveries</string>
<string name="transfer_receivers_pending">%1$d waiting</string>
<string name="transfer_receivers_title">Receivers</string>
<string name="transfer_scan_qr">Scan with VniDrop to receive this transfer</string>
<string name="transfer_share_description">QR code, invitation file, and nearby options</string>
<string name="transfer_share_title">Share</string>
<string name="value_unavailable">Not available</string>
<string name="version_title">App version</string>
<plurals name="transfer_file_count">
<item quantity="other">%1$d files</item>
<item quantity="one">%1$d file</item>
</plurals>
</resources>

View File

@@ -134,13 +134,6 @@ interface CoreGateway {
senderName: String,
accessPolicy: ShareAccessPolicy,
): Result<Share>
suspend fun shareSecurityScopedFileUrl(
fileUrl: String,
displayName: String,
transferName: String,
senderName: String,
accessPolicy: ShareAccessPolicy,
): Result<Share>
/** Multi-source share used by multi-file pickers. */
suspend fun shareSources(
sources: List<uniffi.vnidrop.ShareSource>,
@@ -151,7 +144,6 @@ interface CoreGateway {
suspend fun inspectTicket(ticket: String): Result<TicketInspectionModel>
suspend fun receive(ticket: String, outputDir: String, receiverName: String): Result<Unit>
suspend fun receiveWithOutputSink(ticket: String, outputSink: ReceiveOutputSink, receiverName: String): Result<Unit>
suspend fun receiveIntoSecurityScopedDirectory(ticket: String, outputDirectoryUrl: String, receiverName: String): Result<Unit>
suspend fun cancel(transferId: ULong): Result<Unit>
suspend fun delete(transferId: ULong): Result<Unit>
suspend fun clearReceiveHistory(): Result<ULong>

View File

@@ -113,27 +113,6 @@ class CoreRepository(
accessPolicy = accessPolicy,
)
override suspend fun shareSecurityScopedFileUrl(
fileUrl: String,
displayName: String,
transferName: String,
senderName: String,
accessPolicy: ShareAccessPolicy,
): Result<Share> =
shareSources(
sources = listOf(
ShareSource(
kind = SourceKind.IOS_SECURITY_SCOPED_URL,
value = fileUrl,
displayName = displayName.ifBlank { fileUrl.substringAfterLast('/').ifBlank { "transfer" } },
isDirectory = false,
),
),
transferName = transferName,
senderName = senderName,
accessPolicy = accessPolicy,
)
override suspend fun inspectTicket(ticket: String): Result<TicketInspectionModel> = runCore {
requireCore().inspectTicket(ticket).toModel().also { inspection ->
_state.update { it.copy(lastInspection = inspection) }
@@ -154,17 +133,6 @@ class CoreRepository(
refreshSnapshot()
}
override suspend fun receiveIntoSecurityScopedDirectory(
ticket: String,
outputDirectoryUrl: String,
receiverName: String,
): Result<Unit> = runCore {
withPlatformPathAccess(SourceKind.IOS_SECURITY_SCOPED_URL, outputDirectoryUrl) {
requireCore().receive(ticket, outputDirectoryUrl, receiverName.ifBlank { null })
}
refreshSnapshot()
}
override suspend fun cancel(transferId: ULong): Result<Unit> = runCore {
requireCore().cancelTransfer(transferId)
refreshSnapshot()

View File

@@ -10,9 +10,9 @@ data class PickedShareFile(
/** App-owned picker copy that may be deleted after import or when selection is abandoned. */
val isTemporaryCopy: Boolean = false,
/**
* When true, [value] is a directory (filesystem path, iOS security-scoped
* folder URL, or Android document tree URI). Platform share code expands or
* walks it; Rust cannot treat an Android FD as a directory.
* When true, [value] is a directory (filesystem path or Android document tree
* URI). Platform share code expands or walks it; Rust cannot treat an Android
* FD as a directory.
*/
val isDirectory: Boolean = false,
)

View File

@@ -8,7 +8,6 @@ enum class ReceiveFolderKind {
/** Shared system Downloads via MediaStore (Android 10+). */
AndroidPublicDownloads,
AndroidTreeUri,
IosSecurityScopedUrl,
}
/** Stable token stored in preferences for [ReceiveFolderKind.AndroidPublicDownloads]. */

View File

@@ -2,10 +2,8 @@ package com.vnidrop.app.core
import uniffi.vnidrop.SourceKind
// Platform file handles have different lifetime rules. Desktop paths need no
// extra work, Rust duplicates Android fd sources immediately, and iOS
// security-scoped URLs must remain leased while Rust performs the blocking
// import/export call.
// Desktop paths need no extra work, while Rust duplicates borrowed Android file
// descriptors immediately before the platform closes them.
internal expect suspend fun <T> withPlatformPathAccess(
kind: SourceKind,
value: String,

View File

@@ -161,14 +161,10 @@ class ReceiveViewModel(
it.copy(isReceiving = true, lastReceiveError = null, activeReceiveTransferId = null)
}
val outputSink = fileSystemService.createReceiveOutputSink(folder)
val result = when {
outputSink != null -> repository.receiveWithOutputSink(current.ticket, outputSink, current.receiverName)
folder.kind == ReceiveFolderKind.IosSecurityScopedUrl -> repository.receiveIntoSecurityScopedDirectory(
current.ticket,
folder.value,
current.receiverName,
)
else -> repository.receive(current.ticket, folder.value, current.receiverName)
val result = if (outputSink != null) {
repository.receiveWithOutputSink(current.ticket, outputSink, current.receiverName)
} else {
repository.receive(current.ticket, folder.value, current.receiverName)
}
result.fold(
onSuccess = {

View File

@@ -5,6 +5,7 @@ import com.vnidrop.app.core.CoreEventModel
import com.vnidrop.app.core.Transfer
import com.vnidrop.app.core.TransferStatus
import kotlin.math.roundToInt
import org.jetbrains.compose.resources.PluralStringResource
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import vnidrop.shared.generated.resources.Res
@@ -30,8 +31,7 @@ import vnidrop.shared.generated.resources.status_failed
import vnidrop.shared.generated.resources.status_preparing
import vnidrop.shared.generated.resources.status_receiving
import vnidrop.shared.generated.resources.status_stopped
import vnidrop.shared.generated.resources.transfer_file_count_one
import vnidrop.shared.generated.resources.transfer_file_count_other
import vnidrop.shared.generated.resources.transfer_file_count
enum class WindowClass {
Phone,
@@ -206,9 +206,8 @@ fun summarizeProgress(events: List<CoreEventModel>): List<TransferProgress> =
.take(6)
.mapNotNull { progressForTransfer(events, it) }
/** File-count string resource for transfer subtitles (resolve with [stringResource]). */
fun transferFileCountResource(fileCount: ULong): StringResource =
if (fileCount == 1UL) Res.string.transfer_file_count_one else Res.string.transfer_file_count_other
/** File-count plural resource for transfer subtitles (resolve with [pluralStringResource]). */
fun transferFileCountResource(): PluralStringResource = Res.plurals.transfer_file_count
fun formatBytes(size: ULong): String {
val value = size.toDouble()

View File

@@ -217,7 +217,7 @@ class ViewModelsTest {
fun settingsUsesDefaultReceiveFolderWhenPlatformDoesNotSupportCustomFolders() = runTest {
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
val appDocuments = ReceiveFolder(ReceiveFolderKind.FileSystemPath, "/app/Documents", "Documents")
val externalFolder = ReceiveFolder(ReceiveFolderKind.IosSecurityScopedUrl, "file:///external", "External")
val externalFolder = ReceiveFolder(ReceiveFolderKind.AndroidTreeUri, "content://external", "External")
val preferences = preferences().apply {
mutablePreferences.value = mutablePreferences.value.copy(receiveFolder = externalFolder)
}

View File

@@ -89,13 +89,6 @@ class FakeCoreGateway : CoreGateway {
senderName: String,
accessPolicy: ShareAccessPolicy,
) = Result.failure<Share>(UnsupportedOperationException())
override suspend fun shareSecurityScopedFileUrl(
fileUrl: String,
displayName: String,
transferName: String,
senderName: String,
accessPolicy: ShareAccessPolicy,
) = Result.failure<Share>(UnsupportedOperationException())
override suspend fun shareSources(
sources: List<uniffi.vnidrop.ShareSource>,
transferName: String,
@@ -142,13 +135,6 @@ class FakeCoreGateway : CoreGateway {
awaitReceiveIfNeeded()
return receiveResult
}
override suspend fun receiveIntoSecurityScopedDirectory(ticket: String, outputDirectoryUrl: String, receiverName: String): Result<Unit> {
receiveCount += 1
lastReceiveTicket = ticket
lastReceiveReceiverName = receiverName
awaitReceiveIfNeeded()
return receiveResult
}
override suspend fun cancel(transferId: ULong): Result<Unit> {
cancelledTransfers += transferId
return Result.success(Unit)

View File

@@ -20,8 +20,7 @@ import vnidrop.shared.generated.resources.progress_interrupted
import vnidrop.shared.generated.resources.progress_saving
import vnidrop.shared.generated.resources.progress_sending
import vnidrop.shared.generated.resources.progress_working
import vnidrop.shared.generated.resources.transfer_file_count_one
import vnidrop.shared.generated.resources.transfer_file_count_other
import vnidrop.shared.generated.resources.transfer_file_count
class AppUiModelsTest {
@Test
@@ -235,10 +234,8 @@ class AppUiModelsTest {
}
@Test
fun transferFileCountPicksSingularAndPluralResources() {
assertEquals(Res.string.transfer_file_count_one, transferFileCountResource(1UL))
assertEquals(Res.string.transfer_file_count_other, transferFileCountResource(2UL))
assertEquals(Res.string.transfer_file_count_other, transferFileCountResource(0UL))
fun transferFileCountUsesPluralResource() {
assertEquals(Res.plurals.transfer_file_count, transferFileCountResource())
}
@Test

View File

@@ -1,7 +0,0 @@
package com.vnidrop.app
import androidx.compose.ui.window.ComposeUIViewController
import com.vnidrop.app.feature.receive.ExternalInvitationController
fun MainViewController(externalInvitations: ExternalInvitationController) =
ComposeUIViewController { App(rememberIosAppDependencies(externalInvitations)) }

View File

@@ -1,57 +0,0 @@
package com.vnidrop.app
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vnidrop.app.core.rememberFileSystemService
import com.vnidrop.app.notifications.IosLocalNotificationService
import com.vnidrop.app.feature.receive.ExternalInvitationController
import platform.Foundation.NSBundle
import platform.Foundation.NSApplicationSupportDirectory
import platform.Foundation.NSSearchPathForDirectoriesInDomains
import platform.Foundation.NSUserDomainMask
import platform.UIKit.UIDevice
@Composable
fun rememberIosAppDependencies(externalInvitations: ExternalInvitationController): AppDependencies {
val fileSystemService = rememberFileSystemService()
return remember(fileSystemService) {
val device = UIDevice.currentDevice
AppDependencies(
environment = PlatformEnvironment(
name = device.systemName() + " " + device.systemVersion,
appVersion = NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") as? String ?: "0.1.0",
defaultCoreDataDir = iosApplicationDataDirectory(),
defaultUsername = device.name.takeIf(String::isNotBlank) ?: "Receiver",
),
deviceInfoProvider = IosDeviceInfoProvider(device),
fileSystemService = fileSystemService,
localNotificationService = IosLocalNotificationService(),
externalInvitations = externalInvitations,
)
}
}
private fun iosApplicationDataDirectory(): String =
(NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true).firstOrNull() as? String)
?.trimEnd('/')?.plus("/VniDrop")
?: error("iOS Application Support directory is unavailable")
private class IosDeviceInfoProvider(
private val device: UIDevice,
) : DeviceInfoProvider {
override suspend fun load(): DeviceInfo = DeviceInfo(
deviceName = device.name,
deviceModel = device.model,
operatingSystem = device.systemName() + " " + device.systemVersion,
network = null,
batteryLevel = runCatching {
val wasMonitoring = device.batteryMonitoringEnabled
try {
device.batteryMonitoringEnabled = true
device.batteryLevel.takeIf { it >= 0.0 }?.let { "${(it * 100).toInt()}%" }
} finally {
device.batteryMonitoringEnabled = wasMonitoring
}
}.getOrNull(),
)
}

View File

@@ -1,166 +0,0 @@
package com.vnidrop.app.core
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.readBytes
import platform.Foundation.NSURL
import platform.Foundation.NSFileManager
import platform.Foundation.NSFileSize
import platform.Foundation.NSNumber
import platform.UIKit.UIApplication
import platform.UIKit.UIDocumentPickerDelegateProtocol
import platform.UIKit.UIDocumentPickerViewController
import platform.UIKit.UIDocumentInteractionController
import platform.UIKit.UIImage
import platform.UIKit.UIImagePNGRepresentation
import platform.UIKit.UIModalPresentationFormSheet
import platform.UniformTypeIdentifiers.UTTypeFolder
import platform.UniformTypeIdentifiers.UTTypeItem
import platform.darwin.NSObject
private var retainedPickerDelegate: DocumentPickerDelegate? = null
@Composable
actual fun rememberShareFilePicker(
onFilesPicked: (List<PickedShareFile>) -> Unit,
onError: (String) -> Unit,
): ShareFilePicker = remember(onFilesPicked, onError) {
object : ShareFilePicker {
@OptIn(ExperimentalForeignApi::class)
override fun pickFiles() {
val presenter = UIApplication.sharedApplication.keyWindow?.rootViewController
if (presenter == null) {
onError("Could not find an iOS view controller for the document picker")
return
}
// The composer outlives this callback, so Rust imports a sandbox copy instead of a short-lived provider URL.
val picker = UIDocumentPickerViewController(forOpeningContentTypes = listOf(UTTypeItem), asCopy = true)
picker.allowsMultipleSelection = true
val delegate = DocumentPickerDelegate(
onFilesPicked = onFilesPicked,
onError = onError,
useFileSystemPaths = true,
)
retainedPickerDelegate = delegate
picker.delegate = delegate
picker.modalPresentationStyle = UIModalPresentationFormSheet
presenter.presentViewController(picker, animated = true, completion = null)
}
@OptIn(ExperimentalForeignApi::class)
override fun pickFolder() {
val presenter = UIApplication.sharedApplication.keyWindow?.rootViewController
if (presenter == null) {
onError("Could not find an iOS view controller for the folder picker")
return
}
val picker = UIDocumentPickerViewController(forOpeningContentTypes = listOf(UTTypeFolder), asCopy = true)
val delegate = DocumentPickerDelegate(
onFilesPicked = { folders ->
val folder = folders.firstOrNull() ?: return@DocumentPickerDelegate
onFilesPicked(
listOf(
folder.copy(isDirectory = true),
),
)
},
onError = onError,
forceDirectory = true,
useFileSystemPaths = true,
)
retainedPickerDelegate = delegate
picker.delegate = delegate
picker.modalPresentationStyle = UIModalPresentationFormSheet
presenter.presentViewController(picker, animated = true, completion = null)
}
}
}
@Composable
actual fun rememberReceiveFolderPicker(
onFolderPicked: (ReceiveFolder) -> Unit,
onError: (String) -> Unit,
): ReceiveFolderPicker = remember(onFolderPicked, onError) {
object : ReceiveFolderPicker {
@OptIn(ExperimentalForeignApi::class)
override fun pickFolder() {
val presenter = UIApplication.sharedApplication.keyWindow?.rootViewController
if (presenter == null) {
onError("Could not find an iOS view controller for the folder picker")
return
}
val picker = UIDocumentPickerViewController(forOpeningContentTypes = listOf(UTTypeFolder), asCopy = false)
val delegate = DocumentPickerDelegate(
onFilesPicked = { folders ->
val folder = folders.firstOrNull() ?: return@DocumentPickerDelegate
onFolderPicked(
ReceiveFolder(
kind = ReceiveFolderKind.IosSecurityScopedUrl,
value = folder.value,
displayName = folder.displayName,
),
)
},
onError = onError,
)
retainedPickerDelegate = delegate
picker.delegate = delegate
picker.modalPresentationStyle = UIModalPresentationFormSheet
presenter.presentViewController(picker, animated = true, completion = null)
}
}
}
private class DocumentPickerDelegate(
private val onFilesPicked: (List<PickedShareFile>) -> Unit,
private val onError: (String) -> Unit,
private val forceDirectory: Boolean = false,
private val useFileSystemPaths: Boolean = false,
) : NSObject(), UIDocumentPickerDelegateProtocol {
override fun documentPicker(controller: UIDocumentPickerViewController, didPickDocumentsAtURLs: List<*>) {
val files = didPickDocumentsAtURLs.mapNotNull { raw ->
val url = raw as? NSURL ?: return@mapNotNull null
val displayName = url.lastPathComponent ?: "transfer"
val didStartAccess = url.startAccessingSecurityScopedResource()
val sizeBytes = try {
if (forceDirectory) {
null
} else {
val attributes = url.path?.let { NSFileManager.defaultManager.attributesOfItemAtPath(it, null) }
(attributes?.get(NSFileSize) as? NSNumber)?.unsignedLongLongValue
}
} finally {
if (didStartAccess) url.stopAccessingSecurityScopedResource()
}
PickedShareFile(
if (useFileSystemPaths) url.path.orEmpty() else url.absoluteString ?: url.path.orEmpty(),
displayName,
sizeBytes,
nativeFileIcon(url),
isTemporaryCopy = useFileSystemPaths,
isDirectory = forceDirectory,
)
}
if (files.isEmpty()) {
onError("The selected iOS document URL was invalid")
} else {
onFilesPicked(files)
}
retainedPickerDelegate = null
}
override fun documentPickerWasCancelled(controller: UIDocumentPickerViewController) {
retainedPickerDelegate = null
}
}
@OptIn(ExperimentalForeignApi::class)
private fun nativeFileIcon(url: NSURL): ByteArray? = runCatching {
val controller = UIDocumentInteractionController.interactionControllerWithURL(url)
val icon = controller.icons.lastOrNull() as? UIImage ?: return null
val data = UIImagePNGRepresentation(icon) ?: return null
data.bytes?.readBytes(data.length.toInt())
}.getOrNull()

Some files were not shown because too many files have changed in this diff Show More