Compare commits

..
Author SHA1 Message Date
nvrl a888e0bb28 fix(gui): run the CAPTCHA page in its own process (B195)
On secureblue, WebKit cannot start the sandbox for the verification page:
bubblewrap is kept from the journal socket, xdg-dbus-proxy fails, and
WebKit ends its process with g_error. The webview lived in pdfs-app, so
the whole app crashed after the sign-in (#29).

- gui: `pdfs-app --human-verification` shows the page in its own window,
  reads the URL on stdin and prints the token on stdout; its logs go to
  stderr
- gui: a child that dies or fails without a token ends the sign-in with a
  message that the page could not be opened, instead of "cancelled"
- i18n: translate the new message in every catalog
- docs: B195 in the bug ledger, the cause and a one-time workaround in
  TROUBLESHOOTING.md, the child process in ARCHITECTURE.md, changelog
2026-10-08 10:25:14 +02:00
nvrl c7fc85e943 fix(gui): create the state dir before the tray and app use it (B194)
On a fresh install only the daemon and the CLI created
~/.local/state/proton-drive-linux. The app starts the tray before the
first sign-in, so the tray failed to bind its single-instance socket and
exited, and the Takeout page could not remember staged archives (#29).

- gui: pdfs-tray and pdfs-app call AppDirs::ensure at start, which also
  makes the directories owner-only
- docs: B194 in the bug ledger, changelog entry
2026-10-08 10:25:04 +02:00
nvrl 5bfae0fee1 docs(changelog): add the 3.1.1 entry
3.1.1 shipped the client identity change without a changelog entry. Also
add the missing release links for 3.1.0 and 3.1.1.
2026-10-08 10:24:07 +02:00
nvrl edd62c5f7a chore(release): 3.1.1 2026-10-07 17:01:32 +02:00
nvrl 5c1530b4b9 fix(core,gui): send the release version as the client identity
Every release sent the fixed `external-drive-linux@0.1.0-alpha`. If Proton
ever retires that version, every release fails sign-in at once and no update
can fix it.

- core: APP_VERSION and USER_AGENT follow CARGO_PKG_VERSION
  (`external-drive-linux@<version>-stable`, `proton-drive-linux/<version>`)
- core: `app_version` and `user_agent` in config.json are optional overrides;
  the fixed values older releases stored there read as unset and leave the
  file on the next save, a hand-set value is kept
- gui: the About dialog shows the release, its debug info the identity
  actually sent
- docs: describe the new default and the migration
2026-10-07 17:01:32 +02:00
Nils Pukropp ce324510de chore(aur): update packages to 3.1.0 2026-10-07 10:53:15 +00:00
nvrl 274bfd0ca2 Merge pull request #27 from narrrl/feature/two-password-mode
Feature/two password mode
2026-10-07 12:47:05 +02:00
nvrl b8cfd30ff3 fix(core): give the kernel-keyring unlock owner read access
A new key grants its owner only "view"; read needs possession, which a
process has only when its session keyring links to the user keyring. A
systemd user service or a container does not guarantee that, so the
daemon could store the unlock and then fail to read it back (EACCES).
Set the permissions explicitly after add_key.
2026-10-07 08:17:50 +02:00
nvrl 9fa88ff397 feat(core,cli,gui): support two-password accounts without storing the mailbox password
A two-password account signed in with its login password as the mailbox
password, so the key chain never unlocked and the daemon retried forever.

Login now asks for the mailbox password and checks it against the account
(proton-sdk 0.8 `unlock`), so a typo is a retry. The password is never
stored: the keyring blob keeps the per-key passphrases instead, and for a
two-password account only when the user opts in (`--remember`, or the
"Keep unlocked on this computer" switch). By default they live in the
kernel keyring for the login session and the account is locked after a
reboot until `pdfs unlock` or the app supplies them again.

- core: kernelkey module, Error::Locked / WrongMailboxPassword, unlock,
  lock, key_state, ready; blobs written by 3.0.x are migrated on first
  start; token refresh updates only the tokens in the stored blob
- cli: `pdfs unlock [--remember]`, `pdfs lock [--forget]`; status and
  diagnose report the key state; the daemon waits while locked and is
  marked non-dumpable
- gui: mailbox password dialog at sign-in and on open, locked tray state
- po: all catalogs translated
- bump to 3.1.0, depend on proton-sdk and proton-drive-rs 0.8.0

Refs #26.
2026-10-07 08:17:50 +02:00
nvrl f105159186 fix(cli,fuse): keep answering the systemd watchdog while the daemon retries
The watchdog is armed when the process starts, but only the mount's supervisor
pinged it. A daemon waiting for a login or backing off after a failed mount
(#26: key unlock failing) was aborted after WatchdogSec and left a coredump
instead of its error. Sleep through idle_sleep in those stretches; the mounted
ping still has to pass the control-socket probe.
2026-10-07 08:17:50 +02:00
nvrl a14a63c94c ci: add an on-demand Fedora test build
Manual workflow that builds the RPM and the bare binaries in a Fedora
container, so a user can try a branch against Fedora's own GTK stack.
Uploads them as an artifact and, on request, as one rolling pre-release
per branch that needs no GitHub login to download.
2026-10-07 04:27:22 +02:00
nvrl ed3477d1b3 docs: point contributors at the simulation runs and the ledger rules
DEVELOPMENT.md lists the daemon's current modules, asks for the
simulation runs on FUSE, queue and sync changes, and adds how to handle
a bug fix and a user-visible change, including the six pages the wiki
publisher copies. The release checklist moves to TESTING.md.
2026-10-06 19:29:02 +02:00
nvrl aa384ae2eb chore: cite docs/BUGS.md and drop references to notes outside the repo
66 comments cited `bugs.md B<n>`, the ledger's old name; they now say
`docs/BUGS.md B<n>`, and every cited number has an entry. About 90 more
cited offline.md, devices.md, features.md, mount-architecture.md,
plan.md and improvements.md, planning notes that were never in the
repository. Those citations are removed; the comments keep their text.
2026-10-06 19:29:02 +02:00
nvrl 10c69b0d64 test(acceptance): explain the metadata floor for local-first mounts
The throughput case said every create and unlink on a Drive mount waits
for Drive. Since 3.0.0 that holds only with local_first off; the floor
of 3 operations a second stays for that case.
2026-10-06 19:29:02 +02:00
nvrl 39be354bd2 docs(bugs): give the ledger an index and one order
The ledger ran B193 down to B78, then B1 up to B75, with T1 to T4 in
the middle and a stray top-level heading inside B14. Status lines used a
dozen wordings for the same few states, so finding what is still open
meant reading 6,600 lines.

Add a header: what the numbers promise (permanent, never reused; B76
and B77 were never assigned), the status vocabulary, and how to add an
entry, with a template. Add an index sorted by status, with what is left
for every open or partly fixed entry. Order all entries newest first
under one Entries section, and move the acceptance-runner findings to
their own section. Titles are in sentence case; anchors are unchanged.

Entry text is kept as written, except B89: the file it described,
proton-dev/CLAUDE.md, no longer exists, so it is closed.
2026-10-06 19:29:02 +02:00
nvrl 2cca8afba9 docs: describe local-first changes and sync issues for users
The user guide said only writes were queued. Since 3.0.0 creating,
renaming and deleting through the mount return at once too, and changes
Drive refuses become sync issues. Add a section on changes waiting for
Proton Drive: retry, export, discard, and the two cases that land under
another name or place instead of failing.

Troubleshooting covers changes that have not reached other devices yet,
`pdfs sync issues` and the local_first fallback, and names the real
error for symlinks and hard links (EPERM, not EOPNOTSUPP). CONFIGURATION
lists MemoryHigh and the test variables; CLI notes that `sync export`
takes a folder.
2026-10-06 19:14:29 +02:00
nvrl 6e9beb4b16 docs: rework contributor docs for the 3.0.0 local-first mount
ARCHITECTURE.md described the 2.x write path, where namespace changes
waited for Drive and every mount had its own inode space. Rewrite it
around the local tree (lid, inode = lid + 1, local~<lid> stand-ins), the
queue and its 16 drain workers, sync issues, and the remaining split
between the mount and the mirror engine. Drop the pins.json claim; pins
moved into the database at schema v5.

TESTING.md gains the simulation runs and loses the per-case narrative
that --list and BUGS.md already carry. ROADMAP.md takes over the
post-3.0.0 items from the milestone status file.

Remove MILESTONE-3.0.0-STATUS.md (a working log) and UI_UX_PLAN.md (the
2.x design audit). Code comments and CI that cited the untracked
docs/MILESTONE-3.0.0.md now point at TESTING.md, ARCHITECTURE.md and
ROADMAP.md. Threat-model links follow the section to §10.
2026-10-06 19:14:29 +02:00
nvrl 63484bf4b6 chore(images): add cover image
1920×1080 cover for link previews, social posts and the GitHub social preview.
The website renders its og.png from cover.svg.
2026-10-06 14:46:56 +02:00
nvrl 37c66b7220 chore(support) addd buy me a coffee badge + link 2026-10-06 10:49:24 +02:00
Nils Pukropp 41b6edbecb chore(aur): update packages to 3.0.0 2026-10-05 05:12:08 +00:00
89 changed files with 8476 additions and 6433 deletions

No files matched your search

+1 -1
View File
@@ -64,7 +64,7 @@ jobs:
# Real Proton credentials and /dev/fuse deliberately do not run on pull # Real Proton credentials and /dev/fuse deliberately do not run on pull
# requests. Follow docs/TESTING.md on a dedicated test-account runner. # requests. Follow docs/TESTING.md on a dedicated test-account runner.
# The seeded simulation runs (docs/MILESTONE-3.0.0.md §8.2): real FUSE # The seeded simulation runs (docs/TESTING.md): real FUSE
# daemons against an in-memory Drive, so they need /dev/fuse but no account. # daemons against an in-memory Drive, so they need /dev/fuse but no account.
simulation: simulation:
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
+121
View File
@@ -0,0 +1,121 @@
name: Test build
# On-demand Fedora RPM of any branch, for someone to try a fix before it ships.
# Run it from the Actions tab ("Run workflow", pick the branch). Built inside a
# Fedora container so the binaries link against Fedora's own GTK, libadwaita and
# WebKitGTK, not the Ubuntu ones the release RPM is built against.
#
# The RPM is unsigned and versioned `<Cargo version>-1.test.<sha>`, so it sorts
# below the release it precedes and `dnf` replaces it when that ships.
on:
workflow_dispatch:
inputs:
fedora:
description: 'Fedora release to build on'
required: false
default: '44'
type: string
prerelease:
description: 'Also publish a pre-release anyone can download without a GitHub login'
required: false
default: false
type: boolean
permissions:
contents: read
concurrency:
group: test-build-${{ github.ref }}
cancel-in-progress: true
jobs:
rpm:
name: Fedora ${{ inputs.fedora }} RPM
runs-on: ubuntu-24.04
container: fedora:${{ inputs.fedora }}
steps:
- name: Install build dependencies
run: |
dnf install -y --setopt=install_weak_deps=False \
git rpm-build gcc gcc-c++ make gettext curl \
pkgconf-pkg-config fuse3-devel gtk4-devel libadwaita-devel \
libsecret-devel dbus-devel glib2-devel webkitgtk6.0-devel
- name: Checkout repository
uses: actions/checkout@v7
# Fedora's packaged Rust can trail the workspace's minimum (1.96).
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: '1.96'
cache: false
- name: Cache Cargo target
uses: swatinem/rust-cache@v2
with:
shared-key: fedora-test-build
save-if: false
- name: Build release binaries
run: cargo build --release --locked
- name: Package RPM
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
VERSION=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2)
SHORT=${GITHUB_SHA::7}
mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS,BUILDROOT,db}
rpmbuild -bb \
--nodeps \
--define "_topdir $PWD/rpmbuild" \
--define "_sourcedir $PWD" \
--define "git_dir $PWD" \
--define "prebuilt 1" \
--define "version ${VERSION}" \
--define "dist .test.${SHORT}" \
--define "_dbpath $PWD/rpmbuild/db" \
packaging/proton-drive-linux.spec
mkdir -p out
cp rpmbuild/RPMS/x86_64/*.rpm out/
# The bare binaries, for trying one without installing anything.
tar -czf "out/proton-drive-linux-${VERSION}-${SHORT}-fedora${{ inputs.fedora }}-x86_64.tar.gz" \
-C target/release pdfs pdfs-tray pdfs-app pdfs-prompt
ls -l out
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: proton-drive-linux-fedora${{ inputs.fedora }}-${{ github.sha }}
path: out/*
retention-days: 14
if-no-files-found: error
prerelease:
name: Publish pre-release
needs: rpm
if: inputs.prerelease
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Download build
uses: actions/download-artifact@v8
with:
path: out
merge-multiple: true
# One rolling pre-release per branch: a new run replaces the last one, so
# the link a tester was given keeps pointing at the newest build.
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
TAG="test-$(echo "${GITHUB_REF_NAME}" | tr -c 'A-Za-z0-9._\n' '-')"
gh release delete "$TAG" --cleanup-tag --yes 2>/dev/null || true
gh release create "$TAG" out/* \
--target "$GITHUB_SHA" \
--prerelease \
--title "Test build of ${GITHUB_REF_NAME} (${GITHUB_SHA::7})" \
--notes "Unsigned test build of branch \`${GITHUB_REF_NAME}\` at ${GITHUB_SHA}. Not a release; replaced by the next test build of this branch."
+3 -3
View File
@@ -39,9 +39,9 @@ cargo test --workspace --locked
scripts/fuse-acceptance.sh --offline-only scripts/fuse-acceptance.sh --offline-only
``` ```
Changes to the FUSE layer, sync, or the pending-operation queue should also be exercised against a Changes to the FUSE layer, sync, or the queue should also pass the simulation runs and be
real mount before review — see [`docs/TESTING.md`](docs/TESTING.md). Live runs are destructive: exercised against a real mount before review — see [`docs/TESTING.md`](docs/TESTING.md). Live
use a dedicated test account, never one holding data you cannot lose. runs are destructive: use a dedicated test account, never one holding data you cannot lose.
## Making changes ## Making changes
Generated
+14 -10
View File
@@ -3474,7 +3474,7 @@ checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
[[package]] [[package]]
name = "pdfs-cli" name = "pdfs-cli"
version = "3.0.0" version = "3.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -3485,13 +3485,15 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"zeroize",
] ]
[[package]] [[package]]
name = "pdfs-core" name = "pdfs-core"
version = "3.0.0" version = "3.1.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.23.1",
"dbus-secret-service-keyring-store", "dbus-secret-service-keyring-store",
"directories", "directories",
"ignore", "ignore",
@@ -3508,12 +3510,13 @@ dependencies = [
"thiserror 2.0.20", "thiserror 2.0.20",
"tokio", "tokio",
"tracing", "tracing",
"zeroize",
"zip", "zip",
] ]
[[package]] [[package]]
name = "pdfs-fuse" name = "pdfs-fuse"
version = "3.0.0" version = "3.1.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.23.1", "base64 0.23.1",
@@ -3535,7 +3538,7 @@ dependencies = [
[[package]] [[package]]
name = "pdfs-gui" name = "pdfs-gui"
version = "3.0.0" version = "3.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-channel", "async-channel",
@@ -3572,9 +3575,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "pgp" name = "pgp"
version = "0.20.0" version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfa4743b28656065ff4c0ba09e46b357a65e8c00fc2341e89084b82f87cbdf1" checksum = "ae70f4d9325a391db30d115d6191a7bb67cec856aa0bba131290f0c4e09a532a"
dependencies = [ dependencies = [
"aead 0.5.2", "aead 0.5.2",
"aes 0.8.4", "aes 0.8.4",
@@ -3775,9 +3778,9 @@ dependencies = [
[[package]] [[package]]
name = "proton-drive-rs" name = "proton-drive-rs"
version = "0.7.2" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6ce113d234474821d34787436ee80e1458e3d8a8f97212219615ed315eb70be" checksum = "5bde475139afb5fc2649621e1d283e243f9df2bb412f08802cdf3f4ffaf97c44"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.23.1", "base64 0.23.1",
@@ -3802,9 +3805,9 @@ dependencies = [
[[package]] [[package]]
name = "proton-sdk" name = "proton-sdk"
version = "0.7.2" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a33c67df9694f90ce60e5b34984275956244138511b4640ca0d100e1dac8b3d" checksum = "faf32d0a03f7d13bd9bdcc8c9503e35e9df9318ca9f2a100dcf06cf50cb7ac8c"
dependencies = [ dependencies = [
"aes-gcm 0.11.1", "aes-gcm 0.11.1",
"async-trait", "async-trait",
@@ -3831,6 +3834,7 @@ dependencies = [
"thiserror 2.0.20", "thiserror 2.0.20",
"tokio", "tokio",
"tracing", "tracing",
"zeroize",
] ]
[[package]] [[package]]
+4 -3
View File
@@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/pdfs-core", "crates/pdfs-fuse", "crates/pdfs-cli", "crates/pdfs-gui"] members = ["crates/pdfs-core", "crates/pdfs-fuse", "crates/pdfs-cli", "crates/pdfs-gui"]
[workspace.package] [workspace.package]
version = "3.0.0" version = "3.1.1"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
authors = ["Nils Pukropp <contact@narl.io>"] authors = ["Nils Pukropp <contact@narl.io>"]
@@ -15,8 +15,8 @@ keywords = ["proton", "drive", "fuse", "sync", "linux"]
categories = ["filesystem", "command-line-utilities"] categories = ["filesystem", "command-line-utilities"]
[workspace.dependencies] [workspace.dependencies]
proton-sdk = { version = "0.7.2" } proton-sdk = { version = "0.8.0" }
proton-drive-rs = { version = "0.7.2" } proton-drive-rs = { version = "0.8.0" }
# Internal crates. # Internal crates.
pdfs-core = { path = "crates/pdfs-core" } pdfs-core = { path = "crates/pdfs-core" }
@@ -26,6 +26,7 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "fs",
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
base64 = "0.23" base64 = "0.23"
zeroize = "1"
thiserror = "2" thiserror = "2"
anyhow = "1" anyhow = "1"
# Non-poisoning mutexes. A handler that panics while holding a lock must not # Non-poisoning mutexes. A handler that panics while holding a lock must not
+9 -1
View File
@@ -15,6 +15,7 @@ scriptable CLI.
[![AUR](https://img.shields.io/aur/version/proton-drive-for-linux)](https://aur.archlinux.org/packages/proton-drive-for-linux) [![AUR](https://img.shields.io/aur/version/proton-drive-for-linux)](https://aur.archlinux.org/packages/proton-drive-for-linux)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Rust 1.96+](https://img.shields.io/badge/rust-1.96%2B-orange.svg)](https://www.rust-lang.org) [![Rust 1.96+](https://img.shields.io/badge/rust-1.96%2B-orange.svg)](https://www.rust-lang.org)
[![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/narl)
<img src="images/files.png" alt="My files in the Proton Drive app" width="820"> <img src="images/files.png" alt="My files in the Proton Drive app" width="820">
@@ -148,7 +149,7 @@ keys before it leaves the machine, through a Rust implementation of Proton's Dri
Files you open are decrypted into a local cache so programs can read them. What that means for Files you open are decrypted into a local cache so programs can read them. What that means for
the security of the machine is spelled out in the the security of the machine is spelled out in the
[threat model](docs/ARCHITECTURE.md#8-threat-model-what-this-client-writes-to-disk-in-plaintext). [threat model](docs/ARCHITECTURE.md#10-threat-model-what-this-client-writes-to-disk-in-plaintext).
## Contributing ## Contributing
@@ -156,6 +157,13 @@ Bug reports, translations and patches are welcome. See [CONTRIBUTING.md](CONTRIB
workflow and the checks CI runs, and [SECURITY.md](SECURITY.md) to report a vulnerability workflow and the checks CI runs, and [SECURITY.md](SECURITY.md) to report a vulnerability
privately. privately.
## Support
Proton Drive Linux is free and built in spare time. If it saves you a few headaches, you can
[buy me a coffee](https://buymeacoffee.com/narl).
<a href="https://buymeacoffee.com/narl"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Buy me a coffee"></a>
## License ## License
Released under the [MIT License](LICENSE). Released under the [MIT License](LICENSE).
+1 -1
View File
@@ -31,7 +31,7 @@ the interesting boundaries are:
or the wrong key material to Proton. or the wrong key material to Proton.
What the client deliberately keeps in plaintext on disk, and why, is documented in the What the client deliberately keeps in plaintext on disk, and why, is documented in the
[threat model](docs/ARCHITECTURE.md#8-threat-model-what-this-client-writes-to-disk-in-plaintext). [threat model](docs/ARCHITECTURE.md#10-threat-model-what-this-client-writes-to-disk-in-plaintext).
Behaviour described there is known and not a vulnerability by itself; a way to make it worse Behaviour described there is known and not a vulnerability by itself; a way to make it worse
than described is. than described is.
+1
View File
@@ -23,3 +23,4 @@ tracing-subscriber.workspace = true
clap.workspace = true clap.workspace = true
rpassword.workspace = true rpassword.workspace = true
serde_json.workspace = true serde_json.workspace = true
zeroize.workspace = true
+156 -7
View File
@@ -135,6 +135,24 @@ enum Command {
/// Account email; prompted if omitted. /// Account email; prompted if omitted.
#[arg(short, long)] #[arg(short, long)]
username: Option<String>, username: Option<String>,
/// For a two-password account, keep the mailbox password's unlock in the
/// system keyring so the daemon starts without asking after every login.
/// Without it the unlock lives in kernel memory for this session only.
#[arg(long)]
remember: bool,
},
/// Enter the mailbox password of a two-password account so the daemon can
/// unlock your files. Needed once per login session.
Unlock {
/// Keep the unlock in the system keyring instead of kernel memory only.
#[arg(long)]
remember: bool,
},
/// Forget the unlock held in kernel memory; the account is locked again.
Lock {
/// Also remove an unlock kept in the keyring with `--remember`.
#[arg(long)]
forget: bool,
}, },
/// Forget the stored session. /// Forget the stored session.
Logout, Logout,
@@ -803,7 +821,9 @@ fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
JSON_OUTPUT.store(cli.json, std::sync::atomic::Ordering::Relaxed); JSON_OUTPUT.store(cli.json, std::sync::atomic::Ordering::Relaxed);
match cli.command { match cli.command {
Command::Login { username } => cmd_login(username), Command::Login { username, remember } => cmd_login(username, remember),
Command::Unlock { remember } => cmd_unlock(remember),
Command::Lock { forget } => cmd_lock(forget),
Command::Logout => cmd_logout(), Command::Logout => cmd_logout(),
Command::Status => cmd_status(), Command::Status => cmd_status(),
Command::Locations => cmd_locations(), Command::Locations => cmd_locations(),
@@ -1781,7 +1801,7 @@ fn prompt_line(label: &str) -> Result<String> {
Ok(line.trim().to_owned()) Ok(line.trim().to_owned())
} }
fn cmd_login(username: Option<String>) -> Result<()> { fn cmd_login(username: Option<String>, remember: bool) -> Result<()> {
let username = match username { let username = match username {
Some(u) => u, Some(u) => u,
None => prompt_line("Proton email")?, None => prompt_line("Proton email")?,
@@ -1793,11 +1813,14 @@ fn cmd_login(username: Option<String>) -> Result<()> {
prompt_line("2FA code").map_err(|e| pdfs_core::Error::Other(format!("read 2FA code: {e}"))) prompt_line("2FA code").map_err(|e| pdfs_core::Error::Other(format!("read 2FA code: {e}")))
}; };
// Only a two-password account is ever asked for this.
let get_mailbox = |attempt: u32| prompt_mailbox(attempt, remember);
let rt = tokio::runtime::Runtime::new()?; let rt = tokio::runtime::Runtime::new()?;
// Solving a CAPTCHA needs a browser engine, which the CLI has no business // Solving a CAPTCHA needs a browser engine, which the CLI has no business
// carrying. Point at the app that does rather than failing with the raw API // carrying. Point at the app that does rather than failing with the raw API
// message, which reads as "your login is broken". // message, which reads as "your login is broken".
rt.block_on(auth::login(&username, &password, get_totp)) rt.block_on(auth::login(&username, &password, get_totp, get_mailbox))
.map_err(|e| match e { .map_err(|e| match e {
pdfs_core::Error::HumanVerificationRequired(_) => anyhow::anyhow!( pdfs_core::Error::HumanVerificationRequired(_) => anyhow::anyhow!(
"Proton is asking for a CAPTCHA before it will accept this sign-in.\n\ "Proton is asking for a CAPTCHA before it will accept this sign-in.\n\
@@ -1818,9 +1841,88 @@ fn cmd_login(username: Option<String>) -> Result<()> {
} }
println!("Logged in as {username}. Session stored in the system keyring."); println!("Logged in as {username}. Session stored in the system keyring.");
report_key_state();
Ok(()) Ok(())
} }
/// Ask for the mailbox password of a two-password account, without echo.
fn prompt_mailbox(attempt: u32, remember: bool) -> pdfs_core::Result<auth::Mailbox> {
if attempt == 0 {
eprintln!("This account has a separate mailbox password.");
} else {
eprintln!("That mailbox password did not unlock the account. Try again.");
}
let password = rpassword::prompt_password("Mailbox password: ")
.map_err(|e| pdfs_core::Error::Other(format!("read mailbox password: {e}")))?;
Ok(auth::Mailbox {
password: zeroize::Zeroizing::new(password),
remember,
})
}
/// Say where the account's keys are held, and what that means for the next login.
fn report_key_state() {
match auth::key_state() {
Ok(auth::KeyState::Session) => println!(
"Unlocked for this login session only. After you log out or reboot, run `pdfs unlock`."
),
Ok(auth::KeyState::Locked) => println!("The account is locked. Run `pdfs unlock`."),
_ => {}
}
}
fn cmd_unlock(remember: bool) -> Result<()> {
auth::load().context("not logged in (run `pdfs login` first)")?;
let rt = tokio::runtime::Runtime::new()?;
// Bounded like the login prompt: three wrong answers end the command.
let mut attempt = 0;
loop {
let mailbox = prompt_mailbox(attempt, remember)?;
match rt.block_on(auth::unlock(&mailbox.password, remember)) {
Ok(()) => break,
Err(pdfs_core::Error::WrongMailboxPassword) if attempt < 2 => attempt += 1,
Err(e) => return Err(anyhow::Error::new(e).context("unlock failed")),
}
}
// A daemon parked on the locked account would otherwise notice on its next
// poll; restarting it makes the mount appear now, as after `pdfs login`.
if service::is_active() {
service::restart();
}
println!("Unlocked.");
report_key_state();
Ok(())
}
fn cmd_lock(forget: bool) -> Result<()> {
auth::lock(forget)?;
println!(
"Locked. {}",
if forget {
"Unlock removed from memory and keyring."
} else {
"Run `pdfs unlock` to unlock again."
}
);
// The running daemon keeps its keys until it restarts; say so, since
// "locked" would otherwise suggest the mount stopped.
if service::is_active() {
println!(
"The running daemon keeps its keys until it restarts: `systemctl --user restart proton-drive`."
);
}
Ok(())
}
/// `stored`, `session` or `locked`: where the account's keys are held.
fn key_state_name() -> &'static str {
match auth::key_state() {
Ok(auth::KeyState::Stored) => "stored",
Ok(auth::KeyState::Session) => "session",
_ => "locked",
}
}
fn cmd_logout() -> Result<()> { fn cmd_logout() -> Result<()> {
auth::logout()?; auth::logout()?;
println!("Stored session cleared."); println!("Stored session cleared.");
@@ -1848,6 +1950,7 @@ fn cmd_status() -> Result<()> {
"username": session.as_ref().map(|s| s.username.clone()), "username": session.as_ref().map(|s| s.username.clone()),
"user_id": session.as_ref().map(|s| s.user_id.clone()), "user_id": session.as_ref().map(|s| s.user_id.clone()),
"scopes": session.as_ref().map(|s| s.scopes.clone()), "scopes": session.as_ref().map(|s| s.scopes.clone()),
"keys": session.as_ref().map(|_| key_state_name()),
"mount": mount, "mount": mount,
}); });
println!("{}", serde_json::to_string_pretty(&out)?); println!("{}", serde_json::to_string_pretty(&out)?);
@@ -1858,6 +1961,11 @@ fn cmd_status() -> Result<()> {
Ok(s) => { Ok(s) => {
println!("Logged in as {} (user {})", s.username, s.user_id); println!("Logged in as {} (user {})", s.username, s.user_id);
println!("Scopes: {}", s.scopes.join(", ")); println!("Scopes: {}", s.scopes.join(", "));
match key_state_name() {
"locked" => println!("Keys: locked — run `pdfs unlock`"),
"session" => println!("Keys: unlocked for this login session"),
_ => {}
}
} }
Err(pdfs_core::Error::NotLoggedIn) => { Err(pdfs_core::Error::NotLoggedIn) => {
println!("Not logged in. Run `pdfs login`."); println!("Not logged in. Run `pdfs login`.");
@@ -1989,7 +2097,7 @@ fn login_poll_delay(attempts: u32) -> std::time::Duration {
std::time::Duration::from_secs(BASE_SECS.saturating_mul(factor).min(MAX_SECS)) std::time::Duration::from_secs(BASE_SECS.saturating_mul(factor).min(MAX_SECS))
} }
/// Block until a session is stored, backing off between checks. /// Block until a session is stored and unlocked, backing off between checks.
/// ///
/// `load` and `sleep` are parameters rather than direct calls so the backoff /// `load` and `sleep` are parameters rather than direct calls so the backoff
/// schedule can be tested without a keyring and without real time passing. /// schedule can be tested without a keyring and without real time passing.
@@ -2014,6 +2122,16 @@ where
sleep(delay); sleep(delay);
attempts = attempts.saturating_add(1); attempts = attempts.saturating_add(1);
} }
Err(pdfs_core::Error::Locked) => {
let delay = login_poll_delay(attempts);
if attempts == 0 {
tracing::info!("account is locked; waiting for `pdfs unlock`…");
} else {
tracing::debug!(delay_secs = delay.as_secs(), "account still locked");
}
sleep(delay);
attempts = attempts.saturating_add(1);
}
Err(e) => return Err(e.into()), Err(e) => return Err(e.into()),
} }
} }
@@ -2024,10 +2142,13 @@ where
/// an external unmount triggers a remount; errors back off and retry. This is /// an external unmount triggers a remount; errors back off and retry. This is
/// the entry point for the systemd user service. /// the entry point for the systemd user service.
fn cmd_daemon(mountpoint: Option<PathBuf>) -> Result<()> { fn cmd_daemon(mountpoint: Option<PathBuf>) -> Result<()> {
// The daemon holds the account's key passphrases in memory; keep them out
// of core dumps and away from same-user ptrace.
pdfs_core::kernelkey::forbid_dumps();
loop { loop {
// Wait until a session is stored. The GUI enables this service on login, // Wait until a session is stored. The GUI enables this service on login,
// but the service may also start at boot before the user has logged in. // but the service may also start at boot before the user has logged in.
wait_for_session(|| auth::load().map(|_| ()), std::thread::sleep)?; wait_for_session(auth::ready, pdfs_fuse::idle_sleep)?;
match mount_once(mountpoint.clone()) { match mount_once(mountpoint.clone()) {
Ok(pdfs_fuse::MountOutcome::Shutdown) => { Ok(pdfs_fuse::MountOutcome::Shutdown) => {
@@ -2036,13 +2157,13 @@ fn cmd_daemon(mountpoint: Option<PathBuf>) -> Result<()> {
} }
Ok(pdfs_fuse::MountOutcome::Unmounted) => { Ok(pdfs_fuse::MountOutcome::Unmounted) => {
tracing::warn!("mount ended externally; remounting in 2s"); tracing::warn!("mount ended externally; remounting in 2s");
std::thread::sleep(std::time::Duration::from_secs(2)); pdfs_fuse::idle_sleep(std::time::Duration::from_secs(2));
} }
Err(e) => { Err(e) => {
// `{:#}` so the whole anyhow chain lands in the log — the top // `{:#}` so the whole anyhow chain lands in the log — the top
// context alone ("mount failed") never says *why*. // context alone ("mount failed") never says *why*.
tracing::error!(error = format!("{e:#}"), "mount failed; retrying in 5s"); tracing::error!(error = format!("{e:#}"), "mount failed; retrying in 5s");
std::thread::sleep(std::time::Duration::from_secs(5)); pdfs_fuse::idle_sleep(std::time::Duration::from_secs(5));
} }
} }
} }
@@ -3224,6 +3345,19 @@ fn cmd_diagnose() -> Result<()> {
match pdfs_core::auth::load() { match pdfs_core::auth::load() {
Ok(session) => { Ok(session) => {
report.finding(DiagnoseLevel::Ok, " keyring session", session.username); report.finding(DiagnoseLevel::Ok, " keyring session", session.username);
match key_state_name() {
"locked" => report.finding(
DiagnoseLevel::Fail,
" account keys",
"locked — run `pdfs unlock` (the daemon waits for it)",
),
"session" => report.finding(
DiagnoseLevel::Ok,
" account keys",
"unlocked for this login session",
),
_ => report.finding(DiagnoseLevel::Ok, " account keys", "stored in the keyring"),
}
} }
Err(e) => { Err(e) => {
// A locked keyring and an absent login look the same from here, so // A locked keyring and an absent login look the same from here, so
@@ -3522,6 +3656,21 @@ mod login_wait_tests {
); );
} }
#[test]
fn a_locked_account_is_waited_on_until_it_is_unlocked() {
let mut checks = vec![
Ok(()),
Err(pdfs_core::Error::Locked),
Err(pdfs_core::Error::NotLoggedIn),
Err(pdfs_core::Error::Locked),
];
let mut slept = 0;
wait_for_session(|| checks.pop().unwrap(), |_| slept += 1).unwrap();
assert_eq!(slept, 3, "locked and signed-out are both waited out");
}
#[test] #[test]
fn a_stored_session_is_not_waited_for_at_all() { fn a_stored_session_is_not_waited_for_at_all() {
let mut slept = 0; let mut slept = 0;
+2
View File
@@ -28,3 +28,5 @@ dbus-secret-service-keyring-store.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
async-trait.workspace = true async-trait.workspace = true
libc.workspace = true libc.workspace = true
zeroize.workspace = true
base64.workspace = true
+471 -119
View File
@@ -1,22 +1,50 @@
//! Login, session persistence, and Drive-client construction. //! Login, session persistence, and Drive-client construction.
//! //!
//! A successful login is persisted to the OS keyring as a single JSON blob //! A successful login is persisted to the OS keyring as a single JSON blob
//! holding the resumable session tokens plus the mailbox password (needed to //! holding the resumable session tokens, the account's key salts and — unless
//! rebuild the key chain on resume). The daemon resumes from this blob with no //! the user chose otherwise — the per-key *passphrases* derived from the
//! interactive step; the refresh token auto-renews via the HTTP client's 401 //! mailbox password. The password itself is never stored: the passphrases are
//! path, so no fresh 2FA is required until the refresh token itself expires. //! what it exists to produce, and they are enough to rebuild the key chain on
//! resume. The daemon resumes from this blob with no interactive step; the
//! refresh token auto-renews via the HTTP client's 401 path, so no fresh 2FA is
//! required until the refresh token itself expires.
//!
//! # Two-password accounts
//!
//! An account with a separate mailbox password exists precisely so that no one
//! login holds the means to read the data. Storing that password in the keyring
//! would defeat it, so such an account is **locked by default**: its
//! passphrases are kept only in the kernel keyring ([`crate::kernelkey`]), for
//! the length of the user's session, and `pdfs unlock` (or the app) supplies
//! them again after each login. Storing them in the Secret Service as well is
//! an explicit opt-in ([`Mailbox::remember`]). Single-password accounts have no
//! such separation to protect and are stored, as they always were.
use std::collections::BTreeMap;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use keyring_core::Entry; use keyring_core::Entry;
use proton_drive_rs::{KeySalt, ProtonDriveClient}; use proton_drive_rs::{KeySalt, ProtonDriveClient};
use proton_sdk::account::KeyPassphrases;
use proton_sdk::api::{HumanVerification, HumanVerificationCredential}; use proton_sdk::api::{HumanVerification, HumanVerificationCredential};
use proton_sdk::cache::EncryptedCacheRepository; use proton_sdk::cache::EncryptedCacheRepository;
use proton_sdk::config::ProtonClientConfiguration; use proton_sdk::config::ProtonClientConfiguration;
use proton_sdk::error::ProtonError;
use proton_sdk::session::{PasswordMode, ProtonApiSession, ResumeParameters}; use proton_sdk::session::{PasswordMode, ProtonApiSession, ResumeParameters};
use proton_sdk::telemetry::TracingTelemetry; use proton_sdk::telemetry::TracingTelemetry;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
use crate::config::{APP_VERSION, AppDirs, KEYRING_SERVICE, USER_AGENT}; use crate::config::{APP_VERSION, AppDirs, KEYRING_SERVICE, USER_AGENT};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::kernelkey;
/// How often a wrong mailbox password is asked for again before the login gives up.
const MAILBOX_ATTEMPTS: u32 = 3;
/// Domain-separation label for the key that encrypts the persistent entity cache.
const ENTITY_CACHE_PURPOSE: &[u8] = b"pdfs entity cache v1";
/// Fixed keyring account name for the single stored session blob. /// Fixed keyring account name for the single stored session blob.
const KEYRING_USER: &str = "session"; const KEYRING_USER: &str = "session";
@@ -32,8 +60,11 @@ pub struct StoredSession {
pub scopes: Vec<String>, pub scopes: Vec<String>,
/// `1` = single password, `2` = dual (Proton wire value). /// `1` = single password, `2` = dual (Proton wire value).
pub password_mode: u8, pub password_mode: u8,
/// Mailbox (data) password — required by `ProtonDriveClient::new` to derive /// The mailbox password of a blob written by 3.0.x and earlier.
/// the key chain. Lives only in the OS keyring, never on disk in cleartext. ///
/// Read only, to migrate: [`resume_client`] derives the passphrases from it
/// once and writes the blob back without it. Never serialized again.
#[serde(default, skip_serializing)]
pub mailbox_password: String, pub mailbox_password: String,
/// The account's key salts, captured at login. /// The account's key salts, captured at login.
/// ///
@@ -49,6 +80,11 @@ pub struct StoredSession {
/// backfilling) them. /// backfilling) them.
#[serde(default)] #[serde(default)]
pub key_salts: Vec<KeySalt>, pub key_salts: Vec<KeySalt>,
/// Per-key passphrases (key id → base64), when the user keeps them in the
/// keyring: always for a single-password account, by opt-in for a
/// two-password one. `None` means they are not stored here.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_passphrases: Option<BTreeMap<String, String>>,
} }
impl StoredSession { impl StoredSession {
@@ -73,7 +109,10 @@ fn client_config() -> ProtonClientConfiguration {
let (app_version, user_agent) = match AppDirs::new() { let (app_version, user_agent) = match AppDirs::new() {
Ok(dirs) => { Ok(dirs) => {
let config = dirs.load_config(); let config = dirs.load_config();
(config.app_version, config.user_agent) (
config.resolved_app_version().to_string(),
config.resolved_user_agent().to_string(),
)
} }
Err(_) => (APP_VERSION.to_string(), USER_AGENT.to_string()), Err(_) => (APP_VERSION.to_string(), USER_AGENT.to_string()),
}; };
@@ -89,6 +128,26 @@ fn keyring_entry() -> Result<Entry> {
Ok(Entry::new(KEYRING_SERVICE, KEYRING_USER)?) Ok(Entry::new(KEYRING_SERVICE, KEYRING_USER)?)
} }
/// What the user answered when asked for the mailbox password.
pub struct Mailbox {
pub password: Zeroizing<String>,
/// Keep the derived passphrases in the keyring, so the daemon unlocks by
/// itself after every login. Off by default: it trades the protection a
/// separate mailbox password gives for convenience.
pub remember: bool,
}
/// Where the passphrases that unlock this account's keys currently live.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyState {
/// In the keyring, available after every login.
Stored,
/// In kernel memory for this login session only.
Session,
/// Nowhere: the mailbox password has to be entered (`pdfs unlock`).
Locked,
}
/// Run an interactive SRP + (optional) 2FA login and persist the session. /// Run an interactive SRP + (optional) 2FA login and persist the session.
/// ///
/// `get_totp` is only invoked when the account requires a second factor, so /// `get_totp` is only invoked when the account requires a second factor, so
@@ -96,14 +155,19 @@ fn keyring_entry() -> Result<Entry> {
/// more than once — [`login_interactive`] re-runs the whole login after human /// more than once — [`login_interactive`] re-runs the whole login after human
/// verification, and a 2FA account needs a fresh code on that second attempt. /// verification, and a 2FA account needs a fresh code on that second attempt.
/// ///
/// `get_mailbox` is only invoked for a two-password account. It receives the
/// number of earlier wrong answers (so a prompt can say "try again") and is
/// asked at most [`MAILBOX_ATTEMPTS`] times; an `Err` from it aborts the login.
///
/// A gated login fails with [`Error::HumanVerificationRequired`]; a front-end /// A gated login fails with [`Error::HumanVerificationRequired`]; a front-end
/// that can present the challenge should use [`login_interactive`] instead. /// that can present the challenge should use [`login_interactive`] instead.
pub async fn login( pub async fn login(
username: &str, username: &str,
password: &str, password: &str,
get_totp: impl Fn() -> Result<String>, get_totp: impl Fn() -> Result<String>,
get_mailbox: impl Fn(u32) -> Result<Mailbox>,
) -> Result<()> { ) -> Result<()> {
login_verified(username, password, None, get_totp).await login_verified(username, password, None, get_totp, get_mailbox).await
} }
/// [`login`], able to answer a human-verification gate rather than fail on it. /// [`login`], able to answer a human-verification gate rather than fail on it.
@@ -123,12 +187,20 @@ pub async fn login_interactive(
username: &str, username: &str,
password: &str, password: &str,
get_totp: impl Fn() -> Result<String>, get_totp: impl Fn() -> Result<String>,
get_mailbox: impl Fn(u32) -> Result<Mailbox>,
get_hv: impl FnOnce(HumanVerification) -> Result<HumanVerificationCredential>, get_hv: impl FnOnce(HumanVerification) -> Result<HumanVerificationCredential>,
) -> Result<()> { ) -> Result<()> {
match login_verified(username, password, None, &get_totp).await { match login_verified(username, password, None, &get_totp, &get_mailbox).await {
Err(Error::HumanVerificationRequired(hv)) => { Err(Error::HumanVerificationRequired(hv)) => {
let credential = get_hv(*hv)?; let credential = get_hv(*hv)?;
login_verified(username, password, Some(&credential), &get_totp).await login_verified(
username,
password,
Some(&credential),
&get_totp,
&get_mailbox,
)
.await
} }
other => other, other => other,
} }
@@ -145,15 +217,18 @@ pub async fn login_verified(
password: &str, password: &str,
verification: Option<&HumanVerificationCredential>, verification: Option<&HumanVerificationCredential>,
get_totp: impl Fn() -> Result<String>, get_totp: impl Fn() -> Result<String>,
get_mailbox: impl Fn(u32) -> Result<Mailbox>,
) -> Result<()> { ) -> Result<()> {
let mut session = ProtonApiSession::begin_verified( let password = Zeroizing::new(password.to_owned());
let session = ProtonApiSession::begin_verified(
client_config(), client_config(),
username, username,
password.as_bytes(), password.as_bytes(),
verification, verification,
) )
.await .await
.map_err(classify_verification_gate)?; .map_err(classify_verification_gate);
let mut session = session?;
if session.is_waiting_for_second_factor() { if session.is_waiting_for_second_factor() {
let code = get_totp()?; let code = get_totp()?;
@@ -163,38 +238,38 @@ pub async fn login_verified(
// Grab the key salts while this access token still has the `locked` scope: // Grab the key salts while this access token still has the `locked` scope:
// after its first refresh it never will again, and without them no later // after its first refresh it never will again, and without them no later
// resume can unlock the key chain. See `StoredSession::key_salts`. // resume can unlock the key chain. See `StoredSession::key_salts`.
let client = ProtonDriveClient::new(&session, password.as_bytes().to_vec()); let key_salts = ProtonDriveClient::new(&session, Vec::new())
let key_salts = client.account().key_salts().await?; .account()
.key_salts()
.await?;
register_refresh_handler(&session, password.to_owned(), key_salts.clone()); // A single-password account's mailbox password is the login password. A
// two-password one has a second secret the user must type, and it is checked
save(&session, password, key_salts).await?; // against the account now — a typo found at login is a retry, found later it
Ok(()) // is a daemon that never mounts.
} let (passphrases, remember) = match session.password_mode() {
PasswordMode::Single => (
/// Lift a human-verification gate out of the generic API error into the typed derive_verified(&session, password.as_bytes(), key_salts.clone()).await?,
/// variant a front-end can act on. true,
/// ),
/// Only a gate that names a solvable method is converted. A `9001` with no PasswordMode::Dual => {
/// `Details`, or one offering only `email`/`sms`, stays a plain API error: let mut attempt = 0;
/// promoting it would send the UI to open a verification page it cannot loop {
/// complete, which reads to the user as a hang rather than a refusal. let mailbox = get_mailbox(attempt)?;
fn classify_verification_gate(e: proton_sdk::error::ProtonError) -> Error { match derive_verified(&session, mailbox.password.as_bytes(), key_salts.clone())
let proton_sdk::error::ProtonError::Api(api) = &e else { .await
return e.into(); {
Ok(passphrases) => break (passphrases, mailbox.remember),
Err(Error::WrongMailboxPassword) if attempt + 1 < MAILBOX_ATTEMPTS => {
attempt += 1;
}
Err(e) => return Err(e),
}
}
}
}; };
match api.human_verification() {
Some(hv) if hv.supports_captcha() => Error::HumanVerificationRequired(Box::new(hv)),
_ => e.into(),
}
}
/// Persist the session's current tokens, mailbox password and key salts. register_refresh_handler(&session);
pub async fn save(
session: &ProtonApiSession,
mailbox_password: &str,
key_salts: Vec<KeySalt>,
) -> Result<()> {
let tokens = session.current_tokens().await; let tokens = session.current_tokens().await;
let stored = StoredSession { let stored = StoredSession {
session_id: session.session_id().as_str().to_owned(), session_id: session.session_id().as_str().to_owned(),
@@ -207,10 +282,103 @@ pub async fn save(
PasswordMode::Single => 1, PasswordMode::Single => 1,
PasswordMode::Dual => 2, PasswordMode::Dual => 2,
}, },
mailbox_password: mailbox_password.to_owned(), mailbox_password: String::new(),
key_salts, key_salts,
key_passphrases: None,
}; };
let json = serde_json::to_string(&stored)?; keep_passphrases(stored, &passphrases, remember)
}
/// Derive the key passphrases from `mailbox` and prove they unlock the account.
///
/// [`Error::WrongMailboxPassword`] means the account answered and the password
/// is wrong; any other error says nothing about the password (no network, an
/// API fault) and must not be reported as a typo.
async fn derive_verified(
session: &ProtonApiSession,
mailbox: &[u8],
key_salts: Vec<KeySalt>,
) -> Result<KeyPassphrases> {
let client = ProtonDriveClient::with_key_salts(session, mailbox.to_vec(), key_salts);
client.account().unlock().await.map_err(|e| match e {
ProtonError::KeysLocked => Error::WrongMailboxPassword,
other => other.into(),
})?;
Ok(client.account().key_passphrases().await?)
}
/// Write `stored` with the passphrases in the place the user chose: the
/// keyring (`remember`) or kernel memory only. Whichever is not chosen is
/// cleared, so a revoked choice never leaves a copy behind.
///
/// The kernel entry is written first. If it cannot be (no keyring support), the
/// secret is *not* quietly stored somewhere weaker: the blob is written locked
/// and the error says so.
fn keep_passphrases(
mut stored: StoredSession,
passphrases: &KeyPassphrases,
remember: bool,
) -> Result<()> {
let outcome = if remember {
stored.key_passphrases = Some(encode(passphrases));
// Best-effort: a system without a kernel keyring has nothing to clear.
let _ = kernelkey::clear();
Ok(())
} else {
stored.key_passphrases = None;
kernelkey::store(&encode_secret(passphrases)).map_err(|e| {
Error::Other(format!(
"the kernel keyring is unavailable, so the unlock cannot be kept for this session: {e}"
))
})
};
write_stored(&stored)?;
outcome
}
fn encode(passphrases: &KeyPassphrases) -> BTreeMap<String, String> {
passphrases
.iter()
.map(|(id, secret)| (id.to_owned(), BASE64.encode(secret)))
.collect()
}
fn decode(encoded: &BTreeMap<String, String>) -> Option<KeyPassphrases> {
let mut passphrases = KeyPassphrases::new();
for (id, secret) in encoded {
passphrases.insert(id.clone(), BASE64.decode(secret).ok()?);
}
(!passphrases.is_empty()).then_some(passphrases)
}
/// The kernel-keyring payload: the same map as the blob's, as JSON.
fn encode_secret(passphrases: &KeyPassphrases) -> Zeroizing<Vec<u8>> {
Zeroizing::new(serde_json::to_vec(&encode(passphrases)).unwrap_or_default())
}
fn decode_secret(secret: &[u8]) -> Option<KeyPassphrases> {
decode(&serde_json::from_slice(secret).ok()?)
}
/// Lift a human-verification gate out of the generic API error into the typed
/// variant a front-end can act on.
///
/// Only a gate that names a solvable method is converted. A `9001` with no
/// `Details`, or one offering only `email`/`sms`, stays a plain API error:
/// promoting it would send the UI to open a verification page it cannot
/// complete, which reads to the user as a hang rather than a refusal.
fn classify_verification_gate(e: ProtonError) -> Error {
let ProtonError::Api(api) = &e else {
return e.into();
};
match api.human_verification() {
Some(hv) if hv.supports_captcha() => Error::HumanVerificationRequired(Box::new(hv)),
_ => e.into(),
}
}
fn write_stored(stored: &StoredSession) -> Result<()> {
let json = Zeroizing::new(serde_json::to_string(stored)?);
keyring_entry()?.set_password(&json)?; keyring_entry()?.set_password(&json)?;
Ok(()) Ok(())
} }
@@ -225,12 +393,93 @@ pub fn load() -> Result<StoredSession> {
} }
} }
/// Where the passphrases for the stored session live right now.
///
/// Reads the kernel keyring and the stored blob only; never the network.
pub fn key_state() -> Result<KeyState> {
Ok(key_state_of(&load()?))
}
fn key_state_of(stored: &StoredSession) -> KeyState {
match find_passphrases(stored) {
Ok(Some((_, origin))) => origin,
// A 3.0.x blob still holds its password; `resume_client` migrates it.
_ if !stored.mailbox_password.is_empty() => KeyState::Stored,
_ => KeyState::Locked,
}
}
/// Whether the stored session can be resumed without asking the user:
/// `Ok` when it can, [`Error::NotLoggedIn`] or [`Error::Locked`] when not.
/// What the daemon waits on before it mounts.
pub fn ready() -> Result<()> {
match key_state_of(&load()?) {
KeyState::Locked => Err(Error::Locked),
_ => Ok(()),
}
}
/// The passphrases the stored session can use without being asked, and which
/// of the two places they came from. Prefers the keyring blob: it is what the
/// user opted into. A corrupt entry is treated as absent, so it reads as
/// locked instead of wedging the daemon.
fn find_passphrases(stored: &StoredSession) -> Result<Option<(KeyPassphrases, KeyState)>> {
if let Some(found) = stored.key_passphrases.as_ref().and_then(decode) {
return Ok(Some((found, KeyState::Stored)));
}
match kernelkey::load() {
Ok(Some(secret)) => Ok(decode_secret(&secret).map(|p| (p, KeyState::Session))),
Ok(None) => Ok(None),
// No keyring on this system: nothing was ever kept there.
Err(e) => {
tracing::debug!(error = %e, "kernel keyring not readable; treating the account as locked");
Ok(None)
}
}
}
/// Supply the mailbox password of a locked account.
///
/// Verifies it against the account, then keeps the derived passphrases in
/// kernel memory for this login session — or, with `remember`, in the keyring.
/// A daemon waiting on [`Error::Locked`] picks them up on its next attempt.
///
/// Fails with [`Error::WrongMailboxPassword`] for a wrong password, which
/// changes nothing, and [`Error::ReloginRequired`] for a session that predates
/// stored key salts.
pub async fn unlock(mailbox: &str, remember: bool) -> Result<()> {
let stored = load()?;
if stored.key_salts.is_empty() {
return Err(Error::ReloginRequired);
}
let mailbox = Zeroizing::new(mailbox.to_owned());
let session = ProtonApiSession::resume(client_config(), stored.to_params())?;
register_refresh_handler(&session);
let passphrases =
derive_verified(&session, mailbox.as_bytes(), stored.key_salts.clone()).await?;
keep_passphrases(stored, &passphrases, remember)
}
/// Drop the passphrases held in kernel memory, so the account is locked again
/// until [`unlock`]. With `forget`, also those in the keyring blob.
pub fn lock(forget: bool) -> Result<()> {
kernelkey::clear()?;
if forget {
let mut stored = load()?;
if stored.key_passphrases.take().is_some() {
write_stored(&stored)?;
}
}
Ok(())
}
/// Forget the persisted session (best-effort; absent entry is not an error). /// Forget the persisted session (best-effort; absent entry is not an error).
pub fn logout() -> Result<()> { pub fn logout() -> Result<()> {
// The persisted entity cache describes the account being logged out of. It // The persisted entity cache describes the account being logged out of. It
// is encrypted under that account's mailbox password, but leaving it behind // is encrypted under a key derived from that account's passphrases, but
// would also mean the next account inherits a store it can only ever read as // leaving it behind would also mean the next account inherits a store it can
// misses. Best-effort: failing to remove a cache must not block the logout. // only ever read as misses. Best-effort: failing to remove a cache must not
// block the logout.
if let Ok(dirs) = AppDirs::new() { if let Ok(dirs) = AppDirs::new() {
let path = dirs.state_dir().join("sdk_cache.db"); let path = dirs.state_dir().join("sdk_cache.db");
if let Some(cache) = crate::sdkcache::SdkCache::opened() if let Some(cache) = crate::sdkcache::SdkCache::opened()
@@ -242,6 +491,9 @@ pub fn logout() -> Result<()> {
let _ = std::fs::remove_file(format!("{}{suffix}", path.display())); let _ = std::fs::remove_file(format!("{}{suffix}", path.display()));
} }
} }
if let Err(e) = kernelkey::clear() {
tracing::warn!(error = %e, "clearing the kernel-keyring unlock on logout failed");
}
match keyring_entry()?.delete_credential() { match keyring_entry()?.delete_credential() {
Ok(()) | Err(keyring_core::Error::NoEntry) => Ok(()), Ok(()) | Err(keyring_core::Error::NoEntry) => Ok(()),
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
@@ -250,44 +502,87 @@ pub fn logout() -> Result<()> {
/// Resume a persisted session and build an authenticated Drive client. /// Resume a persisted session and build an authenticated Drive client.
/// ///
/// Returns `Error::NotLoggedIn` when no session has been saved. The caller is /// Returns `Error::NotLoggedIn` when no session has been saved and
/// responsible for persisting rotated tokens over the session's lifetime via /// [`Error::Locked`] when the account is waiting for its mailbox password. Tokens
/// [`persist`] — Proton refresh tokens are single-use, so a refresh that is not /// the session rotates are written back to the keyring as they happen (see
/// written back leaves the keyring holding a stale (now-invalid) refresh token, /// [`register_refresh_handler`]); Proton refresh tokens are single-use, so a
/// and the next resume fails with `InvalidRefreshToken`. /// refresh that is not written back would leave the keyring holding a stale
/// refresh token, and the next resume would fail with `InvalidRefreshToken`.
///
/// Makes no network call for an account whose passphrases are already known, so
/// a daemon starting offline can still mount from its cache.
pub async fn resume_client() -> Result<(ProtonDriveClient, ProtonApiSession)> { pub async fn resume_client() -> Result<(ProtonDriveClient, ProtonApiSession)> {
let stored = load()?; let stored = load()?;
let session = ProtonApiSession::resume(client_config(), stored.to_params())?; let session = ProtonApiSession::resume(client_config(), stored.to_params())?;
let password = stored.mailbox_password.clone(); register_refresh_handler(&session);
// Blobs written before key salts were persisted have none: fetch them once let passphrases = match find_passphrases(&stored)? {
// more and write them back, which only works while the stored access token Some((passphrases, _)) => passphrases,
// still carries the `locked` scope. If it doesn't, the account must be None if !stored.mailbox_password.is_empty() => migrate_legacy(&session, &stored).await?,
// logged into again — no refresh can restore that scope. None => return Err(Error::Locked),
let key_salts = if stored.key_salts.is_empty() {
register_refresh_handler(&session, password.clone(), Vec::new());
let probe = ProtonDriveClient::new(&session, password.clone().into_bytes());
let salts = probe.account().key_salts().await.map_err(|e| match &e {
proton_sdk::error::ProtonError::Api(api) if api.is_insufficient_scope() => {
Error::ReloginRequired
}
_ => e.into(),
})?;
save(&session, &password, salts.clone()).await?;
salts
} else {
stored.key_salts
}; };
register_refresh_handler(&session, password.clone(), key_salts.clone());
let client = tune( let client = tune(
ProtonDriveClient::with_key_salts(&session, password.clone().into_bytes(), key_salts), ProtonDriveClient::with_key_passphrases(&session, passphrases.clone()),
password.as_bytes(), &passphrases,
); );
Ok((client, session)) Ok((client, session))
} }
/// Turn a blob written by 3.0.x — which holds the mailbox password — into one
/// that holds passphrases, so the password stops being stored.
///
/// The old login saved whatever was typed as the mailbox password, which for a
/// two-password account is the *login* password and unlocks nothing. That is
/// checked here: a password the account rejects leaves the account locked (and
/// the stored copy gone) rather than failing every start forever.
async fn migrate_legacy(
session: &ProtonApiSession,
stored: &StoredSession,
) -> Result<KeyPassphrases> {
let legacy = Zeroizing::new(stored.mailbox_password.clone());
// Blobs from before key salts were persisted have none: fetch them once
// more, which only works while the stored access token still carries the
// `locked` scope. If it doesn't, the account must be logged into again — no
// refresh can restore that scope.
let key_salts = if stored.key_salts.is_empty() {
ProtonDriveClient::new(session, Vec::new())
.account()
.key_salts()
.await
.map_err(|e| match &e {
ProtonError::Api(api) if api.is_insufficient_scope() => Error::ReloginRequired,
_ => e.into(),
})?
} else {
stored.key_salts.clone()
};
let mut migrated = stored.clone();
migrated.mailbox_password = String::new();
migrated.key_salts = key_salts.clone();
match derive_verified(session, legacy.as_bytes(), key_salts).await {
Ok(passphrases) => {
migrated.key_passphrases = Some(encode(&passphrases));
write_stored(&migrated)?;
tracing::info!(
"migrated the stored session from a mailbox password to key passphrases"
);
Ok(passphrases)
}
Err(Error::WrongMailboxPassword) => {
write_stored(&migrated)?;
tracing::warn!(
"the stored password does not unlock the account's keys; unlock required"
);
Err(Error::Locked)
}
Err(e) => Err(e),
}
}
/// The client settings every front-end wants, applied wherever a /// The client settings every front-end wants, applied wherever a
/// [`ProtonDriveClient`] is built for real work. /// [`ProtonDriveClient`] is built for real work.
/// ///
@@ -301,23 +596,24 @@ pub async fn resume_client() -> Result<(ProtonDriveClient, ProtonApiSession)> {
/// failure point per write instead of three. /// failure point per write instead of three.
/// - **A persistent entity cache** ([`crate::sdkcache`]), so a restart does not /// - **A persistent entity cache** ([`crate::sdkcache`]), so a restart does not
/// re-fetch and re-decrypt the tree the previous run already walked. It is /// re-fetch and re-decrypt the tree the previous run already walked. It is
/// wrapped in the SDK's [`EncryptedCacheRepository`] keyed by the mailbox /// wrapped in the SDK's [`EncryptedCacheRepository`] keyed by a secret derived
/// password, so the decrypted node names it holds are not readable from the /// from the key passphrases, so the decrypted node names it holds are not
/// file alone — and a password change simply reads as a cold cache, since the /// readable from the file alone — and a password change simply reads as a cold
/// SDK treats an undecryptable entry as a miss and clears the store. /// cache, since the SDK treats an undecryptable entry as a miss and clears the
/// store.
/// ///
/// Opening it is best-effort: a store this process cannot open costs cache /// Opening it is best-effort: a store this process cannot open costs cache
/// hits, not the session, so the client falls back to the SDK's in-memory /// hits, not the session, so the client falls back to the SDK's in-memory
/// default. /// default.
fn tune(client: ProtonDriveClient, mailbox_password: &[u8]) -> ProtonDriveClient { fn tune(client: ProtonDriveClient, passphrases: &KeyPassphrases) -> ProtonDriveClient {
let client = client let client = client
.with_telemetry(TracingTelemetry::shared()) .with_telemetry(TracingTelemetry::shared())
.with_small_file_upload(true); .with_small_file_upload(true);
match AppDirs::new().and_then(|dirs| crate::sdkcache::SdkCache::shared(&dirs)) { match AppDirs::new().and_then(|dirs| crate::sdkcache::SdkCache::shared(&dirs)) {
Ok(cache) => client.with_entity_repository(EncryptedCacheRepository::shared( Ok(cache) => {
cache, let key = passphrases.derive_secret(ENTITY_CACHE_PURPOSE);
mailbox_password.to_vec(), client.with_entity_repository(EncryptedCacheRepository::shared(cache, key.to_vec()))
)), }
Err(e) => { Err(e) => {
tracing::warn!(error = %e, "persistent SDK entity cache unavailable; using memory"); tracing::warn!(error = %e, "persistent SDK entity cache unavailable; using memory");
client client
@@ -327,49 +623,105 @@ fn tune(client: ProtonDriveClient, mailbox_password: &[u8]) -> ProtonDriveClient
/// Write the session's current tokens back to the keyring. /// Write the session's current tokens back to the keyring.
/// ///
/// The session does not carry the mailbox password (it is only needed to rebuild /// Everything else in the stored blob — salts, passphrases — is preserved, so
/// the key chain on resume), so it is re-read from the existing stored blob and /// this never undoes an [`unlock`] or [`lock`] made since the session started.
/// preserved. Call whenever the session may have rotated its tokens through the /// Call whenever the session may have rotated its tokens so a later
/// 401-refresh path so a later [`resume_client`] presents a live refresh token. /// [`resume_client`] presents a live refresh token.
pub async fn persist(session: &ProtonApiSession) -> Result<()> { pub async fn persist(session: &ProtonApiSession) -> Result<()> {
let stored = load()?; let tokens = session.current_tokens().await;
save(session, &stored.mailbox_password, stored.key_salts).await store_tokens(tokens.access_token, tokens.refresh_token)
} }
fn register_refresh_handler( fn store_tokens(access_token: String, refresh_token: String) -> Result<()> {
session: &ProtonApiSession, let mut stored = load()?;
mailbox_password: String, stored.access_token = access_token;
key_salts: Vec<KeySalt>, stored.refresh_token = refresh_token;
) { write_stored(&stored)
let session_id = session.session_id().as_str().to_owned(); }
let username = session.username().to_owned();
let user_id = session.user_id().as_str().to_owned();
let scopes = session.scopes().to_vec();
let password_mode = match session.password_mode() {
PasswordMode::Single => 1,
PasswordMode::Dual => 2,
};
/// Persist rotated tokens the moment the session obtains them.
///
/// The handler re-reads the stored blob instead of capturing one, so it cannot
/// write back a stale copy over a later unlock or lock — and cannot resurrect a
/// session that was logged out while the daemon still ran.
fn register_refresh_handler(session: &ProtonApiSession) {
session.http().set_on_tokens_refreshed(move |tokens| { session.http().set_on_tokens_refreshed(move |tokens| {
let stored = StoredSession { match store_tokens(tokens.access_token, tokens.refresh_token) {
session_id: session_id.clone(), Ok(()) => tracing::info!("successfully auto-persisted refreshed tokens in keyring"),
username: username.clone(), Err(Error::NotLoggedIn) => {}
user_id: user_id.clone(), Err(e) => {
access_token: tokens.access_token, tracing::warn!(error = %e, "failed to auto-persist refreshed tokens in keyring")
refresh_token: tokens.refresh_token,
scopes: scopes.clone(),
password_mode,
mailbox_password: mailbox_password.clone(),
key_salts: key_salts.clone(),
};
if let Ok(json) = serde_json::to_string(&stored)
&& let Ok(entry) = keyring_entry()
{
if let Err(e) = entry.set_password(&json) {
tracing::warn!(error = %e, "failed to auto-persist refreshed tokens in keyring");
} else {
tracing::info!("successfully auto-persisted refreshed tokens in keyring");
} }
} }
}); });
} }
#[cfg(test)]
mod tests {
use super::*;
fn passphrases() -> KeyPassphrases {
let mut p = KeyPassphrases::new();
p.insert("key-a", b"alpha".to_vec());
p.insert("key-b", vec![0, 255, 7]);
p
}
fn blob(extra: &str) -> String {
format!(
r#"{{"session_id":"s","username":"u","user_id":"i","access_token":"a",
"refresh_token":"r","scopes":[],"password_mode":2{extra}}}"#
)
}
#[test]
fn passphrases_survive_the_blob_encoding() {
let decoded = decode(&encode(&passphrases())).expect("decodes");
assert_eq!(decoded, passphrases());
}
#[test]
fn passphrases_survive_the_kernel_payload_encoding() {
let decoded = decode_secret(&encode_secret(&passphrases())).expect("decodes");
assert_eq!(decoded, passphrases());
}
#[test]
fn corrupt_or_empty_secrets_read_as_absent() {
assert!(decode_secret(b"not json").is_none());
assert!(decode(&BTreeMap::new()).is_none());
assert!(decode(&BTreeMap::from([("k".into(), "!!!".into())])).is_none());
}
#[test]
fn legacy_blob_is_read_but_its_password_is_never_written_back() {
let stored: StoredSession =
serde_json::from_str(&blob(r#","mailbox_password":"hunter2""#)).unwrap();
assert_eq!(stored.mailbox_password, "hunter2");
assert!(stored.key_passphrases.is_none());
let written = serde_json::to_string(&stored).unwrap();
assert!(!written.contains("hunter2"));
assert!(!written.contains("mailbox_password"));
}
#[test]
fn locked_blob_has_no_passphrases_field() {
let stored: StoredSession = serde_json::from_str(&blob("")).unwrap();
assert!(
serde_json::to_string(&stored)
.unwrap()
.find("key_passphrases")
.is_none()
);
}
#[test]
fn stored_passphrases_win_over_the_kernel_keyring() {
let mut stored: StoredSession = serde_json::from_str(&blob("")).unwrap();
stored.key_passphrases = Some(encode(&passphrases()));
let (found, origin) = find_passphrases(&stored).unwrap().unwrap();
assert_eq!(origin, KeyState::Stored);
assert_eq!(found, passphrases());
}
}
+13 -15
View File
@@ -91,7 +91,7 @@ const TOUCH_FLUSH_AT: usize = 256;
/// ///
/// Public because it is the *contract* a block has to satisfy, not a detail of /// Public because it is the *contract* a block has to satisfy, not a detail of
/// this cache: the network read path validates against it too, so a block that /// this cache: the network read path validates against it too, so a block that
/// comes back short is refused before it can truncate a file (bugs.md B84). /// comes back short is refused before it can truncate a file (`docs/BUGS.md` B84).
pub fn block_len(size: u64, idx: u64) -> u64 { pub fn block_len(size: u64, idx: u64) -> u64 {
let start = idx.saturating_mul(BLOCK_SIZE); let start = idx.saturating_mul(BLOCK_SIZE);
size.saturating_sub(start).min(BLOCK_SIZE) size.saturating_sub(start).min(BLOCK_SIZE)
@@ -311,8 +311,7 @@ struct PinFile {
pins: BTreeMap<String, Pin>, pins: BTreeMap<String, Pin>,
} }
/// What a staged file actually contains, written beside it as `<name>.json` /// What a staged file actually contains, written beside it as `<name>.json`.
/// (offline.md Phase 2).
/// ///
/// A staged file is **not** necessarily valid whole-file content, and that is /// A staged file is **not** necessarily valid whole-file content, and that is
/// the whole reason this exists. A partial overwrite commits by filling the /// the whole reason this exists. A partial overwrite commits by filling the
@@ -439,9 +438,9 @@ pub struct ContentCache {
/// Subdirectory for write-handle scratch files (disk-backed write buffers). /// Subdirectory for write-handle scratch files (disk-backed write buffers).
/// Emptied on open so a crashed run leaves no orphans. /// Emptied on open so a crashed run leaves no orphans.
scratch_dir: PathBuf, scratch_dir: PathBuf,
/// Subdirectory holding the bytes of writes that have not been uploaded yet /// Subdirectory holding the bytes of writes that have not been uploaded yet.
/// (offline.md Phase 2/3) — every released write passes through here, since /// Every released write passes through here, since the upload is a queued
/// the upload is a queued op performed later. Unlike `scratch_dir` this is /// op performed later. Unlike `scratch_dir` this is
/// **never** emptied on open: these are the only copy of content the user /// **never** emptied on open: these are the only copy of content the user
/// authored, and the whole point is that they outlive the daemon. /// authored, and the whole point is that they outlive the daemon.
staging_dir: PathBuf, staging_dir: PathBuf,
@@ -476,7 +475,7 @@ pub struct ContentCache {
thumb_bytes: AtomicU64, thumb_bytes: AtomicU64,
/// Unified metadata DB. Its `cache_entries` table is the LRU index: every /// Unified metadata DB. Its `cache_entries` table is the LRU index: every
/// store/read/evict updates it, and the budget enforcers query it instead of /// store/read/evict updates it, and the budget enforcers query it instead of
/// scanning the cache directories (plan.md P4). /// scanning the cache directories.
db: Arc<Db>, db: Arc<Db>,
/// LRU touches not yet written to `cache_entries`, keyed by cache key. /// LRU touches not yet written to `cache_entries`, keyed by cache key.
/// ///
@@ -989,7 +988,7 @@ impl ContentCache {
/// a miss ([`cached_block`](Self::cached_block) checks), so a bad block /// a miss ([`cached_block`](Self::cached_block) checks), so a bad block
/// cannot escape this cache even though this does not check. Validating a /// cannot escape this cache even though this does not check. Validating a
/// block is the *reader's* job, at the point it is fetched — see /// block is the *reader's* job, at the point it is fetched — see
/// `Core::read_block` (bugs.md B84). /// `Core::read_block` (`docs/BUGS.md` B84).
pub fn store_block( pub fn store_block(
&self, &self,
uid: &NodeUid, uid: &NodeUid,
@@ -1491,9 +1490,8 @@ impl ContentCache {
/// This is what makes a write survive its upload: the caller is releasing a /// This is what makes a write survive its upload: the caller is releasing a
/// write handle and would otherwise delete the file, so until the bytes are /// write handle and would otherwise delete the file, so until the bytes are
/// on the remote, staging holds the only copy. Every dirty handle goes /// on the remote, staging holds the only copy. Every dirty handle goes
/// through here — the upload is a queued op performed later (offline.md /// through here — the upload is a queued op performed later, and a staged
/// Phase 3), and a staged file is also what a human can recover from if the /// file is also what a human can recover from if the queue never drains.
/// queue never drains.
/// ///
/// Falls back to a copy when the rename crosses a filesystem boundary; a /// Falls back to a copy when the rename crosses a filesystem boundary; a
/// failure here means we could not save the bytes at all, so it is reported /// failure here means we could not save the bytes at all, so it is reported
@@ -2707,7 +2705,7 @@ mod tests {
} }
/// The geometry the whole read path validates against, here and over the /// The geometry the whole read path validates against, here and over the
/// network (bugs.md B84): full blocks, then whatever is left. /// network (`docs/BUGS.md` B84): full blocks, then whatever is left.
#[test] #[test]
fn block_len_is_full_blocks_then_the_remainder() { fn block_len_is_full_blocks_then_the_remainder() {
let size = BLOCK_SIZE * 2 + 17; let size = BLOCK_SIZE * 2 + 17;
@@ -2787,7 +2785,7 @@ mod tests {
/// A cached block is addressed by the range it covers, not by its index /// A cached block is addressed by the range it covers, not by its index
/// alone. Re-planning a file onto its real geometry must therefore miss the /// alone. Re-planning a file onto its real geometry must therefore miss the
/// blocks the uniform fallback cached, rather than serve their bytes at the /// blocks the uniform fallback cached, rather than serve their bytes at the
/// wrong offset (bugs.md B85). /// wrong offset (`docs/BUGS.md` B85).
#[test] #[test]
fn a_cached_block_is_not_served_to_a_different_geometry() { fn a_cached_block_is_not_served_to_a_different_geometry() {
let (c, _d) = cache(); let (c, _d) = cache();
@@ -2854,7 +2852,7 @@ mod tests {
/// A block of the wrong length cannot escape the cache even though the /// A block of the wrong length cannot escape the cache even though the
/// store does not check: the read validates it and reports a miss, so the /// store does not check: the read validates it and reports a miss, so the
/// next read refetches rather than serving a hole (bugs.md B84). /// next read refetches rather than serving a hole (`docs/BUGS.md` B84).
#[test] #[test]
fn a_block_of_the_wrong_length_reads_as_a_miss() { fn a_block_of_the_wrong_length_reads_as_a_miss() {
let (c, _d) = cache(); let (c, _d) = cache();
@@ -2919,7 +2917,7 @@ mod tests {
); );
} }
/// A failed upload must never cost the user their bytes (offline.md Phase 2): /// A failed upload must never cost the user their bytes:
/// the scratch file moves to staging intact, and staging survives a reopen /// the scratch file moves to staging intact, and staging survives a reopen
/// the way scratch deliberately does not. /// the way scratch deliberately does not.
#[test] #[test]
+110 -38
View File
@@ -3,7 +3,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use directories::ProjectDirs; use directories::ProjectDirs;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::menu::PromptConfig; use crate::menu::PromptConfig;
@@ -12,8 +12,19 @@ use crate::syncignore::DEFAULT_IGNORE_PATTERNS;
/// Proton requires `external-drive-{name}@{semver}-{channel}` (channel ∈ /// Proton requires `external-drive-{name}@{semver}-{channel}` (channel ∈
/// stable/beta/alpha); a malformed value trips the 422 anti-abuse path. /// stable/beta/alpha); a malformed value trips the 422 anti-abuse path.
pub const APP_VERSION: &str = "external-drive-linux@0.1.0-alpha"; /// Follows the release so Proton can tell releases apart and retire old ones
pub const USER_AGENT: &str = "proton-drive-linux/0.1.0"; /// without retiring every one.
pub const APP_VERSION: &str = concat!(
"external-drive-linux@",
env!("CARGO_PKG_VERSION"),
"-stable"
);
pub const USER_AGENT: &str = concat!("proton-drive-linux/", env!("CARGO_PKG_VERSION"));
/// The fixed identity every release up to 3.1.0 wrote into `config.json`. Read
/// back as "unset" so those configs follow the release instead of pinning it.
const LEGACY_APP_VERSION: &str = "external-drive-linux@0.1.0-alpha";
const LEGACY_USER_AGENT: &str = "proton-drive-linux/0.1.0";
/// Keyring service name; one entry per credential kind keyed by username. /// Keyring service name; one entry per credential kind keyed by username.
pub const KEYRING_SERVICE: &str = "proton-drive-linux"; pub const KEYRING_SERVICE: &str = "proton-drive-linux";
@@ -76,10 +87,24 @@ fn resolve_sweep_mode(env: Option<&str>, stored: Option<SweepMode>) -> SweepMode
} }
/// Configuration structure allowing the user to customize client identification headers. /// Configuration structure allowing the user to customize client identification headers.
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct AppConfig { pub struct AppConfig {
pub app_version: String, /// Override for [`APP_VERSION`]. `None` means "use the release's own";
pub user_agent: String, /// the old fixed default reads as `None`, so it leaves the file on the
/// next save.
#[serde(
default,
deserialize_with = "identity_override",
skip_serializing_if = "Option::is_none"
)]
pub app_version: Option<String>,
/// Override for [`USER_AGENT`], handled like `app_version`.
#[serde(
default,
deserialize_with = "identity_override",
skip_serializing_if = "Option::is_none"
)]
pub user_agent: Option<String>,
/// Soft cap on the on-disk content cache, in bytes (`0` = unlimited). /// Soft cap on the on-disk content cache, in bytes (`0` = unlimited).
/// `None` means "use [`DEFAULT_CACHE_BUDGET_BYTES`]"; the Settings page /// `None` means "use [`DEFAULT_CACHE_BUDGET_BYTES`]"; the Settings page
/// writes an explicit value here. Defaulted for configs predating the field. /// writes an explicit value here. Defaulted for configs predating the field.
@@ -157,7 +182,7 @@ pub struct AppConfig {
/// Record a new folder, a new file, a rename or a delete through the mount /// Record a new folder, a new file, a rename or a delete through the mount
/// locally and send it from the queue, instead of waiting for Drive in the /// locally and send it from the queue, instead of waiting for Drive in the
/// syscall. `None` means on. `false` brings back the old path for one /// syscall. `None` means on. `false` brings back the old path for one
/// release (docs/MILESTONE-3.0.0.md §10). Defaulted for configs predating /// release (docs/ROADMAP.md). Defaulted for configs predating
/// the field. /// the field.
#[serde(default)] #[serde(default)]
pub local_first: Option<bool>, pub local_first: Option<bool>,
@@ -216,31 +241,28 @@ impl FileSort {
} }
} }
impl Default for AppConfig { /// Reads an `app_version` or `user_agent` override, dropping the fixed
fn default() -> Self { /// identity older releases stored as the default.
Self { fn identity_override<'de, D: Deserializer<'de>>(
app_version: APP_VERSION.to_string(), deserializer: D,
user_agent: USER_AGENT.to_string(), ) -> std::result::Result<Option<String>, D::Error> {
cache_budget: None, let value = Option::<String>::deserialize(deserializer)?;
upload_limit: None, Ok(value.filter(|v| v != LEGACY_APP_VERSION && v != LEGACY_USER_AGENT))
download_limit: None,
mountpoint: None,
ignore_patterns: None,
device_uid: None,
conflict_sweep: None,
open_with: None,
prompt: None,
proton_theme: None,
files_view: FilesView::default(),
tray_hidden: false,
language: None,
online_map: false,
local_first: None,
}
}
} }
impl AppConfig { impl AppConfig {
/// The `x-pm-appversion` sent to Proton: the user's override, or
/// [`APP_VERSION`].
pub fn resolved_app_version(&self) -> &str {
self.app_version.as_deref().unwrap_or(APP_VERSION)
}
/// The `User-Agent` sent to Proton: the user's override, or
/// [`USER_AGENT`].
pub fn resolved_user_agent(&self) -> &str {
self.user_agent.as_deref().unwrap_or(USER_AGENT)
}
/// The effective cache budget in bytes: the user's explicit choice, or /// The effective cache budget in bytes: the user's explicit choice, or
/// [`DEFAULT_CACHE_BUDGET_BYTES`] when unset. /// [`DEFAULT_CACHE_BUDGET_BYTES`] when unset.
pub fn resolved_cache_budget(&self) -> u64 { pub fn resolved_cache_budget(&self) -> u64 {
@@ -432,7 +454,7 @@ impl AppDirs {
/// typical umask makes them `0755`. Until this was fixed, the only thing /// typical umask makes them `0755`. Until this was fixed, the only thing
/// keeping another local user out was `~/.cache` and `~/.local/state` /// keeping another local user out was `~/.cache` and `~/.local/state`
/// happening to be `0700`, which is a convention of the user's system and /// happening to be `0700`, which is a convention of the user's system and
/// not something this client established (bugs.md B6). /// not something this client established (`docs/BUGS.md` B6).
/// ///
/// Applied on every start, not just at creation: a directory that already /// Applied on every start, not just at creation: a directory that already
/// exists with a permissive mode — restored from a backup that flattened /// exists with a permissive mode — restored from a backup that flattened
@@ -483,7 +505,7 @@ fn restrict_dir(dir: &Path) -> Result<()> {
/// session — enumerate, read, upload, trash, share — without presenting any /// session — enumerate, read, upload, trash, share — without presenting any
/// credential, since the daemon already holds one. It is an authority boundary, /// credential, since the daemon already holds one. It is an authority boundary,
/// not merely private data, and `UnixListener::bind` applies the umask like any /// not merely private data, and `UnixListener::bind` applies the umask like any
/// other file (bugs.md B6). /// other file (`docs/BUGS.md` B6).
/// ///
/// There is a window between `bind` and this call during which the socket /// There is a window between `bind` and this call during which the socket
/// carries the umask's mode. It is closed in practice by the containing /// carries the umask's mode. It is closed in practice by the containing
@@ -504,7 +526,7 @@ mod tests {
std::fs::metadata(p).unwrap().permissions().mode() & 0o777 std::fs::metadata(p).unwrap().permissions().mode() & 0o777
} }
/// bugs.md B6. A permissive mode here is not a cosmetic problem: these /// `docs/BUGS.md` B6. A permissive mode here is not a cosmetic problem: these
/// directories hold decrypted content, a plaintext index of every node name, /// directories hold decrypted content, a plaintext index of every node name,
/// and the control socket that commands the daemon's session. /// and the control socket that commands the daemon's session.
#[test] #[test]
@@ -571,8 +593,8 @@ mod tests {
#[test] #[test]
fn test_app_config_serialization() { fn test_app_config_serialization() {
let config = AppConfig { let config = AppConfig {
app_version: "external-drive-test-client@1.0.0".to_string(), app_version: Some("external-drive-test-client@1.0.0".to_string()),
user_agent: "test-agent/1.0".to_string(), user_agent: Some("test-agent/1.0".to_string()),
cache_budget: Some(1234), cache_budget: Some(1234),
upload_limit: Some(500_000), upload_limit: Some(500_000),
download_limit: None, download_limit: None,
@@ -596,8 +618,11 @@ mod tests {
}; };
let json = serde_json::to_string(&config).unwrap(); let json = serde_json::to_string(&config).unwrap();
let decoded: AppConfig = serde_json::from_str(&json).unwrap(); let decoded: AppConfig = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.app_version, "external-drive-test-client@1.0.0"); assert_eq!(
assert_eq!(decoded.user_agent, "test-agent/1.0"); decoded.resolved_app_version(),
"external-drive-test-client@1.0.0"
);
assert_eq!(decoded.resolved_user_agent(), "test-agent/1.0");
assert_eq!(decoded.proton_theme, Some(true)); assert_eq!(decoded.proton_theme, Some(true));
assert!(decoded.tray_hidden); assert!(decoded.tray_hidden);
assert!(decoded.online_map); assert!(decoded.online_map);
@@ -628,8 +653,55 @@ mod tests {
#[test] #[test]
fn test_default_app_config() { fn test_default_app_config() {
let default_config = AppConfig::default(); let default_config = AppConfig::default();
assert_eq!(default_config.app_version, APP_VERSION); assert_eq!(default_config.resolved_app_version(), APP_VERSION);
assert_eq!(default_config.user_agent, USER_AGENT); assert_eq!(default_config.resolved_user_agent(), USER_AGENT);
}
#[test]
fn the_client_identity_follows_the_release() {
let version = env!("CARGO_PKG_VERSION");
assert_eq!(
APP_VERSION,
format!("external-drive-linux@{version}-stable")
);
assert_eq!(USER_AGENT, format!("proton-drive-linux/{version}"));
}
#[test]
fn an_old_config_with_the_fixed_identity_follows_the_release() {
// Every config written up to 3.1.0 holds these two values, so they must
// not pin the identity once Proton retires 0.1.0.
let json = format!(
r#"{{"app_version":"{LEGACY_APP_VERSION}","user_agent":"{LEGACY_USER_AGENT}","tray_hidden":true}}"#
);
let decoded: AppConfig = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.resolved_app_version(), APP_VERSION);
assert_eq!(decoded.resolved_user_agent(), USER_AGENT);
assert!(decoded.tray_hidden, "the rest of the config survives");
let saved = serde_json::to_string(&decoded).unwrap();
assert!(!saved.contains("app_version"), "{saved}");
assert!(!saved.contains("user_agent"), "{saved}");
}
#[test]
fn a_hand_set_client_identity_is_kept() {
let json = r#"{"app_version":"external-drive-linux@9.9.9-beta","user_agent":"custom/1"}"#;
let decoded: AppConfig = serde_json::from_str(json).unwrap();
assert_eq!(
decoded.resolved_app_version(),
"external-drive-linux@9.9.9-beta"
);
assert_eq!(decoded.resolved_user_agent(), "custom/1");
let saved = serde_json::to_string(&decoded).unwrap();
assert!(saved.contains("external-drive-linux@9.9.9-beta"), "{saved}");
}
#[test]
fn a_config_without_a_client_identity_loads() {
let decoded: AppConfig = serde_json::from_str("{}").unwrap();
assert_eq!(decoded.resolved_app_version(), APP_VERSION);
assert_eq!(decoded.resolved_user_agent(), USER_AGENT);
} }
#[test] #[test]
+7 -7
View File
@@ -443,11 +443,11 @@ pub enum Request {
DeleteDevice { uid: String }, DeleteDevice { uid: String },
/// Adopt an existing device as this machine's, pinning its uid in /// Adopt an existing device as this machine's, pinning its uid in
/// `config.json` so a hostname change or reinstall re-attaches to it instead /// `config.json` so a hostname change or reinstall re-attaches to it instead
/// of registering a duplicate (features.md 5.1). `uid: None` clears the pin. /// of registering a duplicate. `uid: None` clears the pin.
/// Replies with [`Response::Ok`]. /// Replies with [`Response::Ok`].
AdoptDevice { uid: Option<String> }, AdoptDevice { uid: Option<String> },
// ---- device folder sync (devices.md) ---------------------------------- // ---- device folder sync ----------------------------------
/// Add a local folder to this machine's device, uploading its tree and /// Add a local folder to this machine's device, uploading its tree and
/// registering the device on first use. Replies with [`Response::Ok`]. /// registering the device on first use. Replies with [`Response::Ok`].
AddSyncFolder { local_path: String }, AddSyncFolder { local_path: String },
@@ -496,7 +496,7 @@ pub enum Request {
/// [`Response::Ok`]. /// [`Response::Ok`].
ResolveConflict { path: String, keep: ConflictKeep }, ResolveConflict { path: String, keep: ConflictKeep },
/// List the folders under this machine's device that can be synced here, /// List the folders under this machine's device that can be synced here,
/// each with a proposed local path (features.md 5.2). Replies with /// each with a proposed local path. Replies with
/// [`Response::RestorableFolders`]. /// [`Response::RestorableFolders`].
ListRestorableFolders, ListRestorableFolders,
/// Attach the given remote device folders to local paths and sync them down. /// Attach the given remote device folders to local paths and sync them down.
@@ -1903,17 +1903,17 @@ pub enum Response {
/// The pin registry. /// The pin registry.
pins: Vec<Pin>, pins: Vec<Pin>,
/// False when the daemon is serving the cached tree because the API is /// False when the daemon is serving the cached tree because the API is
/// unreachable (offline.md Phase 1). Cached and pinned content still /// unreachable. Cached and pinned content still
/// reads; anything else fails until the network is back. /// reads; anything else fails until the network is back.
#[serde(default = "default_online")] #[serde(default = "default_online")]
online: bool, online: bool,
/// Writes accepted locally but not yet uploaded (offline.md Phase 3). /// Writes accepted locally but not yet uploaded.
/// Non-zero means the mount is ahead of the remote — either a copy is /// Non-zero means the mount is ahead of the remote — either a copy is
/// still draining, or it cannot drain because we are offline. /// still draining, or it cannot drain because we are offline.
#[serde(default)] #[serde(default)]
pending_uploads: u64, pending_uploads: u64,
/// Queued mutations that carry no bytes: `mkdir`, `rename`, `trash` /// Queued mutations that carry no bytes: `mkdir`, `rename`, `trash`.
/// (offline.md Phase 3b). Counted apart from `pending_uploads` because /// Counted apart from `pending_uploads` because
/// calling a queued `mkdir` an upload is a lie. /// calling a queued `mkdir` an upload is a lie.
#[serde(default)] #[serde(default)]
pending_changes: u64, pending_changes: u64,
+1 -1
View File
@@ -1,4 +1,4 @@
//! Database health reporting and repair (features.md 4.3). //! Database health reporting and repair.
//! //!
//! Everything here exists for the case where something has already gone wrong: //! Everything here exists for the case where something has already gone wrong:
//! a crash mid-write, a full disk, a cache that has grown out of proportion to //! a crash mid-write, a full disk, a cache that has grown out of proportion to
+7 -7
View File
@@ -577,7 +577,7 @@ CREATE TABLE trash (
); );
"; ";
/// Schema v8: device sync (devices.md). This machine registers as one Proton Drive /// Schema v8: device sync. This machine registers as one Proton Drive
/// **Device** (a share + root folder on the main volume); `device` is a singleton /// **Device** (a share + root folder on the main volume); `device` is a singleton
/// row cached so we reuse the same device across restarts instead of creating a /// row cached so we reuse the same device across restarts instead of creating a
/// new one each run. `sync_folder` is one row per local folder the user added, /// new one each run. `sync_folder` is one row per local folder the user added,
@@ -638,8 +638,8 @@ ALTER TABLE sync_folder ADD COLUMN pending_mode TEXT;
"; ";
/// Schema v11: writes no longer upload inside the FUSE handler. `release` stages /// Schema v11: writes no longer upload inside the FUSE handler. `release` stages
/// the bytes and records the intended upload here, and a drain worker performs it /// the bytes and records the intended upload here, and a drain worker performs it.
/// (offline.md Phase 3). The row outlives the process, so a write survives both a /// The row outlives the process, so a write survives both a
/// dead network and a restart. /// dead network and a restart.
/// ///
/// `blob_path` points into the content cache's `staging/` dir and `meta_json` is /// `blob_path` points into the content cache's `staging/` dir and `meta_json` is
@@ -666,7 +666,7 @@ CREATE INDEX pending_op_uid ON pending_op(uid);
/// Schema v12: `pending_op` also carries mutations that *create* a node, which a /// Schema v12: `pending_op` also carries mutations that *create* a node, which a
/// revision op never had to describe — it always addressed a node the server had /// revision op never had to describe — it always addressed a node the server had
/// already minted a uid for (offline.md Phase 3b). /// already minted a uid for.
/// ///
/// An offline `create`/`mkdir` cannot get a real uid, so the node is invented /// An offline `create`/`mkdir` cannot get a real uid, so the node is invented
/// locally under a `local~<uuid>` placeholder and the op records where it goes /// locally under a `local~<uuid>` placeholder and the op records where it goes
@@ -1056,7 +1056,7 @@ ALTER TABLE pending_op ADD COLUMN access_deferred_since INTEGER NOT NULL DEFAULT
/// as the last sync and left the file the same length — a fixed-size record /// as the last sync and left the file the same length — a fixed-size record
/// rewritten, an image re-exported, a database page updated in place — read as /// rewritten, an image re-exported, a database page updated in place — read as
/// *unchanged* and was never uploaded. The remote copy silently stayed at the /// *unchanged* and was never uploaded. The remote copy silently stayed at the
/// older content until something else about the file moved (bugs.md B25). /// older content until something else about the file moved (`docs/BUGS.md` B25).
/// ///
/// Nullable rather than a converted `local_mtime * 1_000_000_000`: the /// Nullable rather than a converted `local_mtime * 1_000_000_000`: the
/// nanosecond part of an existing baseline is genuinely unknown, and inventing /// nanosecond part of an existing baseline is genuinely unknown, and inventing
@@ -1168,8 +1168,8 @@ const MIGRATION_V35: &str = "
ALTER TABLE trash ADD COLUMN trashed_at INTEGER; ALTER TABLE trash ADD COLUMN trashed_at INTEGER;
"; ";
/// Schema v36: every node row has a local id (`lid`), phase 2 of /// Schema v36: every node row has a local id (`lid`); see
/// docs/MILESTONE-3.0.0.md. /// docs/ARCHITECTURE.md §2.
/// ///
/// The `uid` was the row's key, so a node had no identity until Drive minted /// The `uid` was the row's key, so a node had no identity until Drive minted
/// one, and a create that landed moved its row to a new key. The `lid` is given /// one, and a create that landed moved its row to a new key. The `lid` is given
+1 -2
View File
@@ -568,8 +568,7 @@ impl Db {
} }
/// Load one persisted node back by uid. Used to recover the My Files root /// Load one persisted node back by uid. Used to recover the My Files root
/// when the API is unreachable, so the mount can still serve the cached tree /// when the API is unreachable, so the mount can still serve the cached tree.
/// (offline.md Phase 1).
pub fn node_by_uid(&self, uid: &str) -> Result<Option<Node>> { pub fn node_by_uid(&self, uid: &str) -> Result<Option<Node>> {
let conn = self.read(); let conn = self.read();
let json: Option<String> = conn let json: Option<String> = conn
+5 -6
View File
@@ -15,7 +15,7 @@ use crate::Result;
pub const OP_REVISION: &str = "revision"; pub const OP_REVISION: &str = "revision";
/// The `kind` of a [`PendingOp`] that creates a file that so far exists only /// The `kind` of a [`PendingOp`] that creates a file that so far exists only
/// locally, under a `local:` placeholder uid (offline.md Phase 3b). /// locally, under a `local:` placeholder uid.
/// ///
/// The written bytes ride along on the same row rather than as a follow-on /// The written bytes ride along on the same row rather than as a follow-on
/// [`OP_REVISION`]: draining the create mints the node's real uid, which would /// [`OP_REVISION`]: draining the create mints the node's real uid, which would
@@ -29,7 +29,7 @@ pub const OP_CREATE: &str = "create";
pub const OP_MKDIR: &str = "mkdir"; pub const OP_MKDIR: &str = "mkdir";
/// The `kind` of a [`PendingOp`] that gives a node a new name, a new parent, or /// The `kind` of a [`PendingOp`] that gives a node a new name, a new parent, or
/// both — the queued form of `mv` (offline.md Phase 3b). /// both — the queued form of `mv`.
/// ///
/// `parent_uid` and `name` hold the node's *desired end state*, not a delta, so /// `parent_uid` and `name` hold the node's *desired end state*, not a delta, so
/// a second rename simply replaces the row (see [`Db::enqueue_op`]) and the /// a second rename simply replaces the row (see [`Db::enqueue_op`]) and the
@@ -55,8 +55,7 @@ pub struct RenameMeta {
pub original_name: Option<String>, pub original_name: Option<String>,
} }
/// The `kind` of a [`PendingOp`] that trashes a node the server knows about /// The `kind` of a [`PendingOp`] that trashes a node the server knows about.
/// (offline.md Phase 3b).
/// ///
/// A node that only ever existed locally never gets one of these: there is /// A node that only ever existed locally never gets one of these: there is
/// nothing to trash remotely, so deleting it just drops its queued ops /// nothing to trash remotely, so deleting it just drops its queued ops
@@ -311,7 +310,7 @@ pub const PARK_UNTIL: i64 = 8_000_000_000_000;
pub const PARK_EXPIRY_MS: i64 = 60 * 60 * 1000; pub const PARK_EXPIRY_MS: i64 = 60 * 60 * 1000;
/// A mutation that has been accepted locally but not yet performed against the /// A mutation that has been accepted locally but not yet performed against the
/// API — the durable half of the write-back queue (offline.md Phase 3). /// API — the durable half of the write-back queue.
/// ///
/// The daemon answers the FUSE call the moment this row and its staged blob are /// The daemon answers the FUSE call the moment this row and its staged blob are
/// on disk, so a `cp` into the mount runs at disk speed and the upload happens /// on disk, so a `cp` into the mount runs at disk speed and the upload happens
@@ -887,7 +886,7 @@ impl Db {
/// make — the node is still only a queued intent, and rewriting that intent /// make — the node is still only a queued intent, and rewriting that intent
/// *is* the rename. Returns false when the create has already drained, in /// *is* the rename. Returns false when the create has already drained, in
/// which case the node has a real uid and the caller must rename it there /// which case the node has a real uid and the caller must rename it there
/// instead (offline.md Phase 3b). /// instead.
/// ///
/// A create backing off is made due: what it failed on may have been the /// A create backing off is made due: what it failed on may have been the
/// name it no longer wants, such as one held by a queued change. A parked /// name it no longer wants, such as one held by a queued change. A parked
+4 -4
View File
@@ -25,8 +25,8 @@ pub struct StoredSyncFolder {
} }
/// One per-file sync baseline row: what a path looked like on both sides at the /// One per-file sync baseline row: what a path looked like on both sides at the
/// last successful sync, so the next reconcile can tell which side changed /// last successful sync, so the next reconcile can tell which side changed.
/// (devices.md Phase 2). `remote_rev`/`remote_hash` hold the remote signature — /// `remote_rev`/`remote_hash` hold the remote signature —
/// its modification time and size as strings — since no cheap content hash is /// its modification time and size as strings — since no cheap content hash is
/// exposed; change detection is `(mtime, size)` on each side. /// exposed; change detection is `(mtime, size)` on each side.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@@ -39,7 +39,7 @@ pub struct StoredSyncEntry {
/// ///
/// `local_mtime` alone cannot tell an edit that landed inside the same /// `local_mtime` alone cannot tell an edit that landed inside the same
/// second as the last sync — and left the file the same length — from no /// second as the last sync — and left the file the same length — from no
/// edit at all, so those never uploaded (bugs.md B25). `None` on a row /// edit at all, so those never uploaded (`docs/BUGS.md` B25). `None` on a row
/// written before schema 28, which the comparison reads as "compare whole /// written before schema 28, which the comparison reads as "compare whole
/// seconds", so upgrading does not make an entire mirror look changed. /// seconds", so upgrading does not make an entire mirror look changed.
pub local_mtime_ns: Option<i64>, pub local_mtime_ns: Option<i64>,
@@ -345,7 +345,7 @@ impl Db {
/// Drop the entire baseline for a folder. Used when flipping ondemand→mirror: /// Drop the entire baseline for a folder. Used when flipping ondemand→mirror:
/// the local tree was evicted, so the old baseline is stale and would make the /// the local tree was evicted, so the old baseline is stale and would make the
/// next reconcile mistake "locally deleted" for "must re-download". Clearing it /// next reconcile mistake "locally deleted" for "must re-download". Clearing it
/// leaves an empty baseline + full remote = pure download (devices.md P3). /// leaves an empty baseline + full remote = pure download.
pub fn sync_entries_clear(&self, folder_id: i64) -> Result<()> { pub fn sync_entries_clear(&self, folder_id: i64) -> Result<()> {
let conn = self.conn.lock(); let conn = self.conn.lock();
conn.execute( conn.execute(
+5 -5
View File
@@ -675,8 +675,8 @@ fn foreign_folder(volume: &str, link: &str, parent: NodeUid, name: &str) -> Node
node node
} }
/// Recovering the root by uid is what lets the daemon mount offline /// Recovering the root by uid is what lets the daemon mount offline:
/// (offline.md Phase 1): the uid is remembered in `sync_state`, the node /// the uid is remembered in `sync_state`, the node
/// itself comes back out of `nodes`. /// itself comes back out of `nodes`.
#[test] #[test]
fn node_by_uid_recovers_a_stored_node() { fn node_by_uid_recovers_a_stored_node() {
@@ -796,7 +796,7 @@ fn children_if_listed_gated_on_flag() {
assert_eq!(kids.len(), 2); assert_eq!(kids.len(), 2);
} }
/// The shape of the `mv`-loses-the-file bug (bugs.md B1): a rename deletes the /// The shape of the `mv`-loses-the-file bug (`docs/BUGS.md` B1): a rename deletes the
/// moved node's row, so a destination left marked `listed` is rebuilt from the DB /// moved node's row, so a destination left marked `listed` is rebuilt from the DB
/// without it — the file is gone from the source and absent from the destination, /// without it — the file is gone from the source and absent from the destination,
/// with `rename(2)` having reported success. Clearing the flag is what forces the /// with `rename(2)` having reported success. Clearing the flag is what forces the
@@ -5735,7 +5735,7 @@ fn cache_eviction_candidates_are_lru_ordered_and_limited() {
); );
} }
/// **The B5 checkpoint.** `improvements.md` P2.3 proposes concurrent SQLite /// **The B5 checkpoint.** A performance plan proposed concurrent SQLite
/// reads, on the premise that the single `Mutex<Connection>` serializes the FUSE /// reads, on the premise that the single `Mutex<Connection>` serializes the FUSE
/// workers. This measures what the connection is actually asked to do now that /// workers. This measures what the connection is actually asked to do now that
/// B3 and B4 have landed, so the proposal is decided on evidence. /// B3 and B4 have landed, so the proposal is decided on evidence.
@@ -6291,7 +6291,7 @@ fn migration_v27_adds_the_access_deferral_column_to_a_v26_queue() {
/// back with no nanosecond time. That `None` is the whole point: it makes the /// back with no nanosecond time. That `None` is the whole point: it makes the
/// comparison fall back to whole seconds for rows written before the column /// comparison fall back to whole seconds for rows written before the column
/// existed, instead of asserting a sub-second time of zero and re-uploading /// existed, instead of asserting a sub-second time of zero and re-uploading
/// every already-synced file in the folder (bugs.md B25). /// every already-synced file in the folder (`docs/BUGS.md` B25).
#[test] #[test]
fn migration_v28_adds_sub_second_local_times_without_disturbing_a_v27_baseline() { fn migration_v28_adds_sub_second_local_times_without_disturbing_a_v27_baseline() {
let path = std::env::temp_dir().join(format!( let path = std::env::temp_dir().join(format!(
+1 -1
View File
@@ -102,5 +102,5 @@ impl Db {
Ok(seen) Ok(seen)
} }
// ---- device sync (devices.md) ----------------------------------------- // ---- device sync -----------------------------------------
} }
+15 -3
View File
@@ -32,6 +32,16 @@ pub enum Error {
#[error("saved session can no longer unlock your keys — run `pdfs login` again")] #[error("saved session can no longer unlock your keys — run `pdfs login` again")]
ReloginRequired, ReloginRequired,
/// The account has a separate mailbox password that has not been entered
/// since login, so its keys cannot be unlocked. The fix is
/// [`auth::unlock`](crate::auth::unlock), not a new login.
#[error("account is locked — enter the mailbox password with `pdfs unlock`")]
Locked,
/// The mailbox password was checked against the account and is wrong.
#[error("that mailbox password does not unlock your account")]
WrongMailboxPassword,
/// The login was gated behind human verification. Not a failure the user can /// The login was gated behind human verification. Not a failure the user can
/// fix by retrying: the challenge has to be solved and the login restarted /// fix by retrying: the challenge has to be solved and the login restarted
/// with the resulting token (see [`auth::login_verified`](crate::auth::login_verified)). /// with the resulting token (see [`auth::login_verified`](crate::auth::login_verified)).
@@ -199,9 +209,11 @@ impl From<serde_json::Error> for CoreError {
impl From<Error> for CoreError { impl From<Error> for CoreError {
fn from(e: Error) -> Self { fn from(e: Error) -> Self {
let kind = match &e { let kind = match &e {
Error::NotLoggedIn | Error::ReloginRequired | Error::HumanVerificationRequired(_) => { Error::NotLoggedIn
ErrorKind::Denied | Error::ReloginRequired
} | Error::Locked
| Error::WrongMailboxPassword
| Error::HumanVerificationRequired(_) => ErrorKind::Denied,
Error::Proton(proton_sdk::ProtonError::Api(api)) => api_kind(api.code), Error::Proton(proton_sdk::ProtonError::Api(api)) => api_kind(api.code),
// A `ProtonError` that never reached the API is a transport failure. // A `ProtonError` that never reached the API is a transport failure.
Error::Proton(_) => ErrorKind::Offline, Error::Proton(_) => ErrorKind::Offline,
+204
View File
@@ -0,0 +1,204 @@
//! A secret held in the Linux kernel keyring, for as long as the user is logged in.
//!
//! This is where the key passphrases of a two-password account live by default:
//! the mailbox password is never stored, and the secrets derived from it are
//! kept in kernel memory — not on disk, not in the Secret Service — and vanish
//! when the user's last session ends or the machine reboots. Both the daemon and
//! `pdfs unlock` reach the same entry because it hangs off the *user* keyring.
//!
//! Kernel keys are not paged to swap, are invisible to other users, and are
//! wiped by [`clear`] (`KEYCTL_REVOKE`), which makes them a better home than a
//! process's heap. Processes of the same user can still read them, exactly as
//! they could ptrace the daemon: this protects against disk, backup and other
//! users, not against code already running as you.
//!
//! The keyring may be unavailable (a container, a kernel built without
//! `CONFIG_KEYS`); every operation then reports it as an error and the caller
//! falls back to asking again, never to storing the secret somewhere weaker.
use std::io;
use zeroize::Zeroizing;
/// Key description. Namespaced so it cannot collide with another program's key.
#[cfg(not(test))]
const DESCRIPTION: &[u8] = b"pdfs:key-passphrases\0";
/// Tests must never touch a real unlock.
#[cfg(test)]
const DESCRIPTION: &[u8] = b"pdfs:key-passphrases:test\0";
const KEY_TYPE: &[u8] = b"user\0";
const KEY_SPEC_USER_KEYRING: i32 = -4;
const KEYCTL_REVOKE: libc::c_long = 3;
const KEYCTL_SETPERM: libc::c_long = 5;
const KEYCTL_SEARCH: libc::c_long = 10;
const KEYCTL_READ: libc::c_long = 11;
/// Possessor and owner may view, read, write, search, link and set attributes;
/// group and others get nothing.
///
/// A new key's default grants the owner only *view*, and read access to the
/// possessor. A process possesses the user keyring only when its session
/// keyring links to it, which a systemd user service or a container does not
/// guarantee — so with the default, the daemon could store a key it then
/// cannot read back. The owner is the same user that could ptrace the daemon
/// anyway, so granting it read access gives up nothing.
const PERM_OWNER_AND_POSSESSOR: u32 = 0x3f3f_0000;
fn last_error<T>() -> io::Result<T> {
Err(io::Error::last_os_error())
}
fn find() -> io::Result<Option<libc::c_long>> {
// SAFETY: both strings are NUL-terminated and outlive the call.
let id = unsafe {
libc::syscall(
libc::SYS_keyctl,
KEYCTL_SEARCH,
KEY_SPEC_USER_KEYRING,
KEY_TYPE.as_ptr(),
DESCRIPTION.as_ptr(),
0,
)
};
if id >= 0 {
return Ok(Some(id));
}
let err = io::Error::last_os_error();
match err.raw_os_error() {
// ENOKEY: not stored. EKEYREVOKED/EKEYEXPIRED: stored once, now gone.
Some(libc::ENOKEY | libc::EKEYREVOKED | libc::EKEYEXPIRED) => Ok(None),
_ => Err(err),
}
}
/// Stop this process from being core-dumped or ptraced by an unprivileged
/// process of the same user, so the secrets in its memory stay out of crash
/// reports. Call once at start of a long-lived process that holds them.
pub fn forbid_dumps() {
// SAFETY: `PR_SET_DUMPABLE` takes plain integers and only affects this process.
let rc = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };
if rc != 0 {
tracing::warn!(error = %io::Error::last_os_error(), "could not mark the process non-dumpable");
}
}
/// Store `secret`, replacing any earlier one.
pub fn store(secret: &[u8]) -> io::Result<()> {
// SAFETY: strings are NUL-terminated; the payload pointer/length pair
// describes `secret`, which outlives the call.
let id = unsafe {
libc::syscall(
libc::SYS_add_key,
KEY_TYPE.as_ptr(),
DESCRIPTION.as_ptr(),
secret.as_ptr(),
secret.len(),
KEY_SPEC_USER_KEYRING,
)
};
if id < 0 {
return last_error();
}
// SAFETY: plain integer arguments.
let rc = unsafe {
libc::syscall(
libc::SYS_keyctl,
KEYCTL_SETPERM,
id,
PERM_OWNER_AND_POSSESSOR,
)
};
if rc < 0 {
// A key whose permissions are unknown is not one to leave behind.
let err = io::Error::last_os_error();
let _ = clear();
return Err(err);
}
Ok(())
}
/// Read the stored secret, or `None` when there is none.
pub fn load() -> io::Result<Option<Zeroizing<Vec<u8>>>> {
let Some(id) = find()? else {
return Ok(None);
};
// Size first, then read; retry if the key was replaced in between.
for _ in 0..3 {
// SAFETY: a null buffer asks the kernel only for the payload length.
let len = unsafe {
libc::syscall(
libc::SYS_keyctl,
KEYCTL_READ,
id,
std::ptr::null_mut::<u8>(),
0usize,
)
};
if len < 0 {
return match io::Error::last_os_error().raw_os_error() {
Some(libc::ENOKEY | libc::EKEYREVOKED | libc::EKEYEXPIRED) => Ok(None),
_ => last_error(),
};
}
let mut buf = Zeroizing::new(vec![0u8; len as usize]);
// SAFETY: `buf` is `len` writable bytes.
let read = unsafe {
libc::syscall(
libc::SYS_keyctl,
KEYCTL_READ,
id,
buf.as_mut_ptr(),
buf.len(),
)
};
if read < 0 {
return last_error();
}
if read as usize <= buf.len() {
buf.truncate(read as usize);
return Ok(Some(buf));
}
}
Err(io::Error::other("the key changed while it was being read"))
}
/// Remove the stored secret. Succeeds when there is none.
pub fn clear() -> io::Result<()> {
let Some(id) = find()? else {
return Ok(());
};
// SAFETY: plain integer arguments.
let rc = unsafe { libc::syscall(libc::SYS_keyctl, KEYCTL_REVOKE, id) };
if rc < 0 {
return match io::Error::last_os_error().raw_os_error() {
Some(libc::ENOKEY | libc::EKEYREVOKED) => Ok(()),
_ => last_error(),
};
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
// The kernel keyring is process-wide state and a sandbox may not offer it,
// so one test covers the whole round trip and skips when it is unavailable.
#[test]
fn stored_secret_round_trips_and_clears() {
if store(b"probe").is_err() {
eprintln!("kernel keyring unavailable; skipping");
return;
}
let saved = load().unwrap().expect("stored secret is readable");
assert_eq!(saved.as_slice(), b"probe");
store(b"replaced").unwrap();
assert_eq!(load().unwrap().unwrap().as_slice(), b"replaced");
clear().unwrap();
assert!(load().unwrap().is_none());
clear().unwrap();
}
}
+1
View File
@@ -10,6 +10,7 @@ pub mod config;
pub mod control; pub mod control;
pub mod db; pub mod db;
pub mod error; pub mod error;
pub mod kernelkey;
pub mod localindex; pub mod localindex;
pub mod menu; pub mod menu;
pub mod mounts; pub mod mounts;
+1 -1
View File
@@ -1,4 +1,4 @@
//! The machine profile that travels with a device (features.md 5.3). //! The machine profile that travels with a device.
//! //!
//! Sync folder mappings, pins, and the settings that shape them live in //! Sync folder mappings, pins, and the settings that shape them live in
//! `cache.db` and `config.json` — both on the local disk. When that disk dies, //! `cache.db` and `config.json` — both on the local disk. When that disk dies,
+1 -1
View File
@@ -1,4 +1,4 @@
//! Selective-sync ignore rules for mirror folders (features.md 1.1). //! Selective-sync ignore rules for mirror folders.
//! //!
//! A mirror folder otherwise uploads every path beneath its root, which for a //! A mirror folder otherwise uploads every path beneath its root, which for a
//! project directory means `node_modules/`, `.git/`, and `target/` — quota //! project directory means `node_modules/`, `.git/`, and `target/` — quota
+3 -3
View File
@@ -143,7 +143,7 @@ fn apply_event(core: &Core, event: &DriveEvent, dirty: &mut DirtyParents) -> pdf
// create, and re-fetching would replace the size and mtime of the // create, and re-fetching would replace the size and mtime of the
// write we just accepted with the stale revision's — making a file // write we just accepted with the stale revision's — making a file
// that was copied in seconds ago read as empty until its upload // that was copied in seconds ago read as empty until its upload
// lands (offline.md Phase 3). // lands.
// //
// A write that has already drained is the same story one step later: // A write that has already drained is the same story one step later:
// the drain brought the tree level with the revision it sealed, so // the drain brought the tree level with the revision it sealed, so
@@ -375,7 +375,7 @@ pub(super) async fn run_event_sync(
// server head; persist it so the next restart resumes instead of // server head; persist it so the next restart resumes instead of
// reseeding (which would skip everything that changed offline). // reseeding (which would skip everything that changed offline).
// Seeding needs the network, and this task also runs on mounts that // Seeding needs the network, and this task also runs on mounts that
// started offline (offline.md Phase 1) — so retry rather than giving up, // started offline — so retry rather than giving up,
// which used to disable live sync for the life of the daemon. // which used to disable live sync for the life of the daemon.
Ok(None) => { Ok(None) => {
// Nothing says what changed before this cursor, so a persisted SDK // Nothing says what changed before this cursor, so a persisted SDK
@@ -660,7 +660,7 @@ pub(super) fn run_local_index(
scan_local_once(&db, &indexing, &transfers, &mountpoint); scan_local_once(&db, &indexing, &transfers, &mountpoint);
} }
// Interruptible, so teardown joins this thread instead of leaving it to // Interruptible, so teardown joins this thread instead of leaving it to
// keep rewriting the index for a mount that is gone (bugs.md B44). // keep rewriting the index for a mount that is gone (`docs/BUGS.md` B44).
if !shutdown.sleep(LOCAL_INDEX_CHECK) { if !shutdown.sleep(LOCAL_INDEX_CHECK) {
return; return;
} }
+1 -1
View File
@@ -1816,7 +1816,7 @@ pub(crate) fn run_control_socket(
info!("control socket listening"); info!("control socket listening");
for conn in listener.incoming() { for conn in listener.incoming() {
// Checked on every accept, including the deliberate poke teardown sends // Checked on every accept, including the deliberate poke teardown sends
// to wake this thread out of a blocking accept (bugs.md B44). // to wake this thread out of a blocking accept (`docs/BUGS.md` B44).
if core.shutdown.is_stopping() { if core.shutdown.is_stopping() {
info!("control socket closing"); info!("control socket closing");
return; return;
+7 -9
View File
@@ -1,5 +1,5 @@
//! This machine as a Proton Drive Device, and the local folders it keeps in //! This machine as a Proton Drive Device, and the local folders it keeps in
//! step with the remote (devices.md). //! step with the remote.
//! //!
//! A sync folder runs in one of two modes. `mirror` keeps a real local tree that //! A sync folder runs in one of two modes. `mirror` keeps a real local tree that
//! the engine in [`super::sync`] reconciles both ways; `ondemand` evicts that //! the engine in [`super::sync`] reconciles both ways; `ondemand` evicts that
@@ -184,8 +184,8 @@ impl Core {
} }
/// Adopt an existing device as *this* machine's, pinning it in `config.json` /// Adopt an existing device as *this* machine's, pinning it in `config.json`
/// so hostname changes and reinstalls stop registering duplicates /// so hostname changes and reinstalls stop registering duplicates.
/// (features.md 5.1). `None` clears the pin and returns to hostname matching. /// `None` clears the pin and returns to hostname matching.
/// ///
/// Refuses a uid that is not in the account: the whole point of the pin is /// Refuses a uid that is not in the account: the whole point of the pin is
/// that it is authoritative, so accepting a bad one would defer the failure /// that it is authoritative, so accepting a bad one would defer the failure
@@ -260,7 +260,7 @@ impl Core {
}) })
} }
// ---- device folder sync (devices.md, Phase 1) ------------------------- // ---- device folder sync -------------------------
/// Auto-register (or recover) this machine as a Proton Drive Device, caching /// Auto-register (or recover) this machine as a Proton Drive Device, caching
/// it so restarts reuse the same device. Recovery matches an existing remote /// it so restarts reuse the same device. Recovery matches an existing remote
@@ -270,8 +270,7 @@ impl Core {
/// Resolution order: the *adopted* uid pinned in `config.json`, then the /// Resolution order: the *adopted* uid pinned in `config.json`, then the
/// cached DB row (validated remotely), then a hostname match, then create. /// cached DB row (validated remotely), then a hostname match, then create.
/// The pin exists because the hostname heuristic silently registers a second /// The pin exists because the hostname heuristic silently registers a second
/// device after a rename or reinstall, orphaning the first one's folders /// device after a rename or reinstall, orphaning the first one's folders.
/// (features.md 5.1).
pub(crate) fn ensure_device(&self) -> CoreResult<StoredDevice> { pub(crate) fn ensure_device(&self) -> CoreResult<StoredDevice> {
let name = this_hostname(); let name = this_hostname();
// Enumerate the remote devices once: used to validate the pin, validate // Enumerate the remote devices once: used to validate the pin, validate
@@ -617,7 +616,7 @@ impl Core {
} }
/// A sibling Core for a secondary FUSE session rooted at an `ondemand` sync /// A sibling Core for a secondary FUSE session rooted at an `ondemand` sync
/// folder (devices.md Phase 3). It shares everything with this one, /// folder. It shares everything with this one,
/// the tree included, except what belongs to a session: its root and its /// the tree included, except what belongs to a session: its root and its
/// kernel channel, both filled in when the session is built. /// kernel channel, both filled in when the session is built.
pub(crate) fn fork_state(&self) -> Core { pub(crate) fn fork_state(&self) -> Core {
@@ -986,8 +985,7 @@ impl Core {
/// restart (their local dirs are empty on disk — the files live in the cloud). /// restart (their local dirs are empty on disk — the files live in the cloud).
/// Best-effort per folder: a missing local path or a failed remote fetch marks /// Best-effort per folder: a missing local path or a failed remote fetch marks
/// the folder `error` and moves on rather than aborting the rest. Runs on its /// the folder `error` and moves on rather than aborting the rest. Runs on its
/// own thread from `mount` so the network fetches never block startup /// own thread from `mount` so the network fetches never block startup.
/// (devices.md Phase 4).
pub(crate) fn restore_ondemand_mounts(&self) { pub(crate) fn restore_ondemand_mounts(&self) {
let folders = match self.db.sync_folder_list() { let folders = match self.db.sync_folder_list() {
Ok(f) => f, Ok(f) => f,
+6 -7
View File
@@ -1,7 +1,7 @@
//! The write-back drain: turning queued mutations into remote calls. //! The write-back drain: turning queued mutations into remote calls.
//! //!
//! Every write the kernel hands us is answered the moment its bytes and its //! Every write the kernel hands us is answered the moment its bytes and its
//! `pending_op` row are on disk (offline.md Phase 3), which is what lets a `cp` //! `pending_op` row are on disk, which is what lets a `cp`
//! into the mount run at disk speed and lets an offline write succeed at all. //! into the mount run at disk speed and lets an offline write succeed at all.
//! This module is the other half: the worker that walks that queue and performs //! This module is the other half: the worker that walks that queue and performs
//! the uploads, creates, renames and trashes it recorded. //! the uploads, creates, renames and trashes it recorded.
@@ -276,8 +276,7 @@ fn run_authorized_drain(
} }
impl Core { impl Core {
/// Drain the pending-op queue: the background half of every write /// Drain the pending-op queue: the background half of every write.
/// (offline.md Phase 3).
/// ///
/// Runs for the life of the mount. Ops are replayed oldest-first, each /// Runs for the life of the mount. Ops are replayed oldest-first, each
/// retried with doubling backoff and *never* dropped on failure — the staged /// retried with doubling backoff and *never* dropped on failure — the staged
@@ -306,7 +305,7 @@ impl Core {
loop { loop {
// Between ops, never inside one: an op that has started is either // Between ops, never inside one: an op that has started is either
// retired or released, so stopping here cannot leave the queue with // retired or released, so stopping here cannot leave the queue with
// a row claimed by a worker that no longer exists (bugs.md B44). // a row claimed by a worker that no longer exists (`docs/BUGS.md` B44).
if self.shutdown.is_stopping() { if self.shutdown.is_stopping() {
debug!(primary, "drain worker stopping"); debug!(primary, "drain worker stopping");
return; return;
@@ -1150,7 +1149,7 @@ impl Core {
} }
/// Make a node that so far exists only on this machine real, and adopt the /// Make a node that so far exists only on this machine real, and adopt the
/// uid the server gives it (offline.md Phase 3b). /// uid the server gives it.
pub(crate) fn drain_local_node( pub(crate) fn drain_local_node(
&self, &self,
op: &PendingOp, op: &PendingOp,
@@ -1850,7 +1849,7 @@ impl Core {
/// that window the node can be rewritten by another device, trashed, or /// that window the node can be rewritten by another device, trashed, or
/// deleted outright. Sending the blob anyway would silently drop whatever /// deleted outright. Sending the blob anyway would silently drop whatever
/// happened in between, which is exactly the thing the sync engine refuses /// happened in between, which is exactly the thing the sync engine refuses
/// to do (offline.md Phase 3b). /// to do.
/// ///
/// Only checkable against a recorded baseline: a write staged before /// Only checkable against a recorded baseline: a write staged before
/// [`StagedWrite::based_on`] existed, or one against a node that has never /// [`StagedWrite::based_on`] existed, or one against a node that has never
@@ -2009,7 +2008,7 @@ impl Core {
self.db.delete_op(op.id)?; self.db.delete_op(op.id)?;
// Dropping the pending entry hands the node back to the remote's truth: // Dropping the pending entry hands the node back to the remote's truth:
// reads stop coming from the staged blob, and the event sync stops // reads stop coming from the staged blob, and the event sync stops
// skipping it as "ahead of the server" (offline.md Phase 3a). Only if it // skipping it as "ahead of the server". Only if it
// is still *this* write's entry — see [`Core::release_pending`]. // is still *this* write's entry — see [`Core::release_pending`].
self.release_pending(uid, blob); self.release_pending(uid, blob);
self.cache.discard_staged(blob); self.cache.discard_staged(blob);
+1 -1
View File
@@ -3,7 +3,7 @@
//! `Core` used to hold a concrete [`ProtonDriveClient`], so no test could make //! `Core` used to hold a concrete [`ProtonDriveClient`], so no test could make
//! Drive answer late, answer stale, or not answer at all, and the interleavings //! Drive answer late, answer stale, or not answer at all, and the interleavings
//! behind most recent bugs could only be found on a real account over a slow //! behind most recent bugs could only be found on a real account over a slow
//! link (`docs/MILESTONE-3.0.0.md` §2, §8.1). [`DriveApi`] is the part of the //! link. [`DriveApi`] is the part of the
//! client the mount, the drain, the event feed and the mirror engine use; the //! client the mount, the drain, the event feed and the mirror engine use; the
//! real client implements it by forwarding, and the simulation tests implement //! real client implements it by forwarding, and the simulation tests implement
//! it with an in-memory Drive that injects Drive's known faults. //! it with an in-memory Drive that injects Drive's known faults.
+17 -18
View File
@@ -132,7 +132,7 @@ impl Filesystem for ProtonFs {
// A provisional size is the *ciphertext* size, which is larger than the // A provisional size is the *ciphertext* size, which is larger than the
// file. Publishing it makes every reader that trusts `st_size` — rsync, // file. Publishing it makes every reader that trusts `st_size` — rsync,
// mmap, sendfile, a sized read loop — run off the end of the file and // mmap, sendfile, a sized read loop — run off the end of the file and
// fail. So resolve it before answering rather than after (bugs.md B14). // fail. So resolve it before answering rather than after (`docs/BUGS.md` B14).
// //
// The cost is one batched round trip for the whole folder, not one per // The cost is one batched round trip for the whole folder, not one per
// file: `ls -l` is one `getattr` per entry and they collapse onto a // file: `ls -l` is one `getattr` per entry and they collapse onto a
@@ -172,7 +172,7 @@ impl Filesystem for ProtonFs {
/// here — once per `opendir`, which the kernel pairs exactly with one /// here — once per `opendir`, which the kernel pairs exactly with one
/// `releasedir` — is what makes the offsets stable, so a create or a trash /// `releasedir` — is what makes the offsets stable, so a create or a trash
/// during an enumeration can no longer make the caller skip or repeat an /// during an enumeration can no longer make the caller skip or repeat an
/// entry (bugs.md B43). /// entry (`docs/BUGS.md` B43).
/// ///
/// Enumerating a cold folder is a remote call, so it goes to a worker for /// Enumerating a cold folder is a remote call, so it goes to a worker for
/// the same reason `lookup` and `readdir` do. /// the same reason `lookup` and `readdir` do.
@@ -346,7 +346,7 @@ impl Filesystem for ProtonFs {
self.core.workers.run(Lane::Transfer, "read", move || { self.core.workers.run(Lane::Transfer, "read", move || {
// The revision disagrees with a provisional size, and B84 fails // The revision disagrees with a provisional size, and B84 fails
// such a read outright, so settle the size first. On the network, // such a read outright, so settle the size first. On the network,
// hence here and not above (bugs.md B100). // hence here and not above (`docs/BUGS.md` B100).
let fsize = match provisional { let fsize = match provisional {
true => core.resolve_provisional_size(ino.0, &uid).unwrap_or(fsize), true => core.resolve_provisional_size(ino.0, &uid).unwrap_or(fsize),
false => fsize, false => fsize,
@@ -517,7 +517,7 @@ impl Filesystem for ProtonFs {
// `> file`. This is the second write path into the API, and // `> file`. This is the second write path into the API, and
// queueing it is what lets a redirect work offline at all: it // queueing it is what lets a redirect work offline at all: it
// never reaches `release`, so without this it failed before any // never reaches `release`, so without this it failed before any
// byte was written (offline.md Phase 2/3b). // byte was written.
// //
// The one branch of `setattr` that can reach the network: // The one branch of `setattr` that can reach the network:
// shrinking keeps the prefix, which `queue_truncate` gap-fills // shrinking keeps the prefix, which `queue_truncate` gap-fills
@@ -607,7 +607,7 @@ impl Filesystem for ProtonFs {
} }
/// `close(2)` calls flush before release. The upload is queued in `release` /// `close(2)` calls flush before release. The upload is queued in `release`
/// and performed in the background (offline.md Phase 3), so there is nothing /// and performed in the background, so there is nothing
/// to push here — the written bytes are already in the scratch file. /// to push here — the written bytes are already in the scratch file.
fn flush( fn flush(
&self, &self,
@@ -985,7 +985,7 @@ pub struct ProtonFs {
} }
/// How many times a write `open` will re-read its base after a `release` /// How many times a write `open` will re-read its base after a `release`
/// published a newer queued revision underneath it (bugs.md B32). /// published a newer queued revision underneath it (`docs/BUGS.md` B32).
/// ///
/// One retry covers the race, which needs a close and an open of the same file /// One retry covers the race, which needs a close and an open of the same file
/// to overlap on different threads. More than that is not contention, it is a /// to overlap on different threads. More than that is not contention, it is a
@@ -1021,7 +1021,7 @@ fn build_listing(st: &State, ino: u64) -> DirListing {
/// ///
/// The cookie handed back is the index of the *next* entry, so a resumed call /// The cookie handed back is the index of the *next* entry, so a resumed call
/// continues exactly where the last one stopped — which is only true because /// continues exactly where the last one stopped — which is only true because
/// the listing behind those indexes is a snapshot (bugs.md B43). /// the listing behind those indexes is a snapshot (`docs/BUGS.md` B43).
fn reply_listing(listing: &DirListing, offset: u64, root: u64, mut reply: ReplyDirectory) { fn reply_listing(listing: &DirListing, offset: u64, root: u64, mut reply: ReplyDirectory) {
for (i, (ino, is_dir, name)) in listing.iter().enumerate().skip(offset as usize) { for (i, (ino, is_dir, name)) in listing.iter().enumerate().skip(offset as usize) {
let ft = if *is_dir { let ft = if *is_dir {
@@ -1094,7 +1094,7 @@ impl ProtonFs {
// `getattr` would, so `ls -l` — which is one `lookup` // `getattr` would, so `ls -l` — which is one `lookup`
// per entry and no `getattr` at all — takes its sizes // per entry and no `getattr` at all — takes its sizes
// from here. Resolving only in `getattr` left the whole // from here. Resolving only in `getattr` left the whole
// listing provisional (bugs.md B14). // listing provisional (`docs/BUGS.md` B14).
let provisional = matches!( let provisional = matches!(
&e.node.kind, &e.node.kind,
NodeKind::File { NodeKind::File {
@@ -1276,7 +1276,7 @@ impl ProtonFs {
// revision in between sends this around again. Without the re-check the // revision in between sends this around again. Without the re-check the
// new handle carried the *older* base, and its own release published // new handle carried the *older* base, and its own release published
// content that silently discarded the revision closed a moment earlier // content that silently discarded the revision closed a moment earlier
// (bugs.md B32). A second attempt is already unlikely and a third would // (`docs/BUGS.md` B32). A second attempt is already unlikely and a third would
// mean something is republishing continuously, so give up rather than // mean something is republishing continuously, so give up rather than
// spin: the caller's `open` fails instead of its data being lost. // spin: the caller's `open` fails instead of its data being lost.
// //
@@ -1286,7 +1286,7 @@ impl ProtonFs {
for attempt in 0..OPEN_BASE_ATTEMPTS { for attempt in 0..OPEN_BASE_ATTEMPTS {
// The handle starts at the base's size. A provisional one is the // The handle starts at the base's size. A provisional one is the
// ciphertext size, so the file would grow by the encryption // ciphertext size, so the file would grow by the encryption
// overhead and reading the base would fail (bugs.md B100). A // overhead and reading the base would fail (`docs/BUGS.md` B100). A
// queued revision stamps a real size, so this only goes to the // queued revision stamps a real size, so this only goes to the
// network for a file the listing has not sized yet. // network for a file the listing has not sized yet.
if provisional && self.core.pending_blob(&uid).is_none() { if provisional && self.core.pending_blob(&uid).is_none() {
@@ -1541,7 +1541,7 @@ impl ProtonFs {
let transient = is_transient_name(name); let transient = is_transient_name(name);
// Offline the server cannot mint a uid, so invent one and queue the // Offline the server cannot mint a uid, so invent one and queue the
// create. The file is real to the caller either way; only its identity is // create. The file is real to the caller either way; only its identity is
// provisional until the drain (offline.md Phase 3b). // provisional until the drain.
// //
// A parent that is itself still queued forces the same path even when we // A parent that is itself still queued forces the same path even when we
// are online: the API has no folder to put this in yet. // are online: the API has no folder to put this in yet.
@@ -1652,7 +1652,7 @@ impl ProtonFs {
.ok(); .ok();
// As in `create`: offline — or under a parent that is itself still // As in `create`: offline — or under a parent that is itself still
// queued — the folder becomes a placeholder that the drain turns into a // queued — the folder becomes a placeholder that the drain turns into a
// real one (offline.md Phase 3b). // real one.
// Losing the network on the way queues it the same way. // Losing the network on the way queues it the same way.
let minted = if self.core.sends_inline() && !is_local_uid(&parent_uid) { let minted = if self.core.sends_inline() && !is_local_uid(&parent_uid) {
if let Err(error) = self.core.require_uid_writable(&parent_uid) { if let Err(error) = self.core.require_uid_writable(&parent_uid) {
@@ -1874,7 +1874,7 @@ impl ProtonFs {
// rename, so the victim has to be removed first and the operation stops // rename, so the victim has to be removed first and the operation stops
// being atomic. Without this the 422 surfaced as a blanket EIO and every // being atomic. Without this the 422 surfaced as a blanket EIO and every
// write-to-temp-then-rename tool — rsync, atomic editor saves — failed // write-to-temp-then-rename tool — rsync, atomic editor saves — failed
// at the very end of its transfer (bugs.md B13). // at the very end of its transfer (`docs/BUGS.md` B13).
// //
// `RENAME_EXCHANGE` has no Proton primitive and cannot be emulated // `RENAME_EXCHANGE` has no Proton primitive and cannot be emulated
// without a window in which one of the two names does not exist. // without a window in which one of the two names does not exist.
@@ -1956,8 +1956,7 @@ impl ProtonFs {
}; };
// A node whose own creation is still queued has no server-side identity // A node whose own creation is still queued has no server-side identity
// to rename: the queued op *is* the node, so rewriting its target is the // to rename: the queued op *is* the node, so rewriting its target is the
// whole rename. Nothing reaches the API, which is why this works offline // whole rename. Nothing reaches the API, which is why this works offline.
// (offline.md Phase 3b).
// A create that lands meanwhile leaves no op to rewrite; the node is // A create that lands meanwhile leaves no op to rewrite; the node is
// then renamed by the uid Drive gave it (docs/BUGS.md B142). // then renamed by the uid Drive gave it (docs/BUGS.md B142).
if is_local_uid(&uid) { if is_local_uid(&uid) {
@@ -2156,7 +2155,7 @@ impl ProtonFs {
.is_some_and(|e| e.open_count > 0); .is_some_and(|e| e.open_count > 0);
// A node the server has never heard of cannot be trashed there; deleting // A node the server has never heard of cannot be trashed there; deleting
// it just means its queued creation is no longer wanted. This works // it just means its queued creation is no longer wanted. This works
// offline, which the remote path below cannot (offline.md Phase 3b). // offline, which the remote path below cannot.
if is_local_uid(&uid) { if is_local_uid(&uid) {
let dropped = if open_now { let dropped = if open_now {
self.core.withdraw_queued_ops(&uid) self.core.withdraw_queued_ops(&uid)
@@ -2185,7 +2184,7 @@ impl ProtonFs {
} }
// Offline: queue it. Trashing is the one mutation a user expects to work // Offline: queue it. Trashing is the one mutation a user expects to work
// regardless — the file is gone from their point of view the moment the // regardless — the file is gone from their point of view the moment the
// command returns (offline.md Phase 3b). The same when the network goes // command returns. The same when the network goes
// away under the remote call below. // away under the remote call below.
let queue_it = |reply: ReplyEmpty| match self.core.queue_trash(&uid, name) { let queue_it = |reply: ReplyEmpty| match self.core.queue_trash(&uid, name) {
Ok(()) => reply.ok(), Ok(()) => reply.ok(),
@@ -2234,7 +2233,7 @@ impl ProtonFs {
self.core.invalidate_trash(); self.core.invalidate_trash();
// Every other trash site records itself; this one did not, which made a // Every other trash site records itself; this one did not, which made a
// file found in the trash impossible to attribute after the fact — the // file found in the trash impossible to attribute after the fact — the
// activity log was the only record and it showed nothing (bugs.md B2). // activity log was the only record and it showed nothing (`docs/BUGS.md` B2).
self.core self.core
.log_activity(ActivityKind::Trash, name, "trashed from the mount", true); .log_activity(ActivityKind::Trash, name, "trashed from the mount", true);
reply.ok(); reply.ok();
+34 -35
View File
@@ -123,6 +123,7 @@ use mount::{
}; };
use reads::{BlockFlight, BlockRing, PREFETCH_BUDGET, Prefetch, ReaderSlot, STREAM_BYPASS_MIN}; use reads::{BlockFlight, BlockRing, PREFETCH_BUDGET, Prefetch, ReaderSlot, STREAM_BYPASS_MIN};
use state::{Entry, Intervals, PendingRevision, State, StateGuard, WriteHandle}; use state::{Entry, Intervals, PendingRevision, State, StateGuard, WriteHandle};
pub use systemd::idle_sleep;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use transfers::{CountingWriter, JobGuard, OwnedCountingReader, TransferRegistry}; use transfers::{CountingWriter, JobGuard, OwnedCountingReader, TransferRegistry};
use r#virtual::{ use r#virtual::{
@@ -226,7 +227,7 @@ const DRAIN_WORKERS: usize = 16;
const STAGING_ORPHAN_RETAIN: Duration = Duration::from_secs(30 * 24 * 60 * 60); const STAGING_ORPHAN_RETAIN: Duration = Duration::from_secs(30 * 24 * 60 * 60);
/// First and longest delay between probes for the network coming back after an /// First and longest delay between probes for the network coming back after an
/// offline mount (offline.md Phase 1). Doubles from min to max: a laptop shut in /// offline mount. Doubles from min to max: a laptop shut in
/// a bag is the common case, so the steady state must be cheap, while a brief /// a bag is the common case, so the steady state must be cheap, while a brief
/// blip should still recover in seconds. /// blip should still recover in seconds.
const ONLINE_PROBE_MIN: Duration = Duration::from_secs(5); const ONLINE_PROBE_MIN: Duration = Duration::from_secs(5);
@@ -334,7 +335,7 @@ const MAX_THUMBNAIL_MISSES: usize = 8192;
/// ///
/// Matches the SDK's own `MAX_BATCH_COUNT`, so a chunk is exactly one request: /// Matches the SDK's own `MAX_BATCH_COUNT`, so a chunk is exactly one request:
/// chunking smaller would add round trips, larger would be split anyway and /// chunking smaller would add round trips, larger would be split anyway and
/// delay the waiters this chunking exists to release (bugs.md B14). /// delay the waiters this chunking exists to release (`docs/BUGS.md` B14).
const SIZE_UPGRADE_CHUNK: usize = 150; const SIZE_UPGRADE_CHUNK: usize = 150;
/// How many folders may have a size upgrade in flight at once. /// How many folders may have a size upgrade in flight at once.
@@ -344,7 +345,7 @@ const SIZE_UPGRADE_CHUNK: usize = 150;
/// both with the number of folders it walked. Eight keeps an interactive `ls -l` /// both with the number of folders it walked. Eight keeps an interactive `ls -l`
/// (one folder, sometimes a couple) entirely unaffected while putting a ceiling /// (one folder, sometimes a couple) entirely unaffected while putting a ceiling
/// on the recursive case. A folder past the cap is queued, not dropped: one of /// on the recursive case. A folder past the cap is queued, not dropped: one of
/// the running threads takes it over when its own batch ends (bugs.md B100). /// the running threads takes it over when its own batch ends (`docs/BUGS.md` B100).
const MAX_SIZE_UPGRADES: usize = 8; const MAX_SIZE_UPGRADES: usize = 8;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -535,7 +536,7 @@ struct Core {
workers: Arc<Workers>, workers: Arc<Workers>,
/// Unified SQLite metadata cache: the persistence layer behind the in-memory /// Unified SQLite metadata cache: the persistence layer behind the in-memory
/// `State` maps. Every mutation writes through here, and the maps rehydrate /// `State` maps. Every mutation writes through here, and the maps rehydrate
/// from it on mount (plan.md P1). /// from it on mount.
db: Arc<Db>, db: Arc<Db>,
/// Serializes only shared-list publication and access-loss invalidation. /// Serializes only shared-list publication and access-loss invalidation.
/// Network calls happen before taking it. /// Network calls happen before taking it.
@@ -545,8 +546,8 @@ struct Core {
/// Successful refreshes suppress retries even if persisting their timestamp /// Successful refreshes suppress retries even if persisting their timestamp
/// fails. Shared across primary/fork clones and cleared by access events. /// fails. Shared across primary/fork clones and cleared by access events.
shared_refresh_deadlines: Arc<Mutex<SharedRefreshDeadlines>>, shared_refresh_deadlines: Arc<Mutex<SharedRefreshDeadlines>>,
/// False while the API is unreachable and we are serving the cached tree /// False while the API is unreachable and we are serving the cached tree.
/// (offline.md Phase 1). Set by the probe thread; read by front-ends through /// Set by the probe thread; read by front-ends through
/// `Response::Status` so the UI can say so rather than leaving the user to /// `Response::Status` so the UI can say so rather than leaving the user to
/// infer it from a wall of EIO. /// infer it from a wall of EIO.
online: Arc<AtomicBool>, online: Arc<AtomicBool>,
@@ -557,8 +558,8 @@ struct Core {
/// [`pause::PAUSED_INDEFINITELY`] for "until resumed", `0` when not paused. /// [`pause::PAUSED_INDEFINITELY`] for "until resumed", `0` when not paused.
/// Persisted, so a pause survives a restart (see [`Core::sync_paused`]). /// Persisted, so a pause survives a restart (see [`Core::sync_paused`]).
sync_paused_until: Arc<AtomicI64>, sync_paused_until: Arc<AtomicI64>,
/// Writes accepted from the kernel but not yet uploaded, keyed by node /// Writes accepted from the kernel but not yet uploaded, keyed by node.
/// (offline.md Phase 3). The in-memory face of the `pending_op` table, from /// The in-memory face of the `pending_op` table, from
/// which it is rebuilt on mount. /// which it is rebuilt on mount.
/// ///
/// Two things read it: [`Core::read_range`], because until the op drains the /// Two things read it: [`Core::read_range`], because until the op drains the
@@ -586,7 +587,7 @@ struct Core {
/// the queue instead of waiting out their backoff. /// the queue instead of waiting out their backoff.
drain_wake: Arc<(Mutex<bool>, Condvar)>, drain_wake: Arc<(Mutex<bool>, Condvar)>,
/// The daemon's stop signal, shared with every background loop this `Core` /// The daemon's stop signal, shared with every background loop this `Core`
/// was cloned into so teardown can end and join them (bugs.md B44). /// was cloned into so teardown can end and join them (`docs/BUGS.md` B44).
shutdown: Arc<shutdown::Shutdown>, shutdown: Arc<shutdown::Shutdown>,
/// How long this node's last upload actually took, in milliseconds, with /// How long this node's last upload actually took, in milliseconds, with
/// the time it was measured for eviction. Feeds /// the time it was measured for eviction. Feeds
@@ -695,7 +696,7 @@ struct Core {
/// Size upgrades currently running, per folder inode, across every mount. A `getattr` that /// Size upgrades currently running, per folder inode, across every mount. A `getattr` that
/// needs a real size waits on the entry rather than issuing its own fetch — /// needs a real size waits on the entry rather than issuing its own fetch —
/// `ls -l` of a folder is one `getattr` per file, and they must collapse /// `ls -l` of a folder is one `getattr` per file, and they must collapse
/// onto a single batch (bugs.md B14). /// onto a single batch (`docs/BUGS.md` B14).
size_upgrades: Arc<Mutex<HashMap<u64, Arc<SizeUpgrade>>>>, size_upgrades: Arc<Mutex<HashMap<u64, Arc<SizeUpgrade>>>>,
/// Replies parked on those upgrades. Shared across every mount for the same /// Replies parked on those upgrades. Shared across every mount for the same
/// reason the pool is: the bound that matters is per daemon. /// reason the pool is: the bound that matters is per daemon.
@@ -726,11 +727,11 @@ struct Core {
/// can say what a pass is doing rather than just "syncing". An entry exists /// can say what a pass is doing rather than just "syncing". An entry exists
/// only while that folder's reconcile pass is running. /// only while that folder's reconcile pass is running.
sync_progress: Arc<Mutex<HashMap<i64, SyncProgress>>>, sync_progress: Arc<Mutex<HashMap<i64, SyncProgress>>>,
/// Channel to the folder-sync engine (devices.md Phase 2): nudges it to /// Channel to the folder-sync engine: nudges it to
/// reconcile a folder, reconcile everything, or re-scan its watch set. /// reconcile a folder, reconcile everything, or re-scan its watch set.
sync_tx: std::sync::mpsc::Sender<sync::SyncMsg>, sync_tx: std::sync::mpsc::Sender<sync::SyncMsg>,
/// Secondary FUSE sessions for `ondemand` sync folders, keyed by sync-folder /// Secondary FUSE sessions for `ondemand` sync folders, keyed by sync-folder
/// id (devices.md Phase 3). Each is a `ProtonFs` rooted at the folder's remote /// id. Each is a `ProtonFs` rooted at the folder's remote
/// node, mounted over its local path, sharing this Core's client/cache/db and /// node, mounted over its local path, sharing this Core's client/cache/db and
/// tree but with its own root (`fork_state`). Held so we can unmount on /// tree but with its own root (`fork_state`). Held so we can unmount on
/// toggle back to `mirror` and on daemon shutdown. Each entry retains the /// toggle back to `mirror` and on daemon shutdown. Each entry retains the
@@ -1833,8 +1834,7 @@ impl Core {
info!(nodes = st.entries.len(), "hydrated metadata cache from db"); info!(nodes = st.entries.len(), "hydrated metadata cache from db");
} }
/// Rebuild the in-memory pending map from the `pending_op` table on mount /// Rebuild the in-memory pending map from the `pending_op` table on mount.
/// (offline.md Phase 3).
/// ///
/// A queued write survives a restart — that is the point of persisting it — /// A queued write survives a restart — that is the point of persisting it —
/// so until the drain worker gets to it, reads of that file must still come /// so until the drain worker gets to it, reads of that file must still come
@@ -2774,7 +2774,7 @@ impl Core {
missing.push(uid.clone()); missing.push(uid.clone());
} }
// Returned, not awaited here: the caller is a `getattr` that must not // Returned, not awaited here: the caller is a `getattr` that must not
// answer with a provisional size (bugs.md B14), and it parks its reply // answer with a provisional size (`docs/BUGS.md` B14), and it parks its reply
// on this batch rather than holding a thread until it lands. // on this batch rather than holding a thread until it lands.
self.upgrade_sizes(key, missing) self.upgrade_sizes(key, missing)
} }
@@ -2791,7 +2791,7 @@ impl Core {
/// `total_size_on_storage`, the *ciphertext* size, which is slightly larger /// `total_size_on_storage`, the *ciphertext* size, which is slightly larger
/// than the real one. A read must not plan on it: since B84 a size the /// than the real one. A read must not plan on it: since B84 a size the
/// revision disagrees with fails the read, so `read` settles the size first /// revision disagrees with fails the read, so `read` settles the size first
/// ([`Core::resolve_provisional_size`], bugs.md B100). In `stat` it is a /// ([`Core::resolve_provisional_size`], `docs/BUGS.md` B100). In `stat` it is a
/// discrepancy that closes within a round trip, not a repeat of B11 (which /// discrepancy that closes within a round trip, not a repeat of B11 (which
/// reported **0** and so suppressed reads entirely). /// reported **0** and so suppressed reads entirely).
/// ///
@@ -2865,11 +2865,11 @@ impl Core {
/// Chunked so a waiter is released as soon as *its* file is resolved. A /// Chunked so a waiter is released as soon as *its* file is resolved. A
/// single 793-node batch took ~80 s, which outran [`SizeUpgrade::WAIT`] and /// single 793-node batch took ~80 s, which outran [`SizeUpgrade::WAIT`] and
/// put provisional sizes back in front of callers — the bug this was /// put provisional sizes back in front of callers — the bug this was
/// supposed to fix (bugs.md B14). /// supposed to fix (`docs/BUGS.md` B14).
/// ///
/// When the batch ends, the thread takes over the oldest batch the cap held /// When the batch ends, the thread takes over the oldest batch the cap held
/// back, so at most [`MAX_SIZE_UPGRADES`] threads run however many folders /// back, so at most [`MAX_SIZE_UPGRADES`] threads run however many folders
/// a recursive walk touches (bugs.md B100). /// a recursive walk touches (`docs/BUGS.md` B100).
fn run_size_upgrade(&self, key: u64, uids: Vec<NodeUid>, slot: Arc<SizeUpgrade>) { fn run_size_upgrade(&self, key: u64, uids: Vec<NodeUid>, slot: Arc<SizeUpgrade>) {
let (mut key, mut uids, mut slot) = (key, uids, slot); let (mut key, mut uids, mut slot) = (key, uids, slot);
loop { loop {
@@ -2897,7 +2897,7 @@ impl Core {
/// size it is given against the revision's own block table and fails the /// size it is given against the revision's own block table and fails the
/// read on a mismatch (B84), so reading on the provisional size is an `EIO` /// read on a mismatch (B84), so reading on the provisional size is an `EIO`
/// every time. `getattr` usually settles the size first, but not when its /// every time. `getattr` usually settles the size first, but not when its
/// wait times out, so the read cannot rely on it (bugs.md B100). /// wait times out, so the read cannot rely on it (`docs/BUGS.md` B100).
fn resolve_provisional_size(&self, ino: u64, uid: &NodeUid) -> Option<u64> { fn resolve_provisional_size(&self, ino: u64, uid: &NodeUid) -> Option<u64> {
let parent = self.state().entries.get(&ino)?.parent; let parent = self.state().entries.get(&ino)?.parent;
// Without a network the answer is `None` either way; the bound keeps a // Without a network the answer is `None` either way; the bound keeps a
@@ -3317,8 +3317,7 @@ impl Core {
} }
} }
/// Accept a released write handle's bytes and queue their upload /// Accept a released write handle's bytes and queue their upload.
/// (offline.md Phase 3).
/// ///
/// This is what makes a copy into the mount run at disk speed: the caller's /// This is what makes a copy into the mount run at disk speed: the caller's
/// `close` returns once the bytes are staged on local disk and the intent is /// `close` returns once the bytes are staged on local disk and the intent is
@@ -3785,8 +3784,8 @@ impl Core {
Ok(()) Ok(())
} }
/// Invent a node under `parent_uid` and queue the op that will make it real /// Invent a node under `parent_uid` and queue the op that will make it real.
/// (offline.md Phase 3b). Returns the node to intern, exactly as the online /// Returns the node to intern, exactly as the online
/// path returns the one the server minted. /// path returns the one the server minted.
/// ///
/// The parent may itself be a placeholder — `mkdir -p` offline, or `cp -r` of /// The parent may itself be a placeholder — `mkdir -p` offline, or `cp -r` of
@@ -3842,7 +3841,7 @@ impl Core {
} }
/// Queue giving a node a new parent and/or name, and apply it to the tree /// Queue giving a node a new parent and/or name, and apply it to the tree
/// now (offline.md Phase 3b). /// now.
/// ///
/// The op records the desired end state rather than the step, so it both /// The op records the desired end state rather than the step, so it both
/// supersedes any earlier queued rename and lets the drain skip whichever /// supersedes any earlier queued rename and lets the drain skip whichever
@@ -3950,7 +3949,7 @@ impl Core {
} }
/// Queue trashing a node the server knows about, and drop it from the tree /// Queue trashing a node the server knows about, and drop it from the tree
/// now (offline.md Phase 3b). /// now.
/// ///
/// Anything else queued for this node is discarded first: the user has said /// Anything else queued for this node is discarded first: the user has said
/// the file should not exist, so uploading bytes to it or renaming it are /// the file should not exist, so uploading bytes to it or renaming it are
@@ -4081,7 +4080,7 @@ impl Core {
/// ///
/// The path identifies the revision: publication stages a fresh file and /// The path identifies the revision: publication stages a fresh file and
/// swaps the map entry to point at it, so an unchanged path means the base a /// swaps the map entry to point at it, so an unchanged path means the base a
/// caller sampled earlier is still the base now (bugs.md B32). /// caller sampled earlier is still the base now (`docs/BUGS.md` B32).
fn pending_blob(&self, uid: &NodeUid) -> Option<PathBuf> { fn pending_blob(&self, uid: &NodeUid) -> Option<PathBuf> {
self.pending.lock().get(uid).map(|p| p.path.clone()) self.pending.lock().get(uid).map(|p| p.path.clone())
} }
@@ -5124,7 +5123,7 @@ impl Core {
/// `rename(2)` promises to replace an existing destination atomically. Proton /// `rename(2)` promises to replace an existing destination atomically. Proton
/// offers no such primitive — `rename_node` refuses a name that is already /// offers no such primitive — `rename_node` refuses a name that is already
/// taken — so this is the first half of an emulation that is *not* atomic: /// taken — so this is the first half of an emulation that is *not* atomic:
/// see [`Core::restore_replaced`] for the other half (bugs.md B13). /// see [`Core::restore_replaced`] for the other half (`docs/BUGS.md` B13).
/// ///
/// A node whose own creation is still queued has never reached the server, so /// A node whose own creation is still queued has never reached the server, so
/// dropping its queued ops is the whole removal; nothing goes to the wire and /// dropping its queued ops is the whole removal; nothing goes to the wire and
@@ -5950,8 +5949,8 @@ fn parse_node_uid(s: &str) -> Option<NodeUid> {
} }
/// The uid a node made on this machine goes by until it lands, so it can be /// The uid a node made on this machine goes by until it lands, so it can be
/// interned, listed and written to before the server has ever heard of it /// interned, listed and written to before the server has ever heard of it:
/// (offline.md Phase 3b): its row's local id, as [`pdfs_core::db::local_uid`] /// its row's local id, as [`pdfs_core::db::local_uid`]
/// has it. /// has it.
fn local_node_uid(lid: i64) -> NodeUid { fn local_node_uid(lid: i64) -> NodeUid {
NodeUid::new(VolumeId::from(LOCAL_VOLUME), LinkId::from(lid.to_string())) NodeUid::new(VolumeId::from(LOCAL_VOLUME), LinkId::from(lid.to_string()))
@@ -6114,7 +6113,7 @@ fn dir_is_empty(dir: &Path) -> std::io::Result<bool> {
/// Delete everything inside `dir` but keep `dir` itself (it stays as the FUSE /// Delete everything inside `dir` but keep `dir` itself (it stays as the FUSE
/// mountpoint). Used when a `mirror` folder flips to `ondemand`: the local files /// mountpoint). Used when a `mirror` folder flips to `ondemand`: the local files
/// are the disk we're reclaiming (devices.md Phase 3). /// are the disk we're reclaiming.
fn evict_dir_contents(dir: &Path) -> std::io::Result<()> { fn evict_dir_contents(dir: &Path) -> std::io::Result<()> {
for entry in std::fs::read_dir(dir)? { for entry in std::fs::read_dir(dir)? {
let path = entry?.path(); let path = entry?.path();
@@ -6196,7 +6195,7 @@ struct Progress {
/// ///
/// Waiters are released **per chunk**, not once at the end: a waiter only cares /// Waiters are released **per chunk**, not once at the end: a waiter only cares
/// about its own file, and waiting for the other 792 is what let a large folder /// about its own file, and waiting for the other 792 is what let a large folder
/// outrun the timeout (bugs.md B14). /// outrun the timeout (`docs/BUGS.md` B14).
#[derive(Default)] #[derive(Default)]
struct SizeUpgrade { struct SizeUpgrade {
inner: Mutex<Progress>, inner: Mutex<Progress>,
@@ -6222,7 +6221,7 @@ enum SizeUpgradeClaim {
/// Past the cap this used to return nothing, and the caller answered with the /// Past the cap this used to return nothing, and the caller answered with the
/// provisional size at once. Nothing retried the folder until the next `stat`, /// provisional size at once. Nothing retried the folder until the next `stat`,
/// so a recursive walk over more than [`MAX_SIZE_UPGRADES`] fresh folders showed /// so a recursive walk over more than [`MAX_SIZE_UPGRADES`] fresh folders showed
/// ciphertext sizes for most of them (bugs.md B100). Queued, the batch is still /// ciphertext sizes for most of them (`docs/BUGS.md` B100). Queued, the batch is still
/// fetched, and a waiter gets the real size unless [`SizeUpgrade::WAIT`] runs out. /// fetched, and a waiter gets the real size unless [`SizeUpgrade::WAIT`] runs out.
fn claim_size_upgrade( fn claim_size_upgrade(
in_flight: &mut HashMap<u64, Arc<SizeUpgrade>>, in_flight: &mut HashMap<u64, Arc<SizeUpgrade>>,
@@ -6425,7 +6424,7 @@ impl SizeWaitQueue {
/// Split out from the handler because it is the part that is pure and the part /// Split out from the handler because it is the part that is pure and the part
/// that is dangerous: every `Err` here is a refusal that happens *before* /// that is dangerous: every `Err` here is a refusal that happens *before*
/// anything is trashed, and getting one wrong turns a refusal into the /// anything is trashed, and getting one wrong turns a refusal into the
/// destruction of the destination (bugs.md B13). /// destruction of the destination (`docs/BUGS.md` B13).
/// ///
/// `dst_empty` is only meaningful when `dst_dir`; pass `true` otherwise. /// `dst_empty` is only meaningful when `dst_dir`; pass `true` otherwise.
fn check_replaceable(src_dir: bool, dst_dir: bool, dst_empty: bool) -> Result<(), Errno> { fn check_replaceable(src_dir: bool, dst_dir: bool, dst_empty: bool) -> Result<(), Errno> {
@@ -7155,7 +7154,7 @@ mod size_upgrade_tests {
} }
/// Past the cap a folder used to get no upgrade at all, so a recursive /// Past the cap a folder used to get no upgrade at all, so a recursive
/// walk reported ciphertext sizes for most of what it listed (bugs.md B100). /// walk reported ciphertext sizes for most of what it listed (`docs/BUGS.md` B100).
#[test] #[test]
fn a_folder_past_the_cap_is_queued_not_dropped() { fn a_folder_past_the_cap_is_queued_not_dropped() {
let mut in_flight = HashMap::new(); let mut in_flight = HashMap::new();
@@ -7208,7 +7207,7 @@ mod replace_tests {
/// The case that motivated all of this: rsync renaming its temp file over /// The case that motivated all of this: rsync renaming its temp file over
/// the real one. Two plain files, and it has to be allowed — refusing is /// the real one. Two plain files, and it has to be allowed — refusing is
/// what made every rsync transfer fail at the last step (bugs.md B13). /// what made every rsync transfer fail at the last step (`docs/BUGS.md` B13).
#[test] #[test]
fn a_file_may_replace_a_file() { fn a_file_may_replace_a_file() {
assert!(check_replaceable(false, false, true).is_ok()); assert!(check_replaceable(false, false, true).is_ok());
+1 -1
View File
@@ -248,7 +248,7 @@ impl Core {
} }
/// Watch the link for the life of the mount: sleep while it is up, and /// Watch the link for the life of the mount: sleep while it is up, and
/// probe it back while it is down (offline.md Phase 1). /// probe it back while it is down.
/// ///
/// A mount that started from the cache is down from the first moment; one /// A mount that started from the cache is down from the first moment; one
/// that started online goes down when [`Core::mark_offline`] says so. /// that started online goes down when [`Core::mark_offline`] says so.
+5 -5
View File
@@ -193,7 +193,7 @@ pub(super) fn clear_stale_mount(mountpoint: &Path) {
} }
/// `sync_state` key holding the uid of the My Files root, so a later run can /// `sync_state` key holding the uid of the My Files root, so a later run can
/// recover it from `nodes` without the API (offline.md Phase 1). /// recover it from `nodes` without the API.
const ROOT_UID_KEY: &str = "root_uid"; const ROOT_UID_KEY: &str = "root_uid";
/// The My Files root, and whether we got it from the API (`true`) or from the /// The My Files root, and whether we got it from the API (`true`) or from the
@@ -401,7 +401,7 @@ pub(crate) fn mount_with(
.all_share_access() .all_share_access()
.map_err(|error| std::io::Error::other(format!("load shared access: {error}")))?; .map_err(|error| std::io::Error::other(format!("load shared access: {error}")))?;
// The folder-sync engine (devices.md Phase 2) runs on its own thread and is // The folder-sync engine runs on its own thread and is
// nudged over this channel; the sender lives in Core so control-socket // nudged over this channel; the sender lives in Core so control-socket
// handlers can trigger reconciles. // handlers can trigger reconciles.
let (sync_tx, sync_rx) = std::sync::mpsc::channel::<sync::SyncMsg>(); let (sync_tx, sync_rx) = std::sync::mpsc::channel::<sync::SyncMsg>();
@@ -501,7 +501,7 @@ pub(crate) fn mount_with(
// Every long-lived thread started from here on is kept in `workers` and // Every long-lived thread started from here on is kept in `workers` and
// joined at the end of this function. Leaving them running turned an // joined at the end of this function. Leaving them running turned an
// in-process remount into a second generation of drain workers, sync engines // in-process remount into a second generation of drain workers, sync engines
// and sweeps all operating on a mount that no longer exists (bugs.md B44). // and sweeps all operating on a mount that no longer exists (`docs/BUGS.md` B44).
let mut workers: Vec<std::thread::JoinHandle<()>> = Vec::new(); let mut workers: Vec<std::thread::JoinHandle<()>> = Vec::new();
for worker in 0..DRAIN_WORKERS { for worker in 0..DRAIN_WORKERS {
let core = core.clone(); let core = core.clone();
@@ -738,7 +738,7 @@ pub(crate) fn mount_with(
}; };
// Unmount every on-demand sync folder too, or the kernel mounts linger as // Unmount every on-demand sync folder too, or the kernel mounts linger as
// stale and the next start fails with EBUSY (devices.md Phase 3). // stale and the next start fails with EBUSY.
if let Some(worker) = restore_worker if let Some(worker) = restore_worker
&& worker.join().is_err() && worker.join().is_err()
{ {
@@ -798,7 +798,7 @@ const WORKER_JOIN_DEADLINE: Duration = Duration::from_secs(10);
/// The mount used to return with its drain workers, sync engine, sweep, online /// The mount used to return with its drain workers, sync engine, sweep, online
/// probe, indexer and control listener all still running on `Core` clones. The /// probe, indexer and control listener all still running on `Core` clones. The
/// process usually exited straight afterwards and hid it; an in-process remount /// process usually exited straight afterwards and hid it; an in-process remount
/// did not, and started a second full set on top of the first (bugs.md B44). /// did not, and started a second full set on top of the first (`docs/BUGS.md` B44).
/// ///
/// Two of the waits need waking rather than merely flagging: the drain workers /// Two of the waits need waking rather than merely flagging: the drain workers
/// block on their own condvar (`wake_drain`), and the control listener blocks /// block on their own condvar (`wake_drain`), and the control listener blocks
+2 -2
View File
@@ -1,5 +1,5 @@
//! Reading and writing this machine's `profile.json` in its device root, and //! Reading and writing this machine's `profile.json` in its device root, and
//! the restore that rebuilds a machine from one (features.md 5.2 and 5.3). //! the restore that rebuilds a machine from one.
//! //!
//! The document itself is defined in [`pdfs_core::profile`]; this is the half //! The document itself is defined in [`pdfs_core::profile`]; this is the half
//! that talks to Drive. Saving is best-effort and never blocks a user action: //! that talks to Drive. Saving is best-effort and never blocks a user action:
@@ -246,7 +246,7 @@ impl Core {
Profile::parse(&buf).map(Some).map_err(CoreError::invalid) Profile::parse(&buf).map(Some).map_err(CoreError::invalid)
} }
// ---- restore (features.md 5.2) ---------------------------------------- // ---- restore ----------------------------------------
/// The folders under a device that could be synced here, each carrying a /// The folders under a device that could be synced here, each carrying a
/// proposed local path. `device` names another registered device by uid; /// proposed local path. `device` names another registered device by uid;
+9 -9
View File
@@ -464,7 +464,7 @@ impl Core {
/// it, and the [`BLOCK_SIZE`] assumption when it has not — which is every /// it, and the [`BLOCK_SIZE`] assumption when it has not — which is every
/// first read, because learning it means opening a `RevisionReader`, and /// first read, because learning it means opening a `RevisionReader`, and
/// opening one per read is precisely the per-file key derivation the block /// opening one per read is precisely the per-file key derivation the block
/// cache exists to avoid (bugs.md B12, B85). /// cache exists to avoid (`docs/BUGS.md` B12, B85).
/// ///
/// Being wrong here is not a correctness problem: `read_at` plans over the /// Being wrong here is not a correctness problem: `read_at` plans over the
/// real sizes and clamps to what was asked for either way. It costs a /// real sizes and clamps to what was asked for either way. It costs a
@@ -493,13 +493,13 @@ impl Core {
) -> Result<Vec<u8>, Errno> { ) -> Result<Vec<u8>, Errno> {
// A queued write has not reached the remote yet, so the remote's current // A queued write has not reached the remote yet, so the remote's current
// revision is stale and the staged blob is the truth. Serve from it until // revision is stale and the staged blob is the truth. Serve from it until
// the drain worker lands the upload (offline.md Phase 3). // the drain worker lands the upload.
if let Some(pending) = self.pending.lock().get(uid).cloned() { if let Some(pending) = self.pending.lock().get(uid).cloned() {
return self.read_pending(&pending, offset, len); return self.read_pending(&pending, offset, len);
} }
// A node created offline and never written has no blob and no remote: it // A node created offline and never written has no blob and no remote: it
// is an empty file, and asking the API about a `local~` uid would only // is an empty file, and asking the API about a `local~` uid would only
// earn a 404 (offline.md Phase 3b). Unless its create landed since the // earn a 404. Unless its create landed since the
// read looked: the landing moved the queued write to the uid Drive // read looked: the landing moved the queued write to the uid Drive
// gave it, and the row says which (`docs/BUGS.md` B179). // gave it, and the row says which (`docs/BUGS.md` B179).
if is_local_uid(uid) { if is_local_uid(uid) {
@@ -636,7 +636,7 @@ impl Core {
/// ///
/// A block that comes back shorter than the file's size says it must be is /// A block that comes back shorter than the file's size says it must be is
/// never served, cached or promoted: it is repaired from the whole-file /// never served, cached or promoted: it is repaired from the whole-file
/// download, or the read fails. See [`Core::repair_block`] (bugs.md B84). /// download, or the read fails. See [`Core::repair_block`] (`docs/BUGS.md` B84).
async fn read_block( async fn read_block(
&self, &self,
reader: &Arc<dyn RevisionRead>, reader: &Arc<dyn RevisionRead>,
@@ -651,7 +651,7 @@ impl Core {
// file's boundaries are wrong, and the failure is not confined to short // file's boundaries are wrong, and the failure is not confined to short
// reads: sizes that *overstate* a block shift every block after it and // reads: sizes that *overstate* a block shift every block after it and
// return full-length reads of the wrong bytes. So a disagreement of any // return full-length reads of the wrong bytes. So a disagreement of any
// kind takes the file off the range path entirely (bugs.md B84). // kind takes the file off the range path entirely (`docs/BUGS.md` B84).
let bytes = if reader.size() != fsize { let bytes = if reader.size() != fsize {
warn!( warn!(
%uid, bidx = span.idx, fsize, %uid, bidx = span.idx, fsize,
@@ -664,7 +664,7 @@ impl Core {
// Learned here rather than on a path of its own: this is where a // Learned here rather than on a path of its own: this is where a
// reader is open anyway, and recording the real geometry is what // reader is open anyway, and recording the real geometry is what
// lets the *next* read plan its blocks on the revision's own // lets the *next* read plan its blocks on the revision's own
// boundaries instead of the 4 MiB fallback (bugs.md B85). // boundaries instead of the 4 MiB fallback (`docs/BUGS.md` B85).
if cache_blocks { if cache_blocks {
self.cache self.cache
.store_block_geometry(uid, mtime, fsize, reader.block_sizes()); .store_block_geometry(uid, mtime, fsize, reader.block_sizes());
@@ -706,7 +706,7 @@ impl Core {
/// The range path derives every block boundary from the revision's recorded /// The range path derives every block boundary from the revision's recorded
/// block sizes. When those understate the file, reads come back short with /// block sizes. When those understate the file, reads come back short with
/// no error anywhere, and the caller hands userspace a truncated file that /// no error anywhere, and the caller hands userspace a truncated file that
/// looks like a clean EOF (bugs.md B84). `download_file_to` does not do that /// looks like a clean EOF (`docs/BUGS.md` B84). `download_file_to` does not do that
/// arithmetic — it streams the block list — so it returns the whole file /// arithmetic — it streams the block list — so it returns the whole file
/// where the range path cannot, which is why pinning was the workaround. /// where the range path cannot, which is why pinning was the workaround.
/// This does the same thing on the user's behalf, once, and the resulting /// This does the same thing on the user's behalf, once, and the resulting
@@ -918,7 +918,7 @@ impl Core {
let end = offset.saturating_add(len).min(fsize); let end = offset.saturating_add(len).min(fsize);
let mut out = Vec::with_capacity((end - offset) as usize); let mut out = Vec::with_capacity((end - offset) as usize);
// On the revision's own boundaries where they are known, and on the // On the revision's own boundaries where they are known, and on the
// 4 MiB fallback where they are not (bugs.md B85). // 4 MiB fallback where they are not (`docs/BUGS.md` B85).
let wanted = self.block_geometry(uid, mtime, fsize).spans(offset, end); let wanted = self.block_geometry(uid, mtime, fsize).spans(offset, end);
let (Some(&head), Some(&tail)) = (wanted.first(), wanted.last()) else { let (Some(&head), Some(&tail)) = (wanted.first(), wanted.last()) else {
return Ok(out); return Ok(out);
@@ -1038,7 +1038,7 @@ impl Core {
// — the cache validates it on read, the network path in `read_block` // — the cache validates it on read, the network path in `read_block`
// — so this refuses rather than quietly contributing fewer bytes // — so this refuses rather than quietly contributing fewer bytes
// than the range covers, which is what turned a bad block into a // than the range covers, which is what turned a bad block into a
// truncated file (bugs.md B84). // truncated file (`docs/BUGS.md` B84).
if block.len() as u64 != span.len { if block.len() as u64 != span.len {
error!( error!(
%uid, bidx = span.idx, len = block.len(), expected = span.len, fsize, %uid, bidx = span.idx, len = block.len(), expected = span.len, fsize,
+1 -1
View File
@@ -6,7 +6,7 @@
//! invisible; in an in-process remount — which is how the daemon recovers from a //! invisible; in an in-process remount — which is how the daemon recovers from a
//! mount going away underneath it — it leaks a whole generation of threads that //! mount going away underneath it — it leaks a whole generation of threads that
//! keep writing to the database and calling the API on behalf of a mount that no //! keep writing to the database and calling the API on behalf of a mount that no
//! longer exists (bugs.md B44). //! longer exists (`docs/BUGS.md` B44).
//! //!
//! The primitive is deliberately small: a flag plus a condvar. Every loop that //! The primitive is deliberately small: a flag plus a condvar. Every loop that
//! would otherwise call `thread::sleep` calls [`Shutdown::sleep`] instead, which //! would otherwise call `thread::sleep` calls [`Shutdown::sleep`] instead, which
+1 -1
View File
@@ -1,4 +1,4 @@
//! An in-memory Drive for the simulation tests (`docs/MILESTONE-3.0.0.md` §8.1). //! An in-memory Drive for the simulation tests (`docs/TESTING.md`).
//! //!
//! [`FakeDrive`] is the server: one tree of nodes with uids, revisions and //! [`FakeDrive`] is the server: one tree of nodes with uids, revisions and
//! names, and an event log. Each simulated daemon reaches it through its own //! names, and an event log. Each simulated daemon reaches it through its own
+1 -1
View File
@@ -1,4 +1,4 @@
//! The simulation tests of `docs/MILESTONE-3.0.0.md` §8: a fake Drive with //! The simulation tests of `docs/TESTING.md`: a fake Drive with
//! the faults the real one has, and the tools to drive daemons against it. //! the faults the real one has, and the tools to drive daemons against it.
pub(crate) mod daemon; pub(crate) mod daemon;
+2 -2
View File
@@ -1,5 +1,5 @@
//! The reference file system of the simulation (`docs/MILESTONE-3.0.0.md` //! The reference file system of the simulation (`docs/TESTING.md`,
//! §8.2, property 4): what each syscall a simulated client makes should //! "Simulation runs", property 1): what each syscall a simulated client makes should
//! answer, and the tree it should leave, on a POSIX file system. //! answer, and the tree it should leave, on a POSIX file system.
//! //!
//! A [`Model`] covers one client's own folder; paths are relative to it. The //! A [`Model`] covers one client's own folder; paths are relative to it. The
+1 -1
View File
@@ -1,4 +1,4 @@
//! The simulation runs of `docs/MILESTONE-3.0.0.md` §8.2. //! The simulation runs described in `docs/TESTING.md`.
//! //!
//! A run starts one or more daemons on a shared [`FakeDrive`], each owning a //! A run starts one or more daemons on a shared [`FakeDrive`], each owning a
//! folder `c<n>` that only it writes, so there is one writer per file. A seed //! folder `c<n>` that only it writes, so there is one writer per file. A seed
+4 -4
View File
@@ -183,7 +183,7 @@ impl WriteHandle {
} }
} }
/// A released write whose upload has not happened yet (offline.md Phase 3). /// A released write whose upload has not happened yet.
/// ///
/// The bytes live in the content cache's staging dir and the intent lives in the /// The bytes live in the content cache's staging dir and the intent lives in the
/// `pending_op` table; this pairs them in memory so a read can be served without /// `pending_op` table; this pairs them in memory so a read can be served without
@@ -253,7 +253,7 @@ pub(crate) struct State {
/// two pages shifted every later entry, and the caller silently skipped or /// two pages shifted every later entry, and the caller silently skipped or
/// repeated whatever crossed the page boundary. Each entry is /// repeated whatever crossed the page boundary. Each entry is
/// `(ino, is_dir, name)` — the kernel type is mapped at reply time so this /// `(ino, is_dir, name)` — the kernel type is mapped at reply time so this
/// module stays free of `fuser` (bugs.md B43). /// module stays free of `fuser` (`docs/BUGS.md` B43).
pub(crate) dir_snapshots: HashMap<u64, Arc<DirListing>>, pub(crate) dir_snapshots: HashMap<u64, Arc<DirListing>>,
/// Resident inode attributes whose effective access changed during an /// Resident inode attributes whose effective access changed during an
/// intern/root refresh. Core drains this set and notifies the matching /// intern/root refresh. Core drains this set and notifies the matching
@@ -1198,7 +1198,7 @@ impl State {
/// re-enumerate, which is the cheaper way to stay honest about what the /// re-enumerate, which is the cheaper way to stay honest about what the
/// server did. A queued rename cannot: re-enumerating needs the network, and /// server did. A queued rename cannot: re-enumerating needs the network, and
/// the server has not been told yet in any case — so this *is* the tree's /// the server has not been told yet in any case — so this *is* the tree's
/// new truth until the op drains (offline.md Phase 3b). /// new truth until the op drains.
pub(crate) fn rename_in_place( pub(crate) fn rename_in_place(
&mut self, &mut self,
ino: u64, ino: u64,
@@ -1317,7 +1317,7 @@ impl State {
/// created offline, nothing at all), so this row is the only record that the /// created offline, nothing at all), so this row is the only record that the
/// file is as long as the caller was told it is. Without it a restart serves /// file is as long as the caller was told it is. Without it a restart serves
/// the stale size and the file reads as truncated — or empty — while its /// the stale size and the file reads as truncated — or empty — while its
/// bytes sit safely in staging (offline.md Phase 3). /// bytes sit safely in staging.
/// Returns the node to persist rather than queueing it in the outbox, and is /// Returns the node to persist rather than queueing it in the outbox, and is
/// the one mutation here whose write-through the caller must do itself: /// the one mutation here whose write-through the caller must do itself:
/// failure has to be reported, not logged. Dropping the row silently means /// failure has to be reported, not logged. Dropping the row silently means
+1 -1
View File
@@ -68,7 +68,7 @@ impl Core {
/// docs for the identical/divergent policy. /// docs for the identical/divergent policy.
pub(crate) fn run_conflict_sweep_loop(&self) { pub(crate) fn run_conflict_sweep_loop(&self) {
// Both waits are interruptible, so teardown does not have to sit out a // Both waits are interruptible, so teardown does not have to sit out a
// five-minute interval to join this thread (bugs.md B44). // five-minute interval to join this thread (`docs/BUGS.md` B44).
if !self.shutdown.sleep(CONFLICT_SWEEP_WARMUP) { if !self.shutdown.sleep(CONFLICT_SWEEP_WARMUP) {
return; return;
} }
+10 -10
View File
@@ -1,4 +1,4 @@
//! Two-way folder sync engine (devices.md Phase 2). //! Two-way folder sync engine.
//! //!
//! Each `mirror`-mode synced folder is kept mirrored between a local directory //! Each `mirror`-mode synced folder is kept mirrored between a local directory
//! and a folder under this machine's Proton Drive device. A local filesystem //! and a folder under this machine's Proton Drive device. A local filesystem
@@ -1342,7 +1342,7 @@ impl Core {
/// rename below is the destructive step, and the download in between can run /// rename below is the destructive step, and the download in between can run
/// for minutes, so the destination is re-examined right before it: a local /// for minutes, so the destination is re-examined right before it: a local
/// file that no longer matches the plan is set aside as a conflict copy /// file that no longer matches the plan is set aside as a conflict copy
/// instead of being replaced (bugs.md B39). /// instead of being replaced (`docs/BUGS.md` B39).
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn download_file( async fn download_file(
&self, &self,
@@ -1441,7 +1441,7 @@ impl Core {
/// the pre-upload identity is what makes the next pass see the path as /// the pre-upload identity is what makes the next pass see the path as
/// locally changed and upload a whole, clean revision over it. Recording the /// locally changed and upload a whole, clean revision over it. Recording the
/// post-upload stat instead declared the torn revision synchronized and left /// post-upload stat instead declared the torn revision synchronized and left
/// it as the file's content on Drive (bugs.md B40). /// it as the file's content on Drive (`docs/BUGS.md` B40).
async fn record_file_baseline( async fn record_file_baseline(
&self, &self,
folder_id: i64, folder_id: i64,
@@ -1505,7 +1505,7 @@ fn open_to_upload(path: &Path, rel: &str) -> Result<Option<std::fs::File>, Strin
/// records the identity the upload *read* rather than the one on disk now, so /// records the identity the upload *read* rather than the one on disk now, so
/// the next pass classifies the path as locally changed and replaces the torn /// the next pass classifies the path as locally changed and replaces the torn
/// revision. Without the log there is nothing in the journal to explain the /// revision. Without the log there is nothing in the journal to explain the
/// extra revision (bugs.md B40). /// extra revision (`docs/BUGS.md` B40).
fn warn_if_torn(rel: &str, path: &Path, streamed: LocalSig) { fn warn_if_torn(rel: &str, path: &Path, streamed: LocalSig) {
let now = local_sig(path); let now = local_sig(path);
if !now.is_some_and(|now| now.same_content(&streamed)) { if !now.is_some_and(|now| now.same_content(&streamed)) {
@@ -1537,7 +1537,7 @@ fn local_sig(path: &Path) -> Option<LocalSig> {
/// transfer started, and a download of a large file can run for minutes. Without /// transfer started, and a download of a large file can run for minutes. Without
/// this, a local edit completed inside that window was replaced by the remote /// this, a local edit completed inside that window was replaced by the remote
/// version with no copy of it kept anywhere — the one shape of loss a sync /// version with no copy of it kept anywhere — the one shape of loss a sync
/// engine must not have (bugs.md B39). Keeping the copy makes it re-upload as a /// engine must not have (`docs/BUGS.md` B39). Keeping the copy makes it re-upload as a
/// new file on the next pass, which is the same resolution a both-sides-changed /// new file on the next pass, which is the same resolution a both-sides-changed
/// conflict already gets. /// conflict already gets.
/// ///
@@ -1573,7 +1573,7 @@ fn keep_racing_local_edit(path: &Path, expected: Option<LocalSig>) -> std::io::R
/// exists, so the same removal was re-attempted and re-reported as an error on /// exists, so the same removal was re-attempted and re-reported as an error on
/// every pass, forever. Every other error is still an error, and keeping the /// every pass, forever. Every other error is still an error, and keeping the
/// baseline is exactly right there: the surviving file must not be mistaken for /// baseline is exactly right there: the surviving file must not be mistaken for
/// a new local creation on the next pass and uploaded back (bugs.md B42). /// a new local creation on the next pass and uploaded back (`docs/BUGS.md` B42).
fn removed_locally(result: std::io::Result<()>) -> std::io::Result<()> { fn removed_locally(result: std::io::Result<()>) -> std::io::Result<()> {
match result { match result {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
@@ -1830,7 +1830,7 @@ fn system_mtime(meta: &std::fs::Metadata) -> i64 {
/// ///
/// Whole seconds cannot distinguish an edit made in the same second as the last /// Whole seconds cannot distinguish an edit made in the same second as the last
/// sync from no edit at all, and if the size did not change either, that file /// sync from no edit at all, and if the size did not change either, that file
/// was never uploaded (bugs.md B25). `None` for a clock or filesystem that /// was never uploaded (`docs/BUGS.md` B25). `None` for a clock or filesystem that
/// cannot answer, which falls the comparison back to seconds rather than /// cannot answer, which falls the comparison back to seconds rather than
/// asserting an mtime of zero. /// asserting an mtime of zero.
fn system_mtime_ns(meta: &std::fs::Metadata) -> Option<i64> { fn system_mtime_ns(meta: &std::fs::Metadata) -> Option<i64> {
@@ -1845,7 +1845,7 @@ mod tests {
/// A removal of something already gone has produced the state the sync /// A removal of something already gone has produced the state the sync
/// engine wanted, so its baseline is dropped rather than retried forever. /// engine wanted, so its baseline is dropped rather than retried forever.
/// Anything else stays an error and keeps the baseline (bugs.md B42). /// Anything else stays an error and keeps the baseline (`docs/BUGS.md` B42).
#[test] #[test]
fn an_already_absent_path_counts_as_removed() { fn an_already_absent_path_counts_as_removed() {
let root = std::env::temp_dir().join(format!("pdfs-b42-{}", std::process::id())); let root = std::env::temp_dir().join(format!("pdfs-b42-{}", std::process::id()));
@@ -1862,7 +1862,7 @@ mod tests {
} }
/// The download guard: a destination that still matches the plan is /// The download guard: a destination that still matches the plan is
/// replaced silently, one that has moved on since is kept (bugs.md B39). /// replaced silently, one that has moved on since is kept (`docs/BUGS.md` B39).
#[test] #[test]
fn a_download_keeps_a_local_edit_it_did_not_plan_for() { fn a_download_keeps_a_local_edit_it_did_not_plan_for() {
let root = std::env::temp_dir().join(format!("pdfs-b39-{}", std::process::id())); let root = std::env::temp_dir().join(format!("pdfs-b39-{}", std::process::id()));
@@ -2177,7 +2177,7 @@ mod tests {
std::fs::remove_dir_all(&root).unwrap(); std::fs::remove_dir_all(&root).unwrap();
} }
/// A restored folder (features.md 5.2) starts with an empty baseline against /// A restored folder starts with an empty baseline against
/// an empty local directory and a full remote. That must reconcile as /// an empty local directory and a full remote. That must reconcile as
/// "download everything" rather than tripping the wipe guard — otherwise /// "download everything" rather than tripping the wipe guard — otherwise
/// every restore would wedge its folder on the first pass. /// every restore would wedge its folder on the first pass.
+1 -1
View File
@@ -33,7 +33,7 @@ pub(crate) enum SyncMsg {
/// Re-read the folder set and adjust the filesystem watches (after add/remove). /// Re-read the folder set and adjust the filesystem watches (after add/remove).
Rewatch, Rewatch,
/// The daemon is tearing down: finish the current pass and end the engine /// The daemon is tearing down: finish the current pass and end the engine
/// thread, so the mount can join it (bugs.md B44). /// thread, so the mount can join it (`docs/BUGS.md` B44).
Stop, Stop,
} }
+2 -2
View File
@@ -20,7 +20,7 @@ impl LocalSig {
/// Nanoseconds decide it when both sides have them; otherwise the comparison /// Nanoseconds decide it when both sides have them; otherwise the comparison
/// falls back to whole seconds, which is what a baseline row written before /// falls back to whole seconds, which is what a baseline row written before
/// schema 28 — or a filesystem that reports no sub-second time — can support /// schema 28 — or a filesystem that reports no sub-second time — can support
/// (bugs.md B25). /// (`docs/BUGS.md` B25).
pub(super) fn same_content(&self, other: &Self) -> bool { pub(super) fn same_content(&self, other: &Self) -> bool {
if self.size != other.size { if self.size != other.size {
return false; return false;
@@ -161,7 +161,7 @@ pub(super) enum Pending {
/// was there. A download can run for minutes, so the apply step revalidates /// was there. A download can run for minutes, so the apply step revalidates
/// against it immediately before it renames over the destination: a writer /// against it immediately before it renames over the destination: a writer
/// that finished an edit in the meantime must not be overwritten without a /// that finished an edit in the meantime must not be overwritten without a
/// conflict copy (bugs.md B39). /// conflict copy (`docs/BUGS.md` B39).
Download { Download {
rel: String, rel: String,
uid: NodeUid, uid: NodeUid,
+2 -2
View File
@@ -218,7 +218,7 @@ pub(super) fn plan_file(
} }
let local_changed = local.is_some_and(|item| { let local_changed = local.is_some_and(|item| {
// Nanoseconds where both sides have them (bugs.md B25): whole seconds // Nanoseconds where both sides have them (`docs/BUGS.md` B25): whole seconds
// could not see an edit made in the same second as the last sync that // could not see an edit made in the same second as the last sync that
// left the file the same length, so it was never uploaded. // left the file the same length, so it was never uploaded.
baseline.is_none_or(|base| !LocalSig::from(item).same_content(&LocalSig::from(base))) baseline.is_none_or(|base| !LocalSig::from(item).same_content(&LocalSig::from(base)))
@@ -291,7 +291,7 @@ mod file_plan_tests {
/// Whole seconds cannot see an edit made inside the same second that leaves the /// Whole seconds cannot see an edit made inside the same second that leaves the
/// file the same length, so a mirror silently stopped uploading it. With a /// file the same length, so a mirror silently stopped uploading it. With a
/// nanosecond time on both sides it is an ordinary local change (bugs.md B25). /// nanosecond time on both sides it is an ordinary local change (`docs/BUGS.md` B25).
#[test] #[test]
fn a_same_second_same_size_edit_is_still_a_local_change() { fn a_same_second_same_size_edit_is_still_a_local_change() {
let mut base = baseline(10, 20, 30, 40); let mut base = baseline(10, 20, 30, 40);
+20
View File
@@ -74,6 +74,26 @@ pub(crate) fn watchdog_ping() {
let _ = notify("WATCHDOG=1\n"); let _ = notify("WATCHDOG=1\n");
} }
/// Sleep for `duration`, pinging the watchdog on the way.
///
/// For the stretches where the daemon is alive but has no mount, and so no
/// supervisor: waiting for a login, or backing off after a failed mount. The
/// manager arms the watchdog when the process starts, so without these pings a
/// daemon that is merely retrying is aborted after `WatchdogSec` and leaves a
/// coredump instead of the error it was logging. Never use this while mounted:
/// there the ping must prove the control socket answers.
pub fn idle_sleep(duration: Duration) {
const CHUNK: Duration = Duration::from_secs(10);
let mut left = duration;
watchdog_ping();
while !left.is_zero() {
let step = left.min(CHUNK);
std::thread::sleep(step);
left -= step;
watchdog_ping();
}
}
/// Tell the manager the daemon is stopping, so a slow teardown is not mistaken /// Tell the manager the daemon is stopping, so a slow teardown is not mistaken
/// for a hang by the watchdog it is racing. /// for a hang by the watchdog it is racing.
pub(crate) fn stopping() { pub(crate) fn stopping() {
+25 -8
View File
@@ -119,6 +119,9 @@ struct Ui {
/// keyring (a DBus round-trip). Populated at startup and updated only on /// keyring (a DBus round-trip). Populated at startup and updated only on
/// login / logout. `None` = signed out. /// login / logout. `None` = signed out.
session: RefCell<Option<auth::StoredSession>>, session: RefCell<Option<auth::StoredSession>>,
/// Whether this run already asked for the mailbox password of a locked
/// account, so a refresh never re-opens a dialog the user cancelled.
unlock_offered: Cell<bool>,
/// Whether the last refresh saw a live mount daemon. Gates the unpin buttons /// Whether the last refresh saw a live mount daemon. Gates the unpin buttons
/// (which need the daemon to evict + re-hydrate) and every mutating action. /// (which need the daemon to evict + re-hydrate) and every mutating action.
mounted: RefCell<bool>, mounted: RefCell<bool>,
@@ -199,12 +202,20 @@ impl Ui {
} }
fn main() -> glib::ExitCode { fn main() -> glib::ExitCode {
tracing_subscriber::fmt() let verify = std::env::args().nth(1).as_deref() == Some(VERIFY_ARG);
.with_env_filter( let logs = tracing_subscriber::fmt().with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
) );
.init(); // The verification window's stdout carries its token back to the app.
if verify {
logs.with_writer(std::io::stderr).init();
} else {
logs.init();
}
i18n::init(); i18n::init();
if verify {
return run_verification_window();
}
// The tray launches the app with arguments ("--page locations", // The tray launches the app with arguments ("--page locations",
// "--confirm-stop"), which a running instance must receive too. // "--confirm-stop"), which a running instance must receive too.
@@ -300,6 +311,11 @@ fn build_window(app: &adw::Application) {
return; return;
} }
}; };
// On a fresh install the daemon has not run yet to make these, and the
// Takeout page keeps its staged archives in the state dir (B194).
if let Err(e) = dirs.ensure() {
tracing::warn!("cannot create the app's directories: {e}");
}
let stack = adw::ViewStack::new(); let stack = adw::ViewStack::new();
let (login_page, login_widgets) = build_login_page(); let (login_page, login_widgets) = build_login_page();
@@ -362,6 +378,7 @@ fn build_window(app: &adw::Application) {
busy: Cell::new(0), busy: Cell::new(0),
opening: RefCell::new(HashSet::new()), opening: RefCell::new(HashSet::new()),
session: RefCell::new(auth::load().ok()), session: RefCell::new(auth::load().ok()),
unlock_offered: Cell::new(false),
mounted: RefCell::new(false), mounted: RefCell::new(false),
sidebar: sidebar_list.clone(), sidebar: sidebar_list.clone(),
nav: split.clone(), nav: split.clone(),
@@ -1206,7 +1223,7 @@ fn install_window_actions(ui: &Rc<Ui>, window: &adw::ApplicationWindow) {
let dialog = adw::AboutDialog::builder() let dialog = adw::AboutDialog::builder()
.application_name(gettext("Proton Drive for Linux")) .application_name(gettext("Proton Drive for Linux"))
.application_icon("io.narl.proton-drive-linux") .application_icon("io.narl.proton-drive-linux")
.version(pdfs_core::config::APP_VERSION) .version(env!("CARGO_PKG_VERSION"))
.developer_name("Nils Pukropp") .developer_name("Nils Pukropp")
.website("https://proton-drive.narl.io") .website("https://proton-drive.narl.io")
.issue_url("https://github.com/narrrl/proton-drive-linux/issues") .issue_url("https://github.com/narrrl/proton-drive-linux/issues")
@@ -1346,8 +1363,8 @@ fn debug_info(ui: &Rc<Ui>) -> String {
format!( format!(
"App version: {}\nUser agent: {}\nlibadwaita: {}.{}.{}\nGTK: {}.{}.{}\n\ "App version: {}\nUser agent: {}\nlibadwaita: {}.{}.{}\nGTK: {}.{}.{}\n\
Mountpoint: {}\nControl socket: {}\nMount service running: {}\n", Mountpoint: {}\nControl socket: {}\nMount service running: {}\n",
pdfs_core::config::APP_VERSION, config.resolved_app_version(),
pdfs_core::config::USER_AGENT, config.resolved_user_agent(),
adw::major_version(), adw::major_version(),
adw::minor_version(), adw::minor_version(),
adw::micro_version(), adw::micro_version(),
+2 -2
View File
@@ -832,8 +832,8 @@ fn show_location_column(ui: &Rc<Ui>, visible: bool) {
} }
} }
/// The menu for one entry, in the order of `docs/UI_UX_PLAN.md` §6: open, /// The menu for one entry, in a fixed order: open, offline, sharing,
/// offline, sharing, organising, and the destructive item last on its own. /// organising, and the destructive item last on its own.
pub(crate) fn entry_context_menu(ui: &Rc<Ui>, entry: &DirEntry) -> ActionMenu { pub(crate) fn entry_context_menu(ui: &Rc<Ui>, entry: &DirEntry) -> ActionMenu {
let mut menu = ActionMenu::new(); let mut menu = ActionMenu::new();
let (ui_c, entry_c) = (ui.clone(), entry.clone()); let (ui_c, entry_c) = (ui.clone(), entry.clone());
+1 -1
View File
@@ -7,7 +7,7 @@
//! `~/ProtonDrive` mount, plus each folder this computer backs up, whether it is //! `~/ProtonDrive` mount, plus each folder this computer backs up, whether it is
//! a mirrored local directory or an on-demand FUSE session. The page is called //! a mirrored local directory or an on-demand FUSE session. The page is called
//! *Locations* rather than *Mounts* because a mirror folder is a plain directory //! *Locations* rather than *Mounts* because a mirror folder is a plain directory
//! with no FUSE session behind it (mount-architecture.md §4). //! with no FUSE session behind it.
//! //!
//! The device rows drive the same control requests the Computers page used to: //! The device rows drive the same control requests the Computers page used to:
//! mode switch, sync now, remove. What is new here is that the primary mount is //! mode switch, sync now, remove. What is new here is that the primary mount is
+219 -18
View File
@@ -128,7 +128,12 @@ pub(crate) fn wire_login(ui: &Rc<Ui>) {
set_signing_in(&ui, true); set_signing_in(&ui, true);
ui.login.login_status.set_text(&gettext("Signing in…")); ui.login.login_status.set_text(&gettext("Signing in…"));
let (rx, totp_req_rx, hv_req_rx) = spawn_login(username, password); let LoginChannels {
result: rx,
totp: totp_req_rx,
hv: hv_req_rx,
mailbox: mailbox_req_rx,
} = spawn_login(username, password);
// Surface the 2FA dialog only if the SDK actually asks for a code (i.e. // Surface the 2FA dialog only if the SDK actually asks for a code (i.e.
// the account has 2FA enabled). The worker blocks until the dialog feeds // the account has 2FA enabled). The worker blocks until the dialog feeds
@@ -145,15 +150,42 @@ pub(crate) fn wire_login(ui: &Rc<Ui>) {
}); });
// Same shape for human verification: only fires when the API gates the // Same shape for human verification: only fires when the API gates the
// sign-in, and the worker is blocked on the token until it does. // sign-in, and the worker is blocked on the token until it does. A
// window that never came up is told apart from one the user closed, so
// the result below can say why the sign-in stopped.
let ui_hv = ui.clone(); let ui_hv = ui.clone();
let verification_failed = Rc::new(Cell::new(false));
let hv_failed = verification_failed.clone();
glib::spawn_future_local(async move { glib::spawn_future_local(async move {
if let Ok((url, token_tx)) = hv_req_rx.recv().await { if let Ok((url, token_tx)) = hv_req_rx.recv().await {
ui_hv ui_hv
.login .login
.login_status .login_status
.set_text(&gettext("Complete the verification to continue…")); .set_text(&gettext("Complete the verification to continue…"));
prompt_human_verification(&ui_hv, &url, token_tx); // Anything but a token drops the sender, which the worker reads
// as a cancelled sign-in.
match verify_human(url).await {
Verification::Solved(token) => {
let _ = token_tx.send(token);
}
Verification::Cancelled => {}
Verification::Failed => hv_failed.set(true),
}
}
});
// And for the mailbox password, which only a two-password account has.
// The worker asks again after a wrong answer, so this loops too.
let ui_mailbox = ui.clone();
glib::spawn_future_local(async move {
while let Ok((attempt, answer_tx)) = mailbox_req_rx.recv().await {
ask_mailbox(&ui_mailbox, attempt > 0, move |answer| {
// A cancelled dialog drops the sender, which the worker
// reads as a cancelled login.
if let Some(answer) = answer {
let _ = answer_tx.send(answer);
}
});
} }
}); });
@@ -174,6 +206,9 @@ pub(crate) fn wire_login(ui: &Rc<Ui>) {
let _ = gio::spawn_blocking(service::enable_start).await; let _ = gio::spawn_blocking(service::enable_start).await;
refresh(&ui); refresh(&ui);
} }
Err(_) if verification_failed.get() => ui.login.login_status.set_text(&gettext(
"The verification page couldn't be opened, so the sign-in was cancelled. Your system may keep the web view's sandbox from starting; the troubleshooting guide explains what to do.",
)),
Err(e) => ui.login.login_status.set_text(&e), Err(e) => ui.login.login_status.set_text(&e),
} }
}); });
@@ -209,6 +244,12 @@ pub(crate) fn login_error_message(error: &pdfs_core::Error) -> String {
pdfs_core::Error::Other(message) if message.contains("two-factor") => { pdfs_core::Error::Other(message) if message.contains("two-factor") => {
gettext("Sign-in cancelled: no two-factor code was entered.") gettext("Sign-in cancelled: no two-factor code was entered.")
} }
pdfs_core::Error::Other(message) if message.contains("mailbox") => {
gettext("Sign-in cancelled: no mailbox password was entered.")
}
pdfs_core::Error::WrongMailboxPassword => {
gettext("That mailbox password didn't unlock your account.")
}
pdfs_core::Error::Other(message) if message.contains("verification") => gettext( pdfs_core::Error::Other(message) if message.contains("verification") => gettext(
"Sign-in cancelled: the verification wasn't completed. Sign in again to get a new one.", "Sign-in cancelled: the verification wasn't completed. Sign in again to get a new one.",
), ),
@@ -220,26 +261,31 @@ pub(crate) fn login_error_message(error: &pdfs_core::Error) -> String {
} }
} }
/// What the login worker asks the UI for, and how it reports back.
pub(crate) struct LoginChannels {
/// The final outcome, once.
pub(crate) result: async_channel::Receiver<Result<(), String>>,
/// Fires only if the account needs a 2FA code; carries the sender for it.
pub(crate) totp: async_channel::Receiver<std::sync::mpsc::Sender<String>>,
/// Fires only if the sign-in is gated: the page to show, and the sender for
/// the token the user earns on it.
pub(crate) hv: async_channel::Receiver<(String, std::sync::mpsc::Sender<String>)>,
/// Fires only for a two-password account: how many answers were already
/// wrong, and the sender for the password and the "remember" choice.
pub(crate) mailbox: async_channel::Receiver<(u32, std::sync::mpsc::Sender<(String, bool)>)>,
}
/// Run the async SRP + optional 2FA login on a dedicated current-thread Tokio /// Run the async SRP + optional 2FA login on a dedicated current-thread Tokio
/// runtime. Returns two channels: the first yields the final login result once; /// runtime. The returned [`LoginChannels`] carry the final result and the lazy
/// the second fires *only if* the account needs a 2FA code, carrying a /// prompts. Each login closure blocks the worker on a sender the UI answers, so
/// [`std::sync::mpsc::Sender`] the UI uses to feed the code back. The login /// a code is requested lazily and can't expire before the password proof.
/// closure blocks the worker on that sender until the dialog answers, so the pub(crate) fn spawn_login(username: String, password: String) -> LoginChannels {
/// code is requested lazily and can't expire before the password proof.
#[allow(clippy::type_complexity)]
pub(crate) fn spawn_login(
username: String,
password: String,
) -> (
async_channel::Receiver<Result<(), String>>,
async_channel::Receiver<std::sync::mpsc::Sender<String>>,
async_channel::Receiver<(String, std::sync::mpsc::Sender<String>)>,
) {
let (tx, rx) = async_channel::bounded(1); let (tx, rx) = async_channel::bounded(1);
// Two, not one: a CAPTCHA-gated login restarts, so a 2FA account is asked // Two, not one: a CAPTCHA-gated login restarts, so a 2FA account is asked
// for a code on each attempt and a single slot would deadlock the worker. // for a code on each attempt and a single slot would deadlock the worker.
let (totp_req_tx, totp_req_rx) = async_channel::bounded(2); let (totp_req_tx, totp_req_rx) = async_channel::bounded(2);
let (hv_req_tx, hv_req_rx) = async_channel::bounded(1); let (hv_req_tx, hv_req_rx) = async_channel::bounded(1);
let (mailbox_req_tx, mailbox_req_rx) = async_channel::bounded(1);
std::thread::spawn(move || { std::thread::spawn(move || {
let rt = match tokio::runtime::Builder::new_current_thread() let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
@@ -267,6 +313,19 @@ pub(crate) fn spawn_login(
.recv() .recv()
.map_err(|_| pdfs_core::Error::Other("two-factor entry cancelled".into())) .map_err(|_| pdfs_core::Error::Other("two-factor entry cancelled".into()))
}, },
|attempt| {
let (answer_tx, answer_rx) = std::sync::mpsc::channel::<(String, bool)>();
mailbox_req_tx
.send_blocking((attempt, answer_tx))
.map_err(|_| pdfs_core::Error::Other("mailbox prompt closed".into()))?;
let (password, remember) = answer_rx
.recv()
.map_err(|_| pdfs_core::Error::Other("mailbox entry cancelled".into()))?;
Ok(auth::Mailbox {
password: password.into(),
remember,
})
},
|hv| { |hv| {
// Gated: hand the UI the page to show, block on the token the // Gated: hand the UI the page to show, block on the token the
// user earns by solving it. // user earns by solving it.
@@ -287,7 +346,143 @@ pub(crate) fn spawn_login(
}); });
let _ = tx.send_blocking(result); let _ = tx.send_blocking(result);
}); });
(rx, totp_req_rx, hv_req_rx) LoginChannels {
result: rx,
totp: totp_req_rx,
hv: hv_req_rx,
mailbox: mailbox_req_rx,
}
}
/// Ask for the mailbox password of a two-password account.
///
/// `done` gets the password and the "keep unlocked" choice, or `None` when the
/// dialog was cancelled. `retry` says the last answer was wrong. The choice
/// defaults to off: storing the mailbox unlock in the keyring trades away what
/// a separate mailbox password is for, so it is something to opt into.
pub(crate) fn ask_mailbox(
ui: &Rc<Ui>,
retry: bool,
done: impl FnOnce(Option<(String, bool)>) + 'static,
) {
let body = if retry {
gettext("That mailbox password didn't unlock your account. Try again.")
} else {
gettext("This account has a separate mailbox password. Enter it to unlock your files.")
};
let dialog = adw::AlertDialog::builder()
.heading(gettext("Mailbox Password"))
.body(body)
.build();
let group = adw::PreferencesGroup::new();
let entry = adw::PasswordEntryRow::builder()
.title(gettext("Mailbox password"))
.activates_default(true)
.build();
let remember = adw::SwitchRow::builder()
.title(gettext("Keep unlocked on this computer"))
.subtitle(gettext(
"Stores the unlock in the system keyring, so Proton Drive opens without asking after every login. Otherwise you enter the password once per session.",
))
.build();
group.add(&entry);
group.add(&remember);
dialog.set_extra_child(Some(&group));
dialog.add_response("cancel", &gettext("Cancel"));
dialog.add_response("unlock", &gettext("Unlock"));
dialog.set_response_appearance("unlock", adw::ResponseAppearance::Suggested);
dialog.set_default_response(Some("unlock"));
dialog.set_close_response("cancel");
let done = RefCell::new(Some(done));
dialog.connect_response(None, move |_, resp| {
let Some(done) = done.borrow_mut().take() else {
return;
};
let password = entry.text().to_string();
if resp == "unlock" && !password.is_empty() {
done(Some((password, remember.is_active())));
} else {
done(None);
}
});
let parent = ui_window(ui).map(|w| w.upcast::<gtk4::Window>());
dialog.present(parent.as_ref());
}
/// If the signed-in account is locked, ask for its mailbox password — once per
/// run; after a cancel, a toast keeps an Unlock button within reach.
///
/// The key state is a keyring and kernel-keyring read, so it runs on a worker.
pub(crate) fn offer_unlock(ui: &Rc<Ui>) {
if ui.unlock_offered.get() {
return;
}
let ui = ui.clone();
glib::spawn_future_local(async move {
let state = gio::spawn_blocking(|| auth::key_state().ok()).await;
if matches!(state, Ok(Some(auth::KeyState::Locked))) && !ui.unlock_offered.replace(true) {
prompt_unlock(&ui, false);
}
});
}
enum UnlockOutcome {
Unlocked,
Wrong,
Failed(String),
}
/// Ask for the mailbox password and unlock the account with it.
fn prompt_unlock(ui: &Rc<Ui>, retry: bool) {
let ui_done = ui.clone();
ask_mailbox(ui, retry, move |answer| {
let Some((password, remember)) = answer else {
toast_action(
&ui_done,
&gettext("Proton Drive is locked. Enter your mailbox password to unlock it."),
&gettext("Unlock"),
|ui| prompt_unlock(ui, false),
);
return;
};
glib::spawn_future_local(async move {
// Verifying talks to Proton and the restart talks to systemd, so
// both run on a worker, never on the main loop.
let outcome = gio::spawn_blocking(move || {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => return UnlockOutcome::Failed(e.to_string()),
};
match rt.block_on(auth::unlock(&password, remember)) {
Ok(()) => {
service::restart();
UnlockOutcome::Unlocked
}
Err(pdfs_core::Error::WrongMailboxPassword) => UnlockOutcome::Wrong,
Err(e) => UnlockOutcome::Failed(login_error_message(&e)),
}
})
.await;
match outcome {
Ok(UnlockOutcome::Unlocked) => {
toast(&ui_done, &gettext("Unlocked."));
refresh(&ui_done);
}
Ok(UnlockOutcome::Wrong) => prompt_unlock(&ui_done, true),
Ok(UnlockOutcome::Failed(message)) => {
toast_error(&ui_done, &gettext("Couldn't unlock"), &message)
}
Err(_) => {}
}
});
});
} }
/// Show the lazy two-factor dialog and feed the entered code back to the waiting /// Show the lazy two-factor dialog and feed the entered code back to the waiting
@@ -374,6 +569,12 @@ mod tests {
assert!(login_error_message(&hv).contains("verification wasn't completed")); assert!(login_error_message(&hv).contains("verification wasn't completed"));
} }
#[test]
fn a_cancelled_mailbox_prompt_is_reported_as_such() {
let error = pdfs_core::Error::Other("mailbox entry cancelled".into());
assert!(login_error_message(&error).contains("no mailbox password"));
}
#[test] #[test]
fn an_unmapped_error_keeps_its_text() { fn an_unmapped_error_keeps_its_text() {
let error = pdfs_core::Error::Other("something odd".into()); let error = pdfs_core::Error::Other("something odd".into());
+1
View File
@@ -860,6 +860,7 @@ pub(crate) fn refresh(ui: &Rc<Ui>) {
} }
} }
offer_unlock(ui);
refresh_status(ui); refresh_status(ui);
refresh_transfers(ui); refresh_transfers(ui);
} }
+168 -40
View File
@@ -1,4 +1,4 @@
//! The human-verification (CAPTCHA) dialog. //! The human-verification (CAPTCHA) window.
//! //!
//! Proton gates logins it does not recognise — a new IP, a VPN exit — behind a //! Proton gates logins it does not recognise — a new IP, a VPN exit — behind a
//! CAPTCHA. The API answers the login with a challenge instead of a session, and //! CAPTCHA. The API answers the login with a challenge instead of a session, and
@@ -8,10 +8,26 @@
//! The page is hosted in a `WebKitWebView` rather than handed to the system //! The page is hosted in a `WebKitWebView` rather than handed to the system
//! browser because it reports completion by posting a message to its host, not //! browser because it reports completion by posting a message to its host, not
//! by redirecting: a browser has nowhere to post it back to. //! by redirecting: a browser has nowhere to post it back to.
//!
//! The webview runs in a child `pdfs-app` started with [`VERIFY_ARG`], not in the
//! app itself. WebKit aborts its whole process when its sandbox cannot start, as
//! on hardened systems that keep bubblewrap from the journal socket
//! (`docs/BUGS.md` B195); in a child that costs the verification, not the app.
//! The URL goes to the child on stdin, so it never shows in a process list, and
//! the token comes back on stdout.
use crate::*; use crate::*;
use std::io::{BufRead, BufReader, Write as _};
use std::process::{ExitStatus, Stdio};
use webkit6::prelude::*; use webkit6::prelude::*;
/// The argument that makes `pdfs-app` the verification window instead of the app.
pub(crate) const VERIFY_ARG: &str = "--human-verification";
/// Marks the token among whatever else lands on the child's stdout: GLib and
/// WebKit write debug output there.
const TOKEN_PREFIX: &str = "hv-token ";
/// Bridge the verification page's `postMessage` to a handler this side can read. /// Bridge the verification page's `postMessage` to a handler this side can read.
/// ///
/// The page targets its embedding host (`window.parent`), which in a webview is /// The page targets its embedding host (`window.parent`), which in a webview is
@@ -26,15 +42,100 @@ window.addEventListener('message', function (event) {
}); });
"#; "#;
/// Present the challenge and hand the solved token back through `token_tx`. /// How the verification window ended.
/// #[derive(Debug, PartialEq, Eq)]
/// Closing the dialog without solving drops the sender, which the waiting login pub(crate) enum Verification {
/// worker reads as a cancelled sign-in — the same contract as the 2FA prompt. Solved(String),
pub(crate) fn prompt_human_verification( /// Closed without solving.
ui: &Rc<Ui>, Cancelled,
url: &str, /// Never came up, or died: most likely WebKit could not start its sandbox.
token_tx: std::sync::mpsc::Sender<String>, Failed,
) { }
/// Show the challenge in the verification window and wait until it closes.
pub(crate) async fn verify_human(url: String) -> Verification {
match gio::spawn_blocking(move || run_verification_child(&url)).await {
Ok(Ok(verification)) => verification,
Ok(Err(e)) => {
tracing::error!("cannot run the verification window: {e}");
Verification::Failed
}
Err(_) => Verification::Failed,
}
}
fn run_verification_child(url: &str) -> std::io::Result<Verification> {
let mut child = Command::new(own_exe())
.arg(VERIFY_ARG)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
// A failed write means the child is already gone; its exit status says
// how. Dropping `stdin` closes it.
let _ = writeln!(stdin, "{url}");
}
// Stops reading at the token rather than at EOF: WebKit's helper processes
// inherit the pipe and may hold it open a moment after the child quits.
let token = child
.stdout
.take()
.and_then(|out| find_token(BufReader::new(out)));
let status = child.wait()?;
Ok(outcome(token, status))
}
/// This very binary, so a development build opens its own window. After an
/// upgrade replaced the file, `current_exe` names a deleted path, and the
/// installed `pdfs-app` is the next best.
fn own_exe() -> PathBuf {
std::env::current_exe()
.ok()
.filter(|path| path.is_file())
.unwrap_or_else(|| PathBuf::from("pdfs-app"))
}
fn find_token(out: impl BufRead) -> Option<String> {
out.lines()
.map_while(Result::ok)
.find_map(|line| line.strip_prefix(TOKEN_PREFIX).map(str::to_owned))
}
/// A token wins over the exit status: the user solved the page, even if
/// WebKit's teardown crashed after it.
fn outcome(token: Option<String>, status: ExitStatus) -> Verification {
match token {
Some(token) => Verification::Solved(token),
None if status.success() => Verification::Cancelled,
None => {
tracing::error!(%status, "the verification window failed");
Verification::Failed
}
}
}
/// Run as the verification window: read the page's URL from stdin, show it, and
/// print the token once the user has solved it. Closing the window exits
/// without a token, which the app reads as a cancelled sign-in.
pub(crate) fn run_verification_window() -> glib::ExitCode {
let mut url = String::new();
if std::io::stdin().lock().read_line(&mut url).is_err() || url.trim().is_empty() {
tracing::error!("{VERIFY_ARG} expects the verification URL on stdin");
return glib::ExitCode::FAILURE;
}
let url = url.trim().to_owned();
// Not unique: the app that started it already holds APP_ID.
let app = adw::Application::builder()
.flags(gio::ApplicationFlags::NON_UNIQUE)
.build();
app.connect_activate(move |app| build_verification_window(app, &url));
// GApplication would refuse VERIFY_ARG as an unknown option.
let argv0 = std::env::args().next().unwrap_or_default();
app.run_with_args(&[argv0])
}
fn build_verification_window(app: &adw::Application, url: &str) {
let content = webkit6::UserContentManager::new(); let content = webkit6::UserContentManager::new();
content.add_script(&webkit6::UserScript::new( content.add_script(&webkit6::UserScript::new(
BRIDGE, BRIDGE,
@@ -53,24 +154,19 @@ pub(crate) fn prompt_human_verification(
.hexpand(true) .hexpand(true)
.build(); .build();
let dialog = adw::Dialog::builder() let body = gtk4::Box::new(gtk4::Orientation::Vertical, 0);
body.append(&adw::HeaderBar::new());
body.append(&webview);
let window = adw::ApplicationWindow::builder()
.application(app)
.title(gettext("Verification")) .title(gettext("Verification"))
.content_width(420) .icon_name(APP_ID)
.content_height(560) .default_width(420)
.default_height(560)
.content(&body)
.build(); .build();
let header = adw::HeaderBar::new(); let app = app.downgrade();
let body = gtk4::Box::new(gtk4::Orientation::Vertical, 0);
body.append(&header);
body.append(&webview);
dialog.set_child(Some(&body));
// Taken on the first success *or* on close, so the token is sent at most
// once and a cancelled dialog always drops the sender.
let token_tx = Rc::new(RefCell::new(Some(token_tx)));
let tx = token_tx.clone();
let dlg_weak = dialog.downgrade();
content.connect_script_message_received(Some("hv"), move |_, value| { content.connect_script_message_received(Some("hv"), move |_, value| {
let Some(token) = extract_token(&value.to_str()) else { let Some(token) = extract_token(&value.to_str()) else {
// Every `postMessage` on the page reaches here, most of them the // Every `postMessage` on the page reaches here, most of them the
@@ -78,25 +174,17 @@ pub(crate) fn prompt_human_verification(
// is simply not ours. // is simply not ours.
return; return;
}; };
if let Some(tx) = tx.borrow_mut().take() { let mut stdout = std::io::stdout().lock();
let _ = tx.send(token); if let Err(e) = writeln!(stdout, "{TOKEN_PREFIX}{token}").and_then(|()| stdout.flush()) {
tracing::error!("cannot hand the verification token back: {e}");
} }
if let Some(dlg) = dlg_weak.upgrade() { if let Some(app) = app.upgrade() {
dlg.close(); app.quit();
} }
}); });
// Covers the close button, Escape, and the user giving up.
dialog.connect_closed(move |_| {
token_tx.borrow_mut().take();
});
webview.load_uri(url); webview.load_uri(url);
window.present();
let parent = ui.login.login_button.root().and_downcast::<gtk4::Window>();
// Returns immediately; the login worker stays blocked on the channel until
// the handler above sends, or the dialog closes and drops the sender.
dialog.present(parent.as_ref());
} }
/// Pull the verification token out of a message posted by the verification page. /// Pull the verification token out of a message posted by the verification page.
@@ -133,7 +221,12 @@ fn extract_token(raw: &str) -> Option<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::extract_token; use super::{Verification, extract_token, find_token, outcome};
use std::os::unix::process::ExitStatusExt as _;
use std::process::ExitStatus;
/// The wait status of a process killed by SIGABRT.
const ABORTED: i32 = 6;
#[test] #[test]
fn a_completion_message_yields_its_token() { fn a_completion_message_yields_its_token() {
@@ -163,4 +256,39 @@ mod tests {
assert!(extract_token(raw).is_none(), "accepted: {raw}"); assert!(extract_token(raw).is_none(), "accepted: {raw}");
} }
} }
#[test]
fn the_token_is_found_among_other_output() {
let out = "Gtk-Message: something\nhv-token tok-1\nmore\n";
assert_eq!(find_token(out.as_bytes()).as_deref(), Some("tok-1"));
assert_eq!(find_token("only chatter\n".as_bytes()), None);
}
#[test]
fn a_window_closed_without_a_token_cancels() {
assert_eq!(
outcome(None, ExitStatus::from_raw(0)),
Verification::Cancelled
);
}
/// WebKit aborts when its sandbox cannot start (B195). That must read as a
/// failure the app explains, not as the user giving up.
#[test]
fn a_window_that_died_fails() {
assert_eq!(
outcome(None, ExitStatus::from_raw(ABORTED)),
Verification::Failed
);
let exited_1 = ExitStatus::from_raw(1 << 8);
assert_eq!(outcome(None, exited_1), Verification::Failed);
}
#[test]
fn a_token_counts_even_if_the_window_died_after_it() {
assert_eq!(
outcome(Some("tok-1".into()), ExitStatus::from_raw(ABORTED)),
Verification::Solved("tok-1".into())
);
}
} }
@@ -133,7 +133,7 @@ impl Drop for Busy {
/// from Shared or Shared-with-me may not: it can live under a device folder's /// from Shared or Shared-with-me may not: it can live under a device folder's
/// own inode space, or on someone else's volume, neither of which the primary /// own inode space, or on someone else's volume, neither of which the primary
/// mount's path space can name. Those carry a uid instead, and the daemon /// mount's path space can name. Those carry a uid instead, and the daemon
/// resolves it with `Core::resolve_anywhere` (mount-architecture.md P4). /// resolves it with `Core::resolve_anywhere`.
/// ///
/// The two are separate request variants rather than one request with an /// The two are separate request variants rather than one request with an
/// optional field on purpose: an older daemon that ignored `uid` would resolve /// optional field on purpose: an older daemon that ignored `uid` would resolve
+13
View File
@@ -166,6 +166,15 @@ fn poll_state(socket: &Path, default_mountpoint: &Path) -> DriveState {
// No daemon: describe login state instead so the menu is still useful. // No daemon: describe login state instead so the menu is still useful.
Err(_) => { Err(_) => {
let (line, phase) = match auth::load() { let (line, phase) = match auth::load() {
Ok(s) if matches!(auth::key_state(), Ok(auth::KeyState::Locked)) => (
// Translators: {username} is the Proton account name. The
// account has a mailbox password that has to be entered.
gettext_f(
"Signed in as {username} — locked",
&[("username", &s.username)],
),
Phase::Attention,
),
Ok(s) => ( Ok(s) => (
// Translators: {username} is the Proton account name. // Translators: {username} is the Proton account name.
gettext_f( gettext_f(
@@ -476,6 +485,10 @@ fn main() -> Result<()> {
return Ok(()); return Ok(());
} }
// The socket lives in the state dir, which nothing has made yet when the app
// starts the tray on a fresh install (B194).
dirs.ensure().context("create the app's directories")?;
// Ensure only one instance of the tray runs. // Ensure only one instance of the tray runs.
let tray_sock = dirs.tray_socket(); let tray_sock = dirs.tray_socket();
if std::os::unix::net::UnixStream::connect(&tray_sock).is_ok() { if std::os::unix::net::UnixStream::connect(&tray_sock).is_ok() {
+524 -468
View File
File diff suppressed because it is too large. Load diff
+3676 -3184
View File
File diff suppressed because it is too large. Load diff
+66
View File
@@ -11,6 +11,68 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
## [Unreleased] ## [Unreleased]
### Fixed
- **The app crashed when Proton asked for a CAPTCHA on hardened systems.** On secureblue, WebKit
cannot start the sandbox for the verification page and ends its process, which took the app
down with it. The page now opens in a separate window process: if that process fails, the
sign-in stops with a message that says why, and TROUBLESHOOTING.md has a workaround.
([#29](https://github.com/narrrl/proton-drive-linux/issues/29))
- **On a fresh install the tray icon did not start.** The tray and the app now create their state
directory themselves instead of waiting for the service to do it. This also lets the Takeout
page remember staged archives before the first mount.
([#29](https://github.com/narrrl/proton-drive-linux/issues/29))
## [3.1.1] — 2026-10-07
Schema: **42**, unchanged.
### Changed
- **The client identifies itself with its release.** Sign-in and every API call used to send the
fixed `external-drive-linux@0.1.0-alpha` and `proton-drive-linux/0.1.0`. If Proton retired that
version, every release would fail to sign in at once and no update could fix it. The client now
sends `external-drive-linux@<version>-stable` and `proton-drive-linux/<version>`. `app_version`
and `user_agent` in `config.json` are optional overrides now. The fixed values that releases up
to 3.1.0 wrote there count as unset and are removed on the next save. A value you set by hand is
kept.
- The About dialog shows the release version, and its debug info shows the identity that is
actually sent.
## [3.1.0] — 2026-10-07
Schema: **42**, unchanged.
### Added
- **Two-password accounts.** An account with a separate mailbox password now signs in: the app
and `pdfs login` ask for it after the login password and check it against the account, so a
typo is a retry and not a daemon that never mounts. The mailbox password is never stored.
What it unlocks is kept in kernel memory for the login session, so it is asked for once per
session and is gone after a reboot or when you log out of the machine.
- **`pdfs unlock` and `pdfs lock`.** `pdfs unlock` enters the mailbox password of a locked
account and starts the daemon's mount; `pdfs lock` forgets it again. `--remember` on `login`
and `unlock` (and the "Keep unlocked on this computer" switch in the app) keeps the unlock in
the system keyring instead, so the daemon starts without asking; `pdfs lock --forget` removes
it. Off by default: it gives up the separation a second password is for.
- **Locked state.** `pdfs status`, `pdfs diagnose` and the tray say when the account is waiting
for its mailbox password, and the app asks for it when it opens.
### Changed
- **The keyring no longer holds a password.** The session blob keeps the keys' passphrases
instead of the mailbox password, and a blob written by 3.0.x is converted on the first start.
A stored password the account rejects (a two-password account that was signed in with only
its login password) leaves the account locked instead of failing every start.
- **The daemon is not dumpable.** It holds the unlock in memory, so core dumps and same-user
`ptrace` are off for it.
- **The offline cache is re-keyed** from the new secret, so the first start after the update
reads it as cold and fetches the tree again once.
### Fixed
- **systemd killed a daemon that was only retrying.** `WatchdogSec` was answered only once a
mount existed, so a daemon waiting for a login or retrying a failed mount was aborted after
two minutes and restarted in a loop, hiding the error it logged. It now answers the watchdog
while it waits. ([#26](https://github.com/narrrl/proton-drive-linux/issues/26))
- **A refreshed token could overwrite a newer unlock or revive a signed-out session.** Token
rotation now updates only the tokens in the stored blob.
## [3.0.0] — 2026-10-05 ## [3.0.0] — 2026-10-05
Schema: **42**. The queue table is rebuilt so an id is never given out twice; queued changes Schema: **42**. The queue table is rebuilt so an id is never given out twice; queued changes
@@ -1599,6 +1661,10 @@ First stable release: FUSE files-on-demand mount, sync daemon under `proton-driv
- The outstanding FUSE defects tracked in `docs/BUGS.md`, plus a truncate defect, validated - The outstanding FUSE defects tracked in `docs/BUGS.md`, plus a truncate defect, validated
by a new POSIX compliance suite for the filesystem. by a new POSIX compliance suite for the filesystem.
[3.1.1]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.1.1
[3.1.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.1.0
[3.0.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.0.0 [3.0.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.0.0
[2.10.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v2.10.0 [2.10.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v2.10.0
+1 -1
View File
@@ -83,7 +83,7 @@ commands are for scripts and for cases where the mount is not available.
| `pdfs sync limit [--up RATE] [--down RATE]` | Cap bandwidth, for example `500K` or `2M` per second. `0` removes a cap. Without options, prints the current caps. | | `pdfs sync limit [--up RATE] [--down RATE]` | Cap bandwidth, for example `500K` or `2M` per second. `0` removes a cap. Without options, prints the current caps. |
| `pdfs sync queue` | List changes that have not reached Proton Drive yet. | | `pdfs sync queue` | List changes that have not reached Proton Drive yet. |
| `pdfs sync issues` | List queued changes Proton Drive refused or that keep failing, and what to do. | | `pdfs sync issues` | List queued changes Proton Drive refused or that keep failing, and what to do. |
| `pdfs sync export ID DEST` | Save a copy of a queued upload's content. The upload stays queued. | | `pdfs sync export ID DEST` | Save a copy of a queued upload's content. `DEST` may be a folder; an existing file is never overwritten. The upload stays queued. |
| `pdfs sync discard ID [--yes]` | Drop a queued change and undo it here: a new file or folder goes, anything else goes back to what Proton Drive has. Content not exported first is lost. | | `pdfs sync discard ID [--yes]` | Drop a queued change and undo it here: a new file or folder goes, anything else goes back to what Proton Drive has. Content not exported first is lost. |
| `pdfs sync retry [ID]` | Retry one queued operation, or every failed one. | | `pdfs sync retry [ID]` | Retry one queued operation, or every failed one. |
| `pdfs sync restore [--device UID] [--yes]` | Re-attach this computer's backed-up folders to local folders and download them. `--device` restores another computer's folders. See [RECOVERY.md](RECOVERY.md). | | `pdfs sync restore [--device UID] [--yes]` | Re-attach this computer's backed-up folders to local folders and download them. `--device` restores another computer's folders. See [RECOVERY.md](RECOVERY.md). |
+13 -6
View File
@@ -38,7 +38,7 @@ Inside `content/`:
> not reached Proton Drive and exists nowhere else. > not reached Proton Drive and exists nowhere else.
Everything in these directories except the keyring is **decrypted**. See Everything in these directories except the keyring is **decrypted**. See
[ARCHITECTURE.md §8](ARCHITECTURE.md#8-threat-model-what-this-client-writes-to-disk-in-plaintext). [ARCHITECTURE.md §10](ARCHITECTURE.md#10-threat-model-what-this-client-writes-to-disk-in-plaintext).
## `config.json` ## `config.json`
@@ -89,7 +89,11 @@ fix it.
| `open_with` | object | `xdg-open` | — | See [`open_with`](#open_with) | | `open_with` | object | `xdg-open` | — | See [`open_with`](#open_with) |
| `prompt` | object | built-in window | — | See [`prompt`](#prompt) | | `prompt` | object | built-in window | — | See [`prompt`](#prompt) |
`app_version` and `user_agent` identify the client to Proton. Leave them alone. `app_version` and `user_agent` identify the client to Proton. When the keys are absent, the
client sends its own release (`external-drive-linux@<version>-stable`, `proton-drive-linux/<version>`).
Leave them out unless a support case asks for a different value. The fixed
`external-drive-linux@0.1.0-alpha` and `proton-drive-linux/0.1.0` that releases up to 3.1.0 wrote
here are treated as absent and removed on the next save.
### `ignore_patterns` ### `ignore_patterns`
@@ -190,13 +194,16 @@ Command-line flags win over the file: `--gtk`, `--dmenu`, `--fzf` and `--menu`.
| `PDFS_LOCALEDIR` | app, tray, prompt | Load translations from another directory (development) | | `PDFS_LOCALEDIR` | app, tray, prompt | Load translations from another directory (development) |
| `PDFS_STAND_IN_WIDGETS` | app | `1` uses the libadwaita 1.5 fallback widgets on newer systems (development) | | `PDFS_STAND_IN_WIDGETS` | app | `1` uses the libadwaita 1.5 fallback widgets on newer systems (development) |
The acceptance suite has its own `PDFS_ACCEPTANCE_*` variables, listed in [TESTING.md](TESTING.md). The tests have their own variables (`PDFS_ACCEPTANCE_*`, `PDFS_SIM_*`, `PDFS_MIGRATE_DB`),
listed in [TESTING.md](TESTING.md).
## The systemd service ## The systemd service
`proton-drive.service` is a user unit. It restarts the service whenever it exits, restarts it when `proton-drive.service` is a user unit started with your graphical session
it stops answering for two minutes (`WatchdogSec=120`), and caps its memory at 6 GiB. To change (`WantedBy=graphical-session.target`). It restarts the service whenever it exits, restarts it
the unit without editing the packaged file, use a drop-in: when it stops answering for two minutes (`WatchdogSec=120`), throttles it above 2 GiB of memory
(`MemoryHigh`) and stops it at 6 GiB (`MemoryMax`). To change the unit without editing the
packaged file, use a drop-in:
```bash ```bash
systemctl --user edit proton-drive.service systemctl --user edit proton-drive.service
+16 -6
View File
@@ -31,12 +31,12 @@ time when the installed library has them (`crates/pdfs-gui/src/compat.rs`).
| Path | Contents | | Path | Contents |
|---|---| |---|---|
| `crates/pdfs-core` | Shared library: authentication and keyring (`auth`), configuration (`config`), SQLite schema and queries (`db/`), content cache (`cache`), control protocol (`control`), search scoring, sync ignore rules, Google Takeout parsing, machine profile | | `crates/pdfs-core` | Shared library: authentication and keyring (`auth`), configuration (`config`), SQLite schema and queries (`db/`), content cache (`cache`), control protocol (`control`), search scoring, sync ignore rules, Google Takeout parsing, machine profile |
| `crates/pdfs-fuse` | The daemon: FUSE filesystem, read path (`reads`), write queue (`queue`, `drain`, `upload`), sync engine (`sync/`), remote events (`background`), push events for front ends (`events`), control handlers (`control`), photos, devices, supervisor | | `crates/pdfs-fuse` | The daemon: FUSE handlers (`filesystem`), the tree (`state`), reads (`reads`), the queue and the drain (`queue`, `drain`, `upload`), link state (`link`), the sync engine (`sync`, `sync/`), moves between locations (`relocate`), remote events (`background`), push events (`events`), control handlers (`control`), photos, devices, the supervisor, and the simulation tests (`sim/`) |
| `crates/pdfs-cli` | The `pdfs` binary | | `crates/pdfs-cli` | The `pdfs` binary |
| `crates/pdfs-gui` | `pdfs-app`, `pdfs-tray` and `pdfs-prompt`. Pages in `src/app/pages/`, shared widgets in `src/app/widgets/`, CSS and icons in `resources/` | | `crates/pdfs-gui` | `pdfs-app`, `pdfs-tray` and `pdfs-prompt`. Pages in `src/app/pages/`, shared widgets in `src/app/widgets/`, CSS and icons in `resources/` |
| `po/` | Translation template, catalogs and the scripts that maintain them | | `po/` | Translation template, catalogs and the scripts that maintain them |
| `packaging/` | systemd unit, desktop files, icon, PKGBUILD, RPM spec, AUR recipes | | `packaging/` | systemd unit, desktop files, icon, PKGBUILD, RPM spec, AUR recipes |
| `scripts/` | FUSE acceptance suite, map-data generators | | `scripts/` | FUSE acceptance suite, map-data generators, the wiki publisher |
| `docs/` | This documentation | | `docs/` | This documentation |
The GUI talks to the daemon only through the control socket. Anything a front end needs that the The GUI talks to the daemon only through the control socket. Anything a front end needs that the
@@ -87,8 +87,9 @@ cargo test --workspace --locked
scripts/fuse-acceptance.sh --offline-only scripts/fuse-acceptance.sh --offline-only
``` ```
For changes to the FUSE layer, the write queue or the sync engine, also run the live acceptance For changes to the FUSE layer, the queue or the sync engine, also run the
cases described in [TESTING.md](TESTING.md). [simulation runs](TESTING.md#simulation-runs), which CI runs only after a push to `main`, and the
[acceptance suite](TESTING.md#acceptance-suite) on a test account.
For changes to GUI strings, check that every catalog is complete: For changes to GUI strings, check that every catalog is complete:
@@ -136,6 +137,15 @@ file to `po/POTFILES.in`, run `po/update.sh`, and translate the new entries in e
`#[serde(default)]`, so older files still load, and document it in `#[serde(default)]`, so older files still load, and document it in
[CONFIGURATION.md](CONFIGURATION.md). [CONFIGURATION.md](CONFIGURATION.md).
**A bug fix.** Add or update the entry in [BUGS.md](BUGS.md#adding-an-entry), cover the fix with a
unit test, a simulation scenario or a `regression B<n>` acceptance case, and cite the entry in the
code as `docs/BUGS.md B<n>`. Do not cite planning notes that are not in the repository.
**A user-visible change.** Update the matching page (user guide, CLI reference, configuration)
and the `[Unreleased]` section of [CHANGELOG.md](CHANGELOG.md). `scripts/publish-wiki.sh` copies
the six user pages (`INSTALL`, `USER_GUIDE`, `CLI`, `CONFIGURATION`, `TROUBLESHOOTING`, `RECOVERY`)
to the GitHub wiki, so keep their file names.
## Releasing ## Releasing
Releases are cut from `main` by pushing a `v*` tag. `.github/workflows/release.yml` then: Releases are cut from `main` by pushing a `v*` tag. `.github/workflows/release.yml` then:
@@ -160,5 +170,5 @@ git tag vX.Y.Z
git push origin main vX.Y.Z git push origin main vX.Y.Z
``` ```
Before a release that touches the filesystem or sync, run the managed live mode matrix from Before a release, work through [Before a release](TESTING.md#before-a-release). Open
[TESTING.md](TESTING.md). Open release-assurance items are listed in [ROADMAP.md](ROADMAP.md). release-assurance items are listed in [ROADMAP.md](ROADMAP.md#release-assurance).
-182
View File
@@ -1,182 +0,0 @@
# 3.0.0: what is done and what is left
Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase.
## Done
- **Phase 0**: fake Drive, simulation runs, CI job (2.9.0).
- **Phase 1**: queued-op preconditions and own sealed revisions in the database (2.9.0).
- **Phase 2**: local ids, inode = local id, one tree for every mount (2.10.0).
- **Phase 3, first part**: mkdir, create, rename, unlink and rmdir on the mount are recorded
locally and sent from the queue; `"local_first": false` brings back the old path (5e0f30c).
- Fixes from the simulation and account runs since then: B125, B127, B130 and B141 to B193.
- **Sync issues, first cut** (§6.2): refusals sorted into issues, shown at once in the app,
tray and CLI; Export button and `pdfs sync export`; `pdfs sync issues`; Discard button and
`pdfs sync discard`; a warning badge on the node in the file browser.
## Phase 3: left for 3.0.0
1. **Sync issues (§6.2)**: first cut done.
- [x] The drain sorts a refusal from Drive into an issue kind (quota, access, missing,
limit, rejected) and stores it on the queued op (schema 38).
- [x] An op with an issue counts as "needs attention" at once, not after six failures.
- [x] The queue listing carries the issue kind and whether its content can be exported.
- [x] Export a queued file's content (control request, `pdfs sync export`).
- [x] `pdfs sync issues`, and the issue shown in `pdfs sync queue`.
- [x] The app's Sync page shows each issue with what the user can do; Export and Retry
buttons; translated in every catalog.
- [x] A name collision lands under a conflict name (create, rename, move, re-homed create)
and goes in the activity log, as does a queued edit kept as a conflict copy. The app
words each one in the user's language (the last one since B181).
- [x] Discard an issue on purpose (after export, or for a change with no content), and
undo the local change it stood for: a node made here goes, a node Drive has goes back
to what Drive has. Control request, `pdfs sync discard`, Discard button with a
confirmation; translated in every catalog.
- [x] Show the issue on the node itself: a folder listing carries each node's issue, and
the file browser shows a warning badge with what it means. The listing is published
again when the set of issues changes.
2. **Planner and executor (§5.4, §5.5)**: ops keyed by local id; remove `mint_local_uid`,
`finish_create` and `adopt_real_uid`; a landing create sets `remote_uid` on its row.
- [x] Queued ops carry `lid` and `parent_lid` (schema 39). The claim reads the parent's uid
through `parent_lid`, and a landing folder rewrites the ops inside it by local id.
- [x] Node rows link to their parent by lid (`nodes.parent_lid`, schema 40), and a restart
places each node under its parent's row as it is now.
- [x] Queries over children, subtrees and ancestors by `parent_lid`, and no rewrite of the
children's `parent_uid` when a folder lands (R3), nor of the ops made inside it (R2).
- [x] No more placeholders: a node made here goes by its local id until it lands. In four
parts:
- [x] 6a: a node made here goes by `local~<lid>`, a function of its row, instead of a
minted `local~<ms>-<n>`; schema 41 rewrites the old placeholders (nodes, ops, pins).
- [x] 6b: an op is found by its node's local id under either uid, the claim keeps one op
per local id on the wire, and the drain sends the uid the row has now;
`remap_local_uid` is gone.
- [x] 6c: children, subtrees, ancestors and ops' parents by lid, so a landing create
rewrites no other row or op; what is read for a uid names the parent as its row has it
now. The row keeps its stand-in in `nodes.uid` rather than `NULL`: it is as stable,
and `NULL` would have changed some 40 node queries for no change in behaviour. Pins
stay keyed by uid and move with the row in the landing transaction (B148).
- [x] 6d: a node written under its stand-in after it landed updates its landed row
instead of bringing the placeholder row back, so a write that finds its create
landed takes the uid from the row and no longer polls the tree for up to 2 s
(`landed_uid` is gone). In memory, `pending`, the uploads and the tree stay keyed by
the uid Drive knows: ops are matched by lid in the database (6b), `hidden` and
`creating` only ever hold remote uids, and `landed_row_uid` is the one place at the
mount's edge where a stand-in a handle still holds becomes the real uid.
- [x] An executor that runs ops in dependency order, many at once: 16 drain workers instead
of 3, and a worker that lands an op wakes the idle ones, which slept through the ops it
made claimable. The claim already keeps the order: a create waits for its parent's
uid, a node's ops go one at a time. Ops stay blocking calls on worker threads: each
one spends its time waiting on Drive, so tasks would buy nothing threads do not.
3. **Speed (§7)**: run independent ops in parallel, batch trashes and moves; measure 1,000 files
and 100 folders on Wi-Fi against LAN (target: within 10 %).
- [x] The measurement: `a_thousand_files_drain_as_fast_on_wifi_as_on_lan` in `sim/daemon.rs`,
run with `PDFS_SIM_MEASURE=1`. It prints the times and asserts nothing yet.
- [x] Baseline on eb1c105, three drain threads: the syscalls take 3.7 s on LAN and 4.3 s on
Wi-Fi; the drain takes 138 s on LAN and 938 s on Wi-Fi, 6.8 times as long.
- [x] Sixteen workers: the syscalls take 5.2 s on LAN and 5.0 s on Wi-Fi, which meets the
target; the drain takes 47 s on LAN and 247 s on Wi-Fi. Most of a create on Wi-Fi is
its read-back waiting for Drive to list the new node (250 ms, then 1 s), so batching
(step 8) helps trashes and moves, not creates.
- [x] Batched trashes and moves: left for after 3.0.0 (see below). The target is met
without them, the drain waits on the creates' read-back, and splitting one answer into
many results is where the B15x to B17x races lived.
4. **Done-when checks**:
- [ ] Simulation passes on the Wi-Fi profile: `one_client_on_a_slow_link` failed in CI on
seeds 2 and 3. Two bugs (B127, B151) and a harness gap: a settle did not read the log, so
it could not explain a known bug's damage. All three fixed; not yet confirmed in CI.
Replays then found two races on the LAN profile too (B152, B153), both fixed. CI on
7269cb5 failed LAN seed 9, a regression from the B153 fix (B154), and Wi-Fi seed 2,
a write whose answer was lost (B155). Both fixed. One slow-link run of seed 2 failed
once with a rename answering `ENOENT` as a folder move landed. Not reproduced since;
it may be B152. Replays of slow-link seed 4 failed two of six times on a file written
again after its create lost its answer (B156, fixed). With sixteen drain workers
(16fca10), seed 4 failed every replay on a write after an upload that was not read
back (B157, fixed). Seed 3 then failed four of ten runs on two renames of one file
landing out of order (B158, fixed). Flaky-link seed 4 failed one of ten runs on a
lookup that answered `EAGAIN` while the root was listed again after a restart (B159,
fixed). Slow-link seed 4 also had folders reach Drive 30 s late, after the
drain's idle poll (B160, fixed). Slow-link seed 3 failed on a file moved while its
create was on the wire, which the mount showed under its old name (B161, fixed).
Seed 4 failed on a folder removed and made again, whose create adopted the removed
folder (B162, fixed). CI on 903452a passed every profile. CI on f9b1638 failed
slow-link seed 2: a step that unlinks an open file, appends and reads it back hung
for 150 s. The B130 matcher did not fire, and CI kept no stacks; one replay passed.
It may be a kernel notice sent from a runtime worker by another path than B130's,
which a68dae9 (not yet pushed then) also takes off the runtime. CI now keeps a hung
run's stacks.
- [x] Known-bug list in `sim/run.rs` is empty: the run on 812eb3d (`PDFS_SIM_KNOWN=fail`,
twelve seeds) passed every profile, so the matchers are gone. That confirms B125 to
B133; B134 stays open with `local_first` off only, which the runs do not mount. The confirmation run (`PDFS_SIM_KNOWN=fail`)
on da013c2 failed `one_client_on_a_flaky_link` seeds 6 and 11 on two new bugs, both
after restarts: a write over a create never read back (B182) and a trash that forgot
what its create sent (B183). Replays with those fixed found a third: a create renamed
after it lost its answer (B184). All three fixed; no known-bug matcher fired. It was
stopped before `three_clients`. The rerun on 3073052 found a create that waited out
a retry after a write replaced its blob (B185, fixed), and a file deleted offline
while open that lost the bytes its handle read (B186, fixed; older than 3.0.0).
The run on 2baad67 found that a restart before such a file's trash went out queued
its kept bytes again, for good (B188, fixed), and changes that sat out their retry
after an outage only the drain had met (B189, fixed; older than 3.0.0), and a file
saved just before a stop whose release the stop dropped (B190, fixed; older too). The run
on 1436c5f found a file deleted after its failed create was written again left on Drive
(B191, fixed). Its test for B193 found a new empty file waiting out the idle poll (B192,
fixed), and seed 6 a deleted file's trash taking a file of the user's renamed to the name
its create was sent under (B193, fixed).
- [ ] A clean account run. The one on a6f36d5 (2026-10-04, LAN: 427 passed, 12 failed)
failed B113 and B121 on every on-demand mount, two mirror-to-mirror move cases, and
the journal check. B121 was a bug (B149, fixed); B113's case still expected creates
to reach Drive at once and now accepts queued ones. The move cases were the harness:
it retried every source after one was busy, and expected a refusal where a mirror to
mirror move carries the local copy along. The journal showed a conflict copy retrying
into a deleted folder (B150, fixed). Not confirmed yet. The run on 2.10.0
(2026-10-04: 260 passed, 3 failed) failed a new file that read back empty (B163,
fixed), a 128-entry folder that listed 129 (B164, fixed), and its cleanup, which ran
while the daemon was restarting: the stop waited on a busy thread until systemd killed
it (B165, bounded). The next run on the working tree (2026-10-04: 259 passed, 4
failed) found a wide folder dropping files whose create landed while it was listed
(B166, fixed), or listing one empty: the new file that read back empty again
(B166 too). Its cleanup found no control socket because it ran 1.5 s after the
last restart, before the new daemon had bound it: `pdfs status` reports that as not
running, which the run's queue wait takes for an empty queue (the script's side).
Looking into it found the start spending 1.7 s rebuilding listings (B167, fixed).
Its journal check caught a file removed with its folder while uploading being
sent again, to the root (B168, fixed), and a stop that let a mirror pass go on
until the runtime ended under it (B169, fixed). CI on 9bbc994 failed
`one_client_on_a_good_link`: a write opened as an upload landed took the revision
it replaced as its base, and the next write failed with `EIO` (B170, fixed). The
suite's seed 9 found a deleted file's trash waiting out its create's backoff (B171,
fixed). Run three at a time on 2502af6, seed 8 failed two of nine runs: a file renamed
while its upload was read back showed its old name again (B172, fixed). The loop
on d850265 failed seed 12 once in nine runs: a file deleted just as its create
landed stayed on Drive (B173, fixed). The suite on 378667f failed seed 6: a rename
in the same gap was refused with `EACCES` (B174, fixed). The loop on 378667f failed
seed 8: a file replaced by a rename in that gap stayed on Drive (B175, fixed), and
seed 12: a write closed as its file's create landed was dropped (B176, fixed).
The account run on 378667f: a folder listed as its create landed asked Drive by
its stand-in and answered `EIO` (B177, fixed). A folder trashed while sync was
paused kept a create below it as a trash: a paused drain worker had claimed it
(B178, fixed). A file read as its create landed read empty (B179, fixed).
The suite on 0f3c06f: a stop gave up on a drain worker that slept through its
wake-up (B180, fixed). The account run on 903452a (2026-10-04, Wi-Fi: 262 passed,
1 failed) failed only its cleanup, which found no control socket: after the mirror
target's last restart the queue wait took the missing daemon for an empty queue
(the script's side, fixed in 6539c73).
5. **Release**: 2.10.0 is tagged; the 2.9.0 tag still waits for the user's go-ahead.
## After 3.0.0
- Remove the old online path in `filesystem.rs` and the `local_first` switch (the release after
3.0.0).
- **Phase 4**: one applier for remote changes.
- **Phase 5**: mirror folders on the same planner and executor.
- **Batched trashes and moves** (plan step 8). Not needed for the §7 target: the syscalls take
5.2 s on LAN and 5.0 s on Wi-Fi. The drain's time goes to the creates' read-back (250 ms,
then 1 s each), which batching does not shorten. A batch call answers for many ops at once,
and splitting that answer back per op is the step the B15x to B17x races came from. First
measure `rm -r` of 1,000 files on Wi-Fi. If it is slow, drop the queued trashes of a trashed
folder's children before batching anything: trashing the folder takes them with it.
- **Any time**: `Size` and block geometry as types; a deadline on the remote calls outside
the drain (the drain's have one: `link::upload_deadline`, `Core::block_on_within`).
- Open questions in §12 of the milestone.
- `PLAN.md`: the acceptance script notices network drops and reports `interrupted` instead of
TIMEOUT.
+4 -5
View File
@@ -5,7 +5,7 @@
| Page | What it covers | | Page | What it covers |
|---|---| |---|---|
| [Installation](INSTALL.md) | Packages, building from source, first sign-in, updating and uninstalling | | [Installation](INSTALL.md) | Packages, building from source, first sign-in, updating and uninstalling |
| [User guide](USER_GUIDE.md) | My files, available offline, synced folders, conflicts, Photos, sharing, Trash and versions, Computers, the search launcher, the tray | | [User guide](USER_GUIDE.md) | My files, available offline, synced folders, sync issues and conflicts, Photos, sharing, Trash and versions, Computers, the search launcher, the tray |
| [Command-line reference](CLI.md) | Every `pdfs` command, grouped by task, and scripting with `--json` | | [Command-line reference](CLI.md) | Every `pdfs` command, grouped by task, and scripting with `--json` |
| [Configuration](CONFIGURATION.md) | `config.json`, environment variables, file locations, the systemd service | | [Configuration](CONFIGURATION.md) | `config.json`, environment variables, file locations, the systemd service |
| [Troubleshooting](TROUBLESHOOTING.md) | Diagnosing problems, common symptoms, starting over, reporting a bug | | [Troubleshooting](TROUBLESHOOTING.md) | Diagnosing problems, common symptoms, starting over, reporting a bug |
@@ -16,13 +16,12 @@
| Page | What it covers | | Page | What it covers |
|---|---| |---|---|
| [Development guide](DEVELOPMENT.md) | Building, running and debugging from source; releasing | | [Development guide](DEVELOPMENT.md) | Building, running and debugging from source; common changes; releasing |
| [Architecture](ARCHITECTURE.md) | Crates, read and write paths, sync engine, control protocol, threads, threat model | | [Architecture](ARCHITECTURE.md) | Crates, the local tree, read and write paths, the queue, the sync engine, the control protocol, threads, threat model |
| [Testing](TESTING.md) | Unit tests and the FUSE acceptance suite | | [Testing](TESTING.md) | Unit tests, simulation runs and the FUSE acceptance suite |
| [Translating](TRANSLATING.md) | Adding and updating translations | | [Translating](TRANSLATING.md) | Adding and updating translations |
| [Roadmap](ROADMAP.md) | Planned work and verification still owed | | [Roadmap](ROADMAP.md) | Planned work and verification still owed |
| [Bug ledger](BUGS.md) | Every tracked defect with its analysis and status | | [Bug ledger](BUGS.md) | Every tracked defect with its analysis and status |
| [UI/UX plan](UI_UX_PLAN.md) | Design audit and notes behind the 2.x desktop app |
Also see [CONTRIBUTING.md](../CONTRIBUTING.md) and [SECURITY.md](../SECURITY.md) in the repository Also see [CONTRIBUTING.md](../CONTRIBUTING.md) and [SECURITY.md](../SECURITY.md) in the repository
root. root.
+47 -39
View File
@@ -1,8 +1,7 @@
# Roadmap # Roadmap
Planned work and open verification, as of 3.0.0. Individual defects and their status live in Planned work and open verification after 3.0.0. Individual defects and their status are in
[BUGS.md](BUGS.md), which is the authoritative ledger; this page groups what is left by theme. [BUGS.md](BUGS.md); this page groups what is left by theme. Nothing here has a date.
Nothing here is a promise of a date.
- [Sync engine](#sync-engine) - [Sync engine](#sync-engine)
- [Data safety and recovery](#data-safety-and-recovery) - [Data safety and recovery](#data-safety-and-recovery)
@@ -14,38 +13,42 @@ Nothing here is a promise of a date.
## Sync engine ## Sync engine
3.0.0 records changes made through the mount locally and sends them from the queue 3.0.0 records changes made through the mount locally and sends them from the queue
([MILESTONE-3.0.0.md](MILESTONE-3.0.0.md)). What comes after it: ([ARCHITECTURE.md §4](ARCHITECTURE.md#4-write-path)). What comes next:
| Item | State | Notes | | Item | State | Notes |
|---|---|---| |---|---|---|
| Remove the online path | Not started | The old way of sending a change before the syscall returns, and the `local_first` switch that brings it back, go in the release after 3.0.0 | | Remove the online path | Not started | The old way of sending a change inside the syscall, and the `local_first` switch that brings it back, go in the next release |
| One applier for remote changes | Not started | Phase 4 of the milestone: a remote tree, and remote changes applied through the same planner as local ones | | One applier for remote changes | Not started | Everything that comes from Drive (events, listings) changes the tree in one place, which is also the only place that tells the kernel. Replaces today's invalidation helpers and in-memory overlays |
| Synced folders on the queue | Not started | Phase 5: synced folders sent by the same planner and executor as the mount | | Synced folders on the queue | Not started | Mirrored folders feed the local tree and use the same queue as the mount; `sync_entry` becomes the record of the last agreed state. Switching a folder's mode then moves no data except to evict or fetch content |
| Batch trashes and moves | Deferred | See below | | Batch trashes and moves | Deferred | See below |
| Provisional values as types | Not started | A size or block geometry that is not known yet as its own type, so it cannot reach the kernel as if it were known |
| Deadlines on every remote call | Partly done | The drain's calls and calls a syscall waits on have one; background calls outside the drain do not yet |
**Batching trashes and moves.** `trash_nodes` and `move_nodes_streaming` take up to 150 nodes per **Batching trashes and moves.** `trash_nodes` and `move_nodes_streaming` take many nodes per call,
call, but the queue still sends one node per call. It was left out of 3.0.0 on purpose: but the queue sends one node per call. It was left out of 3.0.0 on purpose:
- The speed target of the milestone (§7) is met without it. Making 1,000 files in 100 folders - The 3.0.0 speed target is met without it. Making 1,000 files in 100 folders through the mount
takes 5.2 s on a LAN and 5.0 s on Wi-Fi. takes 5.2 s on a LAN profile and 5.0 s on a Wi-Fi profile in the simulation
- What the queue spends its time on is the creates. Each one waits for Drive to list the new node (`PDFS_SIM_MEASURE`, see [TESTING.md](TESTING.md#simulation-runs)).
before it counts as landed (250 ms, then 1 s), and batching does not help with that. - The drain spends its time on creates. Each waits for Drive to list the new node before it
- Sending many ops in one call means splitting one answer back into many results. Races in counts as landed, and batching does not shorten that.
exactly that step were the largest class of bugs found for 3.0.0 (B15x to B17x). - One call answering for many ops has to be split back into results per op. Races in exactly that
step were the largest class of bugs found for 3.0.0 (B15x to B17x in [BUGS.md](BUGS.md)).
Before building it, measure `rm -r` of a folder with 1,000 files on Wi-Fi. If that is slow, the Before building it, measure `rm -r` of a folder with 1,000 files on Wi-Fi. If that is slow, the
cheaper fix is to drop the queued trashes of the files in a folder when the folder itself is cheaper fix is to drop the queued trashes of a trashed folder's contents: trashing the folder takes
trashed, since trashing the folder takes its contents with it. them along.
## Data safety and recovery ## Data safety and recovery
| Item | State | Notes | | Item | State | Notes |
|---|---|---| |---|---|---|
| Protect queued changes at shutdown | Not started | A systemd inhibitor lock while `staging/` holds unsent data, a warning when signing out, and a blocking `pdfs sync flush`. Today the queue is visible through `pdfs sync queue`, the tray and **Sync → Overview**, but nothing stops a shutdown (B66) | | Protect queued changes at shutdown | Not started | A systemd inhibitor lock while `staging/` holds unsent data, a warning when signing out, and a `pdfs sync flush` that waits for the queue. Today the queue is visible in `pdfs sync queue`, the tray and **Sync → Overview**, but nothing stops a shutdown (B66) |
| Restore pins from the machine profile | Not started | `profile.json` records pins; `sync restore` does not apply them yet | | Restore pins from the machine profile | Not started | `profile.json` records pins; `pdfs sync restore` does not apply them yet |
| Profile backup health | Not started | A failed profile upload is a `WARN` in the log and nothing in the UI (B68) | | Profile backup health | Not started | A failed profile upload is a `WARN` in the log and nothing in the app (B68) |
| Rotated credentials survive a keyring failure | Open | Surface and retry keyring write failures so a rotated single-use refresh token is not lost (B61) | | Rotated credentials survive a keyring failure | Open | Report and retry keyring write failures, so a rotated single-use refresh token is not lost (B61) |
| Encrypt local state at rest | Not started | SQLCipher for `cache.db` and an AEAD for the content cache, keyed from the keyring. `sdk_cache.db` is already encrypted. See [ARCHITECTURE.md §8](ARCHITECTURE.md#8-threat-model-what-this-client-writes-to-disk-in-plaintext) | | Restore never picks a new device silently | Open | Ask when the machine's device is ambiguous after a loss of local state (B67) |
| Encrypt local state at rest | Not started | SQLCipher for `cache.db` and an AEAD for the content cache, keyed from the keyring. `sdk_cache.db` is already encrypted. See [ARCHITECTURE.md §10](ARCHITECTURE.md#10-threat-model-what-this-client-writes-to-disk-in-plaintext) |
## Features ## Features
@@ -53,9 +56,10 @@ trashed, since trashing the folder takes its contents with it.
|---|---|---| |---|---|---|
| File-manager integration | Not started | Context-menu actions (available offline, share link, versions) and sync badges for Nautilus, Dolphin and Thunar | | File-manager integration | Not started | Context-menu actions (available offline, share link, versions) and sync badges for Nautilus, Dolphin and Thunar |
| Terminal UI | Not started | `pdfs tui` with transfers, sync state and logs | | Terminal UI | Not started | `pdfs tui` with transfers, sync state and logs |
| Multiple accounts | Not started | Separate config, state and mountpoint per profile, one daemon each | | Multiple accounts | Not started | Separate config, state and mountpoint per account, one daemon each |
| Mount options | Not started | Read-only mount, `allow_other` | | Mount options | Not started | Read-only mount, `allow_other` |
| Symbolic links | Not started | Proton Drive has no link type; links would need a placeholder format. Until then, creating one fails with `EOPNOTSUPP` | | Symbolic links | Not started | Proton Drive has no link type; links would need a placeholder format. Until then, `symlink` fails with `EPERM` |
| `fsync` that waits for Drive | Idea | `fsync` means "durable locally". A mount option or an xattr could offer "on Drive" without slowing the default |
| Add someone else's photo to your album | Blocked | Needs `copyPhoto` in the SDK | | Add someone else's photo to your album | Blocked | Needs `copyPhoto` in the SDK |
| Photo name search | Not started | Photos search by date and filters only | | Photo name search | Not started | Photos search by date and filters only |
| Automatic photo upload from a folder | Not started | | | Automatic photo upload from a folder | Not started | |
@@ -66,34 +70,38 @@ trashed, since trashing the folder takes its contents with it.
| Item | Notes | | Item | Notes |
|---|---| |---|---|
| Saturating stream-ring accounting | `self.bytes -= …` in `crates/pdfs-fuse/src/reads.rs` should saturate so an accounting drift cannot wrap in release builds | | Saturating stream-ring accounting | `self.bytes -= …` in `crates/pdfs-fuse/src/reads.rs` should saturate, so an accounting drift cannot wrap in release builds |
| Block geometry learned one read late | The first read of a file plans on 4 MiB blocks; accepted as a bounded cost (B87) | | Block geometry learned one read late | The first read of a file plans on 4 MiB blocks; accepted as a bounded cost (B87) |
| Path-based commands under secondary mounts | `pin`, `rename`, `move`, `rm` and sharing cannot name a path inside an on-demand synced folder yet (B88) | | Path-based commands under online-only folders | `pin`, `unpin`, opening a file, uploads, sharing and versions cannot name a path inside an online-only synced folder yet. `rm`, `rename`, `mkdir` and `move` can (B88) |
| Unicode normalization | NFC and NFD names are distinct, deliberately (B36) | | Unicode normalization | NFC and NFD names are distinct, on purpose (B36) |
| Pipelined node enumeration | Faster cold listings of large folders | | Pipelined node enumeration | Faster first listings of large folders |
## Verification still owed ## Verification still owed
Fixes marked *unverified* in [BUGS.md](BUGS.md) have unit or offline coverage but have not been Fixes marked *unverified* in [BUGS.md](BUGS.md) have unit or simulation coverage but have not
driven against a live account. The larger outstanding runs: been driven against a live account. The larger runs still owed:
- **A clean account run on 3.0.0.** The last account run before the release (2026-10-04, on Wi-Fi)
failed only its cleanup, on a script bug fixed since. No full run with `--journal-check` has
passed on the released code yet.
- **Disaster-recovery drill.** Register a device, sync a folder, delete all local state, restore - **Disaster-recovery drill.** Register a device, sync a folder, delete all local state, restore
with `pdfs sync restore` and compare the tree byte for byte. See [RECOVERY.md](RECOVERY.md). with `pdfs sync restore`, and compare the tree byte for byte. See [RECOVERY.md](RECOVERY.md).
- **Second-account sharing.** Shared trees on a foreign volume, accepting invitations, changing - **Second-account sharing.** Shared folders on a foreign volume, accepting invitations, changing
roles, the viewer cases of `regression B34`, and an editor writing into someone else's share. roles, the viewer cases of `regression B34`, and an editor writing into someone else's share.
- **Fault injection.** Network loss mid-upload, a full disk during staging, and crash points - **Fault injection on a real account.** Network loss mid-upload, a full disk during staging, and
across the write publication path (after each fsync, rename and queue commit). crashes at each step of publishing a write. The simulation covers link loss and restarts against
the fake Drive, not a kill at an arbitrary point.
- **Acceptance runs across a network drop.** A case that spans an outage times out while the queue
replays, instead of being reported as interrupted.
- **CAPTCHA sign-in** against a real verification request (B8). - **CAPTCHA sign-in** against a real verification request (B8).
- **Managed live mode matrix** before each release: - **The overnight hang** (B91) is not diagnosed. The supervisor and `pdfs diagnostics` exist to
`scripts/fuse-acceptance.sh --managed-live <empty-a> <empty-b>`. See [TESTING.md](TESTING.md). catch the next one.
- **The overnight hang** (B91) is not diagnosed yet. The supervisor and `pdfs diagnostics` exist to
catch the next occurrence.
## Release assurance ## Release assurance
| Item | State | | Item | State |
|---|---| |---|---|
| Tag, workspace and PKGBUILD version agreement | Enforced by the release workflow | | Tag, workspace and PKGBUILD version agree | Enforced by the release workflow |
| Clean-install test of the `.deb` and `.rpm` in a VM | Pending (B62) | | Clean-install test of the `.deb` and `.rpm` in a VM | Pending (B62) |
| Protocol version identity between front ends and daemon | Open (B63) | | Protocol version identity between front ends and daemon | Open (B63) |
| SBOM, provenance and checksums for release artifacts | Open (B65). The RPM is GPG-signed | | SBOM, provenance and checksums for release artifacts | Open (B65). The RPM is GPG-signed |
+207 -451
View File
@@ -1,509 +1,265 @@
# Testing # Testing
The normal Rust suite exercises the metadata database, sync planner, offline Three layers check the client, from fast and offline to slow and against a real account:
queue, write staging, cache, and FUSE handler state without requiring a Proton
account:
```console | Layer | Needs | Runs in CI |
|---|---|---|
| [Unit tests](#unit-tests) | Nothing | Every push and pull request |
| [Simulation runs](#simulation-runs) | `/dev/fuse` | Every push to `main` |
| [Acceptance suite](#acceptance-suite) | Nothing for the reference run; a signed-in account for the rest | The reference run only |
## Unit tests
```bash
cargo test --workspace cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings cargo clippy --workspace --all-targets -- -D warnings
``` ```
## Filesystem API and live FUSE acceptance suite Tests sit in `#[cfg(test)]` modules next to the code. Database tests are in
`crates/pdfs-core/src/db/tests.rs`; a schema change adds one that migrates from the previous
version.
The account-free suite exercises the filesystem syscall contract against a To check the migrations against a real database, point `PDFS_MIGRATE_DB` at it and run the
temporary local directory. This validates the runner and covers creation/open ignored test. It migrates a copy taken with `VACUUM INTO` and never writes the database named:
flags, positioned and vectored I/O, truncation and sparse files, `mmap`,
`sendfile`, `copy_file_range`, durability barriers, namespace/error semantics,
`renameat2` flags, open-file lifetime, names and enumeration, readdir stability
under concurrent mutation, metadata updates, extended attributes, allocation and
punched holes, refusal semantics for unimplemented operations, concurrent I/O,
and real application workloads. It never reads application state or contacts
Proton:
```console ```bash
scripts/fuse-acceptance.sh --offline-only PDFS_MIGRATE_DB=~/.local/state/proton-drive-linux/cache.db \
cargo test -p pdfs-core a_copy_of_a_real_database -- --ignored
``` ```
Kernel/FUSE behavior and remote convergence need a real mount. With no ## Simulation runs
arguments, the script tests the account the running service is logged in to:
```console The simulation runs (`crates/pdfs-fuse/src/sim/`) start real daemons with real FUSE mounts against
scripts/fuse-acceptance.sh # every case, every mount kind an in-memory Drive, `FakeDrive`. The daemon runs as in production (drain workers, sync engine,
scripts/fuse-acceptance.sh --quick # one on-demand/mirror pair instead of four event feed, online probe, control socket); only its Drive is fake. `FakeDrive` has the faults the
real one has: listings that lag a create, renames refused for a stale name hash, no atomic
replace, delayed and reordered events, requests that stall or lose their answer, per-request
latency, and blocks of uneven size.
A run draws a sequence of steps from a seed: syscalls through a client's mount, the link going
down and up, restarts, settles (every queue must drain within a budget), and files deleted while
the drain holds a write to them. Every syscall is checked against an in-memory model of the
filesystem as it happens. At the end every link comes up and the run checks:
1. **No loss and POSIX.** Drive's copy of each client's folder is what its model says.
2. **No false conflicts.** No conflict copy anywhere; each file has one writer.
3. **Convergence.** Each mount shows Drive's tree.
4. **Liveness.** No syscall took longer than its budget, and every queue drained once the links
were up. A call that never returns is caught by a watchdog that writes every thread's stack to
`stacks.txt` in the run's state directory.
| Test | Seeds |
|---|---|
| `one_client_on_a_good_link` | 12 |
| `one_client_deleting_files_as_they_upload` | 3 |
| `one_client_with_stale_hashes_and_reordered_echoes` | 6 |
| `one_client_on_a_flaky_link` | 4 |
| `one_client_on_a_slow_link` | 4 |
| `three_clients_one_writer_each` | 3 |
`sim/daemon.rs` holds scenario tests for single bugs, run the same way.
The tests mount FUSE, so they are ignored by default:
```bash
cargo test -p pdfs-fuse --lib sim:: -- --ignored --test-threads=1 # what CI runs
PDFS_SIM_SEEDS=50 cargo test -p pdfs-fuse --lib sim::run -- --ignored --test-threads=1
PDFS_SIM_SEED=7 RUST_LOG=pdfs_fuse=debug cargo test -p pdfs-fuse --lib \
sim::run::tests::one_client_on_a_flaky_link -- --ignored --exact --nocapture
``` ```
An account run does this, in order: | Variable | Effect |
|---|---|
| `PDFS_SIM_SEEDS` | How many seeds each test runs, from 1 |
| `PDFS_SIM_SEED` | Replay one seed. A failing run prints the seed, the last steps, and where it kept its state |
| `PDFS_SIM_MEASURE` | Run `a_thousand_files_drain_as_fast_on_wifi_as_on_lan`, which times 1,000 files in 100 folders on a LAN and a Wi-Fi profile and prints the times |
1. Runs the account-free contract as the reference. A failure is a seed that can be replayed, but the daemon uses real threads, so a race may need
2. Runs the contract in a fresh `pdfs-acceptance-<id>` folder in My files and several replays. In CI, the `simulation` job uploads the stacks of a hung run as an artifact.
diffs it against the reference. After the last case it reads back a file
the run wrote and records its digest. A read that fails there, as one did
with the link down (B119), is a failed result named "a file of this run
reads back at the end", and the cleanup still runs.
3. Registers two new sync folders under `~/.cache/pdfs-acceptance/run-<id>/`
and runs the contract and the move cases in every on-demand/mirror pairing.
4. Removes everything it created, locally and remotely, and reports a
`cleanup` result. The run fails if anything is left behind.
Cleanup identifies remote folders by uid, never by name alone, so nothing of ## Acceptance suite
yours is touched. It removes each sync folder with `sync rm --delete-remote`,
trashes the My files folder, and then deletes both permanently from the trash.
It only counts a folder as gone once a fresh trash listing no longer shows it.
It then reads `pdfs sync queue` and counts any queued op that still names one
of its folders as left behind too, since such an op keeps its staged bytes and
retries forever (B102).
Ops that were queued before a target starts are not the run's. The run prints `scripts/fuse-acceptance.sh` checks the filesystem contract a program sees: creation and open
one `NOTE` line naming them, and the queue waits ignore them. They are told flags, positioned and vectored I/O, truncation and sparse files, `mmap`, `sendfile`,
apart by id and queue time together, since SQLite can give a new op the id of `copy_file_range`, durability barriers, renames and their `renameat2` flags, open-file lifetime,
one that has drained. names, `readdir` under concurrent change, metadata, extended attributes, holes, refusals of
unsupported operations, concurrent I/O, and real application workloads.
A write is queued when the kernel releases the file, and `close(2)` returns > [!CAUTION]
without waiting for that. A case that reads `pdfs sync queue` right after > Every mode except `--offline-only` creates and deletes real files in Proton Drive. Use a test
writing therefore waits for the ops it expects (`Daemon.queued`) instead of > account with nothing you cannot lose.
reading once. "regression B102" saw one revision of two before it did.
Cleanup also runs on a failure, Ctrl-C, `SIGTERM` or `SIGHUP`. While it runs, ### Modes
two more interrupts are ignored; a third abandons it.
Each run records what it is about to create in ```bash
`~/.cache/pdfs-acceptance/run-<id>/manifest.json` before it creates it, and scripts/fuse-acceptance.sh --offline-only # the reference run; no account
holds a lock on that directory. If a run dies without cleaning up (`kill -9`, a scripts/fuse-acceptance.sh # the signed-in account, every mode pairing
hard timeout, a power cut), the next run finds the unlocked manifest and scripts/fuse-acceptance.sh --quick # one on-demand/mirror pairing instead of four
finishes that cleanup first. It then removes stray `pdfs-acceptance-<32 hex>` scripts/fuse-acceptance.sh --live /mnt/testmount # mounts you set up yourself
folders that no live run claims: in My files (older than scripts/fuse-acceptance.sh --managed-live /mnt/a /mnt/b
`PDFS_ACCEPTANCE_REAP_AGE`, default one hour), under the acceptance home, and
in the trash. A run does not start while that reap leaves anything behind.
The run creates and destroys real remote folders, so a dedicated test account
without irreplaceable data is still the safest target.
`--live PATH...` tests mounts you set up yourself and leaves the sync folder
configuration alone. It runs destructive POSIX operations only below a fresh
`pdfs-acceptance-*` directory, refuses paths that are not FUSE mounts, and
removes its directory on success, failure, or interruption:
```console
scripts/fuse-acceptance.sh --live /mnt/on-demand-testmount
``` ```
**The reference run** always runs first, against a temporary local directory. It records the
facts each case establishes, and every mount is then **compared** against those facts. A case
fails when the mount differs from an ordinary filesystem, not only when it raises. A fact that may
legitimately differ (inode numbers, block counts, which optional syscalls exist) is **noted**
instead, and listed under the target's `accepted` line. The choice is made in the test body, so
the accepted differences are visible where they are accepted. Prefer `record`; use `note` only
for something a network filesystem cannot do.
**An account run** (no mode):
1. Runs the reference.
2. Runs the contract in a new `pdfs-acceptance-<id>` folder in My files, and at the end reads back
a file the run wrote.
3. Registers two sync folders under `~/.cache/pdfs-acceptance/run-<id>/` and runs the contract and
the move cases in every pairing of on-demand and mirror. Before a folder switches to on-demand,
a new file is written into it; once mounted, the file must be listed with its bytes. In every
pairing each folder must still hold the file written into it at setup.
4. Removes everything it made, locally and on Drive, and reports `cleanup`. A run that leaves
anything behind fails.
Cleanup finds remote folders by uid, never by name, so nothing of yours is touched. It removes
each sync folder with `sync rm --delete-remote`, trashes the My files folder, deletes both from
the trash for good, and checks that a fresh trash listing no longer shows them. Queued ops that
still name one of its folders count as left behind. Cleanup also runs on a failure, Ctrl-C,
`SIGTERM` or `SIGHUP`; while it runs, two more interrupts are ignored and a third abandons it.
Each run writes what it is about to create to `~/.cache/pdfs-acceptance/run-<id>/manifest.json`
first and holds a lock on that directory. A later run finishes the cleanup of a run that died, and
removes stray `pdfs-acceptance-<32 hex>` folders no live run claims: in My files (older than
`PDFS_ACCEPTANCE_REAP_AGE`, default one hour), under the acceptance home, and in the trash. It
does not start while anything is left.
Ops that were queued before the run started are not the run's: it prints one `NOTE` line naming
them, and its queue waits ignore them.
**`--live PATH...`** runs the contract on mounts you set up, and leaves the sync configuration
alone. It works only below a new `pdfs-acceptance-*` directory, refuses paths that are not FUSE
mounts, and removes its directory afterwards. The first path must be a FUSE mount; later paths may
be another mount or a mirrored folder. If all paths show the **same remote folder**, set
`PDFS_ACCEPTANCE_CONVERGENCE=1` to also check that bytes written through the first appear through
the others.
**`--managed-live A B`** takes two existing, empty, unmounted directories, registers them as sync
folders, and runs every pairing as an account run does, then deletes the remote folders. It
refuses non-empty paths, mounts, and paths already in `pdfs sync list`.
### Reading the output ### Reading the output
A run opens with what it is about to test: the mode, the `pdfs` binary and its A run opens with the mode, the `pdfs` binary and version, Python, the kernel, the per-case limit
version, Python and the kernel, the per-case limit, the options and the start and the options. Each target then names its root, the storage behind it, the daemon and the
time. Each target then names its root, the storage behind it, the daemon it number of cases, followed by one line per case:
talks to and how many cases it runs, followed by one line per case:
``` ```
==> live FUSE /mnt/testmount ==> live FUSE /mnt/testmount
-> root /mnt/testmount/pdfs-acceptance-… -> root /mnt/testmount/pdfs-acceptance-…
-> storage FUSE mount /mnt/testmount (fuse.protondrive) -> storage FUSE mount /mnt/testmount (fuse.protondrive)
-> daemon pdfs 2.8.3, user@proton.me, online, 0 queued ops -> daemon pdfs 3.0.0, user@proton.me, online, 0 queued ops
-> cases 45 of 45 -> cases 45 of 45
[ 1/45] creation and open flags .................................. ok 0.41s [ 1/45] creation and open flags .................................. ok 0.41s
… …
[21/45] throughput floors ........................................ ok 48.20s [21/45] throughput floors ........................................ ok 48.20s
write 31 MiB/s, read 54 MiB/s, 4 KiB writes 12.0 MiB/s, metadata 9 ops/s write 31 MiB/s, read 54 MiB/s, 4 KiB writes 12.0 MiB/s, metadata 9 ops/s
… …
[43/45] regression B115: pdfs ls reports a file's real size ...... ok 6.12s
-> reference matches the ordinary filesystem in 412 compared observations -> reference matches the ordinary filesystem in 412 compared observations
-> result 44 passed, 1 skipped in 11m02s -> result 44 passed, 1 skipped in 11m02s
``` ```
A case ends as `ok`, `FAIL`, `TIMEOUT`, `skip` when it does not apply to the A case ends as `ok`, `FAIL`, `TIMEOUT`, `skip` (it does not apply to the target), or `known` (an
target (no daemon, no share at that role), or `known` when an `open B<n>` case `open B<n>` case reproduced a bug that is still open). Dimmed lines under a case hold its details.
reproduced a bug that is still open (see below). The dimmed lines under a case A failed `regression B<n>` case points to its entry in [BUGS.md](BUGS.md).
are its details: the reason for a skip, measured rates, a hash. A failed
`regression B<n>` case also points to its entry in `docs/BUGS.md`. When a case
prints something itself, such as a service restart, the case line is printed
again once the case ends.
The run ends with a table of every target's counts and time, the report paths, The run ends with a table of every target, the report paths, the slowest cases, cases over
the slowest cases, the cases over `--budget`, the known issues that still `--budget`, known issues that still reproduce, errors the daemon logged, and each failure. The
reproduce, any errors the daemon logged, and each failure with its target. The last line is `PASS:` or `FAIL:` with the totals. Colour is used only on a terminal and never with
last line is `PASS:` or `FAIL:` and the totals. Colour is used only on a `NO_COLOR` set.
terminal and never with `NO_COLOR` set.
### Edge cases and performance ### What the cases cover
Besides the syscall contract, the suite covers: Besides the syscall contract:
- **Block boundaries.** Files of 0 bytes up to two 4 MiB blocks plus 3 bytes, - **Block boundaries.** Files from 0 bytes to two 4 MiB blocks plus 3 bytes, every size next to a
including every size next to a boundary, then overwrites that grow, shrink boundary, and overwrites that grow, shrink and reshape them.
and reshape them. - **Unusual names.** 255 bytes, NFC and NFD forms of the same text, newline, tab, emoji,
- **Unusual names.** A 255-byte name, NFC and NFD forms of the same text, characters Windows forbids, leading and trailing spaces, a case-only rename.
newline, tab, emoji, characters Windows forbids, leading and trailing - **A deep tree and a wide folder.** 24 levels renamed at the top; 128 entries created
spaces, and a case-only rename. concurrently, unlinked concurrently, then `rm -rf`.
- **A deep tree and a wide directory.** 24 levels of nesting renamed at the - **Rename patterns.** Swaps, chains, log rotation, replacing a file under an open reader, the
top; 128 entries created concurrently, concurrent unlinks, then `rm -rf`. `EISDIR`, `ENOTDIR` and `ENOENT` refusals.
Online, each create is a round trip to the server, and the kernel runs - **Handle coherency.** Unsaved writes seen by a second handle, two `O_APPEND` writers, 30 rewrite
creates in one directory one at a time. So the wide case gets twice the generations, a truncate under a reader.
per-case timeout and notes the time per create as `wide.create_ms`. - **Throughput.** 64 MiB written and read, 1 MiB in 4 KiB writes, metadata on 100 files. The case
- **Rename patterns.** Swaps, chains, log rotation, replacing a file under an fails below `PDFS_ACCEPTANCE_MIN_MIBPS` (default 10) or `PDFS_ACCEPTANCE_MIN_OPS` (default 20
open reader, the `EISDIR`/`ENOTDIR`/`ENOENT` refusals, and a folder moved out operations per second, 3 on a FUSE mount). `0` turns a floor off.
and back. - **Operations the filesystem does not implement.** `git`, `rsync`, `cp -a`, `tar` and installers
- **Handle coherency.** Unsynced writes seen by a second handle, two `O_APPEND` probe for symlinks, hard links, FIFOs, device nodes, locks, `O_DIRECT` and `SEEK_HOLE`. Each must
writers, 30 rewrite generations, and a truncate under a reader. work or be refused cleanly: a known errno, never `EIO`, never a hang.
- **Throughput.** 64 MiB written and read sequentially, 1 MiB in 4 KiB writes, - **Moves between locations** (`managed` in `--list`). Trees moved between two sync folders in
and metadata operations on 100 files. The rates are printed. The case fails both directions, several sources in one `pdfs move`, and a tree through My files and out again.
below `PDFS_ACCEPTANCE_MIN_MIBPS` (default 10) or `PDFS_ACCEPTANCE_MIN_OPS` The refusals must move nothing and lose nothing: a name already taken, a folder into itself, a
(default 20 operations per second, or 3 on a FUSE mount, where each create mirrored source holding something Drive lacks, a file still open for writing.
and unlink waits for Drive before it returns). - **The CLI.** `pdfs` into a closed pipe exits 0 with nothing on stderr; `pdfs mkdir`, `rename`
and `rm` by local path, and their refusals.
### The mount is diffed against an ordinary filesystem ### Regressions and open bugs
The local reference run is not only a self-test of the runner. Every case A case named `regression B<n>` reproduces an entry of [BUGS.md](BUGS.md) so it stays fixed. Most
records the facts it established, and a live run is compared against those need a live mount and a reachable daemon, and are skipped without one. `--list` prints every case
recordings, so a case fails when the mount *differs* from an ordinary and the targets it runs on.
filesystem — not only when it raises. The target's `reference` line names the
exact key:
``` A regression case checks the outcome **on Drive**, so it waits for the queue first. Reading back
-> reference 1 observation of 412 differs from the ordinary filesystem: through the mount proves nothing: the local caches return the bytes just written whether or not
! creation and open flags.truncated.size: live FUSE /mnt/testmount=6 reference=0 they reached Drive. A write is queued when the kernel releases the file, after `close(2)` has
``` returned, so a case that reads `pdfs sync queue` right after writing waits for the ops it expects.
Facts are recorded in one of two ways, chosen in the test body: A case named `open B<n>` checks a bug that is still open. While it reproduces, the case ends as
`known`: listed at the end, but not a failure. When it passes, the bug is fixed: rename the case to
`regression B<n>` and make it fail with `check`. No case is open at the moment.
- **compared** — a claim about filesystem semantics. The mount must agree. Some cases need what a test account cannot provide alone and skip without it: B34 needs a folder
- **noted** — context that legitimately differs: inode numbers, block counts, shared read-only by a **second account**, B34b one shared with editor rights, and B79 an on-demand
reported mode bits, and which optional syscalls exist at all. Listed under synced folder.
the target's `accepted` line, never a failure.
Because the choice is made at each call site, the set of accepted divergences ### Timeouts, reports and hung mounts
is visible in the test that accepts it. There is no separate allowlist to drift
out of date. When adding a case, prefer `record`; reach for `note` only when an
ordinary filesystem can do something a network filesystem genuinely cannot.
### Operations the filesystem does not implement A hung FUSE call cannot be interrupted from Python. Each case runs under a soft alarm (`--timeout`,
default 180 s, or `PDFS_ACCEPTANCE_TIMEOUT`) that reports the case and lets cleanup run, backed by
a hard stop that dumps every thread's stack and exits. After a timeout the run checks whether the
mount still answers a `stat` and a listing within 15 s. If it does, the target goes on; if not, its
remaining cases are skipped. Cases that make many round trips by design get twice the limit.
`git`, `rsync`, `cp -a`, `tar`, `df` and most installers probe for symlinks, | Option | Effect |
hard links, FIFOs, device nodes, `statfs`, file locks, `copy_file_range`, |---|---|
`O_DIRECT` and `SEEK_HOLE` whether or not a filesystem offers them. The suite | `--report-json PATH` | Every result and recorded observation. Attach it to a bug report |
asserts that each either works or is *refused cleanly* — a recognised errno, | `--report-junit PATH` | JUnit XML. `known` is reported as skipped |
never `EIO` and never a hang. A refusal that is merely unimplemented is fine; | `--budget SECONDS` | Flags cases that pass but take longer (`PDFS_ACCEPTANCE_BUDGET`) |
one that is dirty breaks software that never asked for the feature. | `--journal-check` | Fails the run if `proton-drive.service` logged an error during it, or the warnings "queued write conflicts; keeping a conflict copy" and "a fuse worker has held the same job for a long time" |
| `--durability` | Restarts the service mid-suite and checks the bytes written before it. Proves the queue survives a restart. `PDFS_ACCEPTANCE_UNIT` names another unit |
### Regressions from the bug ledger | `--fail-fast` | Stops at the first failure |
| `--list` | Prints every case and its targets |
Cases named `regression B<n>` reproduce a bug from `docs/BUGS.md` so it stays
fixed. B69 (identical rewrite must not fork a `(sync-conflict …)` copy), B70 (an
in-flight `.crdownload` must not be sealed as a revision) and B74 (a file
renamed after close must keep its content) need a real mount and a reachable
daemon; they are skipped, not failed, without one.
**B100** (a copied tree reads back at its exact sizes) copies 48 files of
assorted sizes into the mount at once, as `cp -r` does. On a live mount it
waits for the queue, checks every size and byte, runs `pdfs refresh` on each
folder to drop the cached listing, and checks again. The refresh is what
brought back the provisional sizes the bug reported.
**B101** (a write during its create's upload reaches Drive) holds new files open for
different times before writing, so their empty creates land before, during and after the
bytes arrive, then checks every file after the queue drains.
**B113** (moving a file before its upload makes no conflict) pauses sync with
`pdfs sync pause --for 10m` and writes three files. While their uploads are held,
it renames one, moves one into a folder, and renames and moves the third. It then
resumes sync, waits for the queue, and checks the bytes and names and that no
conflict copy appeared. It skips when sync was already paused. If the run dies
before the resume, the pause ends by itself after ten minutes.
**B88** (`pdfs mkdir`, `rename` and `rm` by local path) creates, renames and
removes a folder and a file by their paths in the mount. Then it checks the
refusals: `pdfs rename` of the mount's root to its own name, and `pdfs rm` of a
file outside every mount. `pdfs rm` of the root itself is tried only on a sync
folder the run created. In a mirrored sync folder all three commands must be
refused instead, and nothing may change.
**pdfs output into a closed pipe** runs `pdfs ls`, `pdfs --json ls` and
`pdfs status` with stdout on a pipe whose reader is already gone. Each must exit
0 with nothing on stderr, as `pdfs ls | head -1` needs.
**B102** (removing a folder drops the uploads queued inside it) uploads a file
into each of two nested folders, pauses sync and rewrites both. While the
revisions are held, it removes one folder with `pdfs rm` and the other through
the mount. Neither revision may stay queued, and neither folder may come back
after the resume. It skips when sync was already paused.
**B94** (a deleted `.part` file leaves nothing queued) writes a `.part` file,
checks that its create is parked, and unlinks it. The parked op must leave the
queue within 30 seconds. The sweep that lets an hour-old park through is not
driven.
**B95** (a `.tmp` file uploads at once) writes `index.tmp` and `preview.temp`
and waits for the queue, which fails at once on a parked op. `pdfs ls` must
list both, and their bytes must read back.
**B111** (renaming a file straight back works) renames a file there and back
eight times through the mount. On a mount it also does so three times with
`pdfs rename`, after the queue is empty, so the renames race nothing but Drive.
It runs against the reference too.
**B121** (a file renamed and moved at once lands) renames a file, moves it into
a subfolder and renames it back across folders through the mount, with no pause
between the steps. On a mount it then runs `pdfs rename` and `pdfs move` back to
back. The file must end up where it was sent, with its bytes, and the case
reports how long each half took. It runs against the reference too.
**B98** (an upload superseded by a write ends) writes 64 MiB and waits for its
upload to show in `pdfs transfers`, then appends to the file. The queue must
drain, the old upload must leave the transfer list, and the file must hold
every byte with no conflict copy. It skips when the upload is never seen on
the wire, and it gets twice the per-case timeout.
**B80** (`pdfs search` finds a new file after a rewrite) writes a file whose
name carries a unique token, waits for the queue, rewrites it and waits again.
`pdfs search` must find it within 30 seconds.
**B48** (an emptied folder can be removed at once) writes twelve files into a
folder, lists it, and moves one more in from elsewhere. It unlinks every
child, runs `pdfs refresh` on a mount, and checks that nothing is listed and
that `rmdir` works. It runs against the reference too.
**B114** (a name `pdfs rm` or `rename` took away stops resolving) looks up two
files, removes one with `pdfs rm` and renames the other with `pdfs rename`. At
once, neither old name may be listed or resolve.
**B115** (`pdfs ls` reports a file's real size) writes files of 6 bytes, 4097
bytes and one block plus 7, and compares the sizes `pdfs ls` lists with the
written ones, after the upload and again after `pdfs refresh`.
**B116** (trashing a folder drops the creates queued in its subfolders) makes
`b116/sub/deeper` and waits for Drive. With sync paused, it writes a `.part`
file there and renames it to its final name, so its create is due but held.
It then runs `pdfs rm b116`, and the create must be gone from the queue. It
restores the folder with `pdfs restore` before it resumes sync. If the file
ends up in the mount's root anyway, the case fails and moves the file to the
trash.
**B117** (a moved file's queued upload keeps its path) rewrites an uploaded
file with sync paused and moves it with `pdfs move`. `pdfs sync queue` must
list the upload under its new path, and the journal must not show the upload
put off with "pending operation deferred". The bytes must arrive at the new
path with no conflict copy. Without `journalctl`, only the queue is checked.
**B119** (an uploaded file stays cached) writes a small file and one of a block
plus 119 bytes, waits for the queue, and expects `pdfs ls` to list both as
cached, so reading them does not need the network.
B74 was found by exactly this mechanism: the B70 case failed, and narrowing it
produced a smaller reproduction that got its own case. The defect is fixed and
live-validated (2026-07-26); the case now guards against its return.
**B79** (an on-demand device folder accepts writes) discovers its target from
`pdfs locations --json` and skips when this machine has no mounted on-demand
folder. It is the cheap standing guard against a *cross-mount* permission
regression: the primary mount holds device-folder nodes too, and the queue guard
intersects what every live inode space says about a uid, so a fail-closed
classification in one mount denies writes in another.
**B34** (a viewer-role share is read-only) and **B34b** (an editor-role share
still writes) locate their subject through `pdfs shared-with-me --json`, which
now carries `role` and a mount-relative `path`. Both skip when the account has no
accepted share at that role — B34 needs a **second account** to have shared a
folder read-only, which is the one part of the sharing suite that cannot be
self-served. B34b degrades gracefully in the other direction: a shared *file*
gets its mode bits and `W_OK` checked but is never written to, because it is
someone else's document.
B34's fourth assertion is the load-bearing one: `pending_uploads` and
`pending_changes` must be **unchanged** across the refused writes. Mode bits
alone would pass a fix that still admitted the write into `pending_op`, which is
the actual harm — a background drain failing 403 forever.
**Synthetic `Shared with me/` directory contract** asserts the virtual directory
enumerates, reports `0555`, and refuses `mkdir`/`rmdir`/`rename`. It skips when
the directory is absent (an account with no accepted shares).
A regression case asserts against the *remote* outcome, so it must wait for the
queue to drain before reading. Reading straight back through the mount proves
nothing: the local attribute and content caches will happily return the bytes
that were just written even when nothing reached Drive, which is precisely how
B74 stayed invisible.
### Bugs that are still open
Cases named `open B<n>` check a bug that `docs/BUGS.md` still lists as open.
While the bug reproduces, the case ends as `known`: it is listed at the end of
the run, but does not fail it. So a known bug does not hide new failures, and
the run notices when a fix lands, because the case then passes. Rename it to
`regression B<n>` then, and fail it with `check` instead of `KnownIssue`. No
bug is open at the moment. B114 to B117 started as such cases.
### What a run verifies
A full account run with `--journal-check` checks these `docs/BUGS.md` entries,
many of them fixed but not yet verified:
- B48, B80, B94, B95, B98, B100, B101, B102, B111, B114, B115, B116, B117, B119
and B121, each with its own `regression B<n>` case.
- B118, with "application workloads" and "block boundaries", which failed on
it. It needs Drive to be slow to list a new node, so a clean run is weak
evidence.
- B120, with `--journal-check`, but only in a run that goes offline while it
writes. A mkdir made online goes to Drive at once, so only then does a
folder land from the queue.
- B86, with the mode-switch check in the full matrix.
- B104, B106, B108 and B109, with the cases that found them: "rename
patterns", "application workloads" and the account run's own setup.
B105, B107 and B110 are races. The suite hit each of them once, so a clean run
is weak evidence for them. `--journal-check` reports B110's stall warning, and
every case that writes checks for B105's conflict copies.
The suite does not drive:
- the GUI and the photo gallery (B81, B92, B93, B97);
- states it cannot set up: a `parent_uid` cycle (B82), staged bytes that name
no node (B96), a stuck trash refresh (B103), conflict copies left from an
earlier start (B99), a foreign FUSE mount in the local-file index (B90),
revisions without 4 MiB blocks (B85), and several shares from one owner
(B112);
- B94's hour-long expiry of a park;
- load and failure injection (B22, B27, B30, B37, B42).
Check those by hand, or with the unit tests their entries name.
### Timeouts, reports, and hung mounts
A wedged FUSE operation cannot be interrupted from Python, so each case runs
under a soft alarm (`--timeout`, default 180s) that reports the case and lets
cleanup run, backed by a hard stop that dumps every thread's stack and exits.
That backstop is the only way to learn *where* a mount hung. After a timeout the
run checks whether the mount still answers a `stat` and a listing within 15
seconds. If it does, the target goes on with its next case, so one slow stretch
of the link does not cost every later case. If it does not, the target's later
cases are not run: against a wedged mount they produce noise, not information.
Cases that make many round trips by design ("application workloads", "unusual
but legal names", "a wide directory" and "regression B98") get twice the limit.
```console
scripts/fuse-acceptance.sh --live /mnt/testmount \
--timeout 300 --report-junit results.xml --budget 30
```
`--report-json` also writes every recorded observation, which is what to attach
to a bug report. A known issue has the status `known` there; JUnit has no such
status, so `--report-junit` reports it as skipped. `--budget SECONDS` flags cases that pass but got slower —
B5 was a 186 ms-per-call regression that a pass/fail suite could not see.
`--list` prints every case and which targets it runs against, and `--fail-fast`
stops at the first failure.
`--journal-check` fails the run if `proton-drive.service` logged any error while
it ran. A suite can pass every assertion while the daemon logs failures behind
it; that has happened, and it was caught only by reading the journal by hand.
Two warnings count as errors too: "queued write conflicts; keeping a conflict
copy" (how B113 showed) and "a fuse worker has held the same job for a long
time" (how B110 showed).
With `--live`, abandoned `pdfs-acceptance-*` roots from a crashed run are
removed at startup, but only if they match the exact generated name and are
over an hour old (`PDFS_ACCEPTANCE_REAP_AGE`), so a concurrent run is never
disturbed. An account run instead uses the manifest and reaper described
above.
### Automated mode matrix
An account run drives this matrix itself, in folders it creates. To use
directories of your choice instead, the managed live runner accepts two
existing, empty, unmounted directories. It
registers both as new sync folders, waits for initial synchronization, then
tests all four pairs in order: on-demand/on-demand, on-demand/mirror,
mirror/on-demand, and mirror/mirror. Every transition must become idle, and an
on-demand folder must actually appear as a mount before its contract runs.
```console
mkdir -p /mnt/pdfs-test-a /mnt/pdfs-test-b
scripts/fuse-acceptance.sh --managed-live /mnt/pdfs-test-a /mnt/pdfs-test-b
```
This creates remote folders and permanently deletes those test remotes during
cleanup. It refuses non-empty paths, existing mounts, and paths already present
in `pdfs sync list`. Use `PDFS_ACCEPTANCE_SYNC_TIMEOUT` to extend transition
timeouts and `PDFS_ACCEPTANCE_PDFS` to select a non-installed CLI binary, for
example `target/debug/pdfs`.
After the contract, each mode pair also runs the move cases (`--list` shows
them as `managed`). They move trees between the two folders in both
directions, move several sources in one `pdfs move`, and pass a tree through
My files and out again. Every case waits for Drive before and after the move.
It then checks that the tree is only at the destination, that every byte
arrived, and that neither side made conflict copies. The refusal cases prove
that nothing moves and nothing is lost when:
- the destination already has the name;
- a folder would move into itself;
- a mirror source holds something Drive lacks (a symlink);
- a file in an on-demand source is still open for writing.
The My files case writes a test folder into your real My files and removes it
afterwards. It skips when the daemon reports no My files mount, or when the
locations are on different volumes.
Before a mirror folder switches to on-demand, the runner writes a new file into
it and syncs. Once the folder is mounted, the file must be listed with its
bytes (B86). Each switch reports a `regression B86` result under a separate
`mode switches` target. The bug only shows in a folder that was mounted in an
earlier pairing, because that mount's listing is stale. That happens in the
third pairing, so `--quick` runs the check but does not replay the bug.
In every pairing, each folder must still hold the file the runner wrote into it
at setup, byte for byte. These checks report under `mode switches` as well. A
switch to on-demand frees the local copy, so these reads go to Drive. If the
link drops, the daemon fails such a read with EIO once its two-minute read
budget runs out. That is correct, so the runner waits until the daemon reports
online again, tries once more and notes the retry under the result. Only the
second try decides the result.
### Durability drill
`--durability` restarts `proton-drive.service` in the middle of the suite and
re-verifies the bytes written before it. `staging/` and `recovery/` can hold the
only copy of a file whose upload has not finished, so this is the case that
proves the queue is persistent rather than in-memory. It is opt-in because it
interrupts a running daemon; `PDFS_ACCEPTANCE_UNIT` selects a different unit.
### Selecting cases ### Selecting cases
During development, `PDFS_ACCEPTANCE_ONLY` runs tests whose descriptive name `PDFS_ACCEPTANCE_ONLY` runs the cases whose name contains the text, ignoring case. Separate several
contains the supplied text, ignoring case (for example with commas (`PDFS_ACCEPTANCE_ONLY=B116,B117`). A filter that matches no case at all fails, as the
`PDFS_ACCEPTANCE_ONLY=namespace`). Separate several texts with commas to run typo it is. A release run leaves it unset.
every case that matches any of them (`PDFS_ACCEPTANCE_ONLY=B116,B117`). A
release acceptance run must leave it unset so the complete contract executes.
A filter naming a live-only case (every `regression B<n>` is one) selects nothing | Variable | Effect |
in the account-free reference run, which is not an error — that target simply has |---|---|
nothing to do. Only a filter matching *no case at all* fails, as the typo it is. | `PDFS_ACCEPTANCE_PDFS` | The `pdfs` binary to use, for example `target/debug/pdfs` |
| `PDFS_ACCEPTANCE_SYNC_TIMEOUT` | Seconds a sync folder may take to settle after a change of mode |
| `PDFS_ACCEPTANCE_REAP_AGE` | Age in seconds after which a stray `pdfs-acceptance-*` folder is removed (default 3600) |
| `PDFS_ACCEPTANCE_CONVERGENCE` | `1` checks that several `--live` paths show the same bytes |
| `PDFS_ACCEPTANCE_MIN_MIBPS`, `PDFS_ACCEPTANCE_MIN_OPS` | Throughput floors |
| `PDFS_ACCEPTANCE_UNIT` | The systemd unit `--durability` restarts |
Passing more paths runs the same filesystem suite independently against each. ## Before a release
The first path must be the FUSE mount under test; later paths may be another
FUSE mount or a normal local mirror filesystem:
```console 1. The quality gates and the simulation runs pass in CI.
PDFS_ACCEPTANCE_SYNC_TIMEOUT=180 \ 2. A full account run with `--journal-check` passes on a test account:
scripts/fuse-acceptance.sh --live /mnt/on-demand-testmount /mnt/testmount `scripts/fuse-acceptance.sh --journal-check`.
``` 3. Fixes marked *unverified* in [BUGS.md](BUGS.md) that a run now covers are marked verified.
If two paths really are views of the **same remote folder**, enable an What no automated run covers yet is listed under
additional byte-for-byte convergence check explicitly: [Verification still owed](ROADMAP.md#verification-still-owed).
```console
PDFS_ACCEPTANCE_CONVERGENCE=1 \
scripts/fuse-acceptance.sh --live /mnt/first-view /mnt/second-view
```
Do not enable that flag merely because two sync folders belong to the same
account. Their configured remote UIDs must be identical.
Normally the live suite applies the same API contract independently to each
supplied mount. In convergence mode it exercises the primary once and verifies
that the resulting bytes appear through every secondary view. A pass is
strong evidence for those paths, not a blanket data-loss guarantee. Before a
release, also run the recovery drills in `docs/RECOVERY.md` and inspect
`docs/BUGS.md` for live-verification items.
The runner accepts already-mounted paths instead of starting a daemon. This
keeps authentication and keyring setup explicit and prevents it from silently
reusing or replacing the normal application state directory.
+33 -7
View File
@@ -72,6 +72,20 @@ pdfs status
unfamiliar network. The CLI cannot show it. Sign in with `pdfs-app`, which displays the challenge; unfamiliar network. The CLI cannot show it. Sign in with `pdfs-app`, which displays the challenge;
the stored session then works for the CLI too. the stored session then works for the CLI too.
**"The verification page couldn't be opened"** — WebKit, which shows the CAPTCHA page, could not
start its sandbox. Hardened systems such as secureblue keep its bubblewrap from the journal socket,
and the log then shows `bwrap: Can't get type of source /run/systemd/journal/socket: Permission
denied` and `Failed to fully launch dbus-proxy`. Check that `bwrap` and `xdg-dbus-proxy` are
installed. If they are, sign in once with the sandbox switched off for this one run:
```bash
WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 pdfs-app
```
Quit the app first, so this starts a new instance. The page then runs without WebKit's isolation,
so use this only for the sign-in. The stored session keeps working without it. Signing in from
your usual network, without a VPN, often avoids the CAPTCHA in the first place.
**Sign-in works but is forgotten after a restart** — no Secret Service provider is running, or its **Sign-in works but is forgotten after a restart** — no Secret Service provider is running, or its
keyring is locked. Install and unlock GNOME Keyring, KWallet or KeePassXC (with Secret Service keyring is locked. Install and unlock GNOME Keyring, KWallet or KeePassXC (with Secret Service
enabled). `pdfs diagnose` shows `keyring session` as failing in that case. enabled). `pdfs diagnose` shows `keyring session` as failing in that case.
@@ -113,29 +127,41 @@ pdfs refresh Documents
**"Permission denied" when writing** — the item is in a share where you are a viewer, or under **"Permission denied" when writing** — the item is in a share where you are a viewer, or under
`Shared with me/`, which is read-only as a folder. Check your role with `pdfs shared-with-me`. `Shared with me/`, which is read-only as a folder. Check your role with `pdfs shared-with-me`.
**A program says "Operation not supported"** — Proton Drive cannot store symbolic links, hard **`ln` or another program fails with "Operation not permitted"** — Proton Drive cannot store
links, device files or FIFOs. The error is intentional. Use a synced folder for symbolic links, hard links, device files or FIFOs, so creating one fails at once. The error is
projects that need them, or exclude those paths. intentional. Use a synced folder for projects that need them, or exclude those paths.
## Uploads and sync ## Uploads and sync
**Something is stuck uploading** — list the queue and retry: **A change shows here but not on other devices** — changes are recorded on this computer first
and sent from a queue. List what is still waiting, and what needs attention:
```bash ```bash
pdfs sync queue pdfs sync queue
pdfs sync issues
```
The app shows the same on **Sync → Overview**. A change waits while the service is offline or
sync is paused (`pdfs status`), and backs off after a failure. To retry at once:
```bash
pdfs sync retry # every failed operation pdfs sync retry # every failed operation
pdfs sync retry 42 # one operation pdfs sync retry 42 # one operation
``` ```
The app shows the same list on **Sync → Overview**. An operation that keeps failing shows its A change Proton Drive refuses (storage full, access taken away, folder gone) is listed by
error there. A change Proton Drive will not take can be dropped instead, which undoes it here; `pdfs sync issues` with what to do. It stays queued until you fix the cause or drop it. Dropping
save a file's content first if you want to keep it: it undoes it here; save a file's content first if you want to keep it:
```bash ```bash
pdfs sync export 42 ~/Desktop/ pdfs sync export 42 ~/Desktop/
pdfs sync discard 42 pdfs sync discard 42
``` ```
If you suspect the local-first behaviour of 3.0.0 itself, `"local_first": false` in `config.json`
makes the Proton Drive folder wait for Proton Drive again while online (see
[CONFIGURATION.md](CONFIGURATION.md#local_first)), and please report the problem.
**A file in a synced folder does not upload** — check that: **A file in a synced folder does not upload** — check that:
- it is not matched by `.pdfsignore` or `ignore_patterns`; - it is not matched by `.pdfsignore` or `ignore_patterns`;
-354
View File
@@ -1,354 +0,0 @@
# UI/UX audit and improvement plan
Audit date: 2026-09-23. Scope: `pdfs-gui` (all pages, dialogs, photo viewer), `pdfs-tray`, and the
backend capabilities the UI would need. File references are to `crates/pdfs-gui/src/app/` unless
stated otherwise.
## 1. Summary
The client is functionally rich but the UI is built page by page, and it shows:
- **No sync control surface.** There is no way to pause or resume sync, no list of what is stuck or
failing, and no conflict overview. The backend has no pause at all, no per-op listing, and no
per-file conflict record (conflicts exist only as `(sync-conflict <ts>)` files plus activity rows).
The GUI reads `online`, `pending_uploads` and `pending_changes` from `Status` and ignores
`failing_ops`, `failing_error`, `parked_uploads` and `staged_*`.
- **Information architecture is scattered.** Live transfers are a group on *Settings* while the
*Activity* page is history only. *Locations* and *Computers* point at each other. Import lives under
Settings but is launched from Gallery and returns to Settings. Sign-out, quota, mount status,
pinned files, import and developer info all share the Settings page.
- **Chrome is non-standard.** Every page draws its own title and toolbar row under an almost empty
`AdwHeaderBar` whose title is always "Proton Drive". Two stacked bars, wasted height, no current
folder in the title.
- **Icons and wording drift.** Upload uses `document-send-symbolic` (reads as "send"), offline uses
stars (reads as "favorite"), trash icons mean "stop syncing", "remove device" and "remove
bookmark". "Keep offline" / "Unpin" / "Remove offline copy" / "Available offline" are one concept.
"Computers" vs "device", "Gallery" vs "Photos", "mount service" vs "daemon" vs "Connect".
- **Theming is hardcoded.** `PROTON_PURPLE` overrides the system accent; badge, viewer and tile
colours are hex/rgba literals and ignore light, dark and high-contrast modes.
- **Several visible bugs** (section 9).
## 2. Design principles
1. **Follow GNOME HIG and current libadwaita.** The system has libadwaita 1.9; the crate is pinned to
feature `v1_5`. Raise it to at least `v1_7` to get `adw::Spinner`, `ToggleGroup`,
`InlineViewSwitcher`, `ButtonRow`, `WrapBox`, `BottomSheet`, and (1.8) `ShortcutsDialog`.
2. **Sync state is first class.** The user must always see: up to date / syncing / paused / offline /
needs attention, and be one click from pausing or from the list of problems.
3. **One term, one icon, one place** per concept (glossary in section 8).
4. **Secondary and destructive actions go into menus.** Rows get at most one visible suffix action
plus a `view-more-symbolic` menu. Destructive actions are red inside the menu and in the confirm
dialog, not as filled buttons in headers.
5. **Never block on the network visibly.** Keep old content on screen while reloading, show a header
spinner, never flash the whole page to a "Loading…" status page.
6. **Respect the system.** Use the system accent by default (brand purple stays on login, app icon
and an opt-in toggle). Use named colours (`@success_color`, `@warning_color`, `@error_color`).
## 3. New information architecture
### Sidebar
```
Proton Drive (header: app name)
My files
Photos
Shared with me (badge: pending invitations)
Shared by me
Computers
Trash
─────────────
Sync (badge: issue count, icon reflects state)
───────────── footer
[●] Up to date · Pause ← sync status strip, click opens Sync
▓▓▓░░░ 269 GiB of 1.6 TiB ← quota
(avatar) user@proton.me ⋮ ← account menu: Preferences, Sign out…
```
- **Remove** sidebar rows: *Locations* (merged into Sync), *Activity* (becomes the Sync "History"
view), *Settings* (becomes an `AdwPreferencesDialog`, `Ctrl+,`).
- **Rename** *Gallery* to *Photos* and *Shared* to *Shared by me*.
- Primary menu: Preferences, Keyboard Shortcuts, About. The About dialog's `debug_info` gets app
version, user agent, daemon status and config path (replacing the "Developer" group).
### Sync page (new, replaces Locations + Activity + Settings→Activity)
Top: an `AdwBanner`-style status card with state icon, one-line summary ("Syncing 12 files · 3.4
MB/s", "Paused until you resume", "2 files need attention") and one primary button: **Pause** /
**Resume** (split button with "Pause for 1 hour / until tomorrow / until resumed").
Below, an `InlineViewSwitcher` with four views:
1. **Issues** (default when non-empty): conflicts, failing uploads (`last_error`, attempts, next
retry), folders in `error`/`conflict` state, parked uploads older than N minutes. Each row has a
primary fix action (Resolve…, Retry now) and a menu (Show in folder, Discard change…).
2. **Transfers**: in-flight uploads and downloads with progress, speed, cancel; background jobs
(thumbnail build, Takeout import, re-date) with their own cancel.
3. **Folders**: the old Locations list. Row: path, mode as a dropdown suffix (Synced / On-demand, with
confirm when going on-demand deletes local copies), state subtitle, `Sync now` and a menu (Open,
Change location…, Pause this folder, Stop syncing…). "Add folder…" as a `ButtonRow` at the end.
The primary mount appears first and is not removable.
4. **History**: the old Activity feed, with a kind filter, failures in `@error_color`, rows
activatable (open or reveal the target), and "Clear history".
### Preferences dialog (slimmed Settings)
| Page | Contents |
|---|---|
| General | Start on login; Mount location (path + Change…); Notifications (on sync issues, on import done) |
| Storage | Cache usage bar; Cache budget (spin row, in GB, "Unlimited" switch instead of magic 0); Clear cache…; Offline files (count + "Manage" → Sync › Folders filter or a sub-page) |
| Sync | Ignore patterns (editable list); Conflict clean-up: Off / Report / Remove identical copies (`conflict_sweep`); later: bandwidth limits |
| Advanced | Open-with rules, search prompt config (only if kept in GUI) |
Moved out: account row and Sign out (sidebar account menu, with confirm), quota (sidebar footer),
mount status (sidebar status strip), live transfers (Sync), Google Photos import (Photos page menu),
version/user agent (About → Debug info).
## 4. Backend work required
Everything below is new; nothing like it exists today.
### B-1 Pause and resume (global, then per folder)
- `Request::SetSyncPaused { paused: bool, until: Option<i64> }`, persisted (config key
`sync_paused_until` or a DB setting) so a pause survives restarts; `Response::Status` gains
`paused` and `paused_until`.
- Drain: `run_pending_drain` (`pdfs-fuse/src/drain.rs:265`) already releases claimed ops while
offline; gate on `paused` at the same point and reuse the wake path.
- Sync engine: add `SyncMsg::Pause/Resume` or gate `reconcile_all` / `reconcile_folder`; the 120 s
poll and watcher keep recording, they just do not apply.
- FUSE reads must keep working while paused (on-demand hydration is a read, not a sync); document that
pause means "no uploads, no mirror reconcile".
- Per folder later: `sync_folder.paused` column, skipped in `reconcile_all`.
- Bandwidth limits later: token bucket in `CountingReader` / `CountingWriter`
(`pdfs-fuse/src/transfers.rs`).
### B-2 Pending ops and transfers
- `Request::ListPendingOps` → id, kind, path/name, attempts, `last_error` (classified as
`ErrorKind`), `next_attempt_at`, parked flag, staged bytes.
- `Request::RetryOp { id }` (set `next_attempt_at = now`, wake drain) and `RetryAllFailing`.
- `Request::DiscardOp { id }` for a create/revision whose bytes should be dropped. Must save the
staged blob to a local recovery folder first; never silent data loss (see `docs/RECOVERY.md`).
- `TransferItem` gains a stable id and `Request::CancelTransfer { id }`.
- Per-folder last error message on `SyncFolderInfo` (today the text only exists in the activity feed).
### B-3 Conflicts
- `Request::ListConflicts`: walk nodes whose name parses with `sweep.rs` `conflict_base_name`, for
the mount and every mirror folder. Return copy path, original path (if it exists), both sizes and
mtimes, whether content is identical (size + `content_sha1`, as the sweep already proves), and
which side is local vs remote when known.
- Optionally a `conflict` table written by `keep_as_conflict_copy` / `preserve_conflict_copy` so the
origin (drain vs mirror, device) is recorded instead of inferred from names.
- `Request::ResolveConflict { copy, keep: Original | Copy | Both }`:
- Original: trash the copy (re-verify like the enforcing sweep does).
- Copy: upload the copy's content as a new revision of the original, then trash the copy (the old
content stays in version history, so this is reversible).
- Both: rename the copy to a user-chosen name, drop the `(sync-conflict …)` stamp.
- Activity rows for each resolution.
## 5. Shell and visual polish (all pages)
- **Per-page header bars.** Give each stack page its own `adw::ToolbarView` + `adw::HeaderBar` (or
inject page widgets into the shared bar on page switch). Title = page name, or the current folder
in My files. Page actions move into the header; the extra toolbar row disappears.
- **One page skeleton.** Same margins everywhere (today 12 vs 18), same `Clamp` rule (lists that are
settings-like clamp at 720, file/photo grids are full width), same status pages (`compact`,
`vexpand`, crossfade), same Retry semantics (Retry reloads; "Start Proton Drive" is a separate,
explicitly labelled button when the service is down).
- **Loading.** `adw::Spinner` in the header; keep the old model until the new one lands; status page
only on first load.
- **Theme.** Drop the global `accent_*` override; offer "Use Proton purple accent" in Preferences.
Replace hex colours with named colours; test light, dark and high contrast.
- **Move the 70-line CSS string** in `main.rs:232` to `resources/style.css` in the gresource
(`style-dark.css` if needed), so it can be edited and linted.
- **Shortcuts.** Replace the hand-rolled cheatsheet with `adw::ShortcutsDialog` (or
`GtkShortcutsWindow` before 1.8) listing all bindings, including F5/Ctrl+R and the viewer keys.
- **Toasts vs dialogs.** Dialogs report their own errors inline (versions, share, add bookmark); a
toast behind a modal is invisible. Pluralise properly (`ngettext`-ready helpers, no "item(s)").
- **i18n groundwork.** Wrap user strings in a `gettext!`-style macro now, even without translations,
so later localisation is mechanical.
## 6. Page-by-page plan
### My files
Header: `[‹ ›] Path bar (breadcrumbs, scroll to end, Ctrl+L to edit) … [search] [view ▾] [+ New ▾]`
- **New ▾** (`list-add-symbolic`, suggested): New folder (Ctrl+Shift+N), Upload files (Ctrl+U),
Upload folder. Replaces three separate icons, fixes the "send" icon.
- **View ▾** split button: Grid/List (Ctrl+1/2), Sort by Name/Size/Modified, ascending/descending,
Folders first, zoom slider. Persist choice (config or GSettings).
- **Folder ⋮ menu** (in header or background context menu): Refresh, Build thumbnails, Make folder
available offline, Open in file manager, Share folder…. Build thumbnails leaves the main toolbar.
- **Navigation.** Back/forward history (Alt+←/→), Up (Alt+↑); searching then pressing Back clears
the search consistently.
- **Context menus** as `gtk::PopoverMenu` from `gio::Menu` + actions: keyboard navigable, Menu /
Shift+F10, same action set as details pane. Add a background menu. Order: Open, Open with…, Play
(media) · Available offline (check item) · Share…, Copy link · Rename…, Move to…, Versions… ·
Move to Trash (destructive).
- **Offline state.** One concept "Available offline", toggle semantics. Badges: cloud-only = no
badge or subtle `cloud-outline`; downloaded = check; available offline = pin/`folder-download`
style filled icon. Use named colours. Folders can be made available offline too, or the toggle is
hidden for them (fix `details.rs:259`).
- **List view.** Sortable, resizable columns: Name, Status, Size, Modified (date + time), and
Location for search results. Rubberband selection. One selection model shared between views.
- **Details pane.** On wide windows a real side pane (breakpoint, not overlay); header bar with
close; real thumbnail; sections: Info, Offline, Sharing (people count, link), Versions (latest 3 +
"All versions…"). Same actions as the context menu.
- **Selection bar** (≥1 selected, not ≥2): count + size, Available offline, Move to…, Share (single),
Move to Trash; Select all (Ctrl+A). Drop the confirm dialog for trash since Undo exists (trash is
reversible), or keep confirm and drop Undo — not both.
- **Drag and drop.** Drop external files/folders to upload (reuse Takeout's `FileList` target), drag
the whole selection, highlight drop targets, breadcrumbs accept drops, Undo toast on move.
- **Move to…** a folder-picker dialog (tree/list of Drive folders) instead of a free-text path.
- **Rename** pre-selects the stem, not the extension; drop the redundant "Rename “x”." body.
- **Search.** Scope toggle (This folder / Everywhere), "Showing first 200 results" note, Location
column, correct cached badges (search hits hardcode `cached: false`).
- **Upload feedback.** Toast "Uploading 3 items" with "View" → Sync › Transfers.
### Photos (was Gallery)
- Header: title "Photos", `InlineViewSwitcher` Timeline | Albums in the header, Select toggle,
`+ Upload` (fix: use `adw::ButtonContent`, today `.icon_name()` overwrites `.label()` at
`photos.rs:426-432`), ⋮ menu: Import from Google Photos…, Refresh, Rebuild library.
- Filters: a single filter dropdown/`ToggleGroup` (All, Photos, Videos, RAW, Favorites) instead of
two segmented rows where "Photos" appears twice. Counts follow the active filter.
- **Month scrubber** on the right edge that *jumps* (not filters); the month dropdown becomes a real
"Filter by date" if kept.
- Infinite scroll with prefetch (no "Load more" pill; page size ≥ 200; also trigger when the first
page does not fill the viewport).
- Tiles: `ContentFit::Cover` (crop) in justified rows, known aspect ratio from metadata to prevent
reflow, placeholder as a tinted card of the final size, subtle hover (no 1.06 scale with heavy
shadow on a 2 px gap grid), last row justified up to a max stretch.
- Context menu on tiles: Open, Favorite, Add to album, Download/Save copy, Show in My files, Move to
Trash. Selection: Shift range, Ctrl+A, bulk Favorite, Add to album, Download.
- Empty states per filter ("No favorites yet", "No videos in June 2024") instead of the global
"No photos yet" with upload buttons.
- Import: runs show a banner on Photos ("Importing from Google Photos — 1,204 of 5,000 · View") and
Back returns to where the user came from.
- **Albums:** Create, rename, delete, remove from album (not trash). Fix state bugs: filter row
hidden on the album grid, subtitle count, scroll position kept when switching back.
- **Viewer:** `adw::Window` (or full-window overlay), controls auto-hide after 2 s idle, zoom/pan
(pinch, Ctrl+scroll, double-click), info panel beside the image instead of over the top bar,
pages beyond the loaded 60, counter against the real total, videos play inline with
`gtk::Video`, trash with the same confirm/undo rule as the grid, real filename in "Save a copy",
success toast, "Open with…" app chooser, favorite tooltip reflects state, readable dates.
### Shared with me / Shared by me
- Shared with me: header title shows the drill-down path, with back; invitations as a banner at
the top ("2 invitations · Review") opening a list, Reject confirmed; bookmarks become a separate
view or section via a switcher. Rows: Open, Save to My files, Available offline where supported.
- Shared by me: rows activatable (open), suffix "Manage" becomes a menu with Copy link, Manage
access…, Stop sharing….
- Share dialog: inline validation, loading spinner, confirm "Remove access" and "Delete link",
human role names, link expiry.
### Computers
- Show the real device name for "This computer" with rename inline.
- Other computers: one suffix menu (Rename, Use this identity…, Remove…). Remove uses a destructive
confirm that requires typing the device name since it deletes backups.
- "Restore folders" becomes a per-device action ("Restore to this computer…") with an inline-empty
state when nothing is restorable, and moves the "Add folder" responsibility fully to Sync.
- Terminology: "computer" everywhere in UI copy.
### Trash
- Same skeleton as other list pages (clamped `boxed-list`), sort by deleted date, multi-select with
Restore / Delete permanently, "Empty Trash…" moved into a header ⋮ menu or kept as a flat button
with a destructive confirm, proper plurals, Retry on error.
### Login
- Human error messages mapped from error kinds, spinner in the button, "Create account" and "Forgot
password" links, recovery-code hint in 2FA, cancelling CAPTCHA returns to the form with a message.
## 7. Tray
- Symbolic icon variants: `pdfs-synced`, `pdfs-syncing`, `pdfs-paused`, `pdfs-offline`,
`pdfs-attention`; tooltip with the one-line state; `NeedsAttention` status for issues.
- Menu: status line · "N issues — View" (opens Sync › Issues) · Pause sync ▸ (1 h, until tomorrow,
until resumed) / Resume · Open Proton Drive · Open folder · "Sign in…" when logged out (no
"Connect") · Stop Proton Drive (confirm) · "Hide tray icon" instead of "Quit" (daemon keeps
running and the label must not suggest otherwise).
## 8. Glossary (one term, one icon)
| Concept | UI term | Icon |
|---|---|---|
| Root of Drive | My files | `folder-symbolic` |
| Background service | Proton Drive (running / stopped) — never "daemon" or "mount service" in UI | — |
| Cloud-only file | Online only | none / `cloud-outline` |
| Pinned file | Available offline | pin/download-filled custom icon, not stars |
| Upload | Upload files / Upload folder | `document-upload`-style arrow up, custom pair |
| Folder sync mode | Synced / Online only | — |
| Where My files appears on disk | Proton Drive folder — never "mountpoint" in UI | — |
| Stop syncing a folder | Stop syncing… | `media-playback-stop-symbolic` in a menu, not trash |
| Remove a computer | Remove computer… | destructive, in menu |
| Conflict copy | Conflict | `dialog-warning-symbolic` + warning colour |
| Failure | Failed / Needs attention | `dialog-error-symbolic` + error colour |
| Pause | Pause sync / Resume sync | `media-playback-pause-symbolic` / `-start-` |
| Photos section | Photos | `image-x-generic-symbolic` |
| Sharing | Shared by me / Shared with me | distinct icons (`emblem-shared` / `folder-publicshare`) |
Spelling: US English throughout ("Favorite", "color"), matching the `pdfs favorite` command and
Proton's own apps. Title Case for
dialog headings and buttons in dialogs, Sentence case for menu items and rows (HIG).
## 9. Bugs found during the audit (quick wins)
1. Photos Upload pill loses its label (`photos.rs:426-432`); same for viewer "Show on map"
(`photo_viewer.rs:594-598`).
2. Takeout Back always goes to Settings, sidebar highlights Settings when opened from Photos
(`main.rs:779`, `takeout.rs:131-136`).
3. Thumbnail-build completion message computed then hidden (`browser.rs:1632-1667`).
4. Search results show cached files as cloud-only (`browser.rs:2344`).
5. Details "Available offline" switch shown for folders (`details.rs:259`).
6. Grid and list keep separate selections; details pane can describe a hidden-view item.
7. F2 silently does nothing with multiple items selected.
8. Filter changes on the Albums grid swap to the timeline while the Albums toggle stays active
(`photos.rs:958-997`).
9. Subtitle "60 photos" next to "All 1,416" (loaded count vs total, `photos.rs:2007-2015`); viewer
counter "12 of 60".
10. Viewer info panel covers Close/Trash/Save (`photo_viewer.rs:588` vs `648`).
11. Locations mode switch and removal reload Computers, not Locations; a rejected mode switch stays
flipped until the next tick (`devices.rs:612`, `devices.rs:809`, `locations.rs:374`).
12. Going on-demand deletes local copies without confirmation.
13. Retry on Not-connected silently restarts the systemd unit; Locations' Retry does not.
14. Toasts from Versions/Share dialogs land behind the modal.
15. "Emptied trash —" dangling dash, "item(s)", "archive(s)", raw revision id as version title,
raw lowercase link role.
16. Bulk context menu shows a separator directly under the header when a folder is selected.
17. Sign out, Reject invitation, Remove access, Delete link, tray Disconnect: no confirmation.
18. Stale doc comments (`devices.rs:46-48`, `locations.rs:329-331`, `albums.rs:4`,
`browser.rs:1155`).
## 10. Phased roadmap
Each phase is shippable on its own. Estimates are rough, for one developer.
| Phase | Content | Crates | Size |
|---|---|---|---|
| 0 Quick wins | Section 9 bugs, glossary wording, icon swaps (upload, offline, stop syncing), confirmations, plurals | gui, tray | S (2–3 days) |
| 1 Shell | libadwaita feature bump, per-page header bars, sidebar restructure + footer (status, quota, account), Preferences dialog, About debug info, theme/colour cleanup, CSS to gresource, shortcuts dialog | gui | M (1 week) |
| 2 Sync hub + pause | B-1 global pause, B-2 list/retry pending ops, Sync page (status card, Issues, Transfers, Folders, History), tray states and pause menu, GUI uses `failing_*` / `parked_*` | core, fuse, gui, tray, cli | L (1.5–2 weeks) |
| 3 Conflicts | B-3 list + resolve, Issues rows with a compare/resolve dialog (both versions side by side: size, date, device, preview for images/text), CLI `pdfs conflicts` | core, fuse, gui, cli | M–L |
| 4 File browser | Header layout, New menu, sort/persisted view, PopoverMenu context menus, details side pane, DnD upload, folder picker for Move, search scope | gui | L |
| 5 Photos | Rename, header switcher, filters, infinite scroll + scrubber, tile layout, context menu, album management, viewer rewrite (auto-hide, zoom, inline video) | gui (+ core for album mutations) | L |
| 6 Rest | Shared pages, Computers, Trash, Login, per-folder pause, bandwidth limits, i18n wrapping | all | M |
Phase 2 is where the user-reported gaps (pause, resume, stuck items) get closed; phase 3 closes the
conflict overview. Phases 0–1 fix most of the "unpolished" impression cheaply and should come first
because later phases build on the new header/page skeleton.
### Verification per phase
- `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`,
`cargo test --workspace`.
- Backend phases: DB migration tests in `crates/pdfs-core/src/db/tests.rs` (paused state, conflict
table if added), drain tests proving no op is claimed while paused, resolve-conflict tests proving
the losing side stays recoverable (trash or version history).
- GUI phases: before/after screenshots in `images/` (light and dark), keyboard-only walkthrough of
each page, narrow-window check (sidebar collapse breakpoint).
+37 -9
View File
@@ -7,6 +7,7 @@ How Proton Drive for Linux works and how to use it day to day. For installation
- [My files](#my-files) - [My files](#my-files)
- [Keeping files on this computer](#keeping-files-on-this-computer) - [Keeping files on this computer](#keeping-files-on-this-computer)
- [Syncing local folders](#syncing-local-folders) - [Syncing local folders](#syncing-local-folders)
- [Changes waiting for Proton Drive](#changes-waiting-for-proton-drive)
- [Conflicts](#conflicts) - [Conflicts](#conflicts)
- [Pausing and limiting sync](#pausing-and-limiting-sync) - [Pausing and limiting sync](#pausing-and-limiting-sync)
- [Photos](#photos) - [Photos](#photos)
@@ -33,7 +34,7 @@ the service to do things over a local socket:
Your Drive, your file names and your photos are end-to-end encrypted by Proton. On this computer, Your Drive, your file names and your photos are end-to-end encrypted by Proton. On this computer,
the service must decrypt what it shows you, and it keeps a decrypted cache and index on disk. Use the service must decrypt what it shows you, and it keeps a decrypted cache and index on disk. Use
full-disk or home-directory encryption. [ARCHITECTURE.md §8](ARCHITECTURE.md#8-threat-model-what-this-client-writes-to-disk-in-plaintext) full-disk or home-directory encryption. [ARCHITECTURE.md §10](ARCHITECTURE.md#10-threat-model-what-this-client-writes-to-disk-in-plaintext)
lists exactly what is stored. lists exactly what is stored.
The app uses these words consistently, and so does this guide: The app uses these words consistently, and so does this guide:
@@ -54,9 +55,11 @@ downloads only when a program reads it:
- **Reads fetch only what is needed.** A video player that seeks, or a tool that reads a file - **Reads fetch only what is needed.** A video player that seeks, or a tool that reads a file
header, downloads only those parts. Media streams while it plays. header, downloads only those parts. Media streams while it plays.
- **Writes are safe offline.** When a program closes a file it wrote, the service stores the new - **Changes work at local speed.** Creating, renaming, moving and deleting files and folders
content on disk and queues the upload. The queue survives network loss, restarts and crashes. returns at once, online or offline. When a program closes a file it wrote, the new content is
`pdfs sync queue` lists what has not reached Proton Drive yet. stored on disk. Every change is queued and sent to Proton Drive in the background; the changes
to one file are sent in the order you made them. The queue survives network loss, restarts and
crashes. See [Changes waiting for Proton Drive](#changes-waiting-for-proton-drive).
- **Recently read content is cached** on disk up to the cache size limit (5 GiB by default) and - **Recently read content is cached** on disk up to the cache size limit (5 GiB by default) and
evicted oldest first. evicted oldest first.
- **Items shared with you** appear under `Shared with me/` inside the Proton Drive folder. Shares - **Items shared with you** appear under `Shared with me/` inside the Proton Drive folder. Shares
@@ -143,6 +146,36 @@ places, and both apply:
Rules are re-read on every pass. Ignoring never deletes anything: a file that becomes ignored is Rules are re-read on every pass. Ignoring never deletes anything: a file that becomes ignored is
simply no longer tracked, and its copy on Proton Drive stays. simply no longer tracked, and its copy on Proton Drive stays.
## Changes waiting for Proton Drive
A change you made in the Proton Drive folder exists only on this computer until it reaches
Proton Drive. `pdfs status`, the tray and **Sync → Overview** count what is still waiting, and
`pdfs sync queue` lists it.
Sometimes Proton Drive refuses a change: the storage is full, a share was taken away, the folder
is gone, the folder holds too many items, or Drive does not accept a name. The change then needs
your attention. It shows at once in **Sync → Overview**, in the tray, and in `pdfs sync issues`,
with what it means and what you can do. A file or folder with such a change carries a warning
badge in **Files**; hover it to see why.
Nothing is deleted. The change stays queued and is retried. You can:
- **Retry** it now, from the same list or with `pdfs sync retry`, for example after freeing space.
- **Export** a file's content to keep your version (`pdfs sync export <id> <file>`).
- **Discard** the change (`pdfs sync discard <id>`). It is undone here and never sent: a new file
or folder goes away, anything else goes back to what Proton Drive has. Export first if you want
to keep the content.
Two cases are not refused, and the activity log records both:
- A new file or folder whose name is already taken on Proton Drive by something this computer had
not seen yet lands under a `(sync-conflict …)` name, as described below.
- A new file whose folder was deleted on another device meanwhile lands at the top of My files,
so its content is not lost.
Before you shut down a computer for good, check that nothing is waiting: see
[RECOVERY.md](RECOVERY.md#lost-with-the-machine).
## Conflicts ## Conflicts
When the same file changed on this computer and somewhere else since the last sync, neither side When the same file changed on this computer and somewhere else since the last sync, neither side
@@ -166,11 +199,6 @@ The version you do not keep goes to the Trash, so a wrong choice can be undone.
reading files keeps working. `pdfs sync resume` continues. reading files keeps working. `pdfs sync resume` continues.
- **Limit bandwidth** under **Preferences → General → Network**, or with - **Limit bandwidth** under **Preferences → General → Network**, or with
`pdfs sync limit --up 2M --down 10M`. `0` removes a limit. `pdfs sync limit --up 2M --down 10M`. `0` removes a limit.
- **Retry** a failed upload from **Sync → Overview**, or with `pdfs sync retry`.
- **Discard** a change Proton Drive refused from the same list, or with `pdfs sync discard`: it is
undone here and never sent. Export a file first to keep your version.
- A file or folder with a change Proton Drive refused carries a warning badge in **Files**; hover
it to see why.
## Photos ## Photos
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

+156
View File
@@ -0,0 +1,156 @@
<svg width="1920" height="1080" viewBox="0 0 1920 1080" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1920" y2="1080" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#1A1240"/>
<stop offset="1" stop-color="#0A0618"/>
</linearGradient>
<radialGradient id="glow1" cx="1390" cy="520" r="640" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#6D4AFF" stop-opacity="0.55"/>
<stop offset="0.5" stop-color="#6D4AFF" stop-opacity="0.15"/>
<stop offset="1" stop-color="#6D4AFF" stop-opacity="0"/>
</radialGradient>
<radialGradient id="glow2" cx="160" cy="1080" r="700" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#A855F7" stop-opacity="0.25"/>
<stop offset="1" stop-color="#A855F7" stop-opacity="0"/>
</radialGradient>
<pattern id="dots" width="32" height="32" patternUnits="userSpaceOnUse">
<circle cx="2" cy="2" r="1.6" fill="#FFFFFF"/>
</pattern>
<radialGradient id="dotfade" cx="1390" cy="520" r="900" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF" stop-opacity="1"/>
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
</radialGradient>
<mask id="dotmask" maskUnits="userSpaceOnUse" x="0" y="0" width="1920" height="1080">
<rect width="1920" height="1080" fill="url(#dotfade)"/>
</mask>
<linearGradient id="title2" x1="140" y1="0" x2="760" y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#C9BBFF"/>
<stop offset="1" stop-color="#8A6CFF"/>
</linearGradient>
<!-- app logo -->
<linearGradient id="pdl-face" x1="12" y1="12" x2="116" y2="112" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#A48BFF"/>
<stop offset="0.55" stop-color="#7552FF"/>
<stop offset="1" stop-color="#5B36F2"/>
</linearGradient>
<linearGradient id="pdl-cloud" x1="0" y1="34" x2="0" y2="88" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FFFFFF"/>
<stop offset="1" stop-color="#ECE6FF"/>
</linearGradient>
<mask id="pdl-cut" maskUnits="userSpaceOnUse" x="0" y="0" width="128" height="128">
<g fill="#FFFFFF">
<circle cx="66" cy="57" r="23"/>
<circle cx="42" cy="68" r="16"/>
<circle cx="88" cy="69" r="15"/>
<rect x="26" y="68" width="77" height="20" rx="10"/>
</g>
<g stroke="#000000" stroke-width="7.5" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M64 49v26"/>
<path d="M54.5 65.5l9.5 9.5 9.5-9.5"/>
</g>
</mask>
<filter id="logoshadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="6"/>
</filter>
<filter id="chipshadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="14"/>
</filter>
</defs>
<!-- background -->
<rect width="1920" height="1080" fill="url(#bg)"/>
<rect width="1920" height="1080" fill="url(#glow2)"/>
<rect width="1920" height="1080" fill="url(#glow1)"/>
<rect width="1920" height="1080" fill="url(#dots)" opacity="0.07" mask="url(#dotmask)"/>
<!-- orbit rings -->
<g fill="none" stroke="#FFFFFF">
<circle cx="1390" cy="520" r="300" stroke-opacity="0.10" stroke-width="2"/>
<circle cx="1390" cy="520" r="420" stroke-opacity="0.06" stroke-width="2" stroke-dasharray="4 14" stroke-linecap="round"/>
</g>
<!-- logo, 128 -> 384 -->
<g transform="translate(1198 328) scale(3)">
<rect x="16" y="24" width="96" height="92" rx="24" fill="#2A1080" opacity="0.8" filter="url(#logoshadow)"/>
<rect x="12" y="16" width="104" height="100" rx="26" fill="#4424C0"/>
<rect x="12" y="12" width="104" height="100" rx="26" fill="url(#pdl-face)"/>
<rect x="12.5" y="12.5" width="103" height="99" rx="25.5" fill="none" stroke="#FFFFFF" stroke-opacity="0.2"/>
<rect y="3" width="128" height="128" fill="#2E1596" fill-opacity="0.3" mask="url(#pdl-cut)"/>
<rect width="128" height="128" fill="url(#pdl-cloud)" mask="url(#pdl-cut)"/>
</g>
<!-- feature chips around the logo -->
<g font-family="Adwaita Sans" font-size="26" font-weight="600" fill="#FFFFFF">
<!-- folder, top left -->
<g transform="translate(1000 200)">
<rect x="0" y="10" width="236" height="76" rx="22" fill="#000000" opacity="0.35" filter="url(#chipshadow)"/>
<rect x="0" y="0" width="236" height="76" rx="22" fill="#2A2058" fill-opacity="0.85" stroke="#FFFFFF" stroke-opacity="0.14" stroke-width="1.5"/>
<g transform="translate(22 20)">
<rect width="36" height="36" rx="10" fill="#6D4AFF"/>
<path d="M8 13a2 2 0 0 1 2-2h5l2.5 2.5H26a2 2 0 0 1 2 2V25a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2z" fill="#FFFFFF"/>
</g>
<text x="74" y="47">On demand</text>
</g>
<!-- sync, top right -->
<g transform="translate(1590 290)">
<rect x="0" y="10" width="164" height="76" rx="22" fill="#000000" opacity="0.35" filter="url(#chipshadow)"/>
<rect x="0" y="0" width="164" height="76" rx="22" fill="#2A2058" fill-opacity="0.85" stroke="#FFFFFF" stroke-opacity="0.14" stroke-width="1.5"/>
<g transform="translate(22 20)">
<rect width="36" height="36" rx="10" fill="#1EA885"/>
<g fill="none" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M26 15a8.5 8.5 0 0 0-15-2"/>
<path d="M10 9v4.5h4.5"/>
<path d="M10 21a8.5 8.5 0 0 0 15 2"/>
<path d="M26 27v-4.5h-4.5"/>
</g>
</g>
<text x="74" y="47">Sync</text>
</g>
<!-- photos, bottom right -->
<g transform="translate(1560 690)">
<rect x="0" y="10" width="206" height="76" rx="22" fill="#000000" opacity="0.35" filter="url(#chipshadow)"/>
<rect x="0" y="0" width="206" height="76" rx="22" fill="#2A2058" fill-opacity="0.85" stroke="#FFFFFF" stroke-opacity="0.14" stroke-width="1.5"/>
<g transform="translate(22 20)">
<rect width="36" height="36" rx="10" fill="#E8618C"/>
<rect x="8" y="9" width="20" height="18" rx="3" fill="none" stroke="#FFFFFF" stroke-width="2.4"/>
<circle cx="14" cy="15" r="2" fill="#FFFFFF"/>
<path d="M9.5 25l6-6 4 4 3-3 4 4" fill="none" stroke="#FFFFFF" stroke-width="2.4" stroke-linejoin="round"/>
</g>
<text x="74" y="47">Photos</text>
</g>
<!-- terminal, bottom left -->
<g transform="translate(980 790)">
<rect x="0" y="10" width="206" height="76" rx="22" fill="#000000" opacity="0.35" filter="url(#chipshadow)"/>
<rect x="0" y="0" width="206" height="76" rx="22" fill="#2A2058" fill-opacity="0.85" stroke="#FFFFFF" stroke-opacity="0.14" stroke-width="1.5"/>
<g transform="translate(22 20)">
<rect width="36" height="36" rx="10" fill="#3B3F5C"/>
<path d="M9 12l6 6-6 6" fill="none" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 25h9" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round"/>
</g>
<text x="74" y="48" font-family="JetBrainsMono NF" font-size="25" font-weight="700">pdfs</text>
<text x="150" y="48" font-weight="500" fill="#B9B0D9">CLI</text>
</g>
</g>
<!-- text block -->
<g font-family="Adwaita Sans">
<g transform="translate(140 292)">
<rect width="410" height="48" rx="24" fill="#6D4AFF" fill-opacity="0.18" stroke="#8A6CFF" stroke-opacity="0.6" stroke-width="1.5"/>
<circle cx="28" cy="24" r="6" fill="#A48BFF"/>
<text x="46" y="32" font-size="21" font-weight="700" letter-spacing="2.4" fill="#D6CCFF">UNOFFICIAL · OPEN SOURCE</text>
</g>
<text x="134" y="478" font-size="124" font-weight="800" letter-spacing="-3" fill="#FFFFFF">Proton Drive</text>
<text x="134" y="610" font-size="124" font-weight="800" letter-spacing="-3" fill="url(#title2)">for Linux</text>
<text x="140" y="702" font-size="38" font-weight="400" fill="#B9B0D9">Your Drive as a folder, and a native GTK4 app.</text>
<text x="140" y="752" font-size="38" font-weight="400" fill="#B9B0D9">No Electron.</text>
</g>
<!-- footer -->
<g font-family="JetBrainsMono NF" font-size="24" fill="#8C83B0">
<text x="140" y="968">github.com/narrrl/proton-drive-linux</text>
</g>
<g font-family="Adwaita Sans" font-size="22" fill="#6F6894">
<text x="1780" y="968" text-anchor="end">Not affiliated with Proton AG</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

+1 -1
View File
@@ -9,7 +9,7 @@
# Build + install: cd packaging && makepkg -fi # Build + install: cd packaging && makepkg -fi
pkgname=proton-drive-linux pkgname=proton-drive-linux
pkgver=3.0.0 pkgver=3.1.1
pkgrel=1 pkgrel=1
pkgdesc="Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app + tray" pkgdesc="Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app + tray"
arch=('x86_64') arch=('x86_64')
@@ -1,6 +1,6 @@
pkgbase = proton-drive-for-linux-bin pkgbase = proton-drive-for-linux-bin
pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries) pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)
pkgver = 2.10.0 pkgver = 3.1.0
pkgrel = 1 pkgrel = 1
url = https://proton-drive.narl.io url = https://proton-drive.narl.io
arch = x86_64 arch = x86_64
@@ -21,21 +21,21 @@ pkgbase = proton-drive-for-linux-bin
optdepends = xdg-utils: "Open folder" action in the tray menu optdepends = xdg-utils: "Open folder" action in the tray menu
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf) optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
provides = proton-drive-for-linux=2.10.0 provides = proton-drive-for-linux=3.1.0
provides = proton-drive-linux=2.10.0 provides = proton-drive-linux=3.1.0
provides = pdfs provides = pdfs
conflicts = proton-drive-linux conflicts = proton-drive-linux
conflicts = proton-drive-for-linux conflicts = proton-drive-for-linux
conflicts = proton-drive-for-linux-git conflicts = proton-drive-for-linux-git
options = !strip options = !strip
options = !debug options = !debug
source = proton-drive-linux-2.10.0-x86_64.tar.gz::https://github.com/narrrl/proton-drive-linux/releases/download/v2.10.0/proton-drive-linux-2.10.0-x86_64.tar.gz source = proton-drive-linux-3.1.0-x86_64.tar.gz::https://github.com/narrrl/proton-drive-linux/releases/download/v3.1.0/proton-drive-linux-3.1.0-x86_64.tar.gz
source = io.narl.proton-drive-linux-2.10.0.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.10.0/packaging/io.narl.proton-drive-linux.desktop source = io.narl.proton-drive-linux-3.1.0.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.1.0/packaging/io.narl.proton-drive-linux.desktop
source = io.narl.proton-drive-linux-tray-2.10.0.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.10.0/packaging/io.narl.proton-drive-linux-tray.desktop source = io.narl.proton-drive-linux-tray-3.1.0.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.1.0/packaging/io.narl.proton-drive-linux-tray.desktop
source = io.narl.proton-drive-linux-2.10.0.svg::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.10.0/packaging/io.narl.proton-drive-linux.svg source = io.narl.proton-drive-linux-3.1.0.svg::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.1.0/packaging/io.narl.proton-drive-linux.svg
source = proton-drive-2.10.0.service::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.10.0/packaging/proton-drive.service source = proton-drive-3.1.0.service::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.1.0/packaging/proton-drive.service
source = LICENSE-2.10.0::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v2.10.0/LICENSE source = LICENSE-3.1.0::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.1.0/LICENSE
sha256sums = b653dbd7d5939c504c55a91e14f1cd3e38aabc94d8bbd6344d2f79b3cd670313 sha256sums = bd124e249089d2817101e4f8b705ac511decb79a410ab26b0eac9125888f7300
sha256sums = d190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123 sha256sums = d190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123
sha256sums = 1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0 sha256sums = 1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0
sha256sums = e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828 sha256sums = e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828
@@ -1,7 +1,7 @@
# Maintainer: Nils Pukropp <contact@narl.io> # Maintainer: Nils Pukropp <contact@narl.io>
pkgname=proton-drive-for-linux-bin pkgname=proton-drive-for-linux-bin
pkgver=2.10.0 pkgver=3.1.0
pkgrel=1 pkgrel=1
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)" pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)"
arch=('x86_64') arch=('x86_64')
@@ -34,7 +34,7 @@ source=("proton-drive-linux-$pkgver-x86_64.tar.gz::$_repo/releases/download/$_ta
"io.narl.proton-drive-linux-$pkgver.svg::$_raw/packaging/io.narl.proton-drive-linux.svg" "io.narl.proton-drive-linux-$pkgver.svg::$_raw/packaging/io.narl.proton-drive-linux.svg"
"proton-drive-$pkgver.service::$_raw/packaging/proton-drive.service" "proton-drive-$pkgver.service::$_raw/packaging/proton-drive.service"
"LICENSE-$pkgver::$_raw/LICENSE") "LICENSE-$pkgver::$_raw/LICENSE")
sha256sums=('b653dbd7d5939c504c55a91e14f1cd3e38aabc94d8bbd6344d2f79b3cd670313' sha256sums=('bd124e249089d2817101e4f8b705ac511decb79a410ab26b0eac9125888f7300'
'd190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123' 'd190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123'
'1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0' '1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0'
'e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828' 'e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828'
@@ -1,6 +1,6 @@
pkgbase = proton-drive-for-linux-git pkgbase = proton-drive-for-linux-git
pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git) pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)
pkgver = 2.10.0.r6.ga6f36d5 pkgver = 3.1.0.r0.g274bfd0
pkgrel = 1 pkgrel = 1
url = https://proton-drive.narl.io url = https://proton-drive.narl.io
arch = x86_64 arch = x86_64
@@ -24,8 +24,8 @@ pkgbase = proton-drive-for-linux-git
optdepends = xdg-utils: "Open folder" action in the tray menu optdepends = xdg-utils: "Open folder" action in the tray menu
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf) optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
provides = proton-drive-for-linux=2.10.0.r6.ga6f36d5 provides = proton-drive-for-linux=3.1.0.r0.g274bfd0
provides = proton-drive-linux=2.10.0.r6.ga6f36d5 provides = proton-drive-linux=3.1.0.r0.g274bfd0
provides = pdfs provides = pdfs
conflicts = proton-drive-linux conflicts = proton-drive-linux
conflicts = proton-drive-for-linux conflicts = proton-drive-for-linux
@@ -1,7 +1,7 @@
# Maintainer: Nils Pukropp <contact@narl.io> # Maintainer: Nils Pukropp <contact@narl.io>
pkgname=proton-drive-for-linux-git pkgname=proton-drive-for-linux-git
pkgver=2.10.0.r6.ga6f36d5 pkgver=3.1.0.r0.g274bfd0
pkgrel=1 pkgrel=1
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)" pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)"
arch=('x86_64') arch=('x86_64')
@@ -1,6 +1,6 @@
pkgbase = proton-drive-for-linux pkgbase = proton-drive-for-linux
pkgdesc = Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray pkgdesc = Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray
pkgver = 2.10.0 pkgver = 3.1.0
pkgrel = 1 pkgrel = 1
url = https://proton-drive.narl.io url = https://proton-drive.narl.io
arch = x86_64 arch = x86_64
@@ -23,13 +23,13 @@ pkgbase = proton-drive-for-linux
optdepends = xdg-utils: "Open folder" action in the tray menu optdepends = xdg-utils: "Open folder" action in the tray menu
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf) optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
provides = proton-drive-linux=2.10.0 provides = proton-drive-linux=3.1.0
provides = pdfs provides = pdfs
conflicts = proton-drive-linux conflicts = proton-drive-linux
conflicts = proton-drive-for-linux-bin conflicts = proton-drive-for-linux-bin
conflicts = proton-drive-for-linux-git conflicts = proton-drive-for-linux-git
options = !lto options = !lto
source = proton-drive-for-linux-2.10.0.tar.gz::https://github.com/narrrl/proton-drive-linux/archive/refs/tags/v2.10.0.tar.gz source = proton-drive-for-linux-3.1.0.tar.gz::https://github.com/narrrl/proton-drive-linux/archive/refs/tags/v3.1.0.tar.gz
sha256sums = 7aa5fcb76dca13262bd678c6f280795ab331a20d5549c4aa7d2c3d4eec6fcd8e sha256sums = c7c6710e6f55d2ae3dbc29dd4a552a3e676bf4ecb2ff47ac2486dd90bd354ce2
pkgname = proton-drive-for-linux pkgname = proton-drive-for-linux
@@ -1,7 +1,7 @@
# Maintainer: Nils Pukropp <contact@narl.io> # Maintainer: Nils Pukropp <contact@narl.io>
pkgname=proton-drive-for-linux pkgname=proton-drive-for-linux
pkgver=2.10.0 pkgver=3.1.0
pkgrel=1 pkgrel=1
pkgdesc="Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray" pkgdesc="Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray"
arch=('x86_64') arch=('x86_64')
@@ -24,7 +24,7 @@ conflicts=('proton-drive-linux' 'proton-drive-for-linux-bin' 'proton-drive-for-l
# LTO has broken GTK/Rust links for this workspace; the release profile keeps it off. # LTO has broken GTK/Rust links for this workspace; the release profile keeps it off.
options=('!lto') options=('!lto')
source=("$pkgname-$pkgver.tar.gz::$_repo/archive/refs/tags/v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::$_repo/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('7aa5fcb76dca13262bd678c6f280795ab331a20d5549c4aa7d2c3d4eec6fcd8e') sha256sums=('c7c6710e6f55d2ae3dbc29dd4a552a3e676bf4ecb2ff47ac2486dd90bd354ce2')
_srcname="proton-drive-linux-$pkgver" _srcname="proton-drive-linux-$pkgver"
+218 -141
View File
@@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: proton-drive-linux 2.1.0\n" "Project-Id-Version: proton-drive-linux 2.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-10-05 01:01+0200\n" "POT-Creation-Date: 2026-10-08 10:15+0200\n"
"PO-Revision-Date: 2026-09-23 12:00+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n"
"Last-Translator: Proton Drive for Linux contributors\n" "Last-Translator: Proton Drive for Linux contributors\n"
"Language-Team: German\n" "Language-Team: German\n"
@@ -57,7 +57,7 @@ msgstr "%-d. %b %Y"
msgid "%A, %B %-d, %Y, %H:%M" msgid "%A, %B %-d, %Y, %H:%M"
msgstr "%A, %-d. %B %Y, %H:%M" msgstr "%A, %-d. %B %Y, %H:%M"
#: crates/pdfs-gui/src/app/main.rs:743 #: crates/pdfs-gui/src/app/main.rs:760
#: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539
#: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:454
#: crates/pdfs-gui/src/app/pages/browser.rs:819 #: crates/pdfs-gui/src/app/pages/browser.rs:819
@@ -70,91 +70,91 @@ msgstr "%A, %-d. %B %Y, %H:%M"
msgid "My files" msgid "My files"
msgstr "Meine Dateien" msgstr "Meine Dateien"
#: crates/pdfs-gui/src/app/main.rs:746 crates/pdfs-gui/src/app/main.rs:1390 #: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407
#: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/albums.rs:638
#: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/photos.rs:842
#: crates/pdfs-gui/src/app/pages/places.rs:441 #: crates/pdfs-gui/src/app/pages/places.rs:441
msgid "Photos" msgid "Photos"
msgstr "Fotos" msgstr "Fotos"
#: crates/pdfs-gui/src/app/main.rs:751 #: crates/pdfs-gui/src/app/main.rs:768
#: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:168
#: crates/pdfs-gui/src/app/pages/shared.rs:482 #: crates/pdfs-gui/src/app/pages/shared.rs:482
msgid "Shared with me" msgid "Shared with me"
msgstr "Mit mir geteilt" msgstr "Mit mir geteilt"
#: crates/pdfs-gui/src/app/main.rs:756 #: crates/pdfs-gui/src/app/main.rs:773
#: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120
msgid "Shared by me" msgid "Shared by me"
msgstr "Von mir geteilt" msgstr "Von mir geteilt"
#: crates/pdfs-gui/src/app/main.rs:759 #: crates/pdfs-gui/src/app/main.rs:776
#: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:140
#: crates/pdfs-gui/src/app/pages/devices.rs:481 #: crates/pdfs-gui/src/app/pages/devices.rs:481
msgid "Computers" msgid "Computers"
msgstr "Computer" msgstr "Computer"
#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/main.rs:777
#: crates/pdfs-gui/src/app/pages/trash.rs:101 #: crates/pdfs-gui/src/app/pages/trash.rs:101
msgid "Trash" msgid "Trash"
msgstr "Papierkorb" msgstr "Papierkorb"
#. Translators: sidebar entry for the page listing this computer's synced folders. #. Translators: sidebar entry for the page listing this computer's synced folders.
#: crates/pdfs-gui/src/app/main.rs:762 #: crates/pdfs-gui/src/app/main.rs:779
msgid "Sync" msgid "Sync"
msgstr "Synchronisierung" msgstr "Synchronisierung"
#. Translators: a failed action's toast. {what} says what failed, such as #. Translators: a failed action's toast. {what} says what failed, such as
#. "Couldn't rename"; {detail} is the reason, usually untranslated text #. "Couldn't rename"; {detail} is the reason, usually untranslated text
#. from the mount service. #. from the mount service.
#: crates/pdfs-gui/src/app/main.rs:1079 #: crates/pdfs-gui/src/app/main.rs:1096
#, rust-format #, rust-format
msgid "{what}: {detail}" msgid "{what}: {detail}"
msgstr "{what}: {detail}" msgstr "{what}: {detail}"
#: crates/pdfs-gui/src/app/main.rs:1098 #: crates/pdfs-gui/src/app/main.rs:1115
msgid "You're offline" msgid "You're offline"
msgstr "Keine Internetverbindung" msgstr "Keine Internetverbindung"
#: crates/pdfs-gui/src/app/main.rs:1099 #: crates/pdfs-gui/src/app/main.rs:1116
msgid "That's not there any more" msgid "That's not there any more"
msgstr "Das ist nicht mehr vorhanden" msgstr "Das ist nicht mehr vorhanden"
#: crates/pdfs-gui/src/app/main.rs:1100 #: crates/pdfs-gui/src/app/main.rs:1117
msgid "You don't have access to that" msgid "You don't have access to that"
msgstr "Kein Zugriff darauf" msgstr "Kein Zugriff darauf"
#: crates/pdfs-gui/src/app/main.rs:1101 #: crates/pdfs-gui/src/app/main.rs:1118
msgid "Something changed this first" msgid "Something changed this first"
msgstr "Etwas anderes hat dies zuerst geändert" msgstr "Etwas anderes hat dies zuerst geändert"
#: crates/pdfs-gui/src/app/main.rs:1102 #: crates/pdfs-gui/src/app/main.rs:1119
msgid "Your Proton Drive is full" msgid "Your Proton Drive is full"
msgstr "Das Proton Drive ist voll" msgstr "Das Proton Drive ist voll"
#: crates/pdfs-gui/src/app/main.rs:1143 crates/pdfs-gui/src/app/main.rs:1372 #: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389
#: crates/pdfs-gui/src/app/pages/status.rs:385 #: crates/pdfs-gui/src/app/pages/status.rs:385
msgid "Preferences" msgid "Preferences"
msgstr "Einstellungen" msgstr "Einstellungen"
#: crates/pdfs-gui/src/app/main.rs:1144 #: crates/pdfs-gui/src/app/main.rs:1161
#: crates/pdfs-gui/src/app/widgets/compat.rs:262 #: crates/pdfs-gui/src/app/widgets/compat.rs:262
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "Tastenkürzel" msgstr "Tastenkürzel"
#: crates/pdfs-gui/src/app/main.rs:1146 #: crates/pdfs-gui/src/app/main.rs:1163
msgid "About Proton Drive for Linux" msgid "About Proton Drive for Linux"
msgstr "Info zu Proton Drive for Linux" msgstr "Info zu Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1151 #: crates/pdfs-gui/src/app/main.rs:1168
msgid "Main menu" msgid "Main menu"
msgstr "Hauptmenü" msgstr "Hauptmenü"
#: crates/pdfs-gui/src/app/main.rs:1207 #: crates/pdfs-gui/src/app/main.rs:1224
msgid "Proton Drive for Linux" msgid "Proton Drive for Linux"
msgstr "Proton Drive for Linux" msgstr "Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1215 #: crates/pdfs-gui/src/app/main.rs:1232
msgid "" msgid ""
"Files-on-demand Proton Drive for the Linux desktop.\n" "Files-on-demand Proton Drive for the Linux desktop.\n"
"\n" "\n"
@@ -166,23 +166,23 @@ msgstr ""
"Inoffizieller Client – nicht mit Proton AG verbunden, von Proton AG " "Inoffizieller Client – nicht mit Proton AG verbunden, von Proton AG "
"unterstützt oder befürwortet." "unterstützt oder befürwortet."
#: crates/pdfs-gui/src/app/main.rs:1220 #: crates/pdfs-gui/src/app/main.rs:1237
msgid "Source Code" msgid "Source Code"
msgstr "Quellcode" msgstr "Quellcode"
#: crates/pdfs-gui/src/app/main.rs:1224 #: crates/pdfs-gui/src/app/main.rs:1241
msgid "Proton Terms of Service" msgid "Proton Terms of Service"
msgstr "Nutzungsbedingungen von Proton" msgstr "Nutzungsbedingungen von Proton"
#: crates/pdfs-gui/src/app/main.rs:1228 #: crates/pdfs-gui/src/app/main.rs:1245
msgid "Proton Privacy Policy" msgid "Proton Privacy Policy"
msgstr "Datenschutzerklärung von Proton" msgstr "Datenschutzerklärung von Proton"
#: crates/pdfs-gui/src/app/main.rs:1233 #: crates/pdfs-gui/src/app/main.rs:1250
msgid "Disclaimer" msgid "Disclaimer"
msgstr "Haftungsausschluss" msgstr "Haftungsausschluss"
#: crates/pdfs-gui/src/app/main.rs:1237 #: crates/pdfs-gui/src/app/main.rs:1254
msgid "" msgid ""
"This is an unofficial, community-built client. It is not affiliated with, " "This is an unofficial, community-built client. It is not affiliated with, "
"endorsed by, or supported by Proton AG.\n" "endorsed by, or supported by Proton AG.\n"
@@ -207,7 +207,7 @@ msgstr ""
"bereitgestellt. Daten, deren Verlust nicht hinnehmbar ist, sollten " "bereitgestellt. Daten, deren Verlust nicht hinnehmbar ist, sollten "
"zusätzlich gesichert werden." "zusätzlich gesichert werden."
#: crates/pdfs-gui/src/app/main.rs:1245 #: crates/pdfs-gui/src/app/main.rs:1262
msgid "" msgid ""
"Places are named with data from <a href=\"https://www.geonames.org/" "Places are named with data from <a href=\"https://www.geonames.org/"
"\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/"
@@ -217,7 +217,7 @@ msgstr ""
"\">GeoNames</a>, lizenziert unter <a href=\"https://creativecommons.org/" "\">GeoNames</a>, lizenziert unter <a href=\"https://creativecommons.org/"
"licenses/by/4.0/\">CC BY 4.0</a>." "licenses/by/4.0/\">CC BY 4.0</a>."
#: crates/pdfs-gui/src/app/main.rs:1253 #: crates/pdfs-gui/src/app/main.rs:1270
msgid "" msgid ""
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/" "The places map is drawn from <a href=\"https://www.naturalearthdata.com/"
"\">Natural Earth</a> land outlines and country borders, which are in the " "\">Natural Earth</a> land outlines and country borders, which are in the "
@@ -227,7 +227,7 @@ msgstr ""
"href=\"https://www.naturalearthdata.com/\">Natural Earth</a> gezeichnet, die " "href=\"https://www.naturalearthdata.com/\">Natural Earth</a> gezeichnet, die "
"gemeinfrei sind." "gemeinfrei sind."
#: crates/pdfs-gui/src/app/main.rs:1261 #: crates/pdfs-gui/src/app/main.rs:1278
msgid "" msgid ""
"The street map, when turned on, comes from <a href=\"https://openfreemap.org/" "The street map, when turned on, comes from <a href=\"https://openfreemap.org/"
"\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/" "\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/"
@@ -243,15 +243,15 @@ msgstr ""
#. Translators: replace with your name(s) and email address(es), one #. Translators: replace with your name(s) and email address(es), one
#. per line, to be credited in the About dialog. #. per line, to be credited in the About dialog.
#: crates/pdfs-gui/src/app/main.rs:1272 #: crates/pdfs-gui/src/app/main.rs:1289
msgid "translator-credits" msgid "translator-credits"
msgstr "Mitwirkende an Proton Drive for Linux" msgstr "Mitwirkende an Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1310 #: crates/pdfs-gui/src/app/main.rs:1327
msgid "Stop Proton Drive?" msgid "Stop Proton Drive?"
msgstr "Proton Drive beenden?" msgstr "Proton Drive beenden?"
#: crates/pdfs-gui/src/app/main.rs:1311 #: crates/pdfs-gui/src/app/main.rs:1328
msgid "" msgid ""
"My files goes offline and nothing syncs until you connect again. Proton " "My files goes offline and nothing syncs until you connect again. Proton "
"Drive starts again at your next login." "Drive starts again at your next login."
@@ -260,7 +260,7 @@ msgstr ""
"eine Verbindung hergestellt wird. Proton Drive startet bei der nächsten " "eine Verbindung hergestellt wird. Proton Drive startet bei der nächsten "
"Anmeldung erneut." "Anmeldung erneut."
#: crates/pdfs-gui/src/app/main.rs:1313 crates/pdfs-gui/src/app/main.rs:1588 #: crates/pdfs-gui/src/app/main.rs:1330 crates/pdfs-gui/src/app/main.rs:1605
#: crates/pdfs-gui/src/app/pages/albums.rs:243 #: crates/pdfs-gui/src/app/pages/albums.rs:243
#: crates/pdfs-gui/src/app/pages/albums.rs:428 #: crates/pdfs-gui/src/app/pages/albums.rs:428
#: crates/pdfs-gui/src/app/pages/browser.rs:1892 #: crates/pdfs-gui/src/app/pages/browser.rs:1892
@@ -275,7 +275,8 @@ msgstr ""
#: crates/pdfs-gui/src/app/pages/locations.rs:1013 #: crates/pdfs-gui/src/app/pages/locations.rs:1013
#: crates/pdfs-gui/src/app/pages/locations.rs:1488 #: crates/pdfs-gui/src/app/pages/locations.rs:1488
#: crates/pdfs-gui/src/app/pages/locations.rs:1538 #: crates/pdfs-gui/src/app/pages/locations.rs:1538
#: crates/pdfs-gui/src/app/pages/login.rs:312 #: crates/pdfs-gui/src/app/pages/login.rs:393
#: crates/pdfs-gui/src/app/pages/login.rs:507
#: crates/pdfs-gui/src/app/pages/photos.rs:2055 #: crates/pdfs-gui/src/app/pages/photos.rs:2055
#: crates/pdfs-gui/src/app/pages/shared.rs:946 #: crates/pdfs-gui/src/app/pages/shared.rs:946
#: crates/pdfs-gui/src/app/pages/shared.rs:1027 #: crates/pdfs-gui/src/app/pages/shared.rs:1027
@@ -290,30 +291,30 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
#: crates/pdfs-gui/src/app/main.rs:1314 #: crates/pdfs-gui/src/app/main.rs:1331
msgctxt "verb" msgctxt "verb"
msgid "Stop" msgid "Stop"
msgstr "Beenden" msgstr "Beenden"
#: crates/pdfs-gui/src/app/main.rs:1331 #: crates/pdfs-gui/src/app/main.rs:1348
msgid "Couldn't stop Proton Drive" msgid "Couldn't stop Proton Drive"
msgstr "Proton Drive konnte nicht beendet werden" msgstr "Proton Drive konnte nicht beendet werden"
#: crates/pdfs-gui/src/app/main.rs:1332 #: crates/pdfs-gui/src/app/main.rs:1349
msgid "The Proton Drive service did not stop." msgid "The Proton Drive service did not stop."
msgstr "Der Proton-Drive-Dienst wurde nicht beendet." msgstr "Der Proton-Drive-Dienst wurde nicht beendet."
#: crates/pdfs-gui/src/app/main.rs:1368 #: crates/pdfs-gui/src/app/main.rs:1385
#: crates/pdfs-gui/src/app/pages/status.rs:254 #: crates/pdfs-gui/src/app/pages/status.rs:254
msgid "General" msgid "General"
msgstr "Allgemein" msgstr "Allgemein"
#: crates/pdfs-gui/src/app/main.rs:1370 #: crates/pdfs-gui/src/app/main.rs:1387
#: crates/pdfs-gui/src/app/pages/browser.rs:247 #: crates/pdfs-gui/src/app/pages/browser.rs:247
msgid "Search Drive" msgid "Search Drive"
msgstr "Drive durchsuchen" msgstr "Drive durchsuchen"
#: crates/pdfs-gui/src/app/main.rs:1371 #: crates/pdfs-gui/src/app/main.rs:1388
#: crates/pdfs-gui/src/app/pages/browser.rs:957 #: crates/pdfs-gui/src/app/pages/browser.rs:957
#: crates/pdfs-gui/src/app/pages/devices.rs:554 #: crates/pdfs-gui/src/app/pages/devices.rs:554
#: crates/pdfs-gui/src/app/pages/shared.rs:586 #: crates/pdfs-gui/src/app/pages/shared.rs:586
@@ -322,38 +323,38 @@ msgstr "Drive durchsuchen"
msgid "Refresh" msgid "Refresh"
msgstr "Aktualisieren" msgstr "Aktualisieren"
#: crates/pdfs-gui/src/app/main.rs:1373 #: crates/pdfs-gui/src/app/main.rs:1390
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "Tastenkürzel" msgstr "Tastenkürzel"
#: crates/pdfs-gui/src/app/main.rs:1377 #: crates/pdfs-gui/src/app/main.rs:1394
msgid "Files" msgid "Files"
msgstr "Dateien" msgstr "Dateien"
#: crates/pdfs-gui/src/app/main.rs:1379 #: crates/pdfs-gui/src/app/main.rs:1396
#: crates/pdfs-gui/src/app/pages/browser.rs:294 #: crates/pdfs-gui/src/app/pages/browser.rs:294
msgid "New folder" msgid "New folder"
msgstr "Neuer Ordner" msgstr "Neuer Ordner"
#: crates/pdfs-gui/src/app/main.rs:1380 #: crates/pdfs-gui/src/app/main.rs:1397
#: crates/pdfs-gui/src/app/pages/browser.rs:290 #: crates/pdfs-gui/src/app/pages/browser.rs:290
msgid "Upload files" msgid "Upload files"
msgstr "Dateien hochladen" msgstr "Dateien hochladen"
#: crates/pdfs-gui/src/app/main.rs:1381 #: crates/pdfs-gui/src/app/main.rs:1398
msgid "Back / forward" msgid "Back / forward"
msgstr "Zurück / Vor" msgstr "Zurück / Vor"
#: crates/pdfs-gui/src/app/main.rs:1382 #: crates/pdfs-gui/src/app/main.rs:1399
#: crates/pdfs-gui/src/app/pages/browser.rs:1943 #: crates/pdfs-gui/src/app/pages/browser.rs:1943
msgid "Parent folder" msgid "Parent folder"
msgstr "Übergeordneter Ordner" msgstr "Übergeordneter Ordner"
#: crates/pdfs-gui/src/app/main.rs:1383 #: crates/pdfs-gui/src/app/main.rs:1400
msgid "Grid / list" msgid "Grid / list"
msgstr "Raster / Liste" msgstr "Raster / Liste"
#: crates/pdfs-gui/src/app/main.rs:1384 #: crates/pdfs-gui/src/app/main.rs:1401
#: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/albums.rs:298
#: crates/pdfs-gui/src/app/pages/browser.rs:1871 #: crates/pdfs-gui/src/app/pages/browser.rs:1871
#: crates/pdfs-gui/src/app/pages/browser.rs:1893 #: crates/pdfs-gui/src/app/pages/browser.rs:1893
@@ -362,7 +363,7 @@ msgstr "Raster / Liste"
msgid "Rename" msgid "Rename"
msgstr "Umbenennen" msgstr "Umbenennen"
#: crates/pdfs-gui/src/app/main.rs:1385 crates/pdfs-gui/src/app/main.rs:1405 #: crates/pdfs-gui/src/app/main.rs:1402 crates/pdfs-gui/src/app/main.rs:1422
#: crates/pdfs-gui/src/app/pages/browser.rs:357 #: crates/pdfs-gui/src/app/pages/browser.rs:357
#: crates/pdfs-gui/src/app/pages/browser.rs:900 #: crates/pdfs-gui/src/app/pages/browser.rs:900
#: crates/pdfs-gui/src/app/pages/browser.rs:932 #: crates/pdfs-gui/src/app/pages/browser.rs:932
@@ -372,73 +373,73 @@ msgstr "Umbenennen"
msgid "Move to Trash" msgid "Move to Trash"
msgstr "In den Papierkorb verschieben" msgstr "In den Papierkorb verschieben"
#: crates/pdfs-gui/src/app/main.rs:1386 #: crates/pdfs-gui/src/app/main.rs:1403
msgid "Clear the selection" msgid "Clear the selection"
msgstr "Auswahl aufheben" msgstr "Auswahl aufheben"
#: crates/pdfs-gui/src/app/main.rs:1392 #: crates/pdfs-gui/src/app/main.rs:1409
msgid "Larger thumbnails" msgid "Larger thumbnails"
msgstr "Größere Vorschaubilder" msgstr "Größere Vorschaubilder"
#: crates/pdfs-gui/src/app/main.rs:1393 #: crates/pdfs-gui/src/app/main.rs:1410
msgid "Smaller thumbnails" msgid "Smaller thumbnails"
msgstr "Kleinere Vorschaubilder" msgstr "Kleinere Vorschaubilder"
#: crates/pdfs-gui/src/app/main.rs:1394 #: crates/pdfs-gui/src/app/main.rs:1411
msgid "Reset thumbnail size" msgid "Reset thumbnail size"
msgstr "Vorschaubildgröße zurücksetzen" msgstr "Vorschaubildgröße zurücksetzen"
#: crates/pdfs-gui/src/app/main.rs:1395 #: crates/pdfs-gui/src/app/main.rs:1412
msgid "Select all" msgid "Select all"
msgstr "Alles auswählen" msgstr "Alles auswählen"
#: crates/pdfs-gui/src/app/main.rs:1399 #: crates/pdfs-gui/src/app/main.rs:1416
msgid "Photo Viewer" msgid "Photo Viewer"
msgstr "Fotobetrachter" msgstr "Fotobetrachter"
#: crates/pdfs-gui/src/app/main.rs:1401 #: crates/pdfs-gui/src/app/main.rs:1418
msgid "Previous / next photo" msgid "Previous / next photo"
msgstr "Vorheriges / nächstes Foto" msgstr "Vorheriges / nächstes Foto"
#: crates/pdfs-gui/src/app/main.rs:1402 #: crates/pdfs-gui/src/app/main.rs:1419
msgid "First / last photo" msgid "First / last photo"
msgstr "Erstes / letztes Foto" msgstr "Erstes / letztes Foto"
#: crates/pdfs-gui/src/app/main.rs:1403 #: crates/pdfs-gui/src/app/main.rs:1420
msgid "Show details" msgid "Show details"
msgstr "Details anzeigen" msgstr "Details anzeigen"
#: crates/pdfs-gui/src/app/main.rs:1404 #: crates/pdfs-gui/src/app/main.rs:1421
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Vollbild" msgstr "Vollbild"
#: crates/pdfs-gui/src/app/main.rs:1406 #: crates/pdfs-gui/src/app/main.rs:1423
msgid "Close" msgid "Close"
msgstr "Schließen" msgstr "Schließen"
#: crates/pdfs-gui/src/app/main.rs:1496 #: crates/pdfs-gui/src/app/main.rs:1513
msgid "Select a single item to rename it" msgid "Select a single item to rename it"
msgstr "Zum Umbenennen genau ein Element auswählen" msgstr "Zum Umbenennen genau ein Element auswählen"
#: crates/pdfs-gui/src/app/main.rs:1656 #: crates/pdfs-gui/src/app/main.rs:1673
#: crates/pdfs-gui/src/app/pages/albums.rs:85 #: crates/pdfs-gui/src/app/pages/albums.rs:85
#: crates/pdfs-gui/src/app/pages/places.rs:104 #: crates/pdfs-gui/src/app/pages/places.rs:104
#: crates/pdfs-gui/src/app/pages/status.rs:1370 #: crates/pdfs-gui/src/app/pages/status.rs:1371
msgid "Not connected" msgid "Not connected"
msgstr "Nicht verbunden" msgstr "Nicht verbunden"
#: crates/pdfs-gui/src/app/main.rs:1657 #: crates/pdfs-gui/src/app/main.rs:1674
msgid "The Proton Drive service isn't running." msgid "The Proton Drive service isn't running."
msgstr "Der Proton-Drive-Dienst läuft nicht." msgstr "Der Proton-Drive-Dienst läuft nicht."
#: crates/pdfs-gui/src/app/main.rs:1665 #: crates/pdfs-gui/src/app/main.rs:1682
#: crates/pdfs-gui/src/app/pages/status.rs:336 #: crates/pdfs-gui/src/app/pages/status.rs:336
#: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595 #: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595
#: crates/pdfs-gui/src/main.rs:444 #: crates/pdfs-gui/src/main.rs:453
msgid "Connecting…" msgid "Connecting…"
msgstr "Verbindung wird hergestellt …" msgstr "Verbindung wird hergestellt …"
#: crates/pdfs-gui/src/app/main.rs:1666 #: crates/pdfs-gui/src/app/main.rs:1683
msgid "Waiting for the Proton Drive service to start." msgid "Waiting for the Proton Drive service to start."
msgstr "Warten auf den Start des Proton-Drive-Dienstes." msgstr "Warten auf den Start des Proton-Drive-Dienstes."
@@ -1287,7 +1288,7 @@ msgstr "Alben konnten nicht geladen werden"
#: crates/pdfs-gui/src/app/pages/shared.rs:1006 #: crates/pdfs-gui/src/app/pages/shared.rs:1006
#: crates/pdfs-gui/src/app/pages/shared.rs:1122 #: crates/pdfs-gui/src/app/pages/shared.rs:1122
#: crates/pdfs-gui/src/app/pages/status.rs:750 #: crates/pdfs-gui/src/app/pages/status.rs:750
#: crates/pdfs-gui/src/app/pages/status.rs:1517 #: crates/pdfs-gui/src/app/pages/status.rs:1518
#: crates/pdfs-gui/src/app/pages/takeout.rs:424 #: crates/pdfs-gui/src/app/pages/takeout.rs:424
#: crates/pdfs-gui/src/app/pages/takeout.rs:617 #: crates/pdfs-gui/src/app/pages/takeout.rs:617
#: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438
@@ -1555,7 +1556,7 @@ msgid "Make available offline"
msgstr "Offline verfügbar machen" msgstr "Offline verfügbar machen"
#: crates/pdfs-gui/src/app/pages/browser.rs:347 #: crates/pdfs-gui/src/app/pages/browser.rs:347
#: crates/pdfs-gui/src/app/pages/status.rs:1493 #: crates/pdfs-gui/src/app/pages/status.rs:1494
msgid "Make online only" msgid "Make online only"
msgstr "Nur online verfügbar machen" msgstr "Nur online verfügbar machen"
@@ -2724,7 +2725,7 @@ msgid "Sync paused until you resume"
msgstr "Synchronisierung bis zur Fortsetzung pausiert" msgstr "Synchronisierung bis zur Fortsetzung pausiert"
#: crates/pdfs-gui/src/app/pages/locations.rs:529 #: crates/pdfs-gui/src/app/pages/locations.rs:529
#: crates/pdfs-gui/src/app/pages/status.rs:1323 crates/pdfs-gui/src/main.rs:237 #: crates/pdfs-gui/src/app/pages/status.rs:1324 crates/pdfs-gui/src/main.rs:246
msgid "Sync paused" msgid "Sync paused"
msgstr "Synchronisierung pausiert" msgstr "Synchronisierung pausiert"
@@ -3200,25 +3201,36 @@ msgstr "E-Mail-Adresse und Passwort eingeben."
msgid "Signing in…" msgid "Signing in…"
msgstr "Anmeldung läuft …" msgstr "Anmeldung läuft …"
#: crates/pdfs-gui/src/app/pages/login.rs:155 #: crates/pdfs-gui/src/app/pages/login.rs:164
msgid "Complete the verification to continue…" msgid "Complete the verification to continue…"
msgstr "Zum Fortfahren die Überprüfung abschließen …" msgstr "Zum Fortfahren die Überprüfung abschließen …"
#: crates/pdfs-gui/src/app/pages/login.rs:165 #: crates/pdfs-gui/src/app/pages/login.rs:197
msgid "login cancelled" msgid "login cancelled"
msgstr "Anmeldung abgebrochen" msgstr "Anmeldung abgebrochen"
#: crates/pdfs-gui/src/app/pages/login.rs:198 #: crates/pdfs-gui/src/app/pages/login.rs:210
msgid ""
"The verification page couldn't be opened, so the sign-in was cancelled. Your "
"system may keep the web view's sandbox from starting; the troubleshooting "
"guide explains what to do."
msgstr ""
"Die Überprüfungsseite konnte nicht geöffnet werden, daher wurde die "
"Anmeldung abgebrochen. Möglicherweise verhindert das System, dass die "
"Sandbox der Webansicht startet; die Anleitung zur Fehlerbehebung erklärt, "
"was zu tun ist."
#: crates/pdfs-gui/src/app/pages/login.rs:233
msgid "Too many sign-in attempts. Wait a few minutes and try again." msgid "Too many sign-in attempts. Wait a few minutes and try again."
msgstr "Zu viele Anmeldeversuche. Einige Minuten warten und erneut versuchen." msgstr "Zu viele Anmeldeversuche. Einige Minuten warten und erneut versuchen."
#: crates/pdfs-gui/src/app/pages/login.rs:204 #: crates/pdfs-gui/src/app/pages/login.rs:239
msgid "Couldn't reach Proton. Check your internet connection and try again." msgid "Couldn't reach Proton. Check your internet connection and try again."
msgstr "" msgstr ""
"Proton ist nicht erreichbar. Bitte die Internetverbindung prüfen und erneut " "Proton ist nicht erreichbar. Bitte die Internetverbindung prüfen und erneut "
"versuchen." "versuchen."
#: crates/pdfs-gui/src/app/pages/login.rs:207 #: crates/pdfs-gui/src/app/pages/login.rs:242
msgid "" msgid ""
"Signed in, but the session couldn't be saved to the system keyring. Make " "Signed in, but the session couldn't be saved to the system keyring. Make "
"sure a keyring (GNOME Keyring or KWallet) is running and unlocked." "sure a keyring (GNOME Keyring or KWallet) is running and unlocked."
@@ -3227,11 +3239,19 @@ msgstr ""
"gespeichert werden. Bitte sicherstellen, dass ein Schlüsselbund (GNOME " "gespeichert werden. Bitte sicherstellen, dass ein Schlüsselbund (GNOME "
"Keyring oder KWallet) läuft und entsperrt ist." "Keyring oder KWallet) läuft und entsperrt ist."
#: crates/pdfs-gui/src/app/pages/login.rs:210 #: crates/pdfs-gui/src/app/pages/login.rs:245
msgid "Sign-in cancelled: no two-factor code was entered." msgid "Sign-in cancelled: no two-factor code was entered."
msgstr "Anmeldung abgebrochen: Es wurde kein Zwei-Faktor-Code eingegeben." msgstr "Anmeldung abgebrochen: Es wurde kein Zwei-Faktor-Code eingegeben."
#: crates/pdfs-gui/src/app/pages/login.rs:213 #: crates/pdfs-gui/src/app/pages/login.rs:248
msgid "Sign-in cancelled: no mailbox password was entered."
msgstr "Anmeldung abgebrochen: Es wurde kein Postfach-Passwort eingegeben."
#: crates/pdfs-gui/src/app/pages/login.rs:251
msgid "That mailbox password didn't unlock your account."
msgstr "Das Postfach-Passwort hat Ihr Konto nicht entsperrt."
#: crates/pdfs-gui/src/app/pages/login.rs:254
msgid "" msgid ""
"Sign-in cancelled: the verification wasn't completed. Sign in again to get a " "Sign-in cancelled: the verification wasn't completed. Sign in again to get a "
"new one." "new one."
@@ -3240,35 +3260,87 @@ msgstr ""
"neue Überprüfung erneut anmelden." "neue Überprüfung erneut anmelden."
#. Translators: {error} is an error message, usually in English. #. Translators: {error} is an error message, usually in English.
#: crates/pdfs-gui/src/app/pages/login.rs:218 #: crates/pdfs-gui/src/app/pages/login.rs:259
#, rust-format #, rust-format
msgid "Sign-in failed: {error}" msgid "Sign-in failed: {error}"
msgstr "Anmeldung fehlgeschlagen: {error}" msgstr "Anmeldung fehlgeschlagen: {error}"
#: crates/pdfs-gui/src/app/pages/login.rs:298 #: crates/pdfs-gui/src/app/pages/login.rs:369
msgid "That mailbox password didn't unlock your account. Try again."
msgstr ""
"Das Postfach-Passwort hat Ihr Konto nicht entsperrt. Versuchen Sie es erneut."
#: crates/pdfs-gui/src/app/pages/login.rs:371
msgid ""
"This account has a separate mailbox password. Enter it to unlock your files."
msgstr ""
"Dieses Konto hat ein separates Postfach-Passwort. Geben Sie es ein, um Ihre "
"Dateien zu entsperren."
#: crates/pdfs-gui/src/app/pages/login.rs:374
msgid "Mailbox Password"
msgstr "Postfach-Passwort"
#: crates/pdfs-gui/src/app/pages/login.rs:380
msgid "Mailbox password"
msgstr "Postfach-Passwort"
#: crates/pdfs-gui/src/app/pages/login.rs:384
msgid "Keep unlocked on this computer"
msgstr "Auf diesem Computer entsperrt lassen"
#: crates/pdfs-gui/src/app/pages/login.rs:386
msgid ""
"Stores the unlock in the system keyring, so Proton Drive opens without "
"asking after every login. Otherwise you enter the password once per session."
msgstr ""
"Speichert die Entsperrung im System-Schlüsselbund, sodass sich Proton Drive "
"nach jeder Anmeldung ohne Nachfrage öffnet. Andernfalls geben Sie das "
"Passwort einmal pro Sitzung ein."
#: crates/pdfs-gui/src/app/pages/login.rs:394
#: crates/pdfs-gui/src/app/pages/login.rs:447
msgid "Unlock"
msgstr "Entsperren"
#: crates/pdfs-gui/src/app/pages/login.rs:446
msgid "Proton Drive is locked. Enter your mailbox password to unlock it."
msgstr ""
"Proton Drive ist gesperrt. Geben Sie Ihr Postfach-Passwort ein, um es zu "
"entsperren."
#: crates/pdfs-gui/src/app/pages/login.rs:475
msgid "Unlocked."
msgstr "Entsperrt."
#: crates/pdfs-gui/src/app/pages/login.rs:480
msgid "Couldn't unlock"
msgstr "Entsperren fehlgeschlagen"
#: crates/pdfs-gui/src/app/pages/login.rs:493
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "Zwei-Faktor-Authentifizierung" msgstr "Zwei-Faktor-Authentifizierung"
#: crates/pdfs-gui/src/app/pages/login.rs:300 #: crates/pdfs-gui/src/app/pages/login.rs:495
msgid "" msgid ""
"Enter the code from your authenticator app, or one of your recovery codes." "Enter the code from your authenticator app, or one of your recovery codes."
msgstr "" msgstr ""
"Den Code aus der Authentifizierungs-App oder einen der " "Den Code aus der Authentifizierungs-App oder einen der "
"Wiederherstellungscodes eingeben." "Wiederherstellungscodes eingeben."
#: crates/pdfs-gui/src/app/pages/login.rs:306 #: crates/pdfs-gui/src/app/pages/login.rs:501
msgid "Authentication code" msgid "Authentication code"
msgstr "Authentifizierungscode" msgstr "Authentifizierungscode"
#: crates/pdfs-gui/src/app/pages/login.rs:313 #: crates/pdfs-gui/src/app/pages/login.rs:508
msgid "Confirm" msgid "Confirm"
msgstr "Bestätigen" msgstr "Bestätigen"
#: crates/pdfs-gui/src/app/pages/login.rs:341 #: crates/pdfs-gui/src/app/pages/login.rs:536
msgid "Sign Out?" msgid "Sign Out?"
msgstr "Abmelden?" msgstr "Abmelden?"
#: crates/pdfs-gui/src/app/pages/login.rs:343 #: crates/pdfs-gui/src/app/pages/login.rs:538
msgid "" msgid ""
"Proton Drive will disconnect and stop syncing until you sign in again. Files " "Proton Drive will disconnect and stop syncing until you sign in again. Files "
"already on this computer stay where they are." "already on this computer stay where they are."
@@ -3277,7 +3349,7 @@ msgstr ""
"nächsten Anmeldung. Dateien, die sich bereits auf diesem Computer befinden, " "nächsten Anmeldung. Dateien, die sich bereits auf diesem Computer befinden, "
"bleiben, wo sie sind." "bleiben, wo sie sind."
#: crates/pdfs-gui/src/app/pages/login.rs:345 #: crates/pdfs-gui/src/app/pages/login.rs:540
msgid "Sign Out" msgid "Sign Out"
msgstr "Abmelden" msgstr "Abmelden"
@@ -4645,40 +4717,40 @@ msgid "Restart Now"
msgstr "Jetzt neu starten" msgstr "Jetzt neu starten"
#. Translators: {size} is an amount of storage, such as "1.2 GiB". #. Translators: {size} is an amount of storage, such as "1.2 GiB".
#: crates/pdfs-gui/src/app/pages/status.rs:912 #: crates/pdfs-gui/src/app/pages/status.rs:913
#, rust-format #, rust-format
msgid "{size} used" msgid "{size} used"
msgstr "{size} belegt" msgstr "{size} belegt"
#: crates/pdfs-gui/src/app/pages/status.rs:923 #: crates/pdfs-gui/src/app/pages/status.rs:924
#, rust-format #, rust-format
msgid "{used} of {total} used ({percent}%)" msgid "{used} of {total} used ({percent}%)"
msgstr "{used} von {total} belegt ({percent} %)" msgstr "{used} von {total} belegt ({percent} %)"
#: crates/pdfs-gui/src/app/pages/status.rs:961 #: crates/pdfs-gui/src/app/pages/status.rs:962
msgid "Proton Drive connected" msgid "Proton Drive connected"
msgstr "Proton Drive verbunden" msgstr "Proton Drive verbunden"
#: crates/pdfs-gui/src/app/pages/status.rs:962 #: crates/pdfs-gui/src/app/pages/status.rs:963
msgid "Your files are available." msgid "Your files are available."
msgstr "Deine Dateien sind verfügbar." msgstr "Deine Dateien sind verfügbar."
#: crates/pdfs-gui/src/app/pages/status.rs:967 #: crates/pdfs-gui/src/app/pages/status.rs:968
msgid "Proton Drive disconnected" msgid "Proton Drive disconnected"
msgstr "Proton Drive getrennt" msgstr "Proton Drive getrennt"
#: crates/pdfs-gui/src/app/pages/status.rs:968 #: crates/pdfs-gui/src/app/pages/status.rs:969
msgid "" msgid ""
"The Proton Drive service stopped. Files aren't available until it restarts." "The Proton Drive service stopped. Files aren't available until it restarts."
msgstr "" msgstr ""
"Der Proton-Drive-Dienst wurde beendet. Dateien sind erst nach seinem " "Der Proton-Drive-Dienst wurde beendet. Dateien sind erst nach seinem "
"Neustart wieder verfügbar." "Neustart wieder verfügbar."
#: crates/pdfs-gui/src/app/pages/status.rs:1037 #: crates/pdfs-gui/src/app/pages/status.rs:1038
msgid "Sync complete" msgid "Sync complete"
msgstr "Synchronisierung abgeschlossen" msgstr "Synchronisierung abgeschlossen"
#: crates/pdfs-gui/src/app/pages/status.rs:1039 #: crates/pdfs-gui/src/app/pages/status.rs:1040
#, rust-format #, rust-format
msgid "{n} file finished transferring." msgid "{n} file finished transferring."
msgid_plural "{n} files finished transferring." msgid_plural "{n} files finished transferring."
@@ -4687,27 +4759,27 @@ msgstr[1] "Übertragung von {n} Dateien abgeschlossen."
#. Translators: a background job's progress. {title} is the job, such as #. Translators: a background job's progress. {title} is the job, such as
#. "Scanning Photos"; {done} and {total} are counts. #. "Scanning Photos"; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1117 #: crates/pdfs-gui/src/app/pages/status.rs:1118
#, rust-format #, rust-format
msgid "{title} — {done} of {total}" msgid "{title} — {done} of {total}"
msgstr "{title} – {done} von {total}" msgstr "{title} – {done} von {total}"
#. Translators: a background job's progress. {title} is the job, {detail} #. Translators: a background job's progress. {title} is the job, {detail}
#. what it is working on; {done} and {total} are counts. #. what it is working on; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1120 #: crates/pdfs-gui/src/app/pages/status.rs:1121
#, rust-format #, rust-format
msgid "{title} — {detail} ({done} of {total})" msgid "{title} — {detail} ({done} of {total})"
msgstr "{title} – {detail} ({done} von {total})" msgstr "{title} – {detail} ({done} von {total})"
#. Translators: a background job still running. {title} is the job. #. Translators: a background job still running. {title} is the job.
#: crates/pdfs-gui/src/app/pages/status.rs:1122 #: crates/pdfs-gui/src/app/pages/status.rs:1123
#, rust-format #, rust-format
msgid "{title}…" msgid "{title}…"
msgstr "{title} …" msgstr "{title} …"
#. Translators: a background job still running. {title} is the job, #. Translators: a background job still running. {title} is the job,
#. {detail} what it is working on. #. {detail} what it is working on.
#: crates/pdfs-gui/src/app/pages/status.rs:1125 #: crates/pdfs-gui/src/app/pages/status.rs:1126
#, rust-format #, rust-format
msgid "{title} — {detail}…" msgid "{title} — {detail}…"
msgstr "{title} – {detail} …" msgstr "{title} – {detail} …"
@@ -4715,7 +4787,7 @@ msgstr "{title} – {detail} …"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} the size moved so far and {speed} a size such as #. file name, {done} the size moved so far and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1154 #: crates/pdfs-gui/src/app/pages/status.rs:1155
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} ({speed}/s)" msgid "{arrow} {name} — {done} ({speed}/s)"
msgstr "{arrow} {name} – {done} ({speed}/s)" msgstr "{arrow} {name} – {done} ({speed}/s)"
@@ -4723,118 +4795,118 @@ msgstr "{arrow} {name} – {done} ({speed}/s)"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} and {total} sizes and {speed} a size such as #. file name, {done} and {total} sizes and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1162 #: crates/pdfs-gui/src/app/pages/status.rs:1163
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} of {total} ({speed}/s)" msgid "{arrow} {name} — {done} of {total} ({speed}/s)"
msgstr "{arrow} {name} – {done} von {total} ({speed}/s)" msgstr "{arrow} {name} – {done} von {total} ({speed}/s)"
#: crates/pdfs-gui/src/app/pages/status.rs:1254 #: crates/pdfs-gui/src/app/pages/status.rs:1255
#, rust-format #, rust-format
msgid "{used} cached — no limit set" msgid "{used} cached — no limit set"
msgstr "{used} zwischengespeichert – keine Grenze festgelegt" msgstr "{used} zwischengespeichert – keine Grenze festgelegt"
#: crates/pdfs-gui/src/app/pages/status.rs:1260 #: crates/pdfs-gui/src/app/pages/status.rs:1261
#, rust-format #, rust-format
msgid "{used} of {limit} used" msgid "{used} of {limit} used"
msgstr "{used} von {limit} belegt" msgstr "{used} von {limit} belegt"
#: crates/pdfs-gui/src/app/pages/status.rs:1328 #: crates/pdfs-gui/src/app/pages/status.rs:1329
#, rust-format #, rust-format
msgid "{queued} · resumes {time}" msgid "{queued} · resumes {time}"
msgstr "{queued} · wird {time} fortgesetzt" msgstr "{queued} · wird {time} fortgesetzt"
#. Translators: {time} is when syncing resumes, such as "at 14:30" #. Translators: {time} is when syncing resumes, such as "at 14:30"
#. or "Tue 09:00". #. or "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1333 #: crates/pdfs-gui/src/app/pages/status.rs:1334
#, rust-format #, rust-format
msgid "Resumes {time}" msgid "Resumes {time}"
msgstr "Wird {time} fortgesetzt" msgstr "Wird {time} fortgesetzt"
#: crates/pdfs-gui/src/app/pages/status.rs:1335 #: crates/pdfs-gui/src/app/pages/status.rs:1336
msgid "Until you resume" msgid "Until you resume"
msgstr "Bis zur manuellen Fortsetzung" msgstr "Bis zur manuellen Fortsetzung"
#: crates/pdfs-gui/src/app/pages/status.rs:1342 crates/pdfs-gui/src/main.rs:239 #: crates/pdfs-gui/src/app/pages/status.rs:1343 crates/pdfs-gui/src/main.rs:248
#, rust-format #, rust-format
msgid "{n} change needs attention" msgid "{n} change needs attention"
msgid_plural "{n} changes need attention" msgid_plural "{n} changes need attention"
msgstr[0] "{n} Änderung erfordert Aufmerksamkeit" msgstr[0] "{n} Änderung erfordert Aufmerksamkeit"
msgstr[1] "{n} Änderungen erfordern Aufmerksamkeit" msgstr[1] "{n} Änderungen erfordern Aufmerksamkeit"
#: crates/pdfs-gui/src/app/pages/status.rs:1352 #: crates/pdfs-gui/src/app/pages/status.rs:1353
#: crates/pdfs-gui/src/app/widgets/details.rs:108 #: crates/pdfs-gui/src/app/widgets/details.rs:108
msgctxt "state" msgctxt "state"
msgid "Offline" msgid "Offline"
msgstr "Offline" msgstr "Offline"
#: crates/pdfs-gui/src/app/pages/status.rs:1353 #: crates/pdfs-gui/src/app/pages/status.rs:1354
msgid "Cached files only" msgid "Cached files only"
msgstr "Nur zwischengespeicherte Dateien" msgstr "Nur zwischengespeicherte Dateien"
#: crates/pdfs-gui/src/app/pages/status.rs:1358 #: crates/pdfs-gui/src/app/pages/status.rs:1359
msgctxt "state" msgctxt "state"
msgid "Syncing" msgid "Syncing"
msgstr "Wird synchronisiert" msgstr "Wird synchronisiert"
#: crates/pdfs-gui/src/app/pages/status.rs:1364 #: crates/pdfs-gui/src/app/pages/status.rs:1365
msgid "Up to date" msgid "Up to date"
msgstr "Auf dem neuesten Stand" msgstr "Auf dem neuesten Stand"
#: crates/pdfs-gui/src/app/pages/status.rs:1371 #: crates/pdfs-gui/src/app/pages/status.rs:1372
#: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622 #: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622
msgid "Proton Drive isn't running" msgid "Proton Drive isn't running"
msgstr "Proton Drive läuft nicht" msgstr "Proton Drive läuft nicht"
#: crates/pdfs-gui/src/app/pages/status.rs:1376 #: crates/pdfs-gui/src/app/pages/status.rs:1377
msgid "Not responding" msgid "Not responding"
msgstr "Reagiert nicht" msgstr "Reagiert nicht"
#: crates/pdfs-gui/src/app/pages/status.rs:1377 #: crates/pdfs-gui/src/app/pages/status.rs:1378
msgid "Proton Drive is taking too long to answer" msgid "Proton Drive is taking too long to answer"
msgstr "Proton Drive braucht zu lange für eine Antwort" msgstr "Proton Drive braucht zu lange für eine Antwort"
#. Translators: stands in for a time that could not be read, as in #. Translators: stands in for a time that could not be read, as in
#. "Resumes later". #. "Resumes later".
#: crates/pdfs-gui/src/app/pages/status.rs:1415 #: crates/pdfs-gui/src/app/pages/status.rs:1416
#: crates/pdfs-gui/src/app/pages/status.rs:1430 #: crates/pdfs-gui/src/app/pages/status.rs:1431
msgid "later" msgid "later"
msgstr "später" msgstr "später"
#. Translators: strftime format for a time later today, such as "at 14:30". #. Translators: strftime format for a time later today, such as "at 14:30".
#: crates/pdfs-gui/src/app/pages/status.rs:1419 #: crates/pdfs-gui/src/app/pages/status.rs:1420
msgid "at %H:%M" msgid "at %H:%M"
msgstr "um %H:%M" msgstr "um %H:%M"
#. Translators: strftime format for a time within the next week, such as #. Translators: strftime format for a time within the next week, such as
#. "Tue 09:00". #. "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1423 #: crates/pdfs-gui/src/app/pages/status.rs:1424
msgid "%a %H:%M" msgid "%a %H:%M"
msgstr "am %a um %H:%M" msgstr "am %a um %H:%M"
#. Translators: strftime format for a date further ahead, such as "23 Sep". #. Translators: strftime format for a date further ahead, such as "23 Sep".
#: crates/pdfs-gui/src/app/pages/status.rs:1426 #: crates/pdfs-gui/src/app/pages/status.rs:1427
msgid "%e %b" msgid "%e %b"
msgstr "am %-d. %b" msgstr "am %-d. %b"
#: crates/pdfs-gui/src/app/pages/status.rs:1456 #: crates/pdfs-gui/src/app/pages/status.rs:1457
msgid "No files kept offline" msgid "No files kept offline"
msgstr "Keine offline verfügbaren Dateien" msgstr "Keine offline verfügbaren Dateien"
#: crates/pdfs-gui/src/app/pages/status.rs:1458 #: crates/pdfs-gui/src/app/pages/status.rs:1459
msgid "Right-click a file and choose “Make available offline”." msgid "Right-click a file and choose “Make available offline”."
msgstr "" msgstr ""
"Mit einem Rechtsklick auf eine Datei „Offline verfügbar machen“ auswählen." "Mit einem Rechtsklick auf eine Datei „Offline verfügbar machen“ auswählen."
#: crates/pdfs-gui/src/app/pages/status.rs:1509 #: crates/pdfs-gui/src/app/pages/status.rs:1510
#: crates/pdfs-gui/src/app/pages/status.rs:1516 #: crates/pdfs-gui/src/app/pages/status.rs:1517
msgid "Couldn't make it online only" msgid "Couldn't make it online only"
msgstr "Konnte nicht auf „nur online“ umgestellt werden" msgstr "Konnte nicht auf „nur online“ umgestellt werden"
#: crates/pdfs-gui/src/app/pages/status.rs:1533 #: crates/pdfs-gui/src/app/pages/status.rs:1534
msgid "Show fewer" msgid "Show fewer"
msgstr "Weniger anzeigen" msgstr "Weniger anzeigen"
#: crates/pdfs-gui/src/app/pages/status.rs:1536 #: crates/pdfs-gui/src/app/pages/status.rs:1537
#, rust-format #, rust-format
msgid "Show all {n} file" msgid "Show all {n} file"
msgid_plural "Show all {n} files" msgid_plural "Show all {n} files"
@@ -5266,7 +5338,7 @@ msgstr "Papierkorb geleert"
msgid "Couldn't empty the trash" msgid "Couldn't empty the trash"
msgstr "Der Papierkorb konnte nicht geleert werden" msgstr "Der Papierkorb konnte nicht geleert werden"
#: crates/pdfs-gui/src/app/pages/verify.rs:57 #: crates/pdfs-gui/src/app/pages/verify.rs:162
msgid "Verification" msgid "Verification"
msgstr "Überprüfung" msgstr "Überprüfung"
@@ -5898,40 +5970,45 @@ msgstr[1] "↑ {n} werden hochgeladen"
msgid "Proton Drive isn't responding" msgid "Proton Drive isn't responding"
msgstr "Proton Drive reagiert nicht" msgstr "Proton Drive reagiert nicht"
#: crates/pdfs-gui/src/main.rs:172 #: crates/pdfs-gui/src/main.rs:173
#, rust-format
msgid "Signed in as {username} — locked"
msgstr "Angemeldet als {username} – gesperrt"
#: crates/pdfs-gui/src/main.rs:181
#, rust-format #, rust-format
msgid "Signed in as {username} — not connected" msgid "Signed in as {username} — not connected"
msgstr "Angemeldet als {username} – nicht verbunden" msgstr "Angemeldet als {username} – nicht verbunden"
#: crates/pdfs-gui/src/main.rs:177 #: crates/pdfs-gui/src/main.rs:186
msgid "Not signed in" msgid "Not signed in"
msgstr "Nicht angemeldet" msgstr "Nicht angemeldet"
#. Translators: {error} is an error message, in English. #. Translators: {error} is an error message, in English.
#: crates/pdfs-gui/src/main.rs:180 #: crates/pdfs-gui/src/main.rs:189
#, rust-format #, rust-format
msgid "Error: {error}" msgid "Error: {error}"
msgstr "Fehler: {error}" msgstr "Fehler: {error}"
#: crates/pdfs-gui/src/main.rs:201 #: crates/pdfs-gui/src/main.rs:210
msgid "Proton Drive sent an unexpected reply" msgid "Proton Drive sent an unexpected reply"
msgstr "Proton Drive hat unerwartet geantwortet" msgstr "Proton Drive hat unerwartet geantwortet"
#: crates/pdfs-gui/src/main.rs:246 #: crates/pdfs-gui/src/main.rs:255
#, rust-format #, rust-format
msgid "Up to date in {mountpoint} ({n} pinned)" msgid "Up to date in {mountpoint} ({n} pinned)"
msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)"
msgstr[0] "Aktuell in {mountpoint} ({n} angeheftet)" msgstr[0] "Aktuell in {mountpoint} ({n} angeheftet)"
msgstr[1] "Aktuell in {mountpoint} ({n} angeheftet)" msgstr[1] "Aktuell in {mountpoint} ({n} angeheftet)"
#: crates/pdfs-gui/src/main.rs:253 #: crates/pdfs-gui/src/main.rs:262
#, rust-format #, rust-format
msgid "Syncing — {queued} ({n} pinned)" msgid "Syncing — {queued} ({n} pinned)"
msgid_plural "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)"
msgstr[0] "Wird synchronisiert – {queued} ({n} angeheftet)" msgstr[0] "Wird synchronisiert – {queued} ({n} angeheftet)"
msgstr[1] "Wird synchronisiert – {queued} ({n} angeheftet)" msgstr[1] "Wird synchronisiert – {queued} ({n} angeheftet)"
#: crates/pdfs-gui/src/main.rs:260 #: crates/pdfs-gui/src/main.rs:269
#, rust-format #, rust-format
msgid "Offline — cached files only ({n} pinned)" msgid "Offline — cached files only ({n} pinned)"
msgid_plural "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)"
@@ -5939,52 +6016,52 @@ msgstr[0] "Offline – nur zwischengespeicherte Dateien ({n} angeheftet)"
msgstr[1] "Offline – nur zwischengespeicherte Dateien ({n} angeheftet)" msgstr[1] "Offline – nur zwischengespeicherte Dateien ({n} angeheftet)"
#. Translators: {queued} is a queue summary such as "3 uploads queued". #. Translators: {queued} is a queue summary such as "3 uploads queued".
#: crates/pdfs-gui/src/main.rs:266 #: crates/pdfs-gui/src/main.rs:275
#, rust-format #, rust-format
msgid "Offline — {queued}" msgid "Offline — {queued}"
msgstr "Offline – {queued}" msgstr "Offline – {queued}"
#. Translators: a menu item that opens the list of failing changes. #. Translators: a menu item that opens the list of failing changes.
#: crates/pdfs-gui/src/main.rs:296 #: crates/pdfs-gui/src/main.rs:305
#, rust-format #, rust-format
msgid "{n} issue — View" msgid "{n} issue — View"
msgid_plural "{n} issues — View" msgid_plural "{n} issues — View"
msgstr[0] "{n} Problem – Anzeigen" msgstr[0] "{n} Problem – Anzeigen"
msgstr[1] "{n} Probleme – Anzeigen" msgstr[1] "{n} Probleme – Anzeigen"
#: crates/pdfs-gui/src/main.rs:402 #: crates/pdfs-gui/src/main.rs:411
msgid "Sign In…" msgid "Sign In…"
msgstr "Anmelden …" msgstr "Anmelden …"
#: crates/pdfs-gui/src/main.rs:403 #: crates/pdfs-gui/src/main.rs:412
msgid "Open Proton Drive" msgid "Open Proton Drive"
msgstr "Proton Drive öffnen" msgstr "Proton Drive öffnen"
#: crates/pdfs-gui/src/main.rs:409 #: crates/pdfs-gui/src/main.rs:418
msgid "Open Folder" msgid "Open Folder"
msgstr "Ordner öffnen" msgstr "Ordner öffnen"
#: crates/pdfs-gui/src/main.rs:421 #: crates/pdfs-gui/src/main.rs:430
msgid "Resume Sync" msgid "Resume Sync"
msgstr "Synchronisierung fortsetzen" msgstr "Synchronisierung fortsetzen"
#: crates/pdfs-gui/src/main.rs:423 #: crates/pdfs-gui/src/main.rs:432
msgid "Pause Sync" msgid "Pause Sync"
msgstr "Synchronisierung pausieren" msgstr "Synchronisierung pausieren"
#: crates/pdfs-gui/src/main.rs:442 #: crates/pdfs-gui/src/main.rs:451
msgid "Connect" msgid "Connect"
msgstr "Verbinden" msgstr "Verbinden"
#: crates/pdfs-gui/src/main.rs:449 #: crates/pdfs-gui/src/main.rs:458
msgid "Hide Tray Icon" msgid "Hide Tray Icon"
msgstr "Symbol im Benachrichtigungsfeld ausblenden" msgstr "Symbol im Benachrichtigungsfeld ausblenden"
#: crates/pdfs-gui/src/main.rs:453 #: crates/pdfs-gui/src/main.rs:462
msgid "Stop Proton Drive…" msgid "Stop Proton Drive…"
msgstr "Proton Drive beenden …" msgstr "Proton Drive beenden …"
#: crates/pdfs-gui/src/main.rs:457 #: crates/pdfs-gui/src/main.rs:466
msgid "Quit" msgid "Quit"
msgstr "Beenden" msgstr "Beenden"
+212 -141
View File
@@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: proton-drive-linux 2.1.0\n" "Project-Id-Version: proton-drive-linux 2.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-10-05 01:01+0200\n" "POT-Creation-Date: 2026-10-08 10:15+0200\n"
"PO-Revision-Date: 2026-09-23 12:00+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n"
"Last-Translator: Proton Drive for Linux contributors\n" "Last-Translator: Proton Drive for Linux contributors\n"
"Language-Team: English (United Kingdom)\n" "Language-Team: English (United Kingdom)\n"
@@ -57,7 +57,7 @@ msgstr "%-d %b %Y"
msgid "%A, %B %-d, %Y, %H:%M" msgid "%A, %B %-d, %Y, %H:%M"
msgstr "%A %-d %B %Y, %H:%M" msgstr "%A %-d %B %Y, %H:%M"
#: crates/pdfs-gui/src/app/main.rs:743 #: crates/pdfs-gui/src/app/main.rs:760
#: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539
#: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:454
#: crates/pdfs-gui/src/app/pages/browser.rs:819 #: crates/pdfs-gui/src/app/pages/browser.rs:819
@@ -70,91 +70,91 @@ msgstr "%A %-d %B %Y, %H:%M"
msgid "My files" msgid "My files"
msgstr "My files" msgstr "My files"
#: crates/pdfs-gui/src/app/main.rs:746 crates/pdfs-gui/src/app/main.rs:1390 #: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407
#: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/albums.rs:638
#: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/photos.rs:842
#: crates/pdfs-gui/src/app/pages/places.rs:441 #: crates/pdfs-gui/src/app/pages/places.rs:441
msgid "Photos" msgid "Photos"
msgstr "Photos" msgstr "Photos"
#: crates/pdfs-gui/src/app/main.rs:751 #: crates/pdfs-gui/src/app/main.rs:768
#: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:168
#: crates/pdfs-gui/src/app/pages/shared.rs:482 #: crates/pdfs-gui/src/app/pages/shared.rs:482
msgid "Shared with me" msgid "Shared with me"
msgstr "Shared with me" msgstr "Shared with me"
#: crates/pdfs-gui/src/app/main.rs:756 #: crates/pdfs-gui/src/app/main.rs:773
#: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120
msgid "Shared by me" msgid "Shared by me"
msgstr "Shared by me" msgstr "Shared by me"
#: crates/pdfs-gui/src/app/main.rs:759 #: crates/pdfs-gui/src/app/main.rs:776
#: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:140
#: crates/pdfs-gui/src/app/pages/devices.rs:481 #: crates/pdfs-gui/src/app/pages/devices.rs:481
msgid "Computers" msgid "Computers"
msgstr "Computers" msgstr "Computers"
#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/main.rs:777
#: crates/pdfs-gui/src/app/pages/trash.rs:101 #: crates/pdfs-gui/src/app/pages/trash.rs:101
msgid "Trash" msgid "Trash"
msgstr "Trash" msgstr "Trash"
#. Translators: sidebar entry for the page listing this computer's synced folders. #. Translators: sidebar entry for the page listing this computer's synced folders.
#: crates/pdfs-gui/src/app/main.rs:762 #: crates/pdfs-gui/src/app/main.rs:779
msgid "Sync" msgid "Sync"
msgstr "Sync" msgstr "Sync"
#. Translators: a failed action's toast. {what} says what failed, such as #. Translators: a failed action's toast. {what} says what failed, such as
#. "Couldn't rename"; {detail} is the reason, usually untranslated text #. "Couldn't rename"; {detail} is the reason, usually untranslated text
#. from the mount service. #. from the mount service.
#: crates/pdfs-gui/src/app/main.rs:1079 #: crates/pdfs-gui/src/app/main.rs:1096
#, rust-format #, rust-format
msgid "{what}: {detail}" msgid "{what}: {detail}"
msgstr "{what}: {detail}" msgstr "{what}: {detail}"
#: crates/pdfs-gui/src/app/main.rs:1098 #: crates/pdfs-gui/src/app/main.rs:1115
msgid "You're offline" msgid "You're offline"
msgstr "You're offline" msgstr "You're offline"
#: crates/pdfs-gui/src/app/main.rs:1099 #: crates/pdfs-gui/src/app/main.rs:1116
msgid "That's not there any more" msgid "That's not there any more"
msgstr "That's not there any more" msgstr "That's not there any more"
#: crates/pdfs-gui/src/app/main.rs:1100 #: crates/pdfs-gui/src/app/main.rs:1117
msgid "You don't have access to that" msgid "You don't have access to that"
msgstr "You don't have access to that" msgstr "You don't have access to that"
#: crates/pdfs-gui/src/app/main.rs:1101 #: crates/pdfs-gui/src/app/main.rs:1118
msgid "Something changed this first" msgid "Something changed this first"
msgstr "Something changed this first" msgstr "Something changed this first"
#: crates/pdfs-gui/src/app/main.rs:1102 #: crates/pdfs-gui/src/app/main.rs:1119
msgid "Your Proton Drive is full" msgid "Your Proton Drive is full"
msgstr "Your Proton Drive is full" msgstr "Your Proton Drive is full"
#: crates/pdfs-gui/src/app/main.rs:1143 crates/pdfs-gui/src/app/main.rs:1372 #: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389
#: crates/pdfs-gui/src/app/pages/status.rs:385 #: crates/pdfs-gui/src/app/pages/status.rs:385
msgid "Preferences" msgid "Preferences"
msgstr "Preferences" msgstr "Preferences"
#: crates/pdfs-gui/src/app/main.rs:1144 #: crates/pdfs-gui/src/app/main.rs:1161
#: crates/pdfs-gui/src/app/widgets/compat.rs:262 #: crates/pdfs-gui/src/app/widgets/compat.rs:262
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "Keyboard Shortcuts" msgstr "Keyboard Shortcuts"
#: crates/pdfs-gui/src/app/main.rs:1146 #: crates/pdfs-gui/src/app/main.rs:1163
msgid "About Proton Drive for Linux" msgid "About Proton Drive for Linux"
msgstr "About Proton Drive for Linux" msgstr "About Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1151 #: crates/pdfs-gui/src/app/main.rs:1168
msgid "Main menu" msgid "Main menu"
msgstr "Main menu" msgstr "Main menu"
#: crates/pdfs-gui/src/app/main.rs:1207 #: crates/pdfs-gui/src/app/main.rs:1224
msgid "Proton Drive for Linux" msgid "Proton Drive for Linux"
msgstr "Proton Drive for Linux" msgstr "Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1215 #: crates/pdfs-gui/src/app/main.rs:1232
msgid "" msgid ""
"Files-on-demand Proton Drive for the Linux desktop.\n" "Files-on-demand Proton Drive for the Linux desktop.\n"
"\n" "\n"
@@ -166,23 +166,23 @@ msgstr ""
"Unofficial client — not affiliated with, endorsed by, or supported by Proton " "Unofficial client — not affiliated with, endorsed by, or supported by Proton "
"AG." "AG."
#: crates/pdfs-gui/src/app/main.rs:1220 #: crates/pdfs-gui/src/app/main.rs:1237
msgid "Source Code" msgid "Source Code"
msgstr "Source Code" msgstr "Source Code"
#: crates/pdfs-gui/src/app/main.rs:1224 #: crates/pdfs-gui/src/app/main.rs:1241
msgid "Proton Terms of Service" msgid "Proton Terms of Service"
msgstr "Proton Terms of Service" msgstr "Proton Terms of Service"
#: crates/pdfs-gui/src/app/main.rs:1228 #: crates/pdfs-gui/src/app/main.rs:1245
msgid "Proton Privacy Policy" msgid "Proton Privacy Policy"
msgstr "Proton Privacy Policy" msgstr "Proton Privacy Policy"
#: crates/pdfs-gui/src/app/main.rs:1233 #: crates/pdfs-gui/src/app/main.rs:1250
msgid "Disclaimer" msgid "Disclaimer"
msgstr "Disclaimer" msgstr "Disclaimer"
#: crates/pdfs-gui/src/app/main.rs:1237 #: crates/pdfs-gui/src/app/main.rs:1254
msgid "" msgid ""
"This is an unofficial, community-built client. It is not affiliated with, " "This is an unofficial, community-built client. It is not affiliated with, "
"endorsed by, or supported by Proton AG.\n" "endorsed by, or supported by Proton AG.\n"
@@ -204,7 +204,7 @@ msgstr ""
"This software is provided “as is”, without warranty of any kind. Keep a copy " "This software is provided “as is”, without warranty of any kind. Keep a copy "
"of any data you cannot afford to lose." "of any data you cannot afford to lose."
#: crates/pdfs-gui/src/app/main.rs:1245 #: crates/pdfs-gui/src/app/main.rs:1262
msgid "" msgid ""
"Places are named with data from <a href=\"https://www.geonames.org/" "Places are named with data from <a href=\"https://www.geonames.org/"
"\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/"
@@ -214,7 +214,7 @@ msgstr ""
"\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/"
"licenses/by/4.0/\">CC BY 4.0</a>." "licenses/by/4.0/\">CC BY 4.0</a>."
#: crates/pdfs-gui/src/app/main.rs:1253 #: crates/pdfs-gui/src/app/main.rs:1270
msgid "" msgid ""
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/" "The places map is drawn from <a href=\"https://www.naturalearthdata.com/"
"\">Natural Earth</a> land outlines and country borders, which are in the " "\">Natural Earth</a> land outlines and country borders, which are in the "
@@ -224,7 +224,7 @@ msgstr ""
"\">Natural Earth</a> land outlines and country borders, which are in the " "\">Natural Earth</a> land outlines and country borders, which are in the "
"public domain." "public domain."
#: crates/pdfs-gui/src/app/main.rs:1261 #: crates/pdfs-gui/src/app/main.rs:1278
msgid "" msgid ""
"The street map, when turned on, comes from <a href=\"https://openfreemap.org/" "The street map, when turned on, comes from <a href=\"https://openfreemap.org/"
"\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/" "\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/"
@@ -240,15 +240,15 @@ msgstr ""
#. Translators: replace with your name(s) and email address(es), one #. Translators: replace with your name(s) and email address(es), one
#. per line, to be credited in the About dialog. #. per line, to be credited in the About dialog.
#: crates/pdfs-gui/src/app/main.rs:1272 #: crates/pdfs-gui/src/app/main.rs:1289
msgid "translator-credits" msgid "translator-credits"
msgstr "Proton Drive for Linux contributors" msgstr "Proton Drive for Linux contributors"
#: crates/pdfs-gui/src/app/main.rs:1310 #: crates/pdfs-gui/src/app/main.rs:1327
msgid "Stop Proton Drive?" msgid "Stop Proton Drive?"
msgstr "Stop Proton Drive?" msgstr "Stop Proton Drive?"
#: crates/pdfs-gui/src/app/main.rs:1311 #: crates/pdfs-gui/src/app/main.rs:1328
msgid "" msgid ""
"My files goes offline and nothing syncs until you connect again. Proton " "My files goes offline and nothing syncs until you connect again. Proton "
"Drive starts again at your next login." "Drive starts again at your next login."
@@ -256,7 +256,7 @@ msgstr ""
"My files goes offline and nothing syncs until you connect again. Proton " "My files goes offline and nothing syncs until you connect again. Proton "
"Drive starts again at your next login." "Drive starts again at your next login."
#: crates/pdfs-gui/src/app/main.rs:1313 crates/pdfs-gui/src/app/main.rs:1588 #: crates/pdfs-gui/src/app/main.rs:1330 crates/pdfs-gui/src/app/main.rs:1605
#: crates/pdfs-gui/src/app/pages/albums.rs:243 #: crates/pdfs-gui/src/app/pages/albums.rs:243
#: crates/pdfs-gui/src/app/pages/albums.rs:428 #: crates/pdfs-gui/src/app/pages/albums.rs:428
#: crates/pdfs-gui/src/app/pages/browser.rs:1892 #: crates/pdfs-gui/src/app/pages/browser.rs:1892
@@ -271,7 +271,8 @@ msgstr ""
#: crates/pdfs-gui/src/app/pages/locations.rs:1013 #: crates/pdfs-gui/src/app/pages/locations.rs:1013
#: crates/pdfs-gui/src/app/pages/locations.rs:1488 #: crates/pdfs-gui/src/app/pages/locations.rs:1488
#: crates/pdfs-gui/src/app/pages/locations.rs:1538 #: crates/pdfs-gui/src/app/pages/locations.rs:1538
#: crates/pdfs-gui/src/app/pages/login.rs:312 #: crates/pdfs-gui/src/app/pages/login.rs:393
#: crates/pdfs-gui/src/app/pages/login.rs:507
#: crates/pdfs-gui/src/app/pages/photos.rs:2055 #: crates/pdfs-gui/src/app/pages/photos.rs:2055
#: crates/pdfs-gui/src/app/pages/shared.rs:946 #: crates/pdfs-gui/src/app/pages/shared.rs:946
#: crates/pdfs-gui/src/app/pages/shared.rs:1027 #: crates/pdfs-gui/src/app/pages/shared.rs:1027
@@ -286,30 +287,30 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Cancel" msgstr "Cancel"
#: crates/pdfs-gui/src/app/main.rs:1314 #: crates/pdfs-gui/src/app/main.rs:1331
msgctxt "verb" msgctxt "verb"
msgid "Stop" msgid "Stop"
msgstr "Stop" msgstr "Stop"
#: crates/pdfs-gui/src/app/main.rs:1331 #: crates/pdfs-gui/src/app/main.rs:1348
msgid "Couldn't stop Proton Drive" msgid "Couldn't stop Proton Drive"
msgstr "Couldn't stop Proton Drive" msgstr "Couldn't stop Proton Drive"
#: crates/pdfs-gui/src/app/main.rs:1332 #: crates/pdfs-gui/src/app/main.rs:1349
msgid "The Proton Drive service did not stop." msgid "The Proton Drive service did not stop."
msgstr "The Proton Drive service did not stop." msgstr "The Proton Drive service did not stop."
#: crates/pdfs-gui/src/app/main.rs:1368 #: crates/pdfs-gui/src/app/main.rs:1385
#: crates/pdfs-gui/src/app/pages/status.rs:254 #: crates/pdfs-gui/src/app/pages/status.rs:254
msgid "General" msgid "General"
msgstr "General" msgstr "General"
#: crates/pdfs-gui/src/app/main.rs:1370 #: crates/pdfs-gui/src/app/main.rs:1387
#: crates/pdfs-gui/src/app/pages/browser.rs:247 #: crates/pdfs-gui/src/app/pages/browser.rs:247
msgid "Search Drive" msgid "Search Drive"
msgstr "Search Drive" msgstr "Search Drive"
#: crates/pdfs-gui/src/app/main.rs:1371 #: crates/pdfs-gui/src/app/main.rs:1388
#: crates/pdfs-gui/src/app/pages/browser.rs:957 #: crates/pdfs-gui/src/app/pages/browser.rs:957
#: crates/pdfs-gui/src/app/pages/devices.rs:554 #: crates/pdfs-gui/src/app/pages/devices.rs:554
#: crates/pdfs-gui/src/app/pages/shared.rs:586 #: crates/pdfs-gui/src/app/pages/shared.rs:586
@@ -318,38 +319,38 @@ msgstr "Search Drive"
msgid "Refresh" msgid "Refresh"
msgstr "Refresh" msgstr "Refresh"
#: crates/pdfs-gui/src/app/main.rs:1373 #: crates/pdfs-gui/src/app/main.rs:1390
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "Keyboard shortcuts" msgstr "Keyboard shortcuts"
#: crates/pdfs-gui/src/app/main.rs:1377 #: crates/pdfs-gui/src/app/main.rs:1394
msgid "Files" msgid "Files"
msgstr "Files" msgstr "Files"
#: crates/pdfs-gui/src/app/main.rs:1379 #: crates/pdfs-gui/src/app/main.rs:1396
#: crates/pdfs-gui/src/app/pages/browser.rs:294 #: crates/pdfs-gui/src/app/pages/browser.rs:294
msgid "New folder" msgid "New folder"
msgstr "New folder" msgstr "New folder"
#: crates/pdfs-gui/src/app/main.rs:1380 #: crates/pdfs-gui/src/app/main.rs:1397
#: crates/pdfs-gui/src/app/pages/browser.rs:290 #: crates/pdfs-gui/src/app/pages/browser.rs:290
msgid "Upload files" msgid "Upload files"
msgstr "Upload files" msgstr "Upload files"
#: crates/pdfs-gui/src/app/main.rs:1381 #: crates/pdfs-gui/src/app/main.rs:1398
msgid "Back / forward" msgid "Back / forward"
msgstr "Back / forward" msgstr "Back / forward"
#: crates/pdfs-gui/src/app/main.rs:1382 #: crates/pdfs-gui/src/app/main.rs:1399
#: crates/pdfs-gui/src/app/pages/browser.rs:1943 #: crates/pdfs-gui/src/app/pages/browser.rs:1943
msgid "Parent folder" msgid "Parent folder"
msgstr "Parent folder" msgstr "Parent folder"
#: crates/pdfs-gui/src/app/main.rs:1383 #: crates/pdfs-gui/src/app/main.rs:1400
msgid "Grid / list" msgid "Grid / list"
msgstr "Grid / list" msgstr "Grid / list"
#: crates/pdfs-gui/src/app/main.rs:1384 #: crates/pdfs-gui/src/app/main.rs:1401
#: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/albums.rs:298
#: crates/pdfs-gui/src/app/pages/browser.rs:1871 #: crates/pdfs-gui/src/app/pages/browser.rs:1871
#: crates/pdfs-gui/src/app/pages/browser.rs:1893 #: crates/pdfs-gui/src/app/pages/browser.rs:1893
@@ -358,7 +359,7 @@ msgstr "Grid / list"
msgid "Rename" msgid "Rename"
msgstr "Rename" msgstr "Rename"
#: crates/pdfs-gui/src/app/main.rs:1385 crates/pdfs-gui/src/app/main.rs:1405 #: crates/pdfs-gui/src/app/main.rs:1402 crates/pdfs-gui/src/app/main.rs:1422
#: crates/pdfs-gui/src/app/pages/browser.rs:357 #: crates/pdfs-gui/src/app/pages/browser.rs:357
#: crates/pdfs-gui/src/app/pages/browser.rs:900 #: crates/pdfs-gui/src/app/pages/browser.rs:900
#: crates/pdfs-gui/src/app/pages/browser.rs:932 #: crates/pdfs-gui/src/app/pages/browser.rs:932
@@ -368,73 +369,73 @@ msgstr "Rename"
msgid "Move to Trash" msgid "Move to Trash"
msgstr "Move to Trash" msgstr "Move to Trash"
#: crates/pdfs-gui/src/app/main.rs:1386 #: crates/pdfs-gui/src/app/main.rs:1403
msgid "Clear the selection" msgid "Clear the selection"
msgstr "Clear the selection" msgstr "Clear the selection"
#: crates/pdfs-gui/src/app/main.rs:1392 #: crates/pdfs-gui/src/app/main.rs:1409
msgid "Larger thumbnails" msgid "Larger thumbnails"
msgstr "Larger thumbnails" msgstr "Larger thumbnails"
#: crates/pdfs-gui/src/app/main.rs:1393 #: crates/pdfs-gui/src/app/main.rs:1410
msgid "Smaller thumbnails" msgid "Smaller thumbnails"
msgstr "Smaller thumbnails" msgstr "Smaller thumbnails"
#: crates/pdfs-gui/src/app/main.rs:1394 #: crates/pdfs-gui/src/app/main.rs:1411
msgid "Reset thumbnail size" msgid "Reset thumbnail size"
msgstr "Reset thumbnail size" msgstr "Reset thumbnail size"
#: crates/pdfs-gui/src/app/main.rs:1395 #: crates/pdfs-gui/src/app/main.rs:1412
msgid "Select all" msgid "Select all"
msgstr "Select all" msgstr "Select all"
#: crates/pdfs-gui/src/app/main.rs:1399 #: crates/pdfs-gui/src/app/main.rs:1416
msgid "Photo Viewer" msgid "Photo Viewer"
msgstr "Photo Viewer" msgstr "Photo Viewer"
#: crates/pdfs-gui/src/app/main.rs:1401 #: crates/pdfs-gui/src/app/main.rs:1418
msgid "Previous / next photo" msgid "Previous / next photo"
msgstr "Previous / next photo" msgstr "Previous / next photo"
#: crates/pdfs-gui/src/app/main.rs:1402 #: crates/pdfs-gui/src/app/main.rs:1419
msgid "First / last photo" msgid "First / last photo"
msgstr "First / last photo" msgstr "First / last photo"
#: crates/pdfs-gui/src/app/main.rs:1403 #: crates/pdfs-gui/src/app/main.rs:1420
msgid "Show details" msgid "Show details"
msgstr "Show details" msgstr "Show details"
#: crates/pdfs-gui/src/app/main.rs:1404 #: crates/pdfs-gui/src/app/main.rs:1421
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Fullscreen" msgstr "Fullscreen"
#: crates/pdfs-gui/src/app/main.rs:1406 #: crates/pdfs-gui/src/app/main.rs:1423
msgid "Close" msgid "Close"
msgstr "Close" msgstr "Close"
#: crates/pdfs-gui/src/app/main.rs:1496 #: crates/pdfs-gui/src/app/main.rs:1513
msgid "Select a single item to rename it" msgid "Select a single item to rename it"
msgstr "Select a single item to rename it" msgstr "Select a single item to rename it"
#: crates/pdfs-gui/src/app/main.rs:1656 #: crates/pdfs-gui/src/app/main.rs:1673
#: crates/pdfs-gui/src/app/pages/albums.rs:85 #: crates/pdfs-gui/src/app/pages/albums.rs:85
#: crates/pdfs-gui/src/app/pages/places.rs:104 #: crates/pdfs-gui/src/app/pages/places.rs:104
#: crates/pdfs-gui/src/app/pages/status.rs:1370 #: crates/pdfs-gui/src/app/pages/status.rs:1371
msgid "Not connected" msgid "Not connected"
msgstr "Not connected" msgstr "Not connected"
#: crates/pdfs-gui/src/app/main.rs:1657 #: crates/pdfs-gui/src/app/main.rs:1674
msgid "The Proton Drive service isn't running." msgid "The Proton Drive service isn't running."
msgstr "The Proton Drive service isn't running." msgstr "The Proton Drive service isn't running."
#: crates/pdfs-gui/src/app/main.rs:1665 #: crates/pdfs-gui/src/app/main.rs:1682
#: crates/pdfs-gui/src/app/pages/status.rs:336 #: crates/pdfs-gui/src/app/pages/status.rs:336
#: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595 #: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595
#: crates/pdfs-gui/src/main.rs:444 #: crates/pdfs-gui/src/main.rs:453
msgid "Connecting…" msgid "Connecting…"
msgstr "Connecting…" msgstr "Connecting…"
#: crates/pdfs-gui/src/app/main.rs:1666 #: crates/pdfs-gui/src/app/main.rs:1683
msgid "Waiting for the Proton Drive service to start." msgid "Waiting for the Proton Drive service to start."
msgstr "Waiting for the Proton Drive service to start." msgstr "Waiting for the Proton Drive service to start."
@@ -1275,7 +1276,7 @@ msgstr "Couldn't load albums"
#: crates/pdfs-gui/src/app/pages/shared.rs:1006 #: crates/pdfs-gui/src/app/pages/shared.rs:1006
#: crates/pdfs-gui/src/app/pages/shared.rs:1122 #: crates/pdfs-gui/src/app/pages/shared.rs:1122
#: crates/pdfs-gui/src/app/pages/status.rs:750 #: crates/pdfs-gui/src/app/pages/status.rs:750
#: crates/pdfs-gui/src/app/pages/status.rs:1517 #: crates/pdfs-gui/src/app/pages/status.rs:1518
#: crates/pdfs-gui/src/app/pages/takeout.rs:424 #: crates/pdfs-gui/src/app/pages/takeout.rs:424
#: crates/pdfs-gui/src/app/pages/takeout.rs:617 #: crates/pdfs-gui/src/app/pages/takeout.rs:617
#: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438
@@ -1543,7 +1544,7 @@ msgid "Make available offline"
msgstr "Make available offline" msgstr "Make available offline"
#: crates/pdfs-gui/src/app/pages/browser.rs:347 #: crates/pdfs-gui/src/app/pages/browser.rs:347
#: crates/pdfs-gui/src/app/pages/status.rs:1493 #: crates/pdfs-gui/src/app/pages/status.rs:1494
msgid "Make online only" msgid "Make online only"
msgstr "Make online only" msgstr "Make online only"
@@ -2686,7 +2687,7 @@ msgid "Sync paused until you resume"
msgstr "Sync paused until you resume" msgstr "Sync paused until you resume"
#: crates/pdfs-gui/src/app/pages/locations.rs:529 #: crates/pdfs-gui/src/app/pages/locations.rs:529
#: crates/pdfs-gui/src/app/pages/status.rs:1323 crates/pdfs-gui/src/main.rs:237 #: crates/pdfs-gui/src/app/pages/status.rs:1324 crates/pdfs-gui/src/main.rs:246
msgid "Sync paused" msgid "Sync paused"
msgstr "Sync paused" msgstr "Sync paused"
@@ -3149,23 +3150,33 @@ msgstr "Enter your email and password."
msgid "Signing in…" msgid "Signing in…"
msgstr "Signing in…" msgstr "Signing in…"
#: crates/pdfs-gui/src/app/pages/login.rs:155 #: crates/pdfs-gui/src/app/pages/login.rs:164
msgid "Complete the verification to continue…" msgid "Complete the verification to continue…"
msgstr "Complete the verification to continue…" msgstr "Complete the verification to continue…"
#: crates/pdfs-gui/src/app/pages/login.rs:165 #: crates/pdfs-gui/src/app/pages/login.rs:197
msgid "login cancelled" msgid "login cancelled"
msgstr "login cancelled" msgstr "login cancelled"
#: crates/pdfs-gui/src/app/pages/login.rs:198 #: crates/pdfs-gui/src/app/pages/login.rs:210
msgid ""
"The verification page couldn't be opened, so the sign-in was cancelled. Your "
"system may keep the web view's sandbox from starting; the troubleshooting "
"guide explains what to do."
msgstr ""
"The verification page couldn't be opened, so the sign-in was cancelled. Your "
"system may keep the web view's sandbox from starting; the troubleshooting "
"guide explains what to do."
#: crates/pdfs-gui/src/app/pages/login.rs:233
msgid "Too many sign-in attempts. Wait a few minutes and try again." msgid "Too many sign-in attempts. Wait a few minutes and try again."
msgstr "Too many sign-in attempts. Wait a few minutes and try again." msgstr "Too many sign-in attempts. Wait a few minutes and try again."
#: crates/pdfs-gui/src/app/pages/login.rs:204 #: crates/pdfs-gui/src/app/pages/login.rs:239
msgid "Couldn't reach Proton. Check your internet connection and try again." msgid "Couldn't reach Proton. Check your internet connection and try again."
msgstr "Couldn't reach Proton. Check your internet connection and try again." msgstr "Couldn't reach Proton. Check your internet connection and try again."
#: crates/pdfs-gui/src/app/pages/login.rs:207 #: crates/pdfs-gui/src/app/pages/login.rs:242
msgid "" msgid ""
"Signed in, but the session couldn't be saved to the system keyring. Make " "Signed in, but the session couldn't be saved to the system keyring. Make "
"sure a keyring (GNOME Keyring or KWallet) is running and unlocked." "sure a keyring (GNOME Keyring or KWallet) is running and unlocked."
@@ -3173,11 +3184,19 @@ msgstr ""
"Signed in, but the session couldn't be saved to the system keyring. Make " "Signed in, but the session couldn't be saved to the system keyring. Make "
"sure a keyring (GNOME Keyring or KWallet) is running and unlocked." "sure a keyring (GNOME Keyring or KWallet) is running and unlocked."
#: crates/pdfs-gui/src/app/pages/login.rs:210 #: crates/pdfs-gui/src/app/pages/login.rs:245
msgid "Sign-in cancelled: no two-factor code was entered." msgid "Sign-in cancelled: no two-factor code was entered."
msgstr "Sign-in cancelled: no two-factor code was entered." msgstr "Sign-in cancelled: no two-factor code was entered."
#: crates/pdfs-gui/src/app/pages/login.rs:213 #: crates/pdfs-gui/src/app/pages/login.rs:248
msgid "Sign-in cancelled: no mailbox password was entered."
msgstr "Sign-in cancelled: no mailbox password was entered."
#: crates/pdfs-gui/src/app/pages/login.rs:251
msgid "That mailbox password didn't unlock your account."
msgstr "That mailbox password didn't unlock your account."
#: crates/pdfs-gui/src/app/pages/login.rs:254
msgid "" msgid ""
"Sign-in cancelled: the verification wasn't completed. Sign in again to get a " "Sign-in cancelled: the verification wasn't completed. Sign in again to get a "
"new one." "new one."
@@ -3186,34 +3205,81 @@ msgstr ""
"new one." "new one."
#. Translators: {error} is an error message, usually in English. #. Translators: {error} is an error message, usually in English.
#: crates/pdfs-gui/src/app/pages/login.rs:218 #: crates/pdfs-gui/src/app/pages/login.rs:259
#, rust-format #, rust-format
msgid "Sign-in failed: {error}" msgid "Sign-in failed: {error}"
msgstr "Sign-in failed: {error}" msgstr "Sign-in failed: {error}"
#: crates/pdfs-gui/src/app/pages/login.rs:298 #: crates/pdfs-gui/src/app/pages/login.rs:369
msgid "That mailbox password didn't unlock your account. Try again."
msgstr "That mailbox password didn't unlock your account. Try again."
#: crates/pdfs-gui/src/app/pages/login.rs:371
msgid ""
"This account has a separate mailbox password. Enter it to unlock your files."
msgstr ""
"This account has a separate mailbox password. Enter it to unlock your files."
#: crates/pdfs-gui/src/app/pages/login.rs:374
msgid "Mailbox Password"
msgstr "Mailbox Password"
#: crates/pdfs-gui/src/app/pages/login.rs:380
msgid "Mailbox password"
msgstr "Mailbox password"
#: crates/pdfs-gui/src/app/pages/login.rs:384
msgid "Keep unlocked on this computer"
msgstr "Keep unlocked on this computer"
#: crates/pdfs-gui/src/app/pages/login.rs:386
msgid ""
"Stores the unlock in the system keyring, so Proton Drive opens without "
"asking after every login. Otherwise you enter the password once per session."
msgstr ""
"Stores the unlock in the system keyring, so Proton Drive opens without "
"asking after every login. Otherwise you enter the password once per session."
#: crates/pdfs-gui/src/app/pages/login.rs:394
#: crates/pdfs-gui/src/app/pages/login.rs:447
msgid "Unlock"
msgstr "Unlock"
#: crates/pdfs-gui/src/app/pages/login.rs:446
msgid "Proton Drive is locked. Enter your mailbox password to unlock it."
msgstr "Proton Drive is locked. Enter your mailbox password to unlock it."
#: crates/pdfs-gui/src/app/pages/login.rs:475
msgid "Unlocked."
msgstr "Unlocked."
#: crates/pdfs-gui/src/app/pages/login.rs:480
msgid "Couldn't unlock"
msgstr "Couldn't unlock"
#: crates/pdfs-gui/src/app/pages/login.rs:493
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "Two-Factor Authentication" msgstr "Two-Factor Authentication"
#: crates/pdfs-gui/src/app/pages/login.rs:300 #: crates/pdfs-gui/src/app/pages/login.rs:495
msgid "" msgid ""
"Enter the code from your authenticator app, or one of your recovery codes." "Enter the code from your authenticator app, or one of your recovery codes."
msgstr "" msgstr ""
"Enter the code from your authenticator app, or one of your recovery codes." "Enter the code from your authenticator app, or one of your recovery codes."
#: crates/pdfs-gui/src/app/pages/login.rs:306 #: crates/pdfs-gui/src/app/pages/login.rs:501
msgid "Authentication code" msgid "Authentication code"
msgstr "Authentication code" msgstr "Authentication code"
#: crates/pdfs-gui/src/app/pages/login.rs:313 #: crates/pdfs-gui/src/app/pages/login.rs:508
msgid "Confirm" msgid "Confirm"
msgstr "Confirm" msgstr "Confirm"
#: crates/pdfs-gui/src/app/pages/login.rs:341 #: crates/pdfs-gui/src/app/pages/login.rs:536
msgid "Sign Out?" msgid "Sign Out?"
msgstr "Sign Out?" msgstr "Sign Out?"
#: crates/pdfs-gui/src/app/pages/login.rs:343 #: crates/pdfs-gui/src/app/pages/login.rs:538
msgid "" msgid ""
"Proton Drive will disconnect and stop syncing until you sign in again. Files " "Proton Drive will disconnect and stop syncing until you sign in again. Files "
"already on this computer stay where they are." "already on this computer stay where they are."
@@ -3221,7 +3287,7 @@ msgstr ""
"Proton Drive will disconnect and stop syncing until you sign in again. Files " "Proton Drive will disconnect and stop syncing until you sign in again. Files "
"already on this computer stay where they are." "already on this computer stay where they are."
#: crates/pdfs-gui/src/app/pages/login.rs:345 #: crates/pdfs-gui/src/app/pages/login.rs:540
msgid "Sign Out" msgid "Sign Out"
msgstr "Sign Out" msgstr "Sign Out"
@@ -4556,39 +4622,39 @@ msgid "Restart Now"
msgstr "Restart Now" msgstr "Restart Now"
#. Translators: {size} is an amount of storage, such as "1.2 GiB". #. Translators: {size} is an amount of storage, such as "1.2 GiB".
#: crates/pdfs-gui/src/app/pages/status.rs:912 #: crates/pdfs-gui/src/app/pages/status.rs:913
#, rust-format #, rust-format
msgid "{size} used" msgid "{size} used"
msgstr "{size} used" msgstr "{size} used"
#: crates/pdfs-gui/src/app/pages/status.rs:923 #: crates/pdfs-gui/src/app/pages/status.rs:924
#, rust-format #, rust-format
msgid "{used} of {total} used ({percent}%)" msgid "{used} of {total} used ({percent}%)"
msgstr "{used} of {total} used ({percent}%)" msgstr "{used} of {total} used ({percent}%)"
#: crates/pdfs-gui/src/app/pages/status.rs:961 #: crates/pdfs-gui/src/app/pages/status.rs:962
msgid "Proton Drive connected" msgid "Proton Drive connected"
msgstr "Proton Drive connected" msgstr "Proton Drive connected"
#: crates/pdfs-gui/src/app/pages/status.rs:962 #: crates/pdfs-gui/src/app/pages/status.rs:963
msgid "Your files are available." msgid "Your files are available."
msgstr "Your files are available." msgstr "Your files are available."
#: crates/pdfs-gui/src/app/pages/status.rs:967 #: crates/pdfs-gui/src/app/pages/status.rs:968
msgid "Proton Drive disconnected" msgid "Proton Drive disconnected"
msgstr "Proton Drive disconnected" msgstr "Proton Drive disconnected"
#: crates/pdfs-gui/src/app/pages/status.rs:968 #: crates/pdfs-gui/src/app/pages/status.rs:969
msgid "" msgid ""
"The Proton Drive service stopped. Files aren't available until it restarts." "The Proton Drive service stopped. Files aren't available until it restarts."
msgstr "" msgstr ""
"The Proton Drive service stopped. Files aren't available until it restarts." "The Proton Drive service stopped. Files aren't available until it restarts."
#: crates/pdfs-gui/src/app/pages/status.rs:1037 #: crates/pdfs-gui/src/app/pages/status.rs:1038
msgid "Sync complete" msgid "Sync complete"
msgstr "Sync complete" msgstr "Sync complete"
#: crates/pdfs-gui/src/app/pages/status.rs:1039 #: crates/pdfs-gui/src/app/pages/status.rs:1040
#, rust-format #, rust-format
msgid "{n} file finished transferring." msgid "{n} file finished transferring."
msgid_plural "{n} files finished transferring." msgid_plural "{n} files finished transferring."
@@ -4597,27 +4663,27 @@ msgstr[1] "{n} files finished transferring."
#. Translators: a background job's progress. {title} is the job, such as #. Translators: a background job's progress. {title} is the job, such as
#. "Scanning Photos"; {done} and {total} are counts. #. "Scanning Photos"; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1117 #: crates/pdfs-gui/src/app/pages/status.rs:1118
#, rust-format #, rust-format
msgid "{title} — {done} of {total}" msgid "{title} — {done} of {total}"
msgstr "{title} — {done} of {total}" msgstr "{title} — {done} of {total}"
#. Translators: a background job's progress. {title} is the job, {detail} #. Translators: a background job's progress. {title} is the job, {detail}
#. what it is working on; {done} and {total} are counts. #. what it is working on; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1120 #: crates/pdfs-gui/src/app/pages/status.rs:1121
#, rust-format #, rust-format
msgid "{title} — {detail} ({done} of {total})" msgid "{title} — {detail} ({done} of {total})"
msgstr "{title} — {detail} ({done} of {total})" msgstr "{title} — {detail} ({done} of {total})"
#. Translators: a background job still running. {title} is the job. #. Translators: a background job still running. {title} is the job.
#: crates/pdfs-gui/src/app/pages/status.rs:1122 #: crates/pdfs-gui/src/app/pages/status.rs:1123
#, rust-format #, rust-format
msgid "{title}…" msgid "{title}…"
msgstr "{title}…" msgstr "{title}…"
#. Translators: a background job still running. {title} is the job, #. Translators: a background job still running. {title} is the job,
#. {detail} what it is working on. #. {detail} what it is working on.
#: crates/pdfs-gui/src/app/pages/status.rs:1125 #: crates/pdfs-gui/src/app/pages/status.rs:1126
#, rust-format #, rust-format
msgid "{title} — {detail}…" msgid "{title} — {detail}…"
msgstr "{title} — {detail}…" msgstr "{title} — {detail}…"
@@ -4625,7 +4691,7 @@ msgstr "{title} — {detail}…"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} the size moved so far and {speed} a size such as #. file name, {done} the size moved so far and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1154 #: crates/pdfs-gui/src/app/pages/status.rs:1155
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} ({speed}/s)" msgid "{arrow} {name} — {done} ({speed}/s)"
msgstr "{arrow} {name} — {done} ({speed}/s)" msgstr "{arrow} {name} — {done} ({speed}/s)"
@@ -4633,117 +4699,117 @@ msgstr "{arrow} {name} — {done} ({speed}/s)"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} and {total} sizes and {speed} a size such as #. file name, {done} and {total} sizes and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1162 #: crates/pdfs-gui/src/app/pages/status.rs:1163
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} of {total} ({speed}/s)" msgid "{arrow} {name} — {done} of {total} ({speed}/s)"
msgstr "{arrow} {name} — {done} of {total} ({speed}/s)" msgstr "{arrow} {name} — {done} of {total} ({speed}/s)"
#: crates/pdfs-gui/src/app/pages/status.rs:1254 #: crates/pdfs-gui/src/app/pages/status.rs:1255
#, rust-format #, rust-format
msgid "{used} cached — no limit set" msgid "{used} cached — no limit set"
msgstr "{used} cached — no limit set" msgstr "{used} cached — no limit set"
#: crates/pdfs-gui/src/app/pages/status.rs:1260 #: crates/pdfs-gui/src/app/pages/status.rs:1261
#, rust-format #, rust-format
msgid "{used} of {limit} used" msgid "{used} of {limit} used"
msgstr "{used} of {limit} used" msgstr "{used} of {limit} used"
#: crates/pdfs-gui/src/app/pages/status.rs:1328 #: crates/pdfs-gui/src/app/pages/status.rs:1329
#, rust-format #, rust-format
msgid "{queued} · resumes {time}" msgid "{queued} · resumes {time}"
msgstr "{queued} · resumes {time}" msgstr "{queued} · resumes {time}"
#. Translators: {time} is when syncing resumes, such as "at 14:30" #. Translators: {time} is when syncing resumes, such as "at 14:30"
#. or "Tue 09:00". #. or "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1333 #: crates/pdfs-gui/src/app/pages/status.rs:1334
#, rust-format #, rust-format
msgid "Resumes {time}" msgid "Resumes {time}"
msgstr "Resumes {time}" msgstr "Resumes {time}"
#: crates/pdfs-gui/src/app/pages/status.rs:1335 #: crates/pdfs-gui/src/app/pages/status.rs:1336
msgid "Until you resume" msgid "Until you resume"
msgstr "Until you resume" msgstr "Until you resume"
#: crates/pdfs-gui/src/app/pages/status.rs:1342 crates/pdfs-gui/src/main.rs:239 #: crates/pdfs-gui/src/app/pages/status.rs:1343 crates/pdfs-gui/src/main.rs:248
#, rust-format #, rust-format
msgid "{n} change needs attention" msgid "{n} change needs attention"
msgid_plural "{n} changes need attention" msgid_plural "{n} changes need attention"
msgstr[0] "{n} change needs attention" msgstr[0] "{n} change needs attention"
msgstr[1] "{n} changes need attention" msgstr[1] "{n} changes need attention"
#: crates/pdfs-gui/src/app/pages/status.rs:1352 #: crates/pdfs-gui/src/app/pages/status.rs:1353
#: crates/pdfs-gui/src/app/widgets/details.rs:108 #: crates/pdfs-gui/src/app/widgets/details.rs:108
msgctxt "state" msgctxt "state"
msgid "Offline" msgid "Offline"
msgstr "Offline" msgstr "Offline"
#: crates/pdfs-gui/src/app/pages/status.rs:1353 #: crates/pdfs-gui/src/app/pages/status.rs:1354
msgid "Cached files only" msgid "Cached files only"
msgstr "Cached files only" msgstr "Cached files only"
#: crates/pdfs-gui/src/app/pages/status.rs:1358 #: crates/pdfs-gui/src/app/pages/status.rs:1359
msgctxt "state" msgctxt "state"
msgid "Syncing" msgid "Syncing"
msgstr "Syncing" msgstr "Syncing"
#: crates/pdfs-gui/src/app/pages/status.rs:1364 #: crates/pdfs-gui/src/app/pages/status.rs:1365
msgid "Up to date" msgid "Up to date"
msgstr "Up to date" msgstr "Up to date"
#: crates/pdfs-gui/src/app/pages/status.rs:1371 #: crates/pdfs-gui/src/app/pages/status.rs:1372
#: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622 #: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622
msgid "Proton Drive isn't running" msgid "Proton Drive isn't running"
msgstr "Proton Drive isn't running" msgstr "Proton Drive isn't running"
#: crates/pdfs-gui/src/app/pages/status.rs:1376 #: crates/pdfs-gui/src/app/pages/status.rs:1377
msgid "Not responding" msgid "Not responding"
msgstr "Not responding" msgstr "Not responding"
#: crates/pdfs-gui/src/app/pages/status.rs:1377 #: crates/pdfs-gui/src/app/pages/status.rs:1378
msgid "Proton Drive is taking too long to answer" msgid "Proton Drive is taking too long to answer"
msgstr "Proton Drive is taking too long to answer" msgstr "Proton Drive is taking too long to answer"
#. Translators: stands in for a time that could not be read, as in #. Translators: stands in for a time that could not be read, as in
#. "Resumes later". #. "Resumes later".
#: crates/pdfs-gui/src/app/pages/status.rs:1415 #: crates/pdfs-gui/src/app/pages/status.rs:1416
#: crates/pdfs-gui/src/app/pages/status.rs:1430 #: crates/pdfs-gui/src/app/pages/status.rs:1431
msgid "later" msgid "later"
msgstr "later" msgstr "later"
#. Translators: strftime format for a time later today, such as "at 14:30". #. Translators: strftime format for a time later today, such as "at 14:30".
#: crates/pdfs-gui/src/app/pages/status.rs:1419 #: crates/pdfs-gui/src/app/pages/status.rs:1420
msgid "at %H:%M" msgid "at %H:%M"
msgstr "at %H:%M" msgstr "at %H:%M"
#. Translators: strftime format for a time within the next week, such as #. Translators: strftime format for a time within the next week, such as
#. "Tue 09:00". #. "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1423 #: crates/pdfs-gui/src/app/pages/status.rs:1424
msgid "%a %H:%M" msgid "%a %H:%M"
msgstr "%a %H:%M" msgstr "%a %H:%M"
#. Translators: strftime format for a date further ahead, such as "23 Sep". #. Translators: strftime format for a date further ahead, such as "23 Sep".
#: crates/pdfs-gui/src/app/pages/status.rs:1426 #: crates/pdfs-gui/src/app/pages/status.rs:1427
msgid "%e %b" msgid "%e %b"
msgstr "%e %b" msgstr "%e %b"
#: crates/pdfs-gui/src/app/pages/status.rs:1456 #: crates/pdfs-gui/src/app/pages/status.rs:1457
msgid "No files kept offline" msgid "No files kept offline"
msgstr "No files kept offline" msgstr "No files kept offline"
#: crates/pdfs-gui/src/app/pages/status.rs:1458 #: crates/pdfs-gui/src/app/pages/status.rs:1459
msgid "Right-click a file and choose “Make available offline”." msgid "Right-click a file and choose “Make available offline”."
msgstr "Right-click a file and choose “Make available offline”." msgstr "Right-click a file and choose “Make available offline”."
#: crates/pdfs-gui/src/app/pages/status.rs:1509 #: crates/pdfs-gui/src/app/pages/status.rs:1510
#: crates/pdfs-gui/src/app/pages/status.rs:1516 #: crates/pdfs-gui/src/app/pages/status.rs:1517
msgid "Couldn't make it online only" msgid "Couldn't make it online only"
msgstr "Couldn't make it online only" msgstr "Couldn't make it online only"
#: crates/pdfs-gui/src/app/pages/status.rs:1533 #: crates/pdfs-gui/src/app/pages/status.rs:1534
msgid "Show fewer" msgid "Show fewer"
msgstr "Show fewer" msgstr "Show fewer"
#: crates/pdfs-gui/src/app/pages/status.rs:1536 #: crates/pdfs-gui/src/app/pages/status.rs:1537
#, rust-format #, rust-format
msgid "Show all {n} file" msgid "Show all {n} file"
msgid_plural "Show all {n} files" msgid_plural "Show all {n} files"
@@ -5163,7 +5229,7 @@ msgstr "Trash emptied"
msgid "Couldn't empty the trash" msgid "Couldn't empty the trash"
msgstr "Couldn't empty the trash" msgstr "Couldn't empty the trash"
#: crates/pdfs-gui/src/app/pages/verify.rs:57 #: crates/pdfs-gui/src/app/pages/verify.rs:162
msgid "Verification" msgid "Verification"
msgstr "Verification" msgstr "Verification"
@@ -5788,40 +5854,45 @@ msgstr[1] "↑ {n} uploading"
msgid "Proton Drive isn't responding" msgid "Proton Drive isn't responding"
msgstr "Proton Drive isn't responding" msgstr "Proton Drive isn't responding"
#: crates/pdfs-gui/src/main.rs:172 #: crates/pdfs-gui/src/main.rs:173
#, rust-format
msgid "Signed in as {username} — locked"
msgstr "Signed in as {username} — locked"
#: crates/pdfs-gui/src/main.rs:181
#, rust-format #, rust-format
msgid "Signed in as {username} — not connected" msgid "Signed in as {username} — not connected"
msgstr "Signed in as {username} — not connected" msgstr "Signed in as {username} — not connected"
#: crates/pdfs-gui/src/main.rs:177 #: crates/pdfs-gui/src/main.rs:186
msgid "Not signed in" msgid "Not signed in"
msgstr "Not signed in" msgstr "Not signed in"
#. Translators: {error} is an error message, in English. #. Translators: {error} is an error message, in English.
#: crates/pdfs-gui/src/main.rs:180 #: crates/pdfs-gui/src/main.rs:189
#, rust-format #, rust-format
msgid "Error: {error}" msgid "Error: {error}"
msgstr "Error: {error}" msgstr "Error: {error}"
#: crates/pdfs-gui/src/main.rs:201 #: crates/pdfs-gui/src/main.rs:210
msgid "Proton Drive sent an unexpected reply" msgid "Proton Drive sent an unexpected reply"
msgstr "Proton Drive sent an unexpected reply" msgstr "Proton Drive sent an unexpected reply"
#: crates/pdfs-gui/src/main.rs:246 #: crates/pdfs-gui/src/main.rs:255
#, rust-format #, rust-format
msgid "Up to date in {mountpoint} ({n} pinned)" msgid "Up to date in {mountpoint} ({n} pinned)"
msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)"
msgstr[0] "Up to date in {mountpoint} ({n} pinned)" msgstr[0] "Up to date in {mountpoint} ({n} pinned)"
msgstr[1] "Up to date in {mountpoint} ({n} pinned)" msgstr[1] "Up to date in {mountpoint} ({n} pinned)"
#: crates/pdfs-gui/src/main.rs:253 #: crates/pdfs-gui/src/main.rs:262
#, rust-format #, rust-format
msgid "Syncing — {queued} ({n} pinned)" msgid "Syncing — {queued} ({n} pinned)"
msgid_plural "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)"
msgstr[0] "Syncing — {queued} ({n} pinned)" msgstr[0] "Syncing — {queued} ({n} pinned)"
msgstr[1] "Syncing — {queued} ({n} pinned)" msgstr[1] "Syncing — {queued} ({n} pinned)"
#: crates/pdfs-gui/src/main.rs:260 #: crates/pdfs-gui/src/main.rs:269
#, rust-format #, rust-format
msgid "Offline — cached files only ({n} pinned)" msgid "Offline — cached files only ({n} pinned)"
msgid_plural "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)"
@@ -5829,52 +5900,52 @@ msgstr[0] "Offline — cached files only ({n} pinned)"
msgstr[1] "Offline — cached files only ({n} pinned)" msgstr[1] "Offline — cached files only ({n} pinned)"
#. Translators: {queued} is a queue summary such as "3 uploads queued". #. Translators: {queued} is a queue summary such as "3 uploads queued".
#: crates/pdfs-gui/src/main.rs:266 #: crates/pdfs-gui/src/main.rs:275
#, rust-format #, rust-format
msgid "Offline — {queued}" msgid "Offline — {queued}"
msgstr "Offline — {queued}" msgstr "Offline — {queued}"
#. Translators: a menu item that opens the list of failing changes. #. Translators: a menu item that opens the list of failing changes.
#: crates/pdfs-gui/src/main.rs:296 #: crates/pdfs-gui/src/main.rs:305
#, rust-format #, rust-format
msgid "{n} issue — View" msgid "{n} issue — View"
msgid_plural "{n} issues — View" msgid_plural "{n} issues — View"
msgstr[0] "{n} issue — View" msgstr[0] "{n} issue — View"
msgstr[1] "{n} issues — View" msgstr[1] "{n} issues — View"
#: crates/pdfs-gui/src/main.rs:402 #: crates/pdfs-gui/src/main.rs:411
msgid "Sign In…" msgid "Sign In…"
msgstr "Sign In…" msgstr "Sign In…"
#: crates/pdfs-gui/src/main.rs:403 #: crates/pdfs-gui/src/main.rs:412
msgid "Open Proton Drive" msgid "Open Proton Drive"
msgstr "Open Proton Drive" msgstr "Open Proton Drive"
#: crates/pdfs-gui/src/main.rs:409 #: crates/pdfs-gui/src/main.rs:418
msgid "Open Folder" msgid "Open Folder"
msgstr "Open Folder" msgstr "Open Folder"
#: crates/pdfs-gui/src/main.rs:421 #: crates/pdfs-gui/src/main.rs:430
msgid "Resume Sync" msgid "Resume Sync"
msgstr "Resume Sync" msgstr "Resume Sync"
#: crates/pdfs-gui/src/main.rs:423 #: crates/pdfs-gui/src/main.rs:432
msgid "Pause Sync" msgid "Pause Sync"
msgstr "Pause Sync" msgstr "Pause Sync"
#: crates/pdfs-gui/src/main.rs:442 #: crates/pdfs-gui/src/main.rs:451
msgid "Connect" msgid "Connect"
msgstr "Connect" msgstr "Connect"
#: crates/pdfs-gui/src/main.rs:449 #: crates/pdfs-gui/src/main.rs:458
msgid "Hide Tray Icon" msgid "Hide Tray Icon"
msgstr "Hide Tray Icon" msgstr "Hide Tray Icon"
#: crates/pdfs-gui/src/main.rs:453 #: crates/pdfs-gui/src/main.rs:462
msgid "Stop Proton Drive…" msgid "Stop Proton Drive…"
msgstr "Stop Proton Drive…" msgstr "Stop Proton Drive…"
#: crates/pdfs-gui/src/main.rs:457 #: crates/pdfs-gui/src/main.rs:466
msgid "Quit" msgid "Quit"
msgstr "Quit" msgstr "Quit"
+216 -141
View File
@@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: proton-drive-linux 2.1.0\n" "Project-Id-Version: proton-drive-linux 2.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-10-05 01:01+0200\n" "POT-Creation-Date: 2026-10-08 10:15+0200\n"
"PO-Revision-Date: 2026-09-23 12:00+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n"
"Last-Translator: Proton Drive for Linux contributors\n" "Last-Translator: Proton Drive for Linux contributors\n"
"Language-Team: Spanish\n" "Language-Team: Spanish\n"
@@ -57,7 +57,7 @@ msgstr "%-d %b %Y"
msgid "%A, %B %-d, %Y, %H:%M" msgid "%A, %B %-d, %Y, %H:%M"
msgstr "%A, %-d de %B de %Y, %H:%M" msgstr "%A, %-d de %B de %Y, %H:%M"
#: crates/pdfs-gui/src/app/main.rs:743 #: crates/pdfs-gui/src/app/main.rs:760
#: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539
#: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:454
#: crates/pdfs-gui/src/app/pages/browser.rs:819 #: crates/pdfs-gui/src/app/pages/browser.rs:819
@@ -70,91 +70,91 @@ msgstr "%A, %-d de %B de %Y, %H:%M"
msgid "My files" msgid "My files"
msgstr "Mis archivos" msgstr "Mis archivos"
#: crates/pdfs-gui/src/app/main.rs:746 crates/pdfs-gui/src/app/main.rs:1390 #: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407
#: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/albums.rs:638
#: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/photos.rs:842
#: crates/pdfs-gui/src/app/pages/places.rs:441 #: crates/pdfs-gui/src/app/pages/places.rs:441
msgid "Photos" msgid "Photos"
msgstr "Fotos" msgstr "Fotos"
#: crates/pdfs-gui/src/app/main.rs:751 #: crates/pdfs-gui/src/app/main.rs:768
#: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:168
#: crates/pdfs-gui/src/app/pages/shared.rs:482 #: crates/pdfs-gui/src/app/pages/shared.rs:482
msgid "Shared with me" msgid "Shared with me"
msgstr "Compartido conmigo" msgstr "Compartido conmigo"
#: crates/pdfs-gui/src/app/main.rs:756 #: crates/pdfs-gui/src/app/main.rs:773
#: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120
msgid "Shared by me" msgid "Shared by me"
msgstr "Compartido por mí" msgstr "Compartido por mí"
#: crates/pdfs-gui/src/app/main.rs:759 #: crates/pdfs-gui/src/app/main.rs:776
#: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:140
#: crates/pdfs-gui/src/app/pages/devices.rs:481 #: crates/pdfs-gui/src/app/pages/devices.rs:481
msgid "Computers" msgid "Computers"
msgstr "Equipos" msgstr "Equipos"
#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/main.rs:777
#: crates/pdfs-gui/src/app/pages/trash.rs:101 #: crates/pdfs-gui/src/app/pages/trash.rs:101
msgid "Trash" msgid "Trash"
msgstr "Papelera" msgstr "Papelera"
#. Translators: sidebar entry for the page listing this computer's synced folders. #. Translators: sidebar entry for the page listing this computer's synced folders.
#: crates/pdfs-gui/src/app/main.rs:762 #: crates/pdfs-gui/src/app/main.rs:779
msgid "Sync" msgid "Sync"
msgstr "Sincronización" msgstr "Sincronización"
#. Translators: a failed action's toast. {what} says what failed, such as #. Translators: a failed action's toast. {what} says what failed, such as
#. "Couldn't rename"; {detail} is the reason, usually untranslated text #. "Couldn't rename"; {detail} is the reason, usually untranslated text
#. from the mount service. #. from the mount service.
#: crates/pdfs-gui/src/app/main.rs:1079 #: crates/pdfs-gui/src/app/main.rs:1096
#, rust-format #, rust-format
msgid "{what}: {detail}" msgid "{what}: {detail}"
msgstr "{what}: {detail}" msgstr "{what}: {detail}"
#: crates/pdfs-gui/src/app/main.rs:1098 #: crates/pdfs-gui/src/app/main.rs:1115
msgid "You're offline" msgid "You're offline"
msgstr "Está sin conexión" msgstr "Está sin conexión"
#: crates/pdfs-gui/src/app/main.rs:1099 #: crates/pdfs-gui/src/app/main.rs:1116
msgid "That's not there any more" msgid "That's not there any more"
msgstr "Eso ya no existe" msgstr "Eso ya no existe"
#: crates/pdfs-gui/src/app/main.rs:1100 #: crates/pdfs-gui/src/app/main.rs:1117
msgid "You don't have access to that" msgid "You don't have access to that"
msgstr "No tiene acceso a eso" msgstr "No tiene acceso a eso"
#: crates/pdfs-gui/src/app/main.rs:1101 #: crates/pdfs-gui/src/app/main.rs:1118
msgid "Something changed this first" msgid "Something changed this first"
msgstr "Algo lo cambió antes" msgstr "Algo lo cambió antes"
#: crates/pdfs-gui/src/app/main.rs:1102 #: crates/pdfs-gui/src/app/main.rs:1119
msgid "Your Proton Drive is full" msgid "Your Proton Drive is full"
msgstr "Su Proton Drive está lleno" msgstr "Su Proton Drive está lleno"
#: crates/pdfs-gui/src/app/main.rs:1143 crates/pdfs-gui/src/app/main.rs:1372 #: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389
#: crates/pdfs-gui/src/app/pages/status.rs:385 #: crates/pdfs-gui/src/app/pages/status.rs:385
msgid "Preferences" msgid "Preferences"
msgstr "Preferencias" msgstr "Preferencias"
#: crates/pdfs-gui/src/app/main.rs:1144 #: crates/pdfs-gui/src/app/main.rs:1161
#: crates/pdfs-gui/src/app/widgets/compat.rs:262 #: crates/pdfs-gui/src/app/widgets/compat.rs:262
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "Atajos del teclado" msgstr "Atajos del teclado"
#: crates/pdfs-gui/src/app/main.rs:1146 #: crates/pdfs-gui/src/app/main.rs:1163
msgid "About Proton Drive for Linux" msgid "About Proton Drive for Linux"
msgstr "Acerca de Proton Drive for Linux" msgstr "Acerca de Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1151 #: crates/pdfs-gui/src/app/main.rs:1168
msgid "Main menu" msgid "Main menu"
msgstr "Menú principal" msgstr "Menú principal"
#: crates/pdfs-gui/src/app/main.rs:1207 #: crates/pdfs-gui/src/app/main.rs:1224
msgid "Proton Drive for Linux" msgid "Proton Drive for Linux"
msgstr "Proton Drive for Linux" msgstr "Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1215 #: crates/pdfs-gui/src/app/main.rs:1232
msgid "" msgid ""
"Files-on-demand Proton Drive for the Linux desktop.\n" "Files-on-demand Proton Drive for the Linux desktop.\n"
"\n" "\n"
@@ -166,23 +166,23 @@ msgstr ""
"Cliente no oficial: no está afiliado a Proton AG, ni respaldado ni mantenido " "Cliente no oficial: no está afiliado a Proton AG, ni respaldado ni mantenido "
"por esta." "por esta."
#: crates/pdfs-gui/src/app/main.rs:1220 #: crates/pdfs-gui/src/app/main.rs:1237
msgid "Source Code" msgid "Source Code"
msgstr "Código fuente" msgstr "Código fuente"
#: crates/pdfs-gui/src/app/main.rs:1224 #: crates/pdfs-gui/src/app/main.rs:1241
msgid "Proton Terms of Service" msgid "Proton Terms of Service"
msgstr "Condiciones del servicio de Proton" msgstr "Condiciones del servicio de Proton"
#: crates/pdfs-gui/src/app/main.rs:1228 #: crates/pdfs-gui/src/app/main.rs:1245
msgid "Proton Privacy Policy" msgid "Proton Privacy Policy"
msgstr "Política de privacidad de Proton" msgstr "Política de privacidad de Proton"
#: crates/pdfs-gui/src/app/main.rs:1233 #: crates/pdfs-gui/src/app/main.rs:1250
msgid "Disclaimer" msgid "Disclaimer"
msgstr "Aviso legal" msgstr "Aviso legal"
#: crates/pdfs-gui/src/app/main.rs:1237 #: crates/pdfs-gui/src/app/main.rs:1254
msgid "" msgid ""
"This is an unofficial, community-built client. It is not affiliated with, " "This is an unofficial, community-built client. It is not affiliated with, "
"endorsed by, or supported by Proton AG.\n" "endorsed by, or supported by Proton AG.\n"
@@ -205,7 +205,7 @@ msgstr ""
"Este software se proporciona “tal cual”, sin garantía de ningún tipo. Guarde " "Este software se proporciona “tal cual”, sin garantía de ningún tipo. Guarde "
"una copia de los datos que no pueda permitirse perder." "una copia de los datos que no pueda permitirse perder."
#: crates/pdfs-gui/src/app/main.rs:1245 #: crates/pdfs-gui/src/app/main.rs:1262
msgid "" msgid ""
"Places are named with data from <a href=\"https://www.geonames.org/" "Places are named with data from <a href=\"https://www.geonames.org/"
"\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/"
@@ -215,7 +215,7 @@ msgstr ""
"www.geonames.org/\">GeoNames</a>, con licencia <a href=\"https://" "www.geonames.org/\">GeoNames</a>, con licencia <a href=\"https://"
"creativecommons.org/licenses/by/4.0/\">CC BY 4.0</a>." "creativecommons.org/licenses/by/4.0/\">CC BY 4.0</a>."
#: crates/pdfs-gui/src/app/main.rs:1253 #: crates/pdfs-gui/src/app/main.rs:1270
msgid "" msgid ""
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/" "The places map is drawn from <a href=\"https://www.naturalearthdata.com/"
"\">Natural Earth</a> land outlines and country borders, which are in the " "\">Natural Earth</a> land outlines and country borders, which are in the "
@@ -225,7 +225,7 @@ msgstr ""
"fronteras de países de <a href=\"https://www.naturalearthdata.com/\">Natural " "fronteras de países de <a href=\"https://www.naturalearthdata.com/\">Natural "
"Earth</a>, que son de dominio público." "Earth</a>, que son de dominio público."
#: crates/pdfs-gui/src/app/main.rs:1261 #: crates/pdfs-gui/src/app/main.rs:1278
msgid "" msgid ""
"The street map, when turned on, comes from <a href=\"https://openfreemap.org/" "The street map, when turned on, comes from <a href=\"https://openfreemap.org/"
"\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/" "\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/"
@@ -241,15 +241,15 @@ msgstr ""
#. Translators: replace with your name(s) and email address(es), one #. Translators: replace with your name(s) and email address(es), one
#. per line, to be credited in the About dialog. #. per line, to be credited in the About dialog.
#: crates/pdfs-gui/src/app/main.rs:1272 #: crates/pdfs-gui/src/app/main.rs:1289
msgid "translator-credits" msgid "translator-credits"
msgstr "Colaboradores de Proton Drive for Linux" msgstr "Colaboradores de Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1310 #: crates/pdfs-gui/src/app/main.rs:1327
msgid "Stop Proton Drive?" msgid "Stop Proton Drive?"
msgstr "¿Detener Proton Drive?" msgstr "¿Detener Proton Drive?"
#: crates/pdfs-gui/src/app/main.rs:1311 #: crates/pdfs-gui/src/app/main.rs:1328
msgid "" msgid ""
"My files goes offline and nothing syncs until you connect again. Proton " "My files goes offline and nothing syncs until you connect again. Proton "
"Drive starts again at your next login." "Drive starts again at your next login."
@@ -258,7 +258,7 @@ msgstr ""
"a conectarse. Proton Drive se iniciará de nuevo en su próximo inicio de " "a conectarse. Proton Drive se iniciará de nuevo en su próximo inicio de "
"sesión." "sesión."
#: crates/pdfs-gui/src/app/main.rs:1313 crates/pdfs-gui/src/app/main.rs:1588 #: crates/pdfs-gui/src/app/main.rs:1330 crates/pdfs-gui/src/app/main.rs:1605
#: crates/pdfs-gui/src/app/pages/albums.rs:243 #: crates/pdfs-gui/src/app/pages/albums.rs:243
#: crates/pdfs-gui/src/app/pages/albums.rs:428 #: crates/pdfs-gui/src/app/pages/albums.rs:428
#: crates/pdfs-gui/src/app/pages/browser.rs:1892 #: crates/pdfs-gui/src/app/pages/browser.rs:1892
@@ -273,7 +273,8 @@ msgstr ""
#: crates/pdfs-gui/src/app/pages/locations.rs:1013 #: crates/pdfs-gui/src/app/pages/locations.rs:1013
#: crates/pdfs-gui/src/app/pages/locations.rs:1488 #: crates/pdfs-gui/src/app/pages/locations.rs:1488
#: crates/pdfs-gui/src/app/pages/locations.rs:1538 #: crates/pdfs-gui/src/app/pages/locations.rs:1538
#: crates/pdfs-gui/src/app/pages/login.rs:312 #: crates/pdfs-gui/src/app/pages/login.rs:393
#: crates/pdfs-gui/src/app/pages/login.rs:507
#: crates/pdfs-gui/src/app/pages/photos.rs:2055 #: crates/pdfs-gui/src/app/pages/photos.rs:2055
#: crates/pdfs-gui/src/app/pages/shared.rs:946 #: crates/pdfs-gui/src/app/pages/shared.rs:946
#: crates/pdfs-gui/src/app/pages/shared.rs:1027 #: crates/pdfs-gui/src/app/pages/shared.rs:1027
@@ -288,30 +289,30 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
#: crates/pdfs-gui/src/app/main.rs:1314 #: crates/pdfs-gui/src/app/main.rs:1331
msgctxt "verb" msgctxt "verb"
msgid "Stop" msgid "Stop"
msgstr "Detener" msgstr "Detener"
#: crates/pdfs-gui/src/app/main.rs:1331 #: crates/pdfs-gui/src/app/main.rs:1348
msgid "Couldn't stop Proton Drive" msgid "Couldn't stop Proton Drive"
msgstr "No se pudo detener Proton Drive" msgstr "No se pudo detener Proton Drive"
#: crates/pdfs-gui/src/app/main.rs:1332 #: crates/pdfs-gui/src/app/main.rs:1349
msgid "The Proton Drive service did not stop." msgid "The Proton Drive service did not stop."
msgstr "El servicio de Proton Drive no se detuvo." msgstr "El servicio de Proton Drive no se detuvo."
#: crates/pdfs-gui/src/app/main.rs:1368 #: crates/pdfs-gui/src/app/main.rs:1385
#: crates/pdfs-gui/src/app/pages/status.rs:254 #: crates/pdfs-gui/src/app/pages/status.rs:254
msgid "General" msgid "General"
msgstr "General" msgstr "General"
#: crates/pdfs-gui/src/app/main.rs:1370 #: crates/pdfs-gui/src/app/main.rs:1387
#: crates/pdfs-gui/src/app/pages/browser.rs:247 #: crates/pdfs-gui/src/app/pages/browser.rs:247
msgid "Search Drive" msgid "Search Drive"
msgstr "Buscar en Drive" msgstr "Buscar en Drive"
#: crates/pdfs-gui/src/app/main.rs:1371 #: crates/pdfs-gui/src/app/main.rs:1388
#: crates/pdfs-gui/src/app/pages/browser.rs:957 #: crates/pdfs-gui/src/app/pages/browser.rs:957
#: crates/pdfs-gui/src/app/pages/devices.rs:554 #: crates/pdfs-gui/src/app/pages/devices.rs:554
#: crates/pdfs-gui/src/app/pages/shared.rs:586 #: crates/pdfs-gui/src/app/pages/shared.rs:586
@@ -320,38 +321,38 @@ msgstr "Buscar en Drive"
msgid "Refresh" msgid "Refresh"
msgstr "Actualizar" msgstr "Actualizar"
#: crates/pdfs-gui/src/app/main.rs:1373 #: crates/pdfs-gui/src/app/main.rs:1390
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "Atajos del teclado" msgstr "Atajos del teclado"
#: crates/pdfs-gui/src/app/main.rs:1377 #: crates/pdfs-gui/src/app/main.rs:1394
msgid "Files" msgid "Files"
msgstr "Archivos" msgstr "Archivos"
#: crates/pdfs-gui/src/app/main.rs:1379 #: crates/pdfs-gui/src/app/main.rs:1396
#: crates/pdfs-gui/src/app/pages/browser.rs:294 #: crates/pdfs-gui/src/app/pages/browser.rs:294
msgid "New folder" msgid "New folder"
msgstr "Carpeta nueva" msgstr "Carpeta nueva"
#: crates/pdfs-gui/src/app/main.rs:1380 #: crates/pdfs-gui/src/app/main.rs:1397
#: crates/pdfs-gui/src/app/pages/browser.rs:290 #: crates/pdfs-gui/src/app/pages/browser.rs:290
msgid "Upload files" msgid "Upload files"
msgstr "Subir archivos" msgstr "Subir archivos"
#: crates/pdfs-gui/src/app/main.rs:1381 #: crates/pdfs-gui/src/app/main.rs:1398
msgid "Back / forward" msgid "Back / forward"
msgstr "Atrás / adelante" msgstr "Atrás / adelante"
#: crates/pdfs-gui/src/app/main.rs:1382 #: crates/pdfs-gui/src/app/main.rs:1399
#: crates/pdfs-gui/src/app/pages/browser.rs:1943 #: crates/pdfs-gui/src/app/pages/browser.rs:1943
msgid "Parent folder" msgid "Parent folder"
msgstr "Carpeta superior" msgstr "Carpeta superior"
#: crates/pdfs-gui/src/app/main.rs:1383 #: crates/pdfs-gui/src/app/main.rs:1400
msgid "Grid / list" msgid "Grid / list"
msgstr "Cuadrícula / lista" msgstr "Cuadrícula / lista"
#: crates/pdfs-gui/src/app/main.rs:1384 #: crates/pdfs-gui/src/app/main.rs:1401
#: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/albums.rs:298
#: crates/pdfs-gui/src/app/pages/browser.rs:1871 #: crates/pdfs-gui/src/app/pages/browser.rs:1871
#: crates/pdfs-gui/src/app/pages/browser.rs:1893 #: crates/pdfs-gui/src/app/pages/browser.rs:1893
@@ -360,7 +361,7 @@ msgstr "Cuadrícula / lista"
msgid "Rename" msgid "Rename"
msgstr "Renombrar" msgstr "Renombrar"
#: crates/pdfs-gui/src/app/main.rs:1385 crates/pdfs-gui/src/app/main.rs:1405 #: crates/pdfs-gui/src/app/main.rs:1402 crates/pdfs-gui/src/app/main.rs:1422
#: crates/pdfs-gui/src/app/pages/browser.rs:357 #: crates/pdfs-gui/src/app/pages/browser.rs:357
#: crates/pdfs-gui/src/app/pages/browser.rs:900 #: crates/pdfs-gui/src/app/pages/browser.rs:900
#: crates/pdfs-gui/src/app/pages/browser.rs:932 #: crates/pdfs-gui/src/app/pages/browser.rs:932
@@ -370,73 +371,73 @@ msgstr "Renombrar"
msgid "Move to Trash" msgid "Move to Trash"
msgstr "Mover a la papelera" msgstr "Mover a la papelera"
#: crates/pdfs-gui/src/app/main.rs:1386 #: crates/pdfs-gui/src/app/main.rs:1403
msgid "Clear the selection" msgid "Clear the selection"
msgstr "Anular la selección" msgstr "Anular la selección"
#: crates/pdfs-gui/src/app/main.rs:1392 #: crates/pdfs-gui/src/app/main.rs:1409
msgid "Larger thumbnails" msgid "Larger thumbnails"
msgstr "Miniaturas más grandes" msgstr "Miniaturas más grandes"
#: crates/pdfs-gui/src/app/main.rs:1393 #: crates/pdfs-gui/src/app/main.rs:1410
msgid "Smaller thumbnails" msgid "Smaller thumbnails"
msgstr "Miniaturas más pequeñas" msgstr "Miniaturas más pequeñas"
#: crates/pdfs-gui/src/app/main.rs:1394 #: crates/pdfs-gui/src/app/main.rs:1411
msgid "Reset thumbnail size" msgid "Reset thumbnail size"
msgstr "Restablecer el tamaño de las miniaturas" msgstr "Restablecer el tamaño de las miniaturas"
#: crates/pdfs-gui/src/app/main.rs:1395 #: crates/pdfs-gui/src/app/main.rs:1412
msgid "Select all" msgid "Select all"
msgstr "Seleccionar todo" msgstr "Seleccionar todo"
#: crates/pdfs-gui/src/app/main.rs:1399 #: crates/pdfs-gui/src/app/main.rs:1416
msgid "Photo Viewer" msgid "Photo Viewer"
msgstr "Visor de fotos" msgstr "Visor de fotos"
#: crates/pdfs-gui/src/app/main.rs:1401 #: crates/pdfs-gui/src/app/main.rs:1418
msgid "Previous / next photo" msgid "Previous / next photo"
msgstr "Foto anterior / siguiente" msgstr "Foto anterior / siguiente"
#: crates/pdfs-gui/src/app/main.rs:1402 #: crates/pdfs-gui/src/app/main.rs:1419
msgid "First / last photo" msgid "First / last photo"
msgstr "Primera / última foto" msgstr "Primera / última foto"
#: crates/pdfs-gui/src/app/main.rs:1403 #: crates/pdfs-gui/src/app/main.rs:1420
msgid "Show details" msgid "Show details"
msgstr "Mostrar detalles" msgstr "Mostrar detalles"
#: crates/pdfs-gui/src/app/main.rs:1404 #: crates/pdfs-gui/src/app/main.rs:1421
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Pantalla completa" msgstr "Pantalla completa"
#: crates/pdfs-gui/src/app/main.rs:1406 #: crates/pdfs-gui/src/app/main.rs:1423
msgid "Close" msgid "Close"
msgstr "Cerrar" msgstr "Cerrar"
#: crates/pdfs-gui/src/app/main.rs:1496 #: crates/pdfs-gui/src/app/main.rs:1513
msgid "Select a single item to rename it" msgid "Select a single item to rename it"
msgstr "Seleccione un único elemento para renombrarlo" msgstr "Seleccione un único elemento para renombrarlo"
#: crates/pdfs-gui/src/app/main.rs:1656 #: crates/pdfs-gui/src/app/main.rs:1673
#: crates/pdfs-gui/src/app/pages/albums.rs:85 #: crates/pdfs-gui/src/app/pages/albums.rs:85
#: crates/pdfs-gui/src/app/pages/places.rs:104 #: crates/pdfs-gui/src/app/pages/places.rs:104
#: crates/pdfs-gui/src/app/pages/status.rs:1370 #: crates/pdfs-gui/src/app/pages/status.rs:1371
msgid "Not connected" msgid "Not connected"
msgstr "Sin conexión" msgstr "Sin conexión"
#: crates/pdfs-gui/src/app/main.rs:1657 #: crates/pdfs-gui/src/app/main.rs:1674
msgid "The Proton Drive service isn't running." msgid "The Proton Drive service isn't running."
msgstr "El servicio de Proton Drive no está en ejecución." msgstr "El servicio de Proton Drive no está en ejecución."
#: crates/pdfs-gui/src/app/main.rs:1665 #: crates/pdfs-gui/src/app/main.rs:1682
#: crates/pdfs-gui/src/app/pages/status.rs:336 #: crates/pdfs-gui/src/app/pages/status.rs:336
#: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595 #: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595
#: crates/pdfs-gui/src/main.rs:444 #: crates/pdfs-gui/src/main.rs:453
msgid "Connecting…" msgid "Connecting…"
msgstr "Conectando…" msgstr "Conectando…"
#: crates/pdfs-gui/src/app/main.rs:1666 #: crates/pdfs-gui/src/app/main.rs:1683
msgid "Waiting for the Proton Drive service to start." msgid "Waiting for the Proton Drive service to start."
msgstr "Esperando a que se inicie el servicio de Proton Drive." msgstr "Esperando a que se inicie el servicio de Proton Drive."
@@ -1281,7 +1282,7 @@ msgstr "No se pudieron cargar los álbumes"
#: crates/pdfs-gui/src/app/pages/shared.rs:1006 #: crates/pdfs-gui/src/app/pages/shared.rs:1006
#: crates/pdfs-gui/src/app/pages/shared.rs:1122 #: crates/pdfs-gui/src/app/pages/shared.rs:1122
#: crates/pdfs-gui/src/app/pages/status.rs:750 #: crates/pdfs-gui/src/app/pages/status.rs:750
#: crates/pdfs-gui/src/app/pages/status.rs:1517 #: crates/pdfs-gui/src/app/pages/status.rs:1518
#: crates/pdfs-gui/src/app/pages/takeout.rs:424 #: crates/pdfs-gui/src/app/pages/takeout.rs:424
#: crates/pdfs-gui/src/app/pages/takeout.rs:617 #: crates/pdfs-gui/src/app/pages/takeout.rs:617
#: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438
@@ -1549,7 +1550,7 @@ msgid "Make available offline"
msgstr "Hacer disponible sin conexión" msgstr "Hacer disponible sin conexión"
#: crates/pdfs-gui/src/app/pages/browser.rs:347 #: crates/pdfs-gui/src/app/pages/browser.rs:347
#: crates/pdfs-gui/src/app/pages/status.rs:1493 #: crates/pdfs-gui/src/app/pages/status.rs:1494
msgid "Make online only" msgid "Make online only"
msgstr "Hacer solo en línea" msgstr "Hacer solo en línea"
@@ -2707,7 +2708,7 @@ msgid "Sync paused until you resume"
msgstr "Sincronización en pausa hasta que la reanude" msgstr "Sincronización en pausa hasta que la reanude"
#: crates/pdfs-gui/src/app/pages/locations.rs:529 #: crates/pdfs-gui/src/app/pages/locations.rs:529
#: crates/pdfs-gui/src/app/pages/status.rs:1323 crates/pdfs-gui/src/main.rs:237 #: crates/pdfs-gui/src/app/pages/status.rs:1324 crates/pdfs-gui/src/main.rs:246
msgid "Sync paused" msgid "Sync paused"
msgstr "Sincronización en pausa" msgstr "Sincronización en pausa"
@@ -3174,27 +3175,37 @@ msgstr "Introduzca su correo electrónico y su contraseña."
msgid "Signing in…" msgid "Signing in…"
msgstr "Iniciando sesión…" msgstr "Iniciando sesión…"
#: crates/pdfs-gui/src/app/pages/login.rs:155 #: crates/pdfs-gui/src/app/pages/login.rs:164
msgid "Complete the verification to continue…" msgid "Complete the verification to continue…"
msgstr "Complete la verificación para continuar…" msgstr "Complete la verificación para continuar…"
#: crates/pdfs-gui/src/app/pages/login.rs:165 #: crates/pdfs-gui/src/app/pages/login.rs:197
msgid "login cancelled" msgid "login cancelled"
msgstr "inicio de sesión cancelado" msgstr "inicio de sesión cancelado"
#: crates/pdfs-gui/src/app/pages/login.rs:198 #: crates/pdfs-gui/src/app/pages/login.rs:210
msgid ""
"The verification page couldn't be opened, so the sign-in was cancelled. Your "
"system may keep the web view's sandbox from starting; the troubleshooting "
"guide explains what to do."
msgstr ""
"No se pudo abrir la página de verificación, así que se canceló el inicio de "
"sesión. Es posible que su sistema impida que se inicie el entorno aislado de "
"la vista web; la guía de solución de problemas explica qué hacer."
#: crates/pdfs-gui/src/app/pages/login.rs:233
msgid "Too many sign-in attempts. Wait a few minutes and try again." msgid "Too many sign-in attempts. Wait a few minutes and try again."
msgstr "" msgstr ""
"Demasiados intentos de inicio de sesión. Espere unos minutos e inténtelo de " "Demasiados intentos de inicio de sesión. Espere unos minutos e inténtelo de "
"nuevo." "nuevo."
#: crates/pdfs-gui/src/app/pages/login.rs:204 #: crates/pdfs-gui/src/app/pages/login.rs:239
msgid "Couldn't reach Proton. Check your internet connection and try again." msgid "Couldn't reach Proton. Check your internet connection and try again."
msgstr "" msgstr ""
"No se pudo contactar con Proton. Compruebe su conexión a Internet e " "No se pudo contactar con Proton. Compruebe su conexión a Internet e "
"inténtelo de nuevo." "inténtelo de nuevo."
#: crates/pdfs-gui/src/app/pages/login.rs:207 #: crates/pdfs-gui/src/app/pages/login.rs:242
msgid "" msgid ""
"Signed in, but the session couldn't be saved to the system keyring. Make " "Signed in, but the session couldn't be saved to the system keyring. Make "
"sure a keyring (GNOME Keyring or KWallet) is running and unlocked." "sure a keyring (GNOME Keyring or KWallet) is running and unlocked."
@@ -3203,12 +3214,20 @@ msgstr ""
"sistema. Asegúrese de que un depósito de claves (GNOME Keyring o KWallet) " "sistema. Asegúrese de que un depósito de claves (GNOME Keyring o KWallet) "
"esté en ejecución y desbloqueado." "esté en ejecución y desbloqueado."
#: crates/pdfs-gui/src/app/pages/login.rs:210 #: crates/pdfs-gui/src/app/pages/login.rs:245
msgid "Sign-in cancelled: no two-factor code was entered." msgid "Sign-in cancelled: no two-factor code was entered."
msgstr "" msgstr ""
"Inicio de sesión cancelado: no se introdujo ningún código de dos factores." "Inicio de sesión cancelado: no se introdujo ningún código de dos factores."
#: crates/pdfs-gui/src/app/pages/login.rs:213 #: crates/pdfs-gui/src/app/pages/login.rs:248
msgid "Sign-in cancelled: no mailbox password was entered."
msgstr "Inicio de sesión cancelado: no se introdujo la contraseña del buzón."
#: crates/pdfs-gui/src/app/pages/login.rs:251
msgid "That mailbox password didn't unlock your account."
msgstr "Esa contraseña del buzón no desbloqueó tu cuenta."
#: crates/pdfs-gui/src/app/pages/login.rs:254
msgid "" msgid ""
"Sign-in cancelled: the verification wasn't completed. Sign in again to get a " "Sign-in cancelled: the verification wasn't completed. Sign in again to get a "
"new one." "new one."
@@ -3217,35 +3236,86 @@ msgstr ""
"sesión para obtener una nueva." "sesión para obtener una nueva."
#. Translators: {error} is an error message, usually in English. #. Translators: {error} is an error message, usually in English.
#: crates/pdfs-gui/src/app/pages/login.rs:218 #: crates/pdfs-gui/src/app/pages/login.rs:259
#, rust-format #, rust-format
msgid "Sign-in failed: {error}" msgid "Sign-in failed: {error}"
msgstr "Falló el inicio de sesión: {error}" msgstr "Falló el inicio de sesión: {error}"
#: crates/pdfs-gui/src/app/pages/login.rs:298 #: crates/pdfs-gui/src/app/pages/login.rs:369
msgid "That mailbox password didn't unlock your account. Try again."
msgstr "Esa contraseña del buzón no desbloqueó tu cuenta. Inténtalo de nuevo."
#: crates/pdfs-gui/src/app/pages/login.rs:371
msgid ""
"This account has a separate mailbox password. Enter it to unlock your files."
msgstr ""
"Esta cuenta tiene una contraseña del buzón independiente. Introdúcela para "
"desbloquear tus archivos."
#: crates/pdfs-gui/src/app/pages/login.rs:374
msgid "Mailbox Password"
msgstr "Contraseña del buzón"
#: crates/pdfs-gui/src/app/pages/login.rs:380
msgid "Mailbox password"
msgstr "Contraseña del buzón"
#: crates/pdfs-gui/src/app/pages/login.rs:384
msgid "Keep unlocked on this computer"
msgstr "Mantener desbloqueado en este equipo"
#: crates/pdfs-gui/src/app/pages/login.rs:386
msgid ""
"Stores the unlock in the system keyring, so Proton Drive opens without "
"asking after every login. Otherwise you enter the password once per session."
msgstr ""
"Guarda el desbloqueo en el llavero del sistema, para que Proton Drive se "
"abra sin preguntar después de cada inicio de sesión. De lo contrario, "
"introduces la contraseña una vez por sesión."
#: crates/pdfs-gui/src/app/pages/login.rs:394
#: crates/pdfs-gui/src/app/pages/login.rs:447
msgid "Unlock"
msgstr "Desbloquear"
#: crates/pdfs-gui/src/app/pages/login.rs:446
msgid "Proton Drive is locked. Enter your mailbox password to unlock it."
msgstr ""
"Proton Drive está bloqueado. Introduce la contraseña del buzón para "
"desbloquearlo."
#: crates/pdfs-gui/src/app/pages/login.rs:475
msgid "Unlocked."
msgstr "Desbloqueado."
#: crates/pdfs-gui/src/app/pages/login.rs:480
msgid "Couldn't unlock"
msgstr "No se pudo desbloquear"
#: crates/pdfs-gui/src/app/pages/login.rs:493
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "Autenticación en dos pasos" msgstr "Autenticación en dos pasos"
#: crates/pdfs-gui/src/app/pages/login.rs:300 #: crates/pdfs-gui/src/app/pages/login.rs:495
msgid "" msgid ""
"Enter the code from your authenticator app, or one of your recovery codes." "Enter the code from your authenticator app, or one of your recovery codes."
msgstr "" msgstr ""
"Introduzca el código de su aplicación de autenticación o uno de sus códigos " "Introduzca el código de su aplicación de autenticación o uno de sus códigos "
"de recuperación." "de recuperación."
#: crates/pdfs-gui/src/app/pages/login.rs:306 #: crates/pdfs-gui/src/app/pages/login.rs:501
msgid "Authentication code" msgid "Authentication code"
msgstr "Código de autenticación" msgstr "Código de autenticación"
#: crates/pdfs-gui/src/app/pages/login.rs:313 #: crates/pdfs-gui/src/app/pages/login.rs:508
msgid "Confirm" msgid "Confirm"
msgstr "Confirmar" msgstr "Confirmar"
#: crates/pdfs-gui/src/app/pages/login.rs:341 #: crates/pdfs-gui/src/app/pages/login.rs:536
msgid "Sign Out?" msgid "Sign Out?"
msgstr "¿Cerrar la sesión?" msgstr "¿Cerrar la sesión?"
#: crates/pdfs-gui/src/app/pages/login.rs:343 #: crates/pdfs-gui/src/app/pages/login.rs:538
msgid "" msgid ""
"Proton Drive will disconnect and stop syncing until you sign in again. Files " "Proton Drive will disconnect and stop syncing until you sign in again. Files "
"already on this computer stay where they are." "already on this computer stay where they are."
@@ -3254,7 +3324,7 @@ msgstr ""
"iniciar sesión. Los archivos que ya están en este equipo se quedan donde " "iniciar sesión. Los archivos que ya están en este equipo se quedan donde "
"están." "están."
#: crates/pdfs-gui/src/app/pages/login.rs:345 #: crates/pdfs-gui/src/app/pages/login.rs:540
msgid "Sign Out" msgid "Sign Out"
msgstr "Cerrar la sesión" msgstr "Cerrar la sesión"
@@ -4606,40 +4676,40 @@ msgid "Restart Now"
msgstr "Reiniciar ahora" msgstr "Reiniciar ahora"
#. Translators: {size} is an amount of storage, such as "1.2 GiB". #. Translators: {size} is an amount of storage, such as "1.2 GiB".
#: crates/pdfs-gui/src/app/pages/status.rs:912 #: crates/pdfs-gui/src/app/pages/status.rs:913
#, rust-format #, rust-format
msgid "{size} used" msgid "{size} used"
msgstr "{size} usados" msgstr "{size} usados"
#: crates/pdfs-gui/src/app/pages/status.rs:923 #: crates/pdfs-gui/src/app/pages/status.rs:924
#, rust-format #, rust-format
msgid "{used} of {total} used ({percent}%)" msgid "{used} of {total} used ({percent}%)"
msgstr "{used} de {total} usados ({percent}%)" msgstr "{used} de {total} usados ({percent}%)"
#: crates/pdfs-gui/src/app/pages/status.rs:961 #: crates/pdfs-gui/src/app/pages/status.rs:962
msgid "Proton Drive connected" msgid "Proton Drive connected"
msgstr "Proton Drive conectado" msgstr "Proton Drive conectado"
#: crates/pdfs-gui/src/app/pages/status.rs:962 #: crates/pdfs-gui/src/app/pages/status.rs:963
msgid "Your files are available." msgid "Your files are available."
msgstr "Sus archivos están disponibles." msgstr "Sus archivos están disponibles."
#: crates/pdfs-gui/src/app/pages/status.rs:967 #: crates/pdfs-gui/src/app/pages/status.rs:968
msgid "Proton Drive disconnected" msgid "Proton Drive disconnected"
msgstr "Proton Drive desconectado" msgstr "Proton Drive desconectado"
#: crates/pdfs-gui/src/app/pages/status.rs:968 #: crates/pdfs-gui/src/app/pages/status.rs:969
msgid "" msgid ""
"The Proton Drive service stopped. Files aren't available until it restarts." "The Proton Drive service stopped. Files aren't available until it restarts."
msgstr "" msgstr ""
"El servicio de Proton Drive se detuvo. Los archivos no estarán disponibles " "El servicio de Proton Drive se detuvo. Los archivos no estarán disponibles "
"hasta que se reinicie." "hasta que se reinicie."
#: crates/pdfs-gui/src/app/pages/status.rs:1037 #: crates/pdfs-gui/src/app/pages/status.rs:1038
msgid "Sync complete" msgid "Sync complete"
msgstr "Sincronización completada" msgstr "Sincronización completada"
#: crates/pdfs-gui/src/app/pages/status.rs:1039 #: crates/pdfs-gui/src/app/pages/status.rs:1040
#, rust-format #, rust-format
msgid "{n} file finished transferring." msgid "{n} file finished transferring."
msgid_plural "{n} files finished transferring." msgid_plural "{n} files finished transferring."
@@ -4648,27 +4718,27 @@ msgstr[1] "{n} archivos terminaron de transferirse."
#. Translators: a background job's progress. {title} is the job, such as #. Translators: a background job's progress. {title} is the job, such as
#. "Scanning Photos"; {done} and {total} are counts. #. "Scanning Photos"; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1117 #: crates/pdfs-gui/src/app/pages/status.rs:1118
#, rust-format #, rust-format
msgid "{title} — {done} of {total}" msgid "{title} — {done} of {total}"
msgstr "{title}: {done} de {total}" msgstr "{title}: {done} de {total}"
#. Translators: a background job's progress. {title} is the job, {detail} #. Translators: a background job's progress. {title} is the job, {detail}
#. what it is working on; {done} and {total} are counts. #. what it is working on; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1120 #: crates/pdfs-gui/src/app/pages/status.rs:1121
#, rust-format #, rust-format
msgid "{title} — {detail} ({done} of {total})" msgid "{title} — {detail} ({done} of {total})"
msgstr "{title}: {detail} ({done} de {total})" msgstr "{title}: {detail} ({done} de {total})"
#. Translators: a background job still running. {title} is the job. #. Translators: a background job still running. {title} is the job.
#: crates/pdfs-gui/src/app/pages/status.rs:1122 #: crates/pdfs-gui/src/app/pages/status.rs:1123
#, rust-format #, rust-format
msgid "{title}…" msgid "{title}…"
msgstr "{title}…" msgstr "{title}…"
#. Translators: a background job still running. {title} is the job, #. Translators: a background job still running. {title} is the job,
#. {detail} what it is working on. #. {detail} what it is working on.
#: crates/pdfs-gui/src/app/pages/status.rs:1125 #: crates/pdfs-gui/src/app/pages/status.rs:1126
#, rust-format #, rust-format
msgid "{title} — {detail}…" msgid "{title} — {detail}…"
msgstr "{title}: {detail}…" msgstr "{title}: {detail}…"
@@ -4676,7 +4746,7 @@ msgstr "{title}: {detail}…"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} the size moved so far and {speed} a size such as #. file name, {done} the size moved so far and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1154 #: crates/pdfs-gui/src/app/pages/status.rs:1155
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} ({speed}/s)" msgid "{arrow} {name} — {done} ({speed}/s)"
msgstr "{arrow} {name}: {done} ({speed}/s)" msgstr "{arrow} {name}: {done} ({speed}/s)"
@@ -4684,119 +4754,119 @@ msgstr "{arrow} {name}: {done} ({speed}/s)"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} and {total} sizes and {speed} a size such as #. file name, {done} and {total} sizes and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1162 #: crates/pdfs-gui/src/app/pages/status.rs:1163
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} of {total} ({speed}/s)" msgid "{arrow} {name} — {done} of {total} ({speed}/s)"
msgstr "{arrow} {name}: {done} de {total} ({speed}/s)" msgstr "{arrow} {name}: {done} de {total} ({speed}/s)"
#: crates/pdfs-gui/src/app/pages/status.rs:1254 #: crates/pdfs-gui/src/app/pages/status.rs:1255
#, rust-format #, rust-format
msgid "{used} cached — no limit set" msgid "{used} cached — no limit set"
msgstr "{used} en caché — sin límite" msgstr "{used} en caché — sin límite"
#: crates/pdfs-gui/src/app/pages/status.rs:1260 #: crates/pdfs-gui/src/app/pages/status.rs:1261
#, rust-format #, rust-format
msgid "{used} of {limit} used" msgid "{used} of {limit} used"
msgstr "{used} de {limit} usados" msgstr "{used} de {limit} usados"
#: crates/pdfs-gui/src/app/pages/status.rs:1328 #: crates/pdfs-gui/src/app/pages/status.rs:1329
#, rust-format #, rust-format
msgid "{queued} · resumes {time}" msgid "{queued} · resumes {time}"
msgstr "{queued} · se reanuda {time}" msgstr "{queued} · se reanuda {time}"
#. Translators: {time} is when syncing resumes, such as "at 14:30" #. Translators: {time} is when syncing resumes, such as "at 14:30"
#. or "Tue 09:00". #. or "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1333 #: crates/pdfs-gui/src/app/pages/status.rs:1334
#, rust-format #, rust-format
msgid "Resumes {time}" msgid "Resumes {time}"
msgstr "Se reanuda {time}" msgstr "Se reanuda {time}"
#: crates/pdfs-gui/src/app/pages/status.rs:1335 #: crates/pdfs-gui/src/app/pages/status.rs:1336
msgid "Until you resume" msgid "Until you resume"
msgstr "Hasta que la reanude" msgstr "Hasta que la reanude"
#: crates/pdfs-gui/src/app/pages/status.rs:1342 crates/pdfs-gui/src/main.rs:239 #: crates/pdfs-gui/src/app/pages/status.rs:1343 crates/pdfs-gui/src/main.rs:248
#, rust-format #, rust-format
msgid "{n} change needs attention" msgid "{n} change needs attention"
msgid_plural "{n} changes need attention" msgid_plural "{n} changes need attention"
msgstr[0] "{n} cambio requiere atención" msgstr[0] "{n} cambio requiere atención"
msgstr[1] "{n} cambios requieren atención" msgstr[1] "{n} cambios requieren atención"
#: crates/pdfs-gui/src/app/pages/status.rs:1352 #: crates/pdfs-gui/src/app/pages/status.rs:1353
#: crates/pdfs-gui/src/app/widgets/details.rs:108 #: crates/pdfs-gui/src/app/widgets/details.rs:108
msgctxt "state" msgctxt "state"
msgid "Offline" msgid "Offline"
msgstr "Sin conexión" msgstr "Sin conexión"
#: crates/pdfs-gui/src/app/pages/status.rs:1353 #: crates/pdfs-gui/src/app/pages/status.rs:1354
msgid "Cached files only" msgid "Cached files only"
msgstr "Solo archivos en caché" msgstr "Solo archivos en caché"
#: crates/pdfs-gui/src/app/pages/status.rs:1358 #: crates/pdfs-gui/src/app/pages/status.rs:1359
msgctxt "state" msgctxt "state"
msgid "Syncing" msgid "Syncing"
msgstr "Sincronizando" msgstr "Sincronizando"
#: crates/pdfs-gui/src/app/pages/status.rs:1364 #: crates/pdfs-gui/src/app/pages/status.rs:1365
msgid "Up to date" msgid "Up to date"
msgstr "Actualizado" msgstr "Actualizado"
#: crates/pdfs-gui/src/app/pages/status.rs:1371 #: crates/pdfs-gui/src/app/pages/status.rs:1372
#: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622 #: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622
msgid "Proton Drive isn't running" msgid "Proton Drive isn't running"
msgstr "Proton Drive no está en ejecución" msgstr "Proton Drive no está en ejecución"
#: crates/pdfs-gui/src/app/pages/status.rs:1376 #: crates/pdfs-gui/src/app/pages/status.rs:1377
msgid "Not responding" msgid "Not responding"
msgstr "No responde" msgstr "No responde"
#: crates/pdfs-gui/src/app/pages/status.rs:1377 #: crates/pdfs-gui/src/app/pages/status.rs:1378
msgid "Proton Drive is taking too long to answer" msgid "Proton Drive is taking too long to answer"
msgstr "Proton Drive está tardando demasiado en responder" msgstr "Proton Drive está tardando demasiado en responder"
#. Translators: stands in for a time that could not be read, as in #. Translators: stands in for a time that could not be read, as in
#. "Resumes later". #. "Resumes later".
#: crates/pdfs-gui/src/app/pages/status.rs:1415 #: crates/pdfs-gui/src/app/pages/status.rs:1416
#: crates/pdfs-gui/src/app/pages/status.rs:1430 #: crates/pdfs-gui/src/app/pages/status.rs:1431
msgid "later" msgid "later"
msgstr "más tarde" msgstr "más tarde"
#. Translators: strftime format for a time later today, such as "at 14:30". #. Translators: strftime format for a time later today, such as "at 14:30".
#: crates/pdfs-gui/src/app/pages/status.rs:1419 #: crates/pdfs-gui/src/app/pages/status.rs:1420
msgid "at %H:%M" msgid "at %H:%M"
msgstr "a las %H:%M" msgstr "a las %H:%M"
#. Translators: strftime format for a time within the next week, such as #. Translators: strftime format for a time within the next week, such as
#. "Tue 09:00". #. "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1423 #: crates/pdfs-gui/src/app/pages/status.rs:1424
msgid "%a %H:%M" msgid "%a %H:%M"
msgstr "%a %H:%M" msgstr "%a %H:%M"
#. Translators: strftime format for a date further ahead, such as "23 Sep". #. Translators: strftime format for a date further ahead, such as "23 Sep".
#: crates/pdfs-gui/src/app/pages/status.rs:1426 #: crates/pdfs-gui/src/app/pages/status.rs:1427
msgid "%e %b" msgid "%e %b"
msgstr "%-d %b" msgstr "%-d %b"
#: crates/pdfs-gui/src/app/pages/status.rs:1456 #: crates/pdfs-gui/src/app/pages/status.rs:1457
msgid "No files kept offline" msgid "No files kept offline"
msgstr "No hay archivos disponibles sin conexión" msgstr "No hay archivos disponibles sin conexión"
#: crates/pdfs-gui/src/app/pages/status.rs:1458 #: crates/pdfs-gui/src/app/pages/status.rs:1459
msgid "Right-click a file and choose “Make available offline”." msgid "Right-click a file and choose “Make available offline”."
msgstr "" msgstr ""
"Pulse con el botón derecho en un archivo y elija “Hacer disponible sin " "Pulse con el botón derecho en un archivo y elija “Hacer disponible sin "
"conexión”." "conexión”."
#: crates/pdfs-gui/src/app/pages/status.rs:1509 #: crates/pdfs-gui/src/app/pages/status.rs:1510
#: crates/pdfs-gui/src/app/pages/status.rs:1516 #: crates/pdfs-gui/src/app/pages/status.rs:1517
msgid "Couldn't make it online only" msgid "Couldn't make it online only"
msgstr "No se pudo hacer solo en línea" msgstr "No se pudo hacer solo en línea"
#: crates/pdfs-gui/src/app/pages/status.rs:1533 #: crates/pdfs-gui/src/app/pages/status.rs:1534
msgid "Show fewer" msgid "Show fewer"
msgstr "Mostrar menos" msgstr "Mostrar menos"
#: crates/pdfs-gui/src/app/pages/status.rs:1536 #: crates/pdfs-gui/src/app/pages/status.rs:1537
#, rust-format #, rust-format
msgid "Show all {n} file" msgid "Show all {n} file"
msgid_plural "Show all {n} files" msgid_plural "Show all {n} files"
@@ -5224,7 +5294,7 @@ msgstr "Papelera vaciada"
msgid "Couldn't empty the trash" msgid "Couldn't empty the trash"
msgstr "No se pudo vaciar la papelera" msgstr "No se pudo vaciar la papelera"
#: crates/pdfs-gui/src/app/pages/verify.rs:57 #: crates/pdfs-gui/src/app/pages/verify.rs:162
msgid "Verification" msgid "Verification"
msgstr "Verificación" msgstr "Verificación"
@@ -5858,40 +5928,45 @@ msgstr[1] "↑ {n} subiendo"
msgid "Proton Drive isn't responding" msgid "Proton Drive isn't responding"
msgstr "Proton Drive no responde" msgstr "Proton Drive no responde"
#: crates/pdfs-gui/src/main.rs:172 #: crates/pdfs-gui/src/main.rs:173
#, rust-format
msgid "Signed in as {username} — locked"
msgstr "Sesión iniciada como {username} — bloqueado"
#: crates/pdfs-gui/src/main.rs:181
#, rust-format #, rust-format
msgid "Signed in as {username} — not connected" msgid "Signed in as {username} — not connected"
msgstr "Sesión iniciada como {username} — sin conexión" msgstr "Sesión iniciada como {username} — sin conexión"
#: crates/pdfs-gui/src/main.rs:177 #: crates/pdfs-gui/src/main.rs:186
msgid "Not signed in" msgid "Not signed in"
msgstr "Sesión no iniciada" msgstr "Sesión no iniciada"
#. Translators: {error} is an error message, in English. #. Translators: {error} is an error message, in English.
#: crates/pdfs-gui/src/main.rs:180 #: crates/pdfs-gui/src/main.rs:189
#, rust-format #, rust-format
msgid "Error: {error}" msgid "Error: {error}"
msgstr "Error: {error}" msgstr "Error: {error}"
#: crates/pdfs-gui/src/main.rs:201 #: crates/pdfs-gui/src/main.rs:210
msgid "Proton Drive sent an unexpected reply" msgid "Proton Drive sent an unexpected reply"
msgstr "Proton Drive envió una respuesta inesperada" msgstr "Proton Drive envió una respuesta inesperada"
#: crates/pdfs-gui/src/main.rs:246 #: crates/pdfs-gui/src/main.rs:255
#, rust-format #, rust-format
msgid "Up to date in {mountpoint} ({n} pinned)" msgid "Up to date in {mountpoint} ({n} pinned)"
msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)"
msgstr[0] "Actualizado en {mountpoint} ({n} fijado)" msgstr[0] "Actualizado en {mountpoint} ({n} fijado)"
msgstr[1] "Actualizado en {mountpoint} ({n} fijados)" msgstr[1] "Actualizado en {mountpoint} ({n} fijados)"
#: crates/pdfs-gui/src/main.rs:253 #: crates/pdfs-gui/src/main.rs:262
#, rust-format #, rust-format
msgid "Syncing — {queued} ({n} pinned)" msgid "Syncing — {queued} ({n} pinned)"
msgid_plural "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)"
msgstr[0] "Sincronizando: {queued} ({n} anclado)" msgstr[0] "Sincronizando: {queued} ({n} anclado)"
msgstr[1] "Sincronizando: {queued} ({n} anclados)" msgstr[1] "Sincronizando: {queued} ({n} anclados)"
#: crates/pdfs-gui/src/main.rs:260 #: crates/pdfs-gui/src/main.rs:269
#, rust-format #, rust-format
msgid "Offline — cached files only ({n} pinned)" msgid "Offline — cached files only ({n} pinned)"
msgid_plural "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)"
@@ -5899,52 +5974,52 @@ msgstr[0] "Sin conexión: solo archivos en caché ({n} anclado)"
msgstr[1] "Sin conexión: solo archivos en caché ({n} anclados)" msgstr[1] "Sin conexión: solo archivos en caché ({n} anclados)"
#. Translators: {queued} is a queue summary such as "3 uploads queued". #. Translators: {queued} is a queue summary such as "3 uploads queued".
#: crates/pdfs-gui/src/main.rs:266 #: crates/pdfs-gui/src/main.rs:275
#, rust-format #, rust-format
msgid "Offline — {queued}" msgid "Offline — {queued}"
msgstr "Sin conexión: {queued}" msgstr "Sin conexión: {queued}"
#. Translators: a menu item that opens the list of failing changes. #. Translators: a menu item that opens the list of failing changes.
#: crates/pdfs-gui/src/main.rs:296 #: crates/pdfs-gui/src/main.rs:305
#, rust-format #, rust-format
msgid "{n} issue — View" msgid "{n} issue — View"
msgid_plural "{n} issues — View" msgid_plural "{n} issues — View"
msgstr[0] "{n} problema: ver" msgstr[0] "{n} problema: ver"
msgstr[1] "{n} problemas: ver" msgstr[1] "{n} problemas: ver"
#: crates/pdfs-gui/src/main.rs:402 #: crates/pdfs-gui/src/main.rs:411
msgid "Sign In…" msgid "Sign In…"
msgstr "Iniciar sesión…" msgstr "Iniciar sesión…"
#: crates/pdfs-gui/src/main.rs:403 #: crates/pdfs-gui/src/main.rs:412
msgid "Open Proton Drive" msgid "Open Proton Drive"
msgstr "Abrir Proton Drive" msgstr "Abrir Proton Drive"
#: crates/pdfs-gui/src/main.rs:409 #: crates/pdfs-gui/src/main.rs:418
msgid "Open Folder" msgid "Open Folder"
msgstr "Abrir carpeta" msgstr "Abrir carpeta"
#: crates/pdfs-gui/src/main.rs:421 #: crates/pdfs-gui/src/main.rs:430
msgid "Resume Sync" msgid "Resume Sync"
msgstr "Reanudar la sincronización" msgstr "Reanudar la sincronización"
#: crates/pdfs-gui/src/main.rs:423 #: crates/pdfs-gui/src/main.rs:432
msgid "Pause Sync" msgid "Pause Sync"
msgstr "Pausar la sincronización" msgstr "Pausar la sincronización"
#: crates/pdfs-gui/src/main.rs:442 #: crates/pdfs-gui/src/main.rs:451
msgid "Connect" msgid "Connect"
msgstr "Conectar" msgstr "Conectar"
#: crates/pdfs-gui/src/main.rs:449 #: crates/pdfs-gui/src/main.rs:458
msgid "Hide Tray Icon" msgid "Hide Tray Icon"
msgstr "Ocultar el icono de la bandeja" msgstr "Ocultar el icono de la bandeja"
#: crates/pdfs-gui/src/main.rs:453 #: crates/pdfs-gui/src/main.rs:462
msgid "Stop Proton Drive…" msgid "Stop Proton Drive…"
msgstr "Detener Proton Drive…" msgstr "Detener Proton Drive…"
#: crates/pdfs-gui/src/main.rs:457 #: crates/pdfs-gui/src/main.rs:466
msgid "Quit" msgid "Quit"
msgstr "Salir" msgstr "Salir"
+220 -141
View File
@@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: proton-drive-linux 2.1.0\n" "Project-Id-Version: proton-drive-linux 2.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-10-05 01:01+0200\n" "POT-Creation-Date: 2026-10-08 10:15+0200\n"
"PO-Revision-Date: 2026-09-23 12:00+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n"
"Last-Translator: Proton Drive for Linux contributors\n" "Last-Translator: Proton Drive for Linux contributors\n"
"Language-Team: French\n" "Language-Team: French\n"
@@ -57,7 +57,7 @@ msgstr "%-d %b %Y"
msgid "%A, %B %-d, %Y, %H:%M" msgid "%A, %B %-d, %Y, %H:%M"
msgstr "%A %-d %B %Y à %H:%M" msgstr "%A %-d %B %Y à %H:%M"
#: crates/pdfs-gui/src/app/main.rs:743 #: crates/pdfs-gui/src/app/main.rs:760
#: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539
#: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:454
#: crates/pdfs-gui/src/app/pages/browser.rs:819 #: crates/pdfs-gui/src/app/pages/browser.rs:819
@@ -70,91 +70,91 @@ msgstr "%A %-d %B %Y à %H:%M"
msgid "My files" msgid "My files"
msgstr "Mes fichiers" msgstr "Mes fichiers"
#: crates/pdfs-gui/src/app/main.rs:746 crates/pdfs-gui/src/app/main.rs:1390 #: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407
#: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/albums.rs:638
#: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/photos.rs:842
#: crates/pdfs-gui/src/app/pages/places.rs:441 #: crates/pdfs-gui/src/app/pages/places.rs:441
msgid "Photos" msgid "Photos"
msgstr "Photos" msgstr "Photos"
#: crates/pdfs-gui/src/app/main.rs:751 #: crates/pdfs-gui/src/app/main.rs:768
#: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:168
#: crates/pdfs-gui/src/app/pages/shared.rs:482 #: crates/pdfs-gui/src/app/pages/shared.rs:482
msgid "Shared with me" msgid "Shared with me"
msgstr "Partagés avec moi" msgstr "Partagés avec moi"
#: crates/pdfs-gui/src/app/main.rs:756 #: crates/pdfs-gui/src/app/main.rs:773
#: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120
msgid "Shared by me" msgid "Shared by me"
msgstr "Partagés par moi" msgstr "Partagés par moi"
#: crates/pdfs-gui/src/app/main.rs:759 #: crates/pdfs-gui/src/app/main.rs:776
#: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:140
#: crates/pdfs-gui/src/app/pages/devices.rs:481 #: crates/pdfs-gui/src/app/pages/devices.rs:481
msgid "Computers" msgid "Computers"
msgstr "Ordinateurs" msgstr "Ordinateurs"
#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/main.rs:777
#: crates/pdfs-gui/src/app/pages/trash.rs:101 #: crates/pdfs-gui/src/app/pages/trash.rs:101
msgid "Trash" msgid "Trash"
msgstr "Corbeille" msgstr "Corbeille"
#. Translators: sidebar entry for the page listing this computer's synced folders. #. Translators: sidebar entry for the page listing this computer's synced folders.
#: crates/pdfs-gui/src/app/main.rs:762 #: crates/pdfs-gui/src/app/main.rs:779
msgid "Sync" msgid "Sync"
msgstr "Synchronisation" msgstr "Synchronisation"
#. Translators: a failed action's toast. {what} says what failed, such as #. Translators: a failed action's toast. {what} says what failed, such as
#. "Couldn't rename"; {detail} is the reason, usually untranslated text #. "Couldn't rename"; {detail} is the reason, usually untranslated text
#. from the mount service. #. from the mount service.
#: crates/pdfs-gui/src/app/main.rs:1079 #: crates/pdfs-gui/src/app/main.rs:1096
#, rust-format #, rust-format
msgid "{what}: {detail}" msgid "{what}: {detail}"
msgstr "{what} : {detail}" msgstr "{what} : {detail}"
#: crates/pdfs-gui/src/app/main.rs:1098 #: crates/pdfs-gui/src/app/main.rs:1115
msgid "You're offline" msgid "You're offline"
msgstr "Vous êtes hors ligne" msgstr "Vous êtes hors ligne"
#: crates/pdfs-gui/src/app/main.rs:1099 #: crates/pdfs-gui/src/app/main.rs:1116
msgid "That's not there any more" msgid "That's not there any more"
msgstr "Cet élément n’existe plus" msgstr "Cet élément n’existe plus"
#: crates/pdfs-gui/src/app/main.rs:1100 #: crates/pdfs-gui/src/app/main.rs:1117
msgid "You don't have access to that" msgid "You don't have access to that"
msgstr "Vous n’avez pas accès à cet élément" msgstr "Vous n’avez pas accès à cet élément"
#: crates/pdfs-gui/src/app/main.rs:1101 #: crates/pdfs-gui/src/app/main.rs:1118
msgid "Something changed this first" msgid "Something changed this first"
msgstr "Quelque chose l’a modifié entre-temps" msgstr "Quelque chose l’a modifié entre-temps"
#: crates/pdfs-gui/src/app/main.rs:1102 #: crates/pdfs-gui/src/app/main.rs:1119
msgid "Your Proton Drive is full" msgid "Your Proton Drive is full"
msgstr "Votre Proton Drive est plein" msgstr "Votre Proton Drive est plein"
#: crates/pdfs-gui/src/app/main.rs:1143 crates/pdfs-gui/src/app/main.rs:1372 #: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389
#: crates/pdfs-gui/src/app/pages/status.rs:385 #: crates/pdfs-gui/src/app/pages/status.rs:385
msgid "Preferences" msgid "Preferences"
msgstr "Préférences" msgstr "Préférences"
#: crates/pdfs-gui/src/app/main.rs:1144 #: crates/pdfs-gui/src/app/main.rs:1161
#: crates/pdfs-gui/src/app/widgets/compat.rs:262 #: crates/pdfs-gui/src/app/widgets/compat.rs:262
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "Raccourcis clavier" msgstr "Raccourcis clavier"
#: crates/pdfs-gui/src/app/main.rs:1146 #: crates/pdfs-gui/src/app/main.rs:1163
msgid "About Proton Drive for Linux" msgid "About Proton Drive for Linux"
msgstr "À propos de Proton Drive for Linux" msgstr "À propos de Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1151 #: crates/pdfs-gui/src/app/main.rs:1168
msgid "Main menu" msgid "Main menu"
msgstr "Menu principal" msgstr "Menu principal"
#: crates/pdfs-gui/src/app/main.rs:1207 #: crates/pdfs-gui/src/app/main.rs:1224
msgid "Proton Drive for Linux" msgid "Proton Drive for Linux"
msgstr "Proton Drive for Linux" msgstr "Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1215 #: crates/pdfs-gui/src/app/main.rs:1232
msgid "" msgid ""
"Files-on-demand Proton Drive for the Linux desktop.\n" "Files-on-demand Proton Drive for the Linux desktop.\n"
"\n" "\n"
@@ -166,23 +166,23 @@ msgstr ""
"Client non officiel — non affilié à Proton AG, ni approuvé ni pris en charge " "Client non officiel — non affilié à Proton AG, ni approuvé ni pris en charge "
"par Proton AG." "par Proton AG."
#: crates/pdfs-gui/src/app/main.rs:1220 #: crates/pdfs-gui/src/app/main.rs:1237
msgid "Source Code" msgid "Source Code"
msgstr "Code source" msgstr "Code source"
#: crates/pdfs-gui/src/app/main.rs:1224 #: crates/pdfs-gui/src/app/main.rs:1241
msgid "Proton Terms of Service" msgid "Proton Terms of Service"
msgstr "Conditions d’utilisation de Proton" msgstr "Conditions d’utilisation de Proton"
#: crates/pdfs-gui/src/app/main.rs:1228 #: crates/pdfs-gui/src/app/main.rs:1245
msgid "Proton Privacy Policy" msgid "Proton Privacy Policy"
msgstr "Politique de confidentialité de Proton" msgstr "Politique de confidentialité de Proton"
#: crates/pdfs-gui/src/app/main.rs:1233 #: crates/pdfs-gui/src/app/main.rs:1250
msgid "Disclaimer" msgid "Disclaimer"
msgstr "Avertissement" msgstr "Avertissement"
#: crates/pdfs-gui/src/app/main.rs:1237 #: crates/pdfs-gui/src/app/main.rs:1254
msgid "" msgid ""
"This is an unofficial, community-built client. It is not affiliated with, " "This is an unofficial, community-built client. It is not affiliated with, "
"endorsed by, or supported by Proton AG.\n" "endorsed by, or supported by Proton AG.\n"
@@ -206,7 +206,7 @@ msgstr ""
"une copie de toutes les données que vous ne pouvez pas vous permettre de " "une copie de toutes les données que vous ne pouvez pas vous permettre de "
"perdre." "perdre."
#: crates/pdfs-gui/src/app/main.rs:1245 #: crates/pdfs-gui/src/app/main.rs:1262
msgid "" msgid ""
"Places are named with data from <a href=\"https://www.geonames.org/" "Places are named with data from <a href=\"https://www.geonames.org/"
"\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/"
@@ -216,7 +216,7 @@ msgstr ""
"www.geonames.org/\">GeoNames</a>, sous licence <a href=\"https://" "www.geonames.org/\">GeoNames</a>, sous licence <a href=\"https://"
"creativecommons.org/licenses/by/4.0/\">CC BY 4.0</a>." "creativecommons.org/licenses/by/4.0/\">CC BY 4.0</a>."
#: crates/pdfs-gui/src/app/main.rs:1253 #: crates/pdfs-gui/src/app/main.rs:1270
msgid "" msgid ""
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/" "The places map is drawn from <a href=\"https://www.naturalearthdata.com/"
"\">Natural Earth</a> land outlines and country borders, which are in the " "\">Natural Earth</a> land outlines and country borders, which are in the "
@@ -226,7 +226,7 @@ msgstr ""
"frontières des pays de <a href=\"https://www.naturalearthdata.com/\">Natural " "frontières des pays de <a href=\"https://www.naturalearthdata.com/\">Natural "
"Earth</a>, qui sont dans le domaine public." "Earth</a>, qui sont dans le domaine public."
#: crates/pdfs-gui/src/app/main.rs:1261 #: crates/pdfs-gui/src/app/main.rs:1278
msgid "" msgid ""
"The street map, when turned on, comes from <a href=\"https://openfreemap.org/" "The street map, when turned on, comes from <a href=\"https://openfreemap.org/"
"\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/" "\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/"
@@ -243,15 +243,15 @@ msgstr ""
#. Translators: replace with your name(s) and email address(es), one #. Translators: replace with your name(s) and email address(es), one
#. per line, to be credited in the About dialog. #. per line, to be credited in the About dialog.
#: crates/pdfs-gui/src/app/main.rs:1272 #: crates/pdfs-gui/src/app/main.rs:1289
msgid "translator-credits" msgid "translator-credits"
msgstr "Les contributeurs de Proton Drive for Linux" msgstr "Les contributeurs de Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1310 #: crates/pdfs-gui/src/app/main.rs:1327
msgid "Stop Proton Drive?" msgid "Stop Proton Drive?"
msgstr "Arrêter Proton Drive ?" msgstr "Arrêter Proton Drive ?"
#: crates/pdfs-gui/src/app/main.rs:1311 #: crates/pdfs-gui/src/app/main.rs:1328
msgid "" msgid ""
"My files goes offline and nothing syncs until you connect again. Proton " "My files goes offline and nothing syncs until you connect again. Proton "
"Drive starts again at your next login." "Drive starts again at your next login."
@@ -260,7 +260,7 @@ msgstr ""
"vous reconnectez pas. Proton Drive redémarrera à votre prochaine ouverture " "vous reconnectez pas. Proton Drive redémarrera à votre prochaine ouverture "
"de session." "de session."
#: crates/pdfs-gui/src/app/main.rs:1313 crates/pdfs-gui/src/app/main.rs:1588 #: crates/pdfs-gui/src/app/main.rs:1330 crates/pdfs-gui/src/app/main.rs:1605
#: crates/pdfs-gui/src/app/pages/albums.rs:243 #: crates/pdfs-gui/src/app/pages/albums.rs:243
#: crates/pdfs-gui/src/app/pages/albums.rs:428 #: crates/pdfs-gui/src/app/pages/albums.rs:428
#: crates/pdfs-gui/src/app/pages/browser.rs:1892 #: crates/pdfs-gui/src/app/pages/browser.rs:1892
@@ -275,7 +275,8 @@ msgstr ""
#: crates/pdfs-gui/src/app/pages/locations.rs:1013 #: crates/pdfs-gui/src/app/pages/locations.rs:1013
#: crates/pdfs-gui/src/app/pages/locations.rs:1488 #: crates/pdfs-gui/src/app/pages/locations.rs:1488
#: crates/pdfs-gui/src/app/pages/locations.rs:1538 #: crates/pdfs-gui/src/app/pages/locations.rs:1538
#: crates/pdfs-gui/src/app/pages/login.rs:312 #: crates/pdfs-gui/src/app/pages/login.rs:393
#: crates/pdfs-gui/src/app/pages/login.rs:507
#: crates/pdfs-gui/src/app/pages/photos.rs:2055 #: crates/pdfs-gui/src/app/pages/photos.rs:2055
#: crates/pdfs-gui/src/app/pages/shared.rs:946 #: crates/pdfs-gui/src/app/pages/shared.rs:946
#: crates/pdfs-gui/src/app/pages/shared.rs:1027 #: crates/pdfs-gui/src/app/pages/shared.rs:1027
@@ -290,30 +291,30 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Annuler" msgstr "Annuler"
#: crates/pdfs-gui/src/app/main.rs:1314 #: crates/pdfs-gui/src/app/main.rs:1331
msgctxt "verb" msgctxt "verb"
msgid "Stop" msgid "Stop"
msgstr "Arrêter" msgstr "Arrêter"
#: crates/pdfs-gui/src/app/main.rs:1331 #: crates/pdfs-gui/src/app/main.rs:1348
msgid "Couldn't stop Proton Drive" msgid "Couldn't stop Proton Drive"
msgstr "Impossible d’arrêter Proton Drive" msgstr "Impossible d’arrêter Proton Drive"
#: crates/pdfs-gui/src/app/main.rs:1332 #: crates/pdfs-gui/src/app/main.rs:1349
msgid "The Proton Drive service did not stop." msgid "The Proton Drive service did not stop."
msgstr "Le service Proton Drive ne s’est pas arrêté." msgstr "Le service Proton Drive ne s’est pas arrêté."
#: crates/pdfs-gui/src/app/main.rs:1368 #: crates/pdfs-gui/src/app/main.rs:1385
#: crates/pdfs-gui/src/app/pages/status.rs:254 #: crates/pdfs-gui/src/app/pages/status.rs:254
msgid "General" msgid "General"
msgstr "Général" msgstr "Général"
#: crates/pdfs-gui/src/app/main.rs:1370 #: crates/pdfs-gui/src/app/main.rs:1387
#: crates/pdfs-gui/src/app/pages/browser.rs:247 #: crates/pdfs-gui/src/app/pages/browser.rs:247
msgid "Search Drive" msgid "Search Drive"
msgstr "Rechercher dans Drive" msgstr "Rechercher dans Drive"
#: crates/pdfs-gui/src/app/main.rs:1371 #: crates/pdfs-gui/src/app/main.rs:1388
#: crates/pdfs-gui/src/app/pages/browser.rs:957 #: crates/pdfs-gui/src/app/pages/browser.rs:957
#: crates/pdfs-gui/src/app/pages/devices.rs:554 #: crates/pdfs-gui/src/app/pages/devices.rs:554
#: crates/pdfs-gui/src/app/pages/shared.rs:586 #: crates/pdfs-gui/src/app/pages/shared.rs:586
@@ -322,38 +323,38 @@ msgstr "Rechercher dans Drive"
msgid "Refresh" msgid "Refresh"
msgstr "Actualiser" msgstr "Actualiser"
#: crates/pdfs-gui/src/app/main.rs:1373 #: crates/pdfs-gui/src/app/main.rs:1390
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "Raccourcis clavier" msgstr "Raccourcis clavier"
#: crates/pdfs-gui/src/app/main.rs:1377 #: crates/pdfs-gui/src/app/main.rs:1394
msgid "Files" msgid "Files"
msgstr "Fichiers" msgstr "Fichiers"
#: crates/pdfs-gui/src/app/main.rs:1379 #: crates/pdfs-gui/src/app/main.rs:1396
#: crates/pdfs-gui/src/app/pages/browser.rs:294 #: crates/pdfs-gui/src/app/pages/browser.rs:294
msgid "New folder" msgid "New folder"
msgstr "Nouveau dossier" msgstr "Nouveau dossier"
#: crates/pdfs-gui/src/app/main.rs:1380 #: crates/pdfs-gui/src/app/main.rs:1397
#: crates/pdfs-gui/src/app/pages/browser.rs:290 #: crates/pdfs-gui/src/app/pages/browser.rs:290
msgid "Upload files" msgid "Upload files"
msgstr "Téléverser des fichiers" msgstr "Téléverser des fichiers"
#: crates/pdfs-gui/src/app/main.rs:1381 #: crates/pdfs-gui/src/app/main.rs:1398
msgid "Back / forward" msgid "Back / forward"
msgstr "Précédent / suivant" msgstr "Précédent / suivant"
#: crates/pdfs-gui/src/app/main.rs:1382 #: crates/pdfs-gui/src/app/main.rs:1399
#: crates/pdfs-gui/src/app/pages/browser.rs:1943 #: crates/pdfs-gui/src/app/pages/browser.rs:1943
msgid "Parent folder" msgid "Parent folder"
msgstr "Dossier parent" msgstr "Dossier parent"
#: crates/pdfs-gui/src/app/main.rs:1383 #: crates/pdfs-gui/src/app/main.rs:1400
msgid "Grid / list" msgid "Grid / list"
msgstr "Grille / liste" msgstr "Grille / liste"
#: crates/pdfs-gui/src/app/main.rs:1384 #: crates/pdfs-gui/src/app/main.rs:1401
#: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/albums.rs:298
#: crates/pdfs-gui/src/app/pages/browser.rs:1871 #: crates/pdfs-gui/src/app/pages/browser.rs:1871
#: crates/pdfs-gui/src/app/pages/browser.rs:1893 #: crates/pdfs-gui/src/app/pages/browser.rs:1893
@@ -362,7 +363,7 @@ msgstr "Grille / liste"
msgid "Rename" msgid "Rename"
msgstr "Renommer" msgstr "Renommer"
#: crates/pdfs-gui/src/app/main.rs:1385 crates/pdfs-gui/src/app/main.rs:1405 #: crates/pdfs-gui/src/app/main.rs:1402 crates/pdfs-gui/src/app/main.rs:1422
#: crates/pdfs-gui/src/app/pages/browser.rs:357 #: crates/pdfs-gui/src/app/pages/browser.rs:357
#: crates/pdfs-gui/src/app/pages/browser.rs:900 #: crates/pdfs-gui/src/app/pages/browser.rs:900
#: crates/pdfs-gui/src/app/pages/browser.rs:932 #: crates/pdfs-gui/src/app/pages/browser.rs:932
@@ -372,73 +373,73 @@ msgstr "Renommer"
msgid "Move to Trash" msgid "Move to Trash"
msgstr "Mettre à la corbeille" msgstr "Mettre à la corbeille"
#: crates/pdfs-gui/src/app/main.rs:1386 #: crates/pdfs-gui/src/app/main.rs:1403
msgid "Clear the selection" msgid "Clear the selection"
msgstr "Effacer la sélection" msgstr "Effacer la sélection"
#: crates/pdfs-gui/src/app/main.rs:1392 #: crates/pdfs-gui/src/app/main.rs:1409
msgid "Larger thumbnails" msgid "Larger thumbnails"
msgstr "Miniatures plus grandes" msgstr "Miniatures plus grandes"
#: crates/pdfs-gui/src/app/main.rs:1393 #: crates/pdfs-gui/src/app/main.rs:1410
msgid "Smaller thumbnails" msgid "Smaller thumbnails"
msgstr "Miniatures plus petites" msgstr "Miniatures plus petites"
#: crates/pdfs-gui/src/app/main.rs:1394 #: crates/pdfs-gui/src/app/main.rs:1411
msgid "Reset thumbnail size" msgid "Reset thumbnail size"
msgstr "Réinitialiser la taille des miniatures" msgstr "Réinitialiser la taille des miniatures"
#: crates/pdfs-gui/src/app/main.rs:1395 #: crates/pdfs-gui/src/app/main.rs:1412
msgid "Select all" msgid "Select all"
msgstr "Tout sélectionner" msgstr "Tout sélectionner"
#: crates/pdfs-gui/src/app/main.rs:1399 #: crates/pdfs-gui/src/app/main.rs:1416
msgid "Photo Viewer" msgid "Photo Viewer"
msgstr "Visionneuse de photos" msgstr "Visionneuse de photos"
#: crates/pdfs-gui/src/app/main.rs:1401 #: crates/pdfs-gui/src/app/main.rs:1418
msgid "Previous / next photo" msgid "Previous / next photo"
msgstr "Photo précédente / suivante" msgstr "Photo précédente / suivante"
#: crates/pdfs-gui/src/app/main.rs:1402 #: crates/pdfs-gui/src/app/main.rs:1419
msgid "First / last photo" msgid "First / last photo"
msgstr "Première / dernière photo" msgstr "Première / dernière photo"
#: crates/pdfs-gui/src/app/main.rs:1403 #: crates/pdfs-gui/src/app/main.rs:1420
msgid "Show details" msgid "Show details"
msgstr "Afficher les détails" msgstr "Afficher les détails"
#: crates/pdfs-gui/src/app/main.rs:1404 #: crates/pdfs-gui/src/app/main.rs:1421
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Plein écran" msgstr "Plein écran"
#: crates/pdfs-gui/src/app/main.rs:1406 #: crates/pdfs-gui/src/app/main.rs:1423
msgid "Close" msgid "Close"
msgstr "Fermer" msgstr "Fermer"
#: crates/pdfs-gui/src/app/main.rs:1496 #: crates/pdfs-gui/src/app/main.rs:1513
msgid "Select a single item to rename it" msgid "Select a single item to rename it"
msgstr "Sélectionnez un seul élément pour le renommer" msgstr "Sélectionnez un seul élément pour le renommer"
#: crates/pdfs-gui/src/app/main.rs:1656 #: crates/pdfs-gui/src/app/main.rs:1673
#: crates/pdfs-gui/src/app/pages/albums.rs:85 #: crates/pdfs-gui/src/app/pages/albums.rs:85
#: crates/pdfs-gui/src/app/pages/places.rs:104 #: crates/pdfs-gui/src/app/pages/places.rs:104
#: crates/pdfs-gui/src/app/pages/status.rs:1370 #: crates/pdfs-gui/src/app/pages/status.rs:1371
msgid "Not connected" msgid "Not connected"
msgstr "Non connecté" msgstr "Non connecté"
#: crates/pdfs-gui/src/app/main.rs:1657 #: crates/pdfs-gui/src/app/main.rs:1674
msgid "The Proton Drive service isn't running." msgid "The Proton Drive service isn't running."
msgstr "Le service Proton Drive n’est pas en cours d’exécution." msgstr "Le service Proton Drive n’est pas en cours d’exécution."
#: crates/pdfs-gui/src/app/main.rs:1665 #: crates/pdfs-gui/src/app/main.rs:1682
#: crates/pdfs-gui/src/app/pages/status.rs:336 #: crates/pdfs-gui/src/app/pages/status.rs:336
#: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595 #: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595
#: crates/pdfs-gui/src/main.rs:444 #: crates/pdfs-gui/src/main.rs:453
msgid "Connecting…" msgid "Connecting…"
msgstr "Connexion…" msgstr "Connexion…"
#: crates/pdfs-gui/src/app/main.rs:1666 #: crates/pdfs-gui/src/app/main.rs:1683
msgid "Waiting for the Proton Drive service to start." msgid "Waiting for the Proton Drive service to start."
msgstr "En attente du démarrage du service Proton Drive." msgstr "En attente du démarrage du service Proton Drive."
@@ -1292,7 +1293,7 @@ msgstr "Impossible de charger les albums"
#: crates/pdfs-gui/src/app/pages/shared.rs:1006 #: crates/pdfs-gui/src/app/pages/shared.rs:1006
#: crates/pdfs-gui/src/app/pages/shared.rs:1122 #: crates/pdfs-gui/src/app/pages/shared.rs:1122
#: crates/pdfs-gui/src/app/pages/status.rs:750 #: crates/pdfs-gui/src/app/pages/status.rs:750
#: crates/pdfs-gui/src/app/pages/status.rs:1517 #: crates/pdfs-gui/src/app/pages/status.rs:1518
#: crates/pdfs-gui/src/app/pages/takeout.rs:424 #: crates/pdfs-gui/src/app/pages/takeout.rs:424
#: crates/pdfs-gui/src/app/pages/takeout.rs:617 #: crates/pdfs-gui/src/app/pages/takeout.rs:617
#: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438
@@ -1560,7 +1561,7 @@ msgid "Make available offline"
msgstr "Rendre disponible hors ligne" msgstr "Rendre disponible hors ligne"
#: crates/pdfs-gui/src/app/pages/browser.rs:347 #: crates/pdfs-gui/src/app/pages/browser.rs:347
#: crates/pdfs-gui/src/app/pages/status.rs:1493 #: crates/pdfs-gui/src/app/pages/status.rs:1494
msgid "Make online only" msgid "Make online only"
msgstr "Garder en ligne uniquement" msgstr "Garder en ligne uniquement"
@@ -2724,7 +2725,7 @@ msgid "Sync paused until you resume"
msgstr "Synchronisation en pause jusqu’à la reprise" msgstr "Synchronisation en pause jusqu’à la reprise"
#: crates/pdfs-gui/src/app/pages/locations.rs:529 #: crates/pdfs-gui/src/app/pages/locations.rs:529
#: crates/pdfs-gui/src/app/pages/status.rs:1323 crates/pdfs-gui/src/main.rs:237 #: crates/pdfs-gui/src/app/pages/status.rs:1324 crates/pdfs-gui/src/main.rs:246
msgid "Sync paused" msgid "Sync paused"
msgstr "Synchronisation en pause" msgstr "Synchronisation en pause"
@@ -3200,25 +3201,35 @@ msgstr "Saisissez votre adresse électronique et votre mot de passe."
msgid "Signing in…" msgid "Signing in…"
msgstr "Connexion…" msgstr "Connexion…"
#: crates/pdfs-gui/src/app/pages/login.rs:155 #: crates/pdfs-gui/src/app/pages/login.rs:164
msgid "Complete the verification to continue…" msgid "Complete the verification to continue…"
msgstr "Terminez la vérification pour continuer…" msgstr "Terminez la vérification pour continuer…"
#: crates/pdfs-gui/src/app/pages/login.rs:165 #: crates/pdfs-gui/src/app/pages/login.rs:197
msgid "login cancelled" msgid "login cancelled"
msgstr "connexion annulée" msgstr "connexion annulée"
#: crates/pdfs-gui/src/app/pages/login.rs:198 #: crates/pdfs-gui/src/app/pages/login.rs:210
msgid ""
"The verification page couldn't be opened, so the sign-in was cancelled. Your "
"system may keep the web view's sandbox from starting; the troubleshooting "
"guide explains what to do."
msgstr ""
"La page de vérification n’a pas pu s’ouvrir, la connexion a donc été "
"annulée. Votre système empêche peut-être le démarrage du bac à sable de la "
"vue web ; le guide de dépannage explique la marche à suivre."
#: crates/pdfs-gui/src/app/pages/login.rs:233
msgid "Too many sign-in attempts. Wait a few minutes and try again." msgid "Too many sign-in attempts. Wait a few minutes and try again."
msgstr "" msgstr ""
"Trop de tentatives de connexion. Patientez quelques minutes et réessayez." "Trop de tentatives de connexion. Patientez quelques minutes et réessayez."
#: crates/pdfs-gui/src/app/pages/login.rs:204 #: crates/pdfs-gui/src/app/pages/login.rs:239
msgid "Couldn't reach Proton. Check your internet connection and try again." msgid "Couldn't reach Proton. Check your internet connection and try again."
msgstr "" msgstr ""
"Impossible de joindre Proton. Vérifiez votre connexion Internet et réessayez." "Impossible de joindre Proton. Vérifiez votre connexion Internet et réessayez."
#: crates/pdfs-gui/src/app/pages/login.rs:207 #: crates/pdfs-gui/src/app/pages/login.rs:242
msgid "" msgid ""
"Signed in, but the session couldn't be saved to the system keyring. Make " "Signed in, but the session couldn't be saved to the system keyring. Make "
"sure a keyring (GNOME Keyring or KWallet) is running and unlocked." "sure a keyring (GNOME Keyring or KWallet) is running and unlocked."
@@ -3227,11 +3238,21 @@ msgstr ""
"système. Assurez-vous qu’un trousseau (GNOME Keyring ou KWallet) est en " "système. Assurez-vous qu’un trousseau (GNOME Keyring ou KWallet) est en "
"cours d’exécution et déverrouillé." "cours d’exécution et déverrouillé."
#: crates/pdfs-gui/src/app/pages/login.rs:210 #: crates/pdfs-gui/src/app/pages/login.rs:245
msgid "Sign-in cancelled: no two-factor code was entered." msgid "Sign-in cancelled: no two-factor code was entered."
msgstr "Connexion annulée : aucun code à deux facteurs n’a été saisi." msgstr "Connexion annulée : aucun code à deux facteurs n’a été saisi."
#: crates/pdfs-gui/src/app/pages/login.rs:213 #: crates/pdfs-gui/src/app/pages/login.rs:248
msgid "Sign-in cancelled: no mailbox password was entered."
msgstr ""
"Connexion annulée : aucun mot de passe de boîte aux lettres n’a été saisi."
#: crates/pdfs-gui/src/app/pages/login.rs:251
msgid "That mailbox password didn't unlock your account."
msgstr ""
"Ce mot de passe de boîte aux lettres n’a pas déverrouillé votre compte."
#: crates/pdfs-gui/src/app/pages/login.rs:254
msgid "" msgid ""
"Sign-in cancelled: the verification wasn't completed. Sign in again to get a " "Sign-in cancelled: the verification wasn't completed. Sign in again to get a "
"new one." "new one."
@@ -3240,35 +3261,88 @@ msgstr ""
"pour en obtenir une nouvelle." "pour en obtenir une nouvelle."
#. Translators: {error} is an error message, usually in English. #. Translators: {error} is an error message, usually in English.
#: crates/pdfs-gui/src/app/pages/login.rs:218 #: crates/pdfs-gui/src/app/pages/login.rs:259
#, rust-format #, rust-format
msgid "Sign-in failed: {error}" msgid "Sign-in failed: {error}"
msgstr "Échec de la connexion : {error}" msgstr "Échec de la connexion : {error}"
#: crates/pdfs-gui/src/app/pages/login.rs:298 #: crates/pdfs-gui/src/app/pages/login.rs:369
msgid "That mailbox password didn't unlock your account. Try again."
msgstr ""
"Ce mot de passe de boîte aux lettres n’a pas déverrouillé votre compte. "
"Réessayez."
#: crates/pdfs-gui/src/app/pages/login.rs:371
msgid ""
"This account has a separate mailbox password. Enter it to unlock your files."
msgstr ""
"Ce compte a un mot de passe de boîte aux lettres distinct. Saisissez-le pour "
"déverrouiller vos fichiers."
#: crates/pdfs-gui/src/app/pages/login.rs:374
msgid "Mailbox Password"
msgstr "Mot de passe de la boîte aux lettres"
#: crates/pdfs-gui/src/app/pages/login.rs:380
msgid "Mailbox password"
msgstr "Mot de passe de la boîte aux lettres"
#: crates/pdfs-gui/src/app/pages/login.rs:384
msgid "Keep unlocked on this computer"
msgstr "Garder déverrouillé sur cet ordinateur"
#: crates/pdfs-gui/src/app/pages/login.rs:386
msgid ""
"Stores the unlock in the system keyring, so Proton Drive opens without "
"asking after every login. Otherwise you enter the password once per session."
msgstr ""
"Enregistre le déverrouillage dans le trousseau de clés du système, afin que "
"Proton Drive s’ouvre sans demande après chaque connexion. Sinon, vous "
"saisissez le mot de passe une fois par session."
#: crates/pdfs-gui/src/app/pages/login.rs:394
#: crates/pdfs-gui/src/app/pages/login.rs:447
msgid "Unlock"
msgstr "Déverrouiller"
#: crates/pdfs-gui/src/app/pages/login.rs:446
msgid "Proton Drive is locked. Enter your mailbox password to unlock it."
msgstr ""
"Proton Drive est verrouillé. Saisissez votre mot de passe de boîte aux "
"lettres pour le déverrouiller."
#: crates/pdfs-gui/src/app/pages/login.rs:475
msgid "Unlocked."
msgstr "Déverrouillé."
#: crates/pdfs-gui/src/app/pages/login.rs:480
msgid "Couldn't unlock"
msgstr "Impossible de déverrouiller"
#: crates/pdfs-gui/src/app/pages/login.rs:493
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "Authentification à deux facteurs" msgstr "Authentification à deux facteurs"
#: crates/pdfs-gui/src/app/pages/login.rs:300 #: crates/pdfs-gui/src/app/pages/login.rs:495
msgid "" msgid ""
"Enter the code from your authenticator app, or one of your recovery codes." "Enter the code from your authenticator app, or one of your recovery codes."
msgstr "" msgstr ""
"Saisissez le code de votre application d’authentification ou l’un de vos " "Saisissez le code de votre application d’authentification ou l’un de vos "
"codes de récupération." "codes de récupération."
#: crates/pdfs-gui/src/app/pages/login.rs:306 #: crates/pdfs-gui/src/app/pages/login.rs:501
msgid "Authentication code" msgid "Authentication code"
msgstr "Code d’authentification" msgstr "Code d’authentification"
#: crates/pdfs-gui/src/app/pages/login.rs:313 #: crates/pdfs-gui/src/app/pages/login.rs:508
msgid "Confirm" msgid "Confirm"
msgstr "Confirmer" msgstr "Confirmer"
#: crates/pdfs-gui/src/app/pages/login.rs:341 #: crates/pdfs-gui/src/app/pages/login.rs:536
msgid "Sign Out?" msgid "Sign Out?"
msgstr "Se déconnecter ?" msgstr "Se déconnecter ?"
#: crates/pdfs-gui/src/app/pages/login.rs:343 #: crates/pdfs-gui/src/app/pages/login.rs:538
msgid "" msgid ""
"Proton Drive will disconnect and stop syncing until you sign in again. Files " "Proton Drive will disconnect and stop syncing until you sign in again. Files "
"already on this computer stay where they are." "already on this computer stay where they are."
@@ -3277,7 +3351,7 @@ msgstr ""
"vous vous reconnectiez. Les fichiers déjà présents sur cet ordinateur " "vous vous reconnectiez. Les fichiers déjà présents sur cet ordinateur "
"restent en place." "restent en place."
#: crates/pdfs-gui/src/app/pages/login.rs:345 #: crates/pdfs-gui/src/app/pages/login.rs:540
msgid "Sign Out" msgid "Sign Out"
msgstr "Se déconnecter" msgstr "Se déconnecter"
@@ -4643,40 +4717,40 @@ msgid "Restart Now"
msgstr "Redémarrer maintenant" msgstr "Redémarrer maintenant"
#. Translators: {size} is an amount of storage, such as "1.2 GiB". #. Translators: {size} is an amount of storage, such as "1.2 GiB".
#: crates/pdfs-gui/src/app/pages/status.rs:912 #: crates/pdfs-gui/src/app/pages/status.rs:913
#, rust-format #, rust-format
msgid "{size} used" msgid "{size} used"
msgstr "{size} utilisés" msgstr "{size} utilisés"
#: crates/pdfs-gui/src/app/pages/status.rs:923 #: crates/pdfs-gui/src/app/pages/status.rs:924
#, rust-format #, rust-format
msgid "{used} of {total} used ({percent}%)" msgid "{used} of {total} used ({percent}%)"
msgstr "{used} utilisés sur {total} ({percent} %)" msgstr "{used} utilisés sur {total} ({percent} %)"
#: crates/pdfs-gui/src/app/pages/status.rs:961 #: crates/pdfs-gui/src/app/pages/status.rs:962
msgid "Proton Drive connected" msgid "Proton Drive connected"
msgstr "Proton Drive connecté" msgstr "Proton Drive connecté"
#: crates/pdfs-gui/src/app/pages/status.rs:962 #: crates/pdfs-gui/src/app/pages/status.rs:963
msgid "Your files are available." msgid "Your files are available."
msgstr "Vos fichiers sont disponibles." msgstr "Vos fichiers sont disponibles."
#: crates/pdfs-gui/src/app/pages/status.rs:967 #: crates/pdfs-gui/src/app/pages/status.rs:968
msgid "Proton Drive disconnected" msgid "Proton Drive disconnected"
msgstr "Proton Drive déconnecté" msgstr "Proton Drive déconnecté"
#: crates/pdfs-gui/src/app/pages/status.rs:968 #: crates/pdfs-gui/src/app/pages/status.rs:969
msgid "" msgid ""
"The Proton Drive service stopped. Files aren't available until it restarts." "The Proton Drive service stopped. Files aren't available until it restarts."
msgstr "" msgstr ""
"Le service Proton Drive s’est arrêté. Les fichiers ne sont pas disponibles " "Le service Proton Drive s’est arrêté. Les fichiers ne sont pas disponibles "
"tant qu’il n’a pas redémarré." "tant qu’il n’a pas redémarré."
#: crates/pdfs-gui/src/app/pages/status.rs:1037 #: crates/pdfs-gui/src/app/pages/status.rs:1038
msgid "Sync complete" msgid "Sync complete"
msgstr "Synchronisation terminée" msgstr "Synchronisation terminée"
#: crates/pdfs-gui/src/app/pages/status.rs:1039 #: crates/pdfs-gui/src/app/pages/status.rs:1040
#, rust-format #, rust-format
msgid "{n} file finished transferring." msgid "{n} file finished transferring."
msgid_plural "{n} files finished transferring." msgid_plural "{n} files finished transferring."
@@ -4685,27 +4759,27 @@ msgstr[1] "Le transfert de {n} fichiers est terminé."
#. Translators: a background job's progress. {title} is the job, such as #. Translators: a background job's progress. {title} is the job, such as
#. "Scanning Photos"; {done} and {total} are counts. #. "Scanning Photos"; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1117 #: crates/pdfs-gui/src/app/pages/status.rs:1118
#, rust-format #, rust-format
msgid "{title} — {done} of {total}" msgid "{title} — {done} of {total}"
msgstr "{title} — {done} sur {total}" msgstr "{title} — {done} sur {total}"
#. Translators: a background job's progress. {title} is the job, {detail} #. Translators: a background job's progress. {title} is the job, {detail}
#. what it is working on; {done} and {total} are counts. #. what it is working on; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1120 #: crates/pdfs-gui/src/app/pages/status.rs:1121
#, rust-format #, rust-format
msgid "{title} — {detail} ({done} of {total})" msgid "{title} — {detail} ({done} of {total})"
msgstr "{title} — {detail} ({done} sur {total})" msgstr "{title} — {detail} ({done} sur {total})"
#. Translators: a background job still running. {title} is the job. #. Translators: a background job still running. {title} is the job.
#: crates/pdfs-gui/src/app/pages/status.rs:1122 #: crates/pdfs-gui/src/app/pages/status.rs:1123
#, rust-format #, rust-format
msgid "{title}…" msgid "{title}…"
msgstr "{title}…" msgstr "{title}…"
#. Translators: a background job still running. {title} is the job, #. Translators: a background job still running. {title} is the job,
#. {detail} what it is working on. #. {detail} what it is working on.
#: crates/pdfs-gui/src/app/pages/status.rs:1125 #: crates/pdfs-gui/src/app/pages/status.rs:1126
#, rust-format #, rust-format
msgid "{title} — {detail}…" msgid "{title} — {detail}…"
msgstr "{title} — {detail}…" msgstr "{title} — {detail}…"
@@ -4713,7 +4787,7 @@ msgstr "{title} — {detail}…"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} the size moved so far and {speed} a size such as #. file name, {done} the size moved so far and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1154 #: crates/pdfs-gui/src/app/pages/status.rs:1155
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} ({speed}/s)" msgid "{arrow} {name} — {done} ({speed}/s)"
msgstr "{arrow} {name} — {done} ({speed}/s)" msgstr "{arrow} {name} — {done} ({speed}/s)"
@@ -4721,119 +4795,119 @@ msgstr "{arrow} {name} — {done} ({speed}/s)"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} and {total} sizes and {speed} a size such as #. file name, {done} and {total} sizes and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1162 #: crates/pdfs-gui/src/app/pages/status.rs:1163
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} of {total} ({speed}/s)" msgid "{arrow} {name} — {done} of {total} ({speed}/s)"
msgstr "{arrow} {name} — {done} sur {total} ({speed}/s)" msgstr "{arrow} {name} — {done} sur {total} ({speed}/s)"
#: crates/pdfs-gui/src/app/pages/status.rs:1254 #: crates/pdfs-gui/src/app/pages/status.rs:1255
#, rust-format #, rust-format
msgid "{used} cached — no limit set" msgid "{used} cached — no limit set"
msgstr "{used} en cache — aucune limite définie" msgstr "{used} en cache — aucune limite définie"
#: crates/pdfs-gui/src/app/pages/status.rs:1260 #: crates/pdfs-gui/src/app/pages/status.rs:1261
#, rust-format #, rust-format
msgid "{used} of {limit} used" msgid "{used} of {limit} used"
msgstr "{used} utilisés sur {limit}" msgstr "{used} utilisés sur {limit}"
#: crates/pdfs-gui/src/app/pages/status.rs:1328 #: crates/pdfs-gui/src/app/pages/status.rs:1329
#, rust-format #, rust-format
msgid "{queued} · resumes {time}" msgid "{queued} · resumes {time}"
msgstr "{queued} · reprise {time}" msgstr "{queued} · reprise {time}"
#. Translators: {time} is when syncing resumes, such as "at 14:30" #. Translators: {time} is when syncing resumes, such as "at 14:30"
#. or "Tue 09:00". #. or "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1333 #: crates/pdfs-gui/src/app/pages/status.rs:1334
#, rust-format #, rust-format
msgid "Resumes {time}" msgid "Resumes {time}"
msgstr "Reprise {time}" msgstr "Reprise {time}"
#: crates/pdfs-gui/src/app/pages/status.rs:1335 #: crates/pdfs-gui/src/app/pages/status.rs:1336
msgid "Until you resume" msgid "Until you resume"
msgstr "Jusqu’à la reprise" msgstr "Jusqu’à la reprise"
#: crates/pdfs-gui/src/app/pages/status.rs:1342 crates/pdfs-gui/src/main.rs:239 #: crates/pdfs-gui/src/app/pages/status.rs:1343 crates/pdfs-gui/src/main.rs:248
#, rust-format #, rust-format
msgid "{n} change needs attention" msgid "{n} change needs attention"
msgid_plural "{n} changes need attention" msgid_plural "{n} changes need attention"
msgstr[0] "{n} modification requiert votre attention" msgstr[0] "{n} modification requiert votre attention"
msgstr[1] "{n} modifications requièrent votre attention" msgstr[1] "{n} modifications requièrent votre attention"
#: crates/pdfs-gui/src/app/pages/status.rs:1352 #: crates/pdfs-gui/src/app/pages/status.rs:1353
#: crates/pdfs-gui/src/app/widgets/details.rs:108 #: crates/pdfs-gui/src/app/widgets/details.rs:108
msgctxt "state" msgctxt "state"
msgid "Offline" msgid "Offline"
msgstr "Hors ligne" msgstr "Hors ligne"
#: crates/pdfs-gui/src/app/pages/status.rs:1353 #: crates/pdfs-gui/src/app/pages/status.rs:1354
msgid "Cached files only" msgid "Cached files only"
msgstr "Fichiers en cache uniquement" msgstr "Fichiers en cache uniquement"
#: crates/pdfs-gui/src/app/pages/status.rs:1358 #: crates/pdfs-gui/src/app/pages/status.rs:1359
msgctxt "state" msgctxt "state"
msgid "Syncing" msgid "Syncing"
msgstr "Synchronisation" msgstr "Synchronisation"
#: crates/pdfs-gui/src/app/pages/status.rs:1364 #: crates/pdfs-gui/src/app/pages/status.rs:1365
msgid "Up to date" msgid "Up to date"
msgstr "À jour" msgstr "À jour"
#: crates/pdfs-gui/src/app/pages/status.rs:1371 #: crates/pdfs-gui/src/app/pages/status.rs:1372
#: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622 #: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622
msgid "Proton Drive isn't running" msgid "Proton Drive isn't running"
msgstr "Proton Drive n’est pas en cours d’exécution" msgstr "Proton Drive n’est pas en cours d’exécution"
#: crates/pdfs-gui/src/app/pages/status.rs:1376 #: crates/pdfs-gui/src/app/pages/status.rs:1377
msgid "Not responding" msgid "Not responding"
msgstr "Ne répond pas" msgstr "Ne répond pas"
#: crates/pdfs-gui/src/app/pages/status.rs:1377 #: crates/pdfs-gui/src/app/pages/status.rs:1378
msgid "Proton Drive is taking too long to answer" msgid "Proton Drive is taking too long to answer"
msgstr "Proton Drive met trop de temps à répondre" msgstr "Proton Drive met trop de temps à répondre"
#. Translators: stands in for a time that could not be read, as in #. Translators: stands in for a time that could not be read, as in
#. "Resumes later". #. "Resumes later".
#: crates/pdfs-gui/src/app/pages/status.rs:1415 #: crates/pdfs-gui/src/app/pages/status.rs:1416
#: crates/pdfs-gui/src/app/pages/status.rs:1430 #: crates/pdfs-gui/src/app/pages/status.rs:1431
msgid "later" msgid "later"
msgstr "plus tard" msgstr "plus tard"
#. Translators: strftime format for a time later today, such as "at 14:30". #. Translators: strftime format for a time later today, such as "at 14:30".
#: crates/pdfs-gui/src/app/pages/status.rs:1419 #: crates/pdfs-gui/src/app/pages/status.rs:1420
msgid "at %H:%M" msgid "at %H:%M"
msgstr "à %H:%M" msgstr "à %H:%M"
#. Translators: strftime format for a time within the next week, such as #. Translators: strftime format for a time within the next week, such as
#. "Tue 09:00". #. "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1423 #: crates/pdfs-gui/src/app/pages/status.rs:1424
msgid "%a %H:%M" msgid "%a %H:%M"
msgstr "%a %H:%M" msgstr "%a %H:%M"
#. Translators: strftime format for a date further ahead, such as "23 Sep". #. Translators: strftime format for a date further ahead, such as "23 Sep".
#: crates/pdfs-gui/src/app/pages/status.rs:1426 #: crates/pdfs-gui/src/app/pages/status.rs:1427
msgid "%e %b" msgid "%e %b"
msgstr "%e %b" msgstr "%e %b"
#: crates/pdfs-gui/src/app/pages/status.rs:1456 #: crates/pdfs-gui/src/app/pages/status.rs:1457
msgid "No files kept offline" msgid "No files kept offline"
msgstr "Aucun fichier disponible hors ligne" msgstr "Aucun fichier disponible hors ligne"
#: crates/pdfs-gui/src/app/pages/status.rs:1458 #: crates/pdfs-gui/src/app/pages/status.rs:1459
msgid "Right-click a file and choose “Make available offline”." msgid "Right-click a file and choose “Make available offline”."
msgstr "" msgstr ""
"Faites un clic droit sur un fichier et choisissez « Rendre disponible hors " "Faites un clic droit sur un fichier et choisissez « Rendre disponible hors "
"ligne »." "ligne »."
#: crates/pdfs-gui/src/app/pages/status.rs:1509 #: crates/pdfs-gui/src/app/pages/status.rs:1510
#: crates/pdfs-gui/src/app/pages/status.rs:1516 #: crates/pdfs-gui/src/app/pages/status.rs:1517
msgid "Couldn't make it online only" msgid "Couldn't make it online only"
msgstr "Impossible de le garder en ligne uniquement" msgstr "Impossible de le garder en ligne uniquement"
#: crates/pdfs-gui/src/app/pages/status.rs:1533 #: crates/pdfs-gui/src/app/pages/status.rs:1534
msgid "Show fewer" msgid "Show fewer"
msgstr "Afficher moins" msgstr "Afficher moins"
#: crates/pdfs-gui/src/app/pages/status.rs:1536 #: crates/pdfs-gui/src/app/pages/status.rs:1537
#, rust-format #, rust-format
msgid "Show all {n} file" msgid "Show all {n} file"
msgid_plural "Show all {n} files" msgid_plural "Show all {n} files"
@@ -5264,7 +5338,7 @@ msgstr "Corbeille vidée"
msgid "Couldn't empty the trash" msgid "Couldn't empty the trash"
msgstr "Impossible de vider la corbeille" msgstr "Impossible de vider la corbeille"
#: crates/pdfs-gui/src/app/pages/verify.rs:57 #: crates/pdfs-gui/src/app/pages/verify.rs:162
msgid "Verification" msgid "Verification"
msgstr "Vérification" msgstr "Vérification"
@@ -5899,40 +5973,45 @@ msgstr[1] "↑ {n} en téléversement"
msgid "Proton Drive isn't responding" msgid "Proton Drive isn't responding"
msgstr "Proton Drive ne répond pas" msgstr "Proton Drive ne répond pas"
#: crates/pdfs-gui/src/main.rs:172 #: crates/pdfs-gui/src/main.rs:173
#, rust-format
msgid "Signed in as {username} — locked"
msgstr "Connecté en tant que {username} — verrouillé"
#: crates/pdfs-gui/src/main.rs:181
#, rust-format #, rust-format
msgid "Signed in as {username} — not connected" msgid "Signed in as {username} — not connected"
msgstr "Connecté en tant que {username} — non connecté" msgstr "Connecté en tant que {username} — non connecté"
#: crates/pdfs-gui/src/main.rs:177 #: crates/pdfs-gui/src/main.rs:186
msgid "Not signed in" msgid "Not signed in"
msgstr "Non connecté" msgstr "Non connecté"
#. Translators: {error} is an error message, in English. #. Translators: {error} is an error message, in English.
#: crates/pdfs-gui/src/main.rs:180 #: crates/pdfs-gui/src/main.rs:189
#, rust-format #, rust-format
msgid "Error: {error}" msgid "Error: {error}"
msgstr "Erreur : {error}" msgstr "Erreur : {error}"
#: crates/pdfs-gui/src/main.rs:201 #: crates/pdfs-gui/src/main.rs:210
msgid "Proton Drive sent an unexpected reply" msgid "Proton Drive sent an unexpected reply"
msgstr "Proton Drive a envoyé une réponse inattendue" msgstr "Proton Drive a envoyé une réponse inattendue"
#: crates/pdfs-gui/src/main.rs:246 #: crates/pdfs-gui/src/main.rs:255
#, rust-format #, rust-format
msgid "Up to date in {mountpoint} ({n} pinned)" msgid "Up to date in {mountpoint} ({n} pinned)"
msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)"
msgstr[0] "À jour dans {mountpoint} ({n} épinglé)" msgstr[0] "À jour dans {mountpoint} ({n} épinglé)"
msgstr[1] "À jour dans {mountpoint} ({n} épinglés)" msgstr[1] "À jour dans {mountpoint} ({n} épinglés)"
#: crates/pdfs-gui/src/main.rs:253 #: crates/pdfs-gui/src/main.rs:262
#, rust-format #, rust-format
msgid "Syncing — {queued} ({n} pinned)" msgid "Syncing — {queued} ({n} pinned)"
msgid_plural "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)"
msgstr[0] "Synchronisation — {queued} ({n} épinglé)" msgstr[0] "Synchronisation — {queued} ({n} épinglé)"
msgstr[1] "Synchronisation — {queued} ({n} épinglés)" msgstr[1] "Synchronisation — {queued} ({n} épinglés)"
#: crates/pdfs-gui/src/main.rs:260 #: crates/pdfs-gui/src/main.rs:269
#, rust-format #, rust-format
msgid "Offline — cached files only ({n} pinned)" msgid "Offline — cached files only ({n} pinned)"
msgid_plural "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)"
@@ -5940,52 +6019,52 @@ msgstr[0] "Hors ligne — fichiers en cache uniquement ({n} épinglé)"
msgstr[1] "Hors ligne — fichiers en cache uniquement ({n} épinglés)" msgstr[1] "Hors ligne — fichiers en cache uniquement ({n} épinglés)"
#. Translators: {queued} is a queue summary such as "3 uploads queued". #. Translators: {queued} is a queue summary such as "3 uploads queued".
#: crates/pdfs-gui/src/main.rs:266 #: crates/pdfs-gui/src/main.rs:275
#, rust-format #, rust-format
msgid "Offline — {queued}" msgid "Offline — {queued}"
msgstr "Hors ligne — {queued}" msgstr "Hors ligne — {queued}"
#. Translators: a menu item that opens the list of failing changes. #. Translators: a menu item that opens the list of failing changes.
#: crates/pdfs-gui/src/main.rs:296 #: crates/pdfs-gui/src/main.rs:305
#, rust-format #, rust-format
msgid "{n} issue — View" msgid "{n} issue — View"
msgid_plural "{n} issues — View" msgid_plural "{n} issues — View"
msgstr[0] "{n} problème — Afficher" msgstr[0] "{n} problème — Afficher"
msgstr[1] "{n} problèmes — Afficher" msgstr[1] "{n} problèmes — Afficher"
#: crates/pdfs-gui/src/main.rs:402 #: crates/pdfs-gui/src/main.rs:411
msgid "Sign In…" msgid "Sign In…"
msgstr "Se connecter…" msgstr "Se connecter…"
#: crates/pdfs-gui/src/main.rs:403 #: crates/pdfs-gui/src/main.rs:412
msgid "Open Proton Drive" msgid "Open Proton Drive"
msgstr "Ouvrir Proton Drive" msgstr "Ouvrir Proton Drive"
#: crates/pdfs-gui/src/main.rs:409 #: crates/pdfs-gui/src/main.rs:418
msgid "Open Folder" msgid "Open Folder"
msgstr "Ouvrir le dossier" msgstr "Ouvrir le dossier"
#: crates/pdfs-gui/src/main.rs:421 #: crates/pdfs-gui/src/main.rs:430
msgid "Resume Sync" msgid "Resume Sync"
msgstr "Reprendre la synchronisation" msgstr "Reprendre la synchronisation"
#: crates/pdfs-gui/src/main.rs:423 #: crates/pdfs-gui/src/main.rs:432
msgid "Pause Sync" msgid "Pause Sync"
msgstr "Mettre en pause la synchronisation" msgstr "Mettre en pause la synchronisation"
#: crates/pdfs-gui/src/main.rs:442 #: crates/pdfs-gui/src/main.rs:451
msgid "Connect" msgid "Connect"
msgstr "Connecter" msgstr "Connecter"
#: crates/pdfs-gui/src/main.rs:449 #: crates/pdfs-gui/src/main.rs:458
msgid "Hide Tray Icon" msgid "Hide Tray Icon"
msgstr "Masquer l’icône de la zone de notification" msgstr "Masquer l’icône de la zone de notification"
#: crates/pdfs-gui/src/main.rs:453 #: crates/pdfs-gui/src/main.rs:462
msgid "Stop Proton Drive…" msgid "Stop Proton Drive…"
msgstr "Arrêter Proton Drive…" msgstr "Arrêter Proton Drive…"
#: crates/pdfs-gui/src/main.rs:457 #: crates/pdfs-gui/src/main.rs:466
msgid "Quit" msgid "Quit"
msgstr "Quitter" msgstr "Quitter"
+221 -141
View File
@@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: proton-drive-linux 2.1.0\n" "Project-Id-Version: proton-drive-linux 2.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-10-05 01:01+0200\n" "POT-Creation-Date: 2026-10-08 10:15+0200\n"
"PO-Revision-Date: 2026-09-23 12:00+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n"
"Last-Translator: Proton Drive for Linux contributors\n" "Last-Translator: Proton Drive for Linux contributors\n"
"Language-Team: Italian\n" "Language-Team: Italian\n"
@@ -57,7 +57,7 @@ msgstr "%-d %b %Y"
msgid "%A, %B %-d, %Y, %H:%M" msgid "%A, %B %-d, %Y, %H:%M"
msgstr "%A %-d %B %Y, %H:%M" msgstr "%A %-d %B %Y, %H:%M"
#: crates/pdfs-gui/src/app/main.rs:743 #: crates/pdfs-gui/src/app/main.rs:760
#: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539
#: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:454
#: crates/pdfs-gui/src/app/pages/browser.rs:819 #: crates/pdfs-gui/src/app/pages/browser.rs:819
@@ -70,91 +70,91 @@ msgstr "%A %-d %B %Y, %H:%M"
msgid "My files" msgid "My files"
msgstr "I miei file" msgstr "I miei file"
#: crates/pdfs-gui/src/app/main.rs:746 crates/pdfs-gui/src/app/main.rs:1390 #: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407
#: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/albums.rs:638
#: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/photos.rs:842
#: crates/pdfs-gui/src/app/pages/places.rs:441 #: crates/pdfs-gui/src/app/pages/places.rs:441
msgid "Photos" msgid "Photos"
msgstr "Foto" msgstr "Foto"
#: crates/pdfs-gui/src/app/main.rs:751 #: crates/pdfs-gui/src/app/main.rs:768
#: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:168
#: crates/pdfs-gui/src/app/pages/shared.rs:482 #: crates/pdfs-gui/src/app/pages/shared.rs:482
msgid "Shared with me" msgid "Shared with me"
msgstr "Condivisi con me" msgstr "Condivisi con me"
#: crates/pdfs-gui/src/app/main.rs:756 #: crates/pdfs-gui/src/app/main.rs:773
#: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120
msgid "Shared by me" msgid "Shared by me"
msgstr "Condivisi da me" msgstr "Condivisi da me"
#: crates/pdfs-gui/src/app/main.rs:759 #: crates/pdfs-gui/src/app/main.rs:776
#: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:140
#: crates/pdfs-gui/src/app/pages/devices.rs:481 #: crates/pdfs-gui/src/app/pages/devices.rs:481
msgid "Computers" msgid "Computers"
msgstr "Computer" msgstr "Computer"
#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/main.rs:777
#: crates/pdfs-gui/src/app/pages/trash.rs:101 #: crates/pdfs-gui/src/app/pages/trash.rs:101
msgid "Trash" msgid "Trash"
msgstr "Cestino" msgstr "Cestino"
#. Translators: sidebar entry for the page listing this computer's synced folders. #. Translators: sidebar entry for the page listing this computer's synced folders.
#: crates/pdfs-gui/src/app/main.rs:762 #: crates/pdfs-gui/src/app/main.rs:779
msgid "Sync" msgid "Sync"
msgstr "Sincronizzazione" msgstr "Sincronizzazione"
#. Translators: a failed action's toast. {what} says what failed, such as #. Translators: a failed action's toast. {what} says what failed, such as
#. "Couldn't rename"; {detail} is the reason, usually untranslated text #. "Couldn't rename"; {detail} is the reason, usually untranslated text
#. from the mount service. #. from the mount service.
#: crates/pdfs-gui/src/app/main.rs:1079 #: crates/pdfs-gui/src/app/main.rs:1096
#, rust-format #, rust-format
msgid "{what}: {detail}" msgid "{what}: {detail}"
msgstr "{what}: {detail}" msgstr "{what}: {detail}"
#: crates/pdfs-gui/src/app/main.rs:1098 #: crates/pdfs-gui/src/app/main.rs:1115
msgid "You're offline" msgid "You're offline"
msgstr "Sei offline" msgstr "Sei offline"
#: crates/pdfs-gui/src/app/main.rs:1099 #: crates/pdfs-gui/src/app/main.rs:1116
msgid "That's not there any more" msgid "That's not there any more"
msgstr "Non esiste più" msgstr "Non esiste più"
#: crates/pdfs-gui/src/app/main.rs:1100 #: crates/pdfs-gui/src/app/main.rs:1117
msgid "You don't have access to that" msgid "You don't have access to that"
msgstr "Non hai accesso a questo elemento" msgstr "Non hai accesso a questo elemento"
#: crates/pdfs-gui/src/app/main.rs:1101 #: crates/pdfs-gui/src/app/main.rs:1118
msgid "Something changed this first" msgid "Something changed this first"
msgstr "È stato modificato prima da qualcos'altro" msgstr "È stato modificato prima da qualcos'altro"
#: crates/pdfs-gui/src/app/main.rs:1102 #: crates/pdfs-gui/src/app/main.rs:1119
msgid "Your Proton Drive is full" msgid "Your Proton Drive is full"
msgstr "Il tuo Proton Drive è pieno" msgstr "Il tuo Proton Drive è pieno"
#: crates/pdfs-gui/src/app/main.rs:1143 crates/pdfs-gui/src/app/main.rs:1372 #: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389
#: crates/pdfs-gui/src/app/pages/status.rs:385 #: crates/pdfs-gui/src/app/pages/status.rs:385
msgid "Preferences" msgid "Preferences"
msgstr "Preferenze" msgstr "Preferenze"
#: crates/pdfs-gui/src/app/main.rs:1144 #: crates/pdfs-gui/src/app/main.rs:1161
#: crates/pdfs-gui/src/app/widgets/compat.rs:262 #: crates/pdfs-gui/src/app/widgets/compat.rs:262
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "Scorciatoie da tastiera" msgstr "Scorciatoie da tastiera"
#: crates/pdfs-gui/src/app/main.rs:1146 #: crates/pdfs-gui/src/app/main.rs:1163
msgid "About Proton Drive for Linux" msgid "About Proton Drive for Linux"
msgstr "Informazioni su Proton Drive for Linux" msgstr "Informazioni su Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1151 #: crates/pdfs-gui/src/app/main.rs:1168
msgid "Main menu" msgid "Main menu"
msgstr "Menu principale" msgstr "Menu principale"
#: crates/pdfs-gui/src/app/main.rs:1207 #: crates/pdfs-gui/src/app/main.rs:1224
msgid "Proton Drive for Linux" msgid "Proton Drive for Linux"
msgstr "Proton Drive for Linux" msgstr "Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1215 #: crates/pdfs-gui/src/app/main.rs:1232
msgid "" msgid ""
"Files-on-demand Proton Drive for the Linux desktop.\n" "Files-on-demand Proton Drive for the Linux desktop.\n"
"\n" "\n"
@@ -165,23 +165,23 @@ msgstr ""
"\n" "\n"
"Client non ufficiale: non affiliato, approvato né supportato da Proton AG." "Client non ufficiale: non affiliato, approvato né supportato da Proton AG."
#: crates/pdfs-gui/src/app/main.rs:1220 #: crates/pdfs-gui/src/app/main.rs:1237
msgid "Source Code" msgid "Source Code"
msgstr "Codice sorgente" msgstr "Codice sorgente"
#: crates/pdfs-gui/src/app/main.rs:1224 #: crates/pdfs-gui/src/app/main.rs:1241
msgid "Proton Terms of Service" msgid "Proton Terms of Service"
msgstr "Termini di servizio di Proton" msgstr "Termini di servizio di Proton"
#: crates/pdfs-gui/src/app/main.rs:1228 #: crates/pdfs-gui/src/app/main.rs:1245
msgid "Proton Privacy Policy" msgid "Proton Privacy Policy"
msgstr "Informativa sulla privacy di Proton" msgstr "Informativa sulla privacy di Proton"
#: crates/pdfs-gui/src/app/main.rs:1233 #: crates/pdfs-gui/src/app/main.rs:1250
msgid "Disclaimer" msgid "Disclaimer"
msgstr "Esclusione di responsabilità" msgstr "Esclusione di responsabilità"
#: crates/pdfs-gui/src/app/main.rs:1237 #: crates/pdfs-gui/src/app/main.rs:1254
msgid "" msgid ""
"This is an unofficial, community-built client. It is not affiliated with, " "This is an unofficial, community-built client. It is not affiliated with, "
"endorsed by, or supported by Proton AG.\n" "endorsed by, or supported by Proton AG.\n"
@@ -204,7 +204,7 @@ msgstr ""
"Questo software è fornito “così com’è”, senza alcuna garanzia. Conserva una " "Questo software è fornito “così com’è”, senza alcuna garanzia. Conserva una "
"copia dei dati che non puoi permetterti di perdere." "copia dei dati che non puoi permetterti di perdere."
#: crates/pdfs-gui/src/app/main.rs:1245 #: crates/pdfs-gui/src/app/main.rs:1262
msgid "" msgid ""
"Places are named with data from <a href=\"https://www.geonames.org/" "Places are named with data from <a href=\"https://www.geonames.org/"
"\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/"
@@ -214,7 +214,7 @@ msgstr ""
"\">GeoNames</a>, con licenza <a href=\"https://creativecommons.org/licenses/" "\">GeoNames</a>, con licenza <a href=\"https://creativecommons.org/licenses/"
"by/4.0/\">CC BY 4.0</a>." "by/4.0/\">CC BY 4.0</a>."
#: crates/pdfs-gui/src/app/main.rs:1253 #: crates/pdfs-gui/src/app/main.rs:1270
msgid "" msgid ""
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/" "The places map is drawn from <a href=\"https://www.naturalearthdata.com/"
"\">Natural Earth</a> land outlines and country borders, which are in the " "\">Natural Earth</a> land outlines and country borders, which are in the "
@@ -224,7 +224,7 @@ msgstr ""
"confini nazionali di <a href=\"https://www.naturalearthdata.com/\">Natural " "confini nazionali di <a href=\"https://www.naturalearthdata.com/\">Natural "
"Earth</a>, che sono di pubblico dominio." "Earth</a>, che sono di pubblico dominio."
#: crates/pdfs-gui/src/app/main.rs:1261 #: crates/pdfs-gui/src/app/main.rs:1278
msgid "" msgid ""
"The street map, when turned on, comes from <a href=\"https://openfreemap.org/" "The street map, when turned on, comes from <a href=\"https://openfreemap.org/"
"\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/" "\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/"
@@ -240,15 +240,15 @@ msgstr ""
#. Translators: replace with your name(s) and email address(es), one #. Translators: replace with your name(s) and email address(es), one
#. per line, to be credited in the About dialog. #. per line, to be credited in the About dialog.
#: crates/pdfs-gui/src/app/main.rs:1272 #: crates/pdfs-gui/src/app/main.rs:1289
msgid "translator-credits" msgid "translator-credits"
msgstr "Collaboratori di Proton Drive for Linux" msgstr "Collaboratori di Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1310 #: crates/pdfs-gui/src/app/main.rs:1327
msgid "Stop Proton Drive?" msgid "Stop Proton Drive?"
msgstr "Arrestare Proton Drive?" msgstr "Arrestare Proton Drive?"
#: crates/pdfs-gui/src/app/main.rs:1311 #: crates/pdfs-gui/src/app/main.rs:1328
msgid "" msgid ""
"My files goes offline and nothing syncs until you connect again. Proton " "My files goes offline and nothing syncs until you connect again. Proton "
"Drive starts again at your next login." "Drive starts again at your next login."
@@ -256,7 +256,7 @@ msgstr ""
"I miei file andranno offline e nulla verrà sincronizzato finché non ti " "I miei file andranno offline e nulla verrà sincronizzato finché non ti "
"riconnetti. Proton Drive si riavvierà al prossimo accesso." "riconnetti. Proton Drive si riavvierà al prossimo accesso."
#: crates/pdfs-gui/src/app/main.rs:1313 crates/pdfs-gui/src/app/main.rs:1588 #: crates/pdfs-gui/src/app/main.rs:1330 crates/pdfs-gui/src/app/main.rs:1605
#: crates/pdfs-gui/src/app/pages/albums.rs:243 #: crates/pdfs-gui/src/app/pages/albums.rs:243
#: crates/pdfs-gui/src/app/pages/albums.rs:428 #: crates/pdfs-gui/src/app/pages/albums.rs:428
#: crates/pdfs-gui/src/app/pages/browser.rs:1892 #: crates/pdfs-gui/src/app/pages/browser.rs:1892
@@ -271,7 +271,8 @@ msgstr ""
#: crates/pdfs-gui/src/app/pages/locations.rs:1013 #: crates/pdfs-gui/src/app/pages/locations.rs:1013
#: crates/pdfs-gui/src/app/pages/locations.rs:1488 #: crates/pdfs-gui/src/app/pages/locations.rs:1488
#: crates/pdfs-gui/src/app/pages/locations.rs:1538 #: crates/pdfs-gui/src/app/pages/locations.rs:1538
#: crates/pdfs-gui/src/app/pages/login.rs:312 #: crates/pdfs-gui/src/app/pages/login.rs:393
#: crates/pdfs-gui/src/app/pages/login.rs:507
#: crates/pdfs-gui/src/app/pages/photos.rs:2055 #: crates/pdfs-gui/src/app/pages/photos.rs:2055
#: crates/pdfs-gui/src/app/pages/shared.rs:946 #: crates/pdfs-gui/src/app/pages/shared.rs:946
#: crates/pdfs-gui/src/app/pages/shared.rs:1027 #: crates/pdfs-gui/src/app/pages/shared.rs:1027
@@ -286,30 +287,30 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Annulla" msgstr "Annulla"
#: crates/pdfs-gui/src/app/main.rs:1314 #: crates/pdfs-gui/src/app/main.rs:1331
msgctxt "verb" msgctxt "verb"
msgid "Stop" msgid "Stop"
msgstr "Arresta" msgstr "Arresta"
#: crates/pdfs-gui/src/app/main.rs:1331 #: crates/pdfs-gui/src/app/main.rs:1348
msgid "Couldn't stop Proton Drive" msgid "Couldn't stop Proton Drive"
msgstr "Impossibile arrestare Proton Drive" msgstr "Impossibile arrestare Proton Drive"
#: crates/pdfs-gui/src/app/main.rs:1332 #: crates/pdfs-gui/src/app/main.rs:1349
msgid "The Proton Drive service did not stop." msgid "The Proton Drive service did not stop."
msgstr "Il servizio di Proton Drive non si è arrestato." msgstr "Il servizio di Proton Drive non si è arrestato."
#: crates/pdfs-gui/src/app/main.rs:1368 #: crates/pdfs-gui/src/app/main.rs:1385
#: crates/pdfs-gui/src/app/pages/status.rs:254 #: crates/pdfs-gui/src/app/pages/status.rs:254
msgid "General" msgid "General"
msgstr "Generale" msgstr "Generale"
#: crates/pdfs-gui/src/app/main.rs:1370 #: crates/pdfs-gui/src/app/main.rs:1387
#: crates/pdfs-gui/src/app/pages/browser.rs:247 #: crates/pdfs-gui/src/app/pages/browser.rs:247
msgid "Search Drive" msgid "Search Drive"
msgstr "Cerca in Drive" msgstr "Cerca in Drive"
#: crates/pdfs-gui/src/app/main.rs:1371 #: crates/pdfs-gui/src/app/main.rs:1388
#: crates/pdfs-gui/src/app/pages/browser.rs:957 #: crates/pdfs-gui/src/app/pages/browser.rs:957
#: crates/pdfs-gui/src/app/pages/devices.rs:554 #: crates/pdfs-gui/src/app/pages/devices.rs:554
#: crates/pdfs-gui/src/app/pages/shared.rs:586 #: crates/pdfs-gui/src/app/pages/shared.rs:586
@@ -318,38 +319,38 @@ msgstr "Cerca in Drive"
msgid "Refresh" msgid "Refresh"
msgstr "Aggiorna" msgstr "Aggiorna"
#: crates/pdfs-gui/src/app/main.rs:1373 #: crates/pdfs-gui/src/app/main.rs:1390
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "Scorciatoie da tastiera" msgstr "Scorciatoie da tastiera"
#: crates/pdfs-gui/src/app/main.rs:1377 #: crates/pdfs-gui/src/app/main.rs:1394
msgid "Files" msgid "Files"
msgstr "File" msgstr "File"
#: crates/pdfs-gui/src/app/main.rs:1379 #: crates/pdfs-gui/src/app/main.rs:1396
#: crates/pdfs-gui/src/app/pages/browser.rs:294 #: crates/pdfs-gui/src/app/pages/browser.rs:294
msgid "New folder" msgid "New folder"
msgstr "Nuova cartella" msgstr "Nuova cartella"
#: crates/pdfs-gui/src/app/main.rs:1380 #: crates/pdfs-gui/src/app/main.rs:1397
#: crates/pdfs-gui/src/app/pages/browser.rs:290 #: crates/pdfs-gui/src/app/pages/browser.rs:290
msgid "Upload files" msgid "Upload files"
msgstr "Carica file" msgstr "Carica file"
#: crates/pdfs-gui/src/app/main.rs:1381 #: crates/pdfs-gui/src/app/main.rs:1398
msgid "Back / forward" msgid "Back / forward"
msgstr "Indietro / avanti" msgstr "Indietro / avanti"
#: crates/pdfs-gui/src/app/main.rs:1382 #: crates/pdfs-gui/src/app/main.rs:1399
#: crates/pdfs-gui/src/app/pages/browser.rs:1943 #: crates/pdfs-gui/src/app/pages/browser.rs:1943
msgid "Parent folder" msgid "Parent folder"
msgstr "Cartella superiore" msgstr "Cartella superiore"
#: crates/pdfs-gui/src/app/main.rs:1383 #: crates/pdfs-gui/src/app/main.rs:1400
msgid "Grid / list" msgid "Grid / list"
msgstr "Griglia / elenco" msgstr "Griglia / elenco"
#: crates/pdfs-gui/src/app/main.rs:1384 #: crates/pdfs-gui/src/app/main.rs:1401
#: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/albums.rs:298
#: crates/pdfs-gui/src/app/pages/browser.rs:1871 #: crates/pdfs-gui/src/app/pages/browser.rs:1871
#: crates/pdfs-gui/src/app/pages/browser.rs:1893 #: crates/pdfs-gui/src/app/pages/browser.rs:1893
@@ -358,7 +359,7 @@ msgstr "Griglia / elenco"
msgid "Rename" msgid "Rename"
msgstr "Rinomina" msgstr "Rinomina"
#: crates/pdfs-gui/src/app/main.rs:1385 crates/pdfs-gui/src/app/main.rs:1405 #: crates/pdfs-gui/src/app/main.rs:1402 crates/pdfs-gui/src/app/main.rs:1422
#: crates/pdfs-gui/src/app/pages/browser.rs:357 #: crates/pdfs-gui/src/app/pages/browser.rs:357
#: crates/pdfs-gui/src/app/pages/browser.rs:900 #: crates/pdfs-gui/src/app/pages/browser.rs:900
#: crates/pdfs-gui/src/app/pages/browser.rs:932 #: crates/pdfs-gui/src/app/pages/browser.rs:932
@@ -368,73 +369,73 @@ msgstr "Rinomina"
msgid "Move to Trash" msgid "Move to Trash"
msgstr "Sposta nel cestino" msgstr "Sposta nel cestino"
#: crates/pdfs-gui/src/app/main.rs:1386 #: crates/pdfs-gui/src/app/main.rs:1403
msgid "Clear the selection" msgid "Clear the selection"
msgstr "Annulla la selezione" msgstr "Annulla la selezione"
#: crates/pdfs-gui/src/app/main.rs:1392 #: crates/pdfs-gui/src/app/main.rs:1409
msgid "Larger thumbnails" msgid "Larger thumbnails"
msgstr "Miniature più grandi" msgstr "Miniature più grandi"
#: crates/pdfs-gui/src/app/main.rs:1393 #: crates/pdfs-gui/src/app/main.rs:1410
msgid "Smaller thumbnails" msgid "Smaller thumbnails"
msgstr "Miniature più piccole" msgstr "Miniature più piccole"
#: crates/pdfs-gui/src/app/main.rs:1394 #: crates/pdfs-gui/src/app/main.rs:1411
msgid "Reset thumbnail size" msgid "Reset thumbnail size"
msgstr "Ripristina la dimensione delle miniature" msgstr "Ripristina la dimensione delle miniature"
#: crates/pdfs-gui/src/app/main.rs:1395 #: crates/pdfs-gui/src/app/main.rs:1412
msgid "Select all" msgid "Select all"
msgstr "Seleziona tutto" msgstr "Seleziona tutto"
#: crates/pdfs-gui/src/app/main.rs:1399 #: crates/pdfs-gui/src/app/main.rs:1416
msgid "Photo Viewer" msgid "Photo Viewer"
msgstr "Visualizzatore di foto" msgstr "Visualizzatore di foto"
#: crates/pdfs-gui/src/app/main.rs:1401 #: crates/pdfs-gui/src/app/main.rs:1418
msgid "Previous / next photo" msgid "Previous / next photo"
msgstr "Foto precedente / successiva" msgstr "Foto precedente / successiva"
#: crates/pdfs-gui/src/app/main.rs:1402 #: crates/pdfs-gui/src/app/main.rs:1419
msgid "First / last photo" msgid "First / last photo"
msgstr "Prima / ultima foto" msgstr "Prima / ultima foto"
#: crates/pdfs-gui/src/app/main.rs:1403 #: crates/pdfs-gui/src/app/main.rs:1420
msgid "Show details" msgid "Show details"
msgstr "Mostra dettagli" msgstr "Mostra dettagli"
#: crates/pdfs-gui/src/app/main.rs:1404 #: crates/pdfs-gui/src/app/main.rs:1421
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Schermo intero" msgstr "Schermo intero"
#: crates/pdfs-gui/src/app/main.rs:1406 #: crates/pdfs-gui/src/app/main.rs:1423
msgid "Close" msgid "Close"
msgstr "Chiudi" msgstr "Chiudi"
#: crates/pdfs-gui/src/app/main.rs:1496 #: crates/pdfs-gui/src/app/main.rs:1513
msgid "Select a single item to rename it" msgid "Select a single item to rename it"
msgstr "Seleziona un solo elemento per rinominarlo" msgstr "Seleziona un solo elemento per rinominarlo"
#: crates/pdfs-gui/src/app/main.rs:1656 #: crates/pdfs-gui/src/app/main.rs:1673
#: crates/pdfs-gui/src/app/pages/albums.rs:85 #: crates/pdfs-gui/src/app/pages/albums.rs:85
#: crates/pdfs-gui/src/app/pages/places.rs:104 #: crates/pdfs-gui/src/app/pages/places.rs:104
#: crates/pdfs-gui/src/app/pages/status.rs:1370 #: crates/pdfs-gui/src/app/pages/status.rs:1371
msgid "Not connected" msgid "Not connected"
msgstr "Non connesso" msgstr "Non connesso"
#: crates/pdfs-gui/src/app/main.rs:1657 #: crates/pdfs-gui/src/app/main.rs:1674
msgid "The Proton Drive service isn't running." msgid "The Proton Drive service isn't running."
msgstr "Il servizio di Proton Drive non è in esecuzione." msgstr "Il servizio di Proton Drive non è in esecuzione."
#: crates/pdfs-gui/src/app/main.rs:1665 #: crates/pdfs-gui/src/app/main.rs:1682
#: crates/pdfs-gui/src/app/pages/status.rs:336 #: crates/pdfs-gui/src/app/pages/status.rs:336
#: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595 #: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595
#: crates/pdfs-gui/src/main.rs:444 #: crates/pdfs-gui/src/main.rs:453
msgid "Connecting…" msgid "Connecting…"
msgstr "Connessione…" msgstr "Connessione…"
#: crates/pdfs-gui/src/app/main.rs:1666 #: crates/pdfs-gui/src/app/main.rs:1683
msgid "Waiting for the Proton Drive service to start." msgid "Waiting for the Proton Drive service to start."
msgstr "In attesa dell’avvio del servizio di Proton Drive." msgstr "In attesa dell’avvio del servizio di Proton Drive."
@@ -1286,7 +1287,7 @@ msgstr "Impossibile caricare gli album"
#: crates/pdfs-gui/src/app/pages/shared.rs:1006 #: crates/pdfs-gui/src/app/pages/shared.rs:1006
#: crates/pdfs-gui/src/app/pages/shared.rs:1122 #: crates/pdfs-gui/src/app/pages/shared.rs:1122
#: crates/pdfs-gui/src/app/pages/status.rs:750 #: crates/pdfs-gui/src/app/pages/status.rs:750
#: crates/pdfs-gui/src/app/pages/status.rs:1517 #: crates/pdfs-gui/src/app/pages/status.rs:1518
#: crates/pdfs-gui/src/app/pages/takeout.rs:424 #: crates/pdfs-gui/src/app/pages/takeout.rs:424
#: crates/pdfs-gui/src/app/pages/takeout.rs:617 #: crates/pdfs-gui/src/app/pages/takeout.rs:617
#: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438
@@ -1554,7 +1555,7 @@ msgid "Make available offline"
msgstr "Rendi disponibile offline" msgstr "Rendi disponibile offline"
#: crates/pdfs-gui/src/app/pages/browser.rs:347 #: crates/pdfs-gui/src/app/pages/browser.rs:347
#: crates/pdfs-gui/src/app/pages/status.rs:1493 #: crates/pdfs-gui/src/app/pages/status.rs:1494
msgid "Make online only" msgid "Make online only"
msgstr "Rendi solo online" msgstr "Rendi solo online"
@@ -2722,7 +2723,7 @@ msgid "Sync paused until you resume"
msgstr "Sincronizzazione in pausa finché non riprendi" msgstr "Sincronizzazione in pausa finché non riprendi"
#: crates/pdfs-gui/src/app/pages/locations.rs:529 #: crates/pdfs-gui/src/app/pages/locations.rs:529
#: crates/pdfs-gui/src/app/pages/status.rs:1323 crates/pdfs-gui/src/main.rs:237 #: crates/pdfs-gui/src/app/pages/status.rs:1324 crates/pdfs-gui/src/main.rs:246
msgid "Sync paused" msgid "Sync paused"
msgstr "Sincronizzazione in pausa" msgstr "Sincronizzazione in pausa"
@@ -3192,25 +3193,35 @@ msgstr "Inserisci la tua email e la password."
msgid "Signing in…" msgid "Signing in…"
msgstr "Accesso in corso…" msgstr "Accesso in corso…"
#: crates/pdfs-gui/src/app/pages/login.rs:155 #: crates/pdfs-gui/src/app/pages/login.rs:164
msgid "Complete the verification to continue…" msgid "Complete the verification to continue…"
msgstr "Completa la verifica per continuare…" msgstr "Completa la verifica per continuare…"
#: crates/pdfs-gui/src/app/pages/login.rs:165 #: crates/pdfs-gui/src/app/pages/login.rs:197
msgid "login cancelled" msgid "login cancelled"
msgstr "accesso annullato" msgstr "accesso annullato"
#: crates/pdfs-gui/src/app/pages/login.rs:198 #: crates/pdfs-gui/src/app/pages/login.rs:210
msgid ""
"The verification page couldn't be opened, so the sign-in was cancelled. Your "
"system may keep the web view's sandbox from starting; the troubleshooting "
"guide explains what to do."
msgstr ""
"Impossibile aprire la pagina di verifica, quindi l'accesso è stato "
"annullato. Il sistema potrebbe impedire l'avvio della sandbox della vista "
"web; la guida alla risoluzione dei problemi spiega cosa fare."
#: crates/pdfs-gui/src/app/pages/login.rs:233
msgid "Too many sign-in attempts. Wait a few minutes and try again." msgid "Too many sign-in attempts. Wait a few minutes and try again."
msgstr "Troppi tentativi di accesso. Attendi qualche minuto e riprova." msgstr "Troppi tentativi di accesso. Attendi qualche minuto e riprova."
#: crates/pdfs-gui/src/app/pages/login.rs:204 #: crates/pdfs-gui/src/app/pages/login.rs:239
msgid "Couldn't reach Proton. Check your internet connection and try again." msgid "Couldn't reach Proton. Check your internet connection and try again."
msgstr "" msgstr ""
"Impossibile raggiungere Proton. Controlla la connessione a Internet e " "Impossibile raggiungere Proton. Controlla la connessione a Internet e "
"riprova." "riprova."
#: crates/pdfs-gui/src/app/pages/login.rs:207 #: crates/pdfs-gui/src/app/pages/login.rs:242
msgid "" msgid ""
"Signed in, but the session couldn't be saved to the system keyring. Make " "Signed in, but the session couldn't be saved to the system keyring. Make "
"sure a keyring (GNOME Keyring or KWallet) is running and unlocked." "sure a keyring (GNOME Keyring or KWallet) is running and unlocked."
@@ -3219,13 +3230,24 @@ msgstr ""
"portachiavi di sistema. Assicurati che un portachiavi (GNOME Keyring o " "portachiavi di sistema. Assicurati che un portachiavi (GNOME Keyring o "
"KWallet) sia in esecuzione e sbloccato." "KWallet) sia in esecuzione e sbloccato."
#: crates/pdfs-gui/src/app/pages/login.rs:210 #: crates/pdfs-gui/src/app/pages/login.rs:245
msgid "Sign-in cancelled: no two-factor code was entered." msgid "Sign-in cancelled: no two-factor code was entered."
msgstr "" msgstr ""
"Accesso annullato: non è stato inserito alcun codice di autenticazione a due " "Accesso annullato: non è stato inserito alcun codice di autenticazione a due "
"fattori." "fattori."
#: crates/pdfs-gui/src/app/pages/login.rs:213 #: crates/pdfs-gui/src/app/pages/login.rs:248
msgid "Sign-in cancelled: no mailbox password was entered."
msgstr ""
"Accesso annullato: non è stata inserita alcuna password della casella di "
"posta."
#: crates/pdfs-gui/src/app/pages/login.rs:251
msgid "That mailbox password didn't unlock your account."
msgstr ""
"Quella password della casella di posta non ha sbloccato il tuo account."
#: crates/pdfs-gui/src/app/pages/login.rs:254
msgid "" msgid ""
"Sign-in cancelled: the verification wasn't completed. Sign in again to get a " "Sign-in cancelled: the verification wasn't completed. Sign in again to get a "
"new one." "new one."
@@ -3234,35 +3256,88 @@ msgstr ""
"riceverne una nuova." "riceverne una nuova."
#. Translators: {error} is an error message, usually in English. #. Translators: {error} is an error message, usually in English.
#: crates/pdfs-gui/src/app/pages/login.rs:218 #: crates/pdfs-gui/src/app/pages/login.rs:259
#, rust-format #, rust-format
msgid "Sign-in failed: {error}" msgid "Sign-in failed: {error}"
msgstr "Accesso non riuscito: {error}" msgstr "Accesso non riuscito: {error}"
#: crates/pdfs-gui/src/app/pages/login.rs:298 #: crates/pdfs-gui/src/app/pages/login.rs:369
msgid "That mailbox password didn't unlock your account. Try again."
msgstr ""
"Quella password della casella di posta non ha sbloccato il tuo account. "
"Riprova."
#: crates/pdfs-gui/src/app/pages/login.rs:371
msgid ""
"This account has a separate mailbox password. Enter it to unlock your files."
msgstr ""
"Questo account ha una password della casella di posta separata. Inseriscila "
"per sbloccare i tuoi file."
#: crates/pdfs-gui/src/app/pages/login.rs:374
msgid "Mailbox Password"
msgstr "Password della casella di posta"
#: crates/pdfs-gui/src/app/pages/login.rs:380
msgid "Mailbox password"
msgstr "Password della casella di posta"
#: crates/pdfs-gui/src/app/pages/login.rs:384
msgid "Keep unlocked on this computer"
msgstr "Mantieni sbloccato su questo computer"
#: crates/pdfs-gui/src/app/pages/login.rs:386
msgid ""
"Stores the unlock in the system keyring, so Proton Drive opens without "
"asking after every login. Otherwise you enter the password once per session."
msgstr ""
"Salva lo sblocco nel portachiavi di sistema, così Proton Drive si apre senza "
"chiedere dopo ogni accesso. Altrimenti inserisci la password una volta per "
"sessione."
#: crates/pdfs-gui/src/app/pages/login.rs:394
#: crates/pdfs-gui/src/app/pages/login.rs:447
msgid "Unlock"
msgstr "Sblocca"
#: crates/pdfs-gui/src/app/pages/login.rs:446
msgid "Proton Drive is locked. Enter your mailbox password to unlock it."
msgstr ""
"Proton Drive è bloccato. Inserisci la password della casella di posta per "
"sbloccarlo."
#: crates/pdfs-gui/src/app/pages/login.rs:475
msgid "Unlocked."
msgstr "Sbloccato."
#: crates/pdfs-gui/src/app/pages/login.rs:480
msgid "Couldn't unlock"
msgstr "Impossibile sbloccare"
#: crates/pdfs-gui/src/app/pages/login.rs:493
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "Autenticazione a due fattori" msgstr "Autenticazione a due fattori"
#: crates/pdfs-gui/src/app/pages/login.rs:300 #: crates/pdfs-gui/src/app/pages/login.rs:495
msgid "" msgid ""
"Enter the code from your authenticator app, or one of your recovery codes." "Enter the code from your authenticator app, or one of your recovery codes."
msgstr "" msgstr ""
"Inserisci il codice dell'app di autenticazione o uno dei tuoi codici di " "Inserisci il codice dell'app di autenticazione o uno dei tuoi codici di "
"recupero." "recupero."
#: crates/pdfs-gui/src/app/pages/login.rs:306 #: crates/pdfs-gui/src/app/pages/login.rs:501
msgid "Authentication code" msgid "Authentication code"
msgstr "Codice di autenticazione" msgstr "Codice di autenticazione"
#: crates/pdfs-gui/src/app/pages/login.rs:313 #: crates/pdfs-gui/src/app/pages/login.rs:508
msgid "Confirm" msgid "Confirm"
msgstr "Conferma" msgstr "Conferma"
#: crates/pdfs-gui/src/app/pages/login.rs:341 #: crates/pdfs-gui/src/app/pages/login.rs:536
msgid "Sign Out?" msgid "Sign Out?"
msgstr "Disconnettersi?" msgstr "Disconnettersi?"
#: crates/pdfs-gui/src/app/pages/login.rs:343 #: crates/pdfs-gui/src/app/pages/login.rs:538
msgid "" msgid ""
"Proton Drive will disconnect and stop syncing until you sign in again. Files " "Proton Drive will disconnect and stop syncing until you sign in again. Files "
"already on this computer stay where they are." "already on this computer stay where they are."
@@ -3270,7 +3345,7 @@ msgstr ""
"Proton Drive si disconnetterà e interromperà la sincronizzazione finché non " "Proton Drive si disconnetterà e interromperà la sincronizzazione finché non "
"accedi di nuovo. I file già presenti su questo computer restano dove sono." "accedi di nuovo. I file già presenti su questo computer restano dove sono."
#: crates/pdfs-gui/src/app/pages/login.rs:345 #: crates/pdfs-gui/src/app/pages/login.rs:540
msgid "Sign Out" msgid "Sign Out"
msgstr "Disconnetti" msgstr "Disconnetti"
@@ -4621,40 +4696,40 @@ msgid "Restart Now"
msgstr "Riavvia ora" msgstr "Riavvia ora"
#. Translators: {size} is an amount of storage, such as "1.2 GiB". #. Translators: {size} is an amount of storage, such as "1.2 GiB".
#: crates/pdfs-gui/src/app/pages/status.rs:912 #: crates/pdfs-gui/src/app/pages/status.rs:913
#, rust-format #, rust-format
msgid "{size} used" msgid "{size} used"
msgstr "{size} utilizzati" msgstr "{size} utilizzati"
#: crates/pdfs-gui/src/app/pages/status.rs:923 #: crates/pdfs-gui/src/app/pages/status.rs:924
#, rust-format #, rust-format
msgid "{used} of {total} used ({percent}%)" msgid "{used} of {total} used ({percent}%)"
msgstr "{used} di {total} utilizzati ({percent}%)" msgstr "{used} di {total} utilizzati ({percent}%)"
#: crates/pdfs-gui/src/app/pages/status.rs:961 #: crates/pdfs-gui/src/app/pages/status.rs:962
msgid "Proton Drive connected" msgid "Proton Drive connected"
msgstr "Proton Drive connesso" msgstr "Proton Drive connesso"
#: crates/pdfs-gui/src/app/pages/status.rs:962 #: crates/pdfs-gui/src/app/pages/status.rs:963
msgid "Your files are available." msgid "Your files are available."
msgstr "I tuoi file sono disponibili." msgstr "I tuoi file sono disponibili."
#: crates/pdfs-gui/src/app/pages/status.rs:967 #: crates/pdfs-gui/src/app/pages/status.rs:968
msgid "Proton Drive disconnected" msgid "Proton Drive disconnected"
msgstr "Proton Drive disconnesso" msgstr "Proton Drive disconnesso"
#: crates/pdfs-gui/src/app/pages/status.rs:968 #: crates/pdfs-gui/src/app/pages/status.rs:969
msgid "" msgid ""
"The Proton Drive service stopped. Files aren't available until it restarts." "The Proton Drive service stopped. Files aren't available until it restarts."
msgstr "" msgstr ""
"Il servizio di Proton Drive si è arrestato. I file non sono disponibili " "Il servizio di Proton Drive si è arrestato. I file non sono disponibili "
"finché non si riavvia." "finché non si riavvia."
#: crates/pdfs-gui/src/app/pages/status.rs:1037 #: crates/pdfs-gui/src/app/pages/status.rs:1038
msgid "Sync complete" msgid "Sync complete"
msgstr "Sincronizzazione completata" msgstr "Sincronizzazione completata"
#: crates/pdfs-gui/src/app/pages/status.rs:1039 #: crates/pdfs-gui/src/app/pages/status.rs:1040
#, rust-format #, rust-format
msgid "{n} file finished transferring." msgid "{n} file finished transferring."
msgid_plural "{n} files finished transferring." msgid_plural "{n} files finished transferring."
@@ -4663,27 +4738,27 @@ msgstr[1] "Trasferimento di {n} file completato."
#. Translators: a background job's progress. {title} is the job, such as #. Translators: a background job's progress. {title} is the job, such as
#. "Scanning Photos"; {done} and {total} are counts. #. "Scanning Photos"; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1117 #: crates/pdfs-gui/src/app/pages/status.rs:1118
#, rust-format #, rust-format
msgid "{title} — {done} of {total}" msgid "{title} — {done} of {total}"
msgstr "{title}: {done} di {total}" msgstr "{title}: {done} di {total}"
#. Translators: a background job's progress. {title} is the job, {detail} #. Translators: a background job's progress. {title} is the job, {detail}
#. what it is working on; {done} and {total} are counts. #. what it is working on; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1120 #: crates/pdfs-gui/src/app/pages/status.rs:1121
#, rust-format #, rust-format
msgid "{title} — {detail} ({done} of {total})" msgid "{title} — {detail} ({done} of {total})"
msgstr "{title}: {detail} ({done} di {total})" msgstr "{title}: {detail} ({done} di {total})"
#. Translators: a background job still running. {title} is the job. #. Translators: a background job still running. {title} is the job.
#: crates/pdfs-gui/src/app/pages/status.rs:1122 #: crates/pdfs-gui/src/app/pages/status.rs:1123
#, rust-format #, rust-format
msgid "{title}…" msgid "{title}…"
msgstr "{title}…" msgstr "{title}…"
#. Translators: a background job still running. {title} is the job, #. Translators: a background job still running. {title} is the job,
#. {detail} what it is working on. #. {detail} what it is working on.
#: crates/pdfs-gui/src/app/pages/status.rs:1125 #: crates/pdfs-gui/src/app/pages/status.rs:1126
#, rust-format #, rust-format
msgid "{title} — {detail}…" msgid "{title} — {detail}…"
msgstr "{title}: {detail}…" msgstr "{title}: {detail}…"
@@ -4691,7 +4766,7 @@ msgstr "{title}: {detail}…"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} the size moved so far and {speed} a size such as #. file name, {done} the size moved so far and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1154 #: crates/pdfs-gui/src/app/pages/status.rs:1155
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} ({speed}/s)" msgid "{arrow} {name} — {done} ({speed}/s)"
msgstr "{arrow} {name}: {done} ({speed}/s)" msgstr "{arrow} {name}: {done} ({speed}/s)"
@@ -4699,117 +4774,117 @@ msgstr "{arrow} {name}: {done} ({speed}/s)"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} and {total} sizes and {speed} a size such as #. file name, {done} and {total} sizes and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1162 #: crates/pdfs-gui/src/app/pages/status.rs:1163
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} of {total} ({speed}/s)" msgid "{arrow} {name} — {done} of {total} ({speed}/s)"
msgstr "{arrow} {name}: {done} di {total} ({speed}/s)" msgstr "{arrow} {name}: {done} di {total} ({speed}/s)"
#: crates/pdfs-gui/src/app/pages/status.rs:1254 #: crates/pdfs-gui/src/app/pages/status.rs:1255
#, rust-format #, rust-format
msgid "{used} cached — no limit set" msgid "{used} cached — no limit set"
msgstr "{used} in cache — nessun limite impostato" msgstr "{used} in cache — nessun limite impostato"
#: crates/pdfs-gui/src/app/pages/status.rs:1260 #: crates/pdfs-gui/src/app/pages/status.rs:1261
#, rust-format #, rust-format
msgid "{used} of {limit} used" msgid "{used} of {limit} used"
msgstr "{used} di {limit} utilizzati" msgstr "{used} di {limit} utilizzati"
#: crates/pdfs-gui/src/app/pages/status.rs:1328 #: crates/pdfs-gui/src/app/pages/status.rs:1329
#, rust-format #, rust-format
msgid "{queued} · resumes {time}" msgid "{queued} · resumes {time}"
msgstr "{queued} · riprende {time}" msgstr "{queued} · riprende {time}"
#. Translators: {time} is when syncing resumes, such as "at 14:30" #. Translators: {time} is when syncing resumes, such as "at 14:30"
#. or "Tue 09:00". #. or "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1333 #: crates/pdfs-gui/src/app/pages/status.rs:1334
#, rust-format #, rust-format
msgid "Resumes {time}" msgid "Resumes {time}"
msgstr "Riprende {time}" msgstr "Riprende {time}"
#: crates/pdfs-gui/src/app/pages/status.rs:1335 #: crates/pdfs-gui/src/app/pages/status.rs:1336
msgid "Until you resume" msgid "Until you resume"
msgstr "Finché non riprendi" msgstr "Finché non riprendi"
#: crates/pdfs-gui/src/app/pages/status.rs:1342 crates/pdfs-gui/src/main.rs:239 #: crates/pdfs-gui/src/app/pages/status.rs:1343 crates/pdfs-gui/src/main.rs:248
#, rust-format #, rust-format
msgid "{n} change needs attention" msgid "{n} change needs attention"
msgid_plural "{n} changes need attention" msgid_plural "{n} changes need attention"
msgstr[0] "{n} modifica richiede attenzione" msgstr[0] "{n} modifica richiede attenzione"
msgstr[1] "{n} modifiche richiedono attenzione" msgstr[1] "{n} modifiche richiedono attenzione"
#: crates/pdfs-gui/src/app/pages/status.rs:1352 #: crates/pdfs-gui/src/app/pages/status.rs:1353
#: crates/pdfs-gui/src/app/widgets/details.rs:108 #: crates/pdfs-gui/src/app/widgets/details.rs:108
msgctxt "state" msgctxt "state"
msgid "Offline" msgid "Offline"
msgstr "Offline" msgstr "Offline"
#: crates/pdfs-gui/src/app/pages/status.rs:1353 #: crates/pdfs-gui/src/app/pages/status.rs:1354
msgid "Cached files only" msgid "Cached files only"
msgstr "Solo file in cache" msgstr "Solo file in cache"
#: crates/pdfs-gui/src/app/pages/status.rs:1358 #: crates/pdfs-gui/src/app/pages/status.rs:1359
msgctxt "state" msgctxt "state"
msgid "Syncing" msgid "Syncing"
msgstr "Sincronizzazione in corso" msgstr "Sincronizzazione in corso"
#: crates/pdfs-gui/src/app/pages/status.rs:1364 #: crates/pdfs-gui/src/app/pages/status.rs:1365
msgid "Up to date" msgid "Up to date"
msgstr "Aggiornato" msgstr "Aggiornato"
#: crates/pdfs-gui/src/app/pages/status.rs:1371 #: crates/pdfs-gui/src/app/pages/status.rs:1372
#: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622 #: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622
msgid "Proton Drive isn't running" msgid "Proton Drive isn't running"
msgstr "Proton Drive non è in esecuzione" msgstr "Proton Drive non è in esecuzione"
#: crates/pdfs-gui/src/app/pages/status.rs:1376 #: crates/pdfs-gui/src/app/pages/status.rs:1377
msgid "Not responding" msgid "Not responding"
msgstr "Non risponde" msgstr "Non risponde"
#: crates/pdfs-gui/src/app/pages/status.rs:1377 #: crates/pdfs-gui/src/app/pages/status.rs:1378
msgid "Proton Drive is taking too long to answer" msgid "Proton Drive is taking too long to answer"
msgstr "Proton Drive impiega troppo tempo a rispondere" msgstr "Proton Drive impiega troppo tempo a rispondere"
#. Translators: stands in for a time that could not be read, as in #. Translators: stands in for a time that could not be read, as in
#. "Resumes later". #. "Resumes later".
#: crates/pdfs-gui/src/app/pages/status.rs:1415 #: crates/pdfs-gui/src/app/pages/status.rs:1416
#: crates/pdfs-gui/src/app/pages/status.rs:1430 #: crates/pdfs-gui/src/app/pages/status.rs:1431
msgid "later" msgid "later"
msgstr "più tardi" msgstr "più tardi"
#. Translators: strftime format for a time later today, such as "at 14:30". #. Translators: strftime format for a time later today, such as "at 14:30".
#: crates/pdfs-gui/src/app/pages/status.rs:1419 #: crates/pdfs-gui/src/app/pages/status.rs:1420
msgid "at %H:%M" msgid "at %H:%M"
msgstr "alle %H:%M" msgstr "alle %H:%M"
#. Translators: strftime format for a time within the next week, such as #. Translators: strftime format for a time within the next week, such as
#. "Tue 09:00". #. "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1423 #: crates/pdfs-gui/src/app/pages/status.rs:1424
msgid "%a %H:%M" msgid "%a %H:%M"
msgstr "%a %H:%M" msgstr "%a %H:%M"
#. Translators: strftime format for a date further ahead, such as "23 Sep". #. Translators: strftime format for a date further ahead, such as "23 Sep".
#: crates/pdfs-gui/src/app/pages/status.rs:1426 #: crates/pdfs-gui/src/app/pages/status.rs:1427
msgid "%e %b" msgid "%e %b"
msgstr "%e %b" msgstr "%e %b"
#: crates/pdfs-gui/src/app/pages/status.rs:1456 #: crates/pdfs-gui/src/app/pages/status.rs:1457
msgid "No files kept offline" msgid "No files kept offline"
msgstr "Nessun file mantenuto offline" msgstr "Nessun file mantenuto offline"
#: crates/pdfs-gui/src/app/pages/status.rs:1458 #: crates/pdfs-gui/src/app/pages/status.rs:1459
msgid "Right-click a file and choose “Make available offline”." msgid "Right-click a file and choose “Make available offline”."
msgstr "Fai clic destro su un file e scegli “Rendi disponibile offline”." msgstr "Fai clic destro su un file e scegli “Rendi disponibile offline”."
#: crates/pdfs-gui/src/app/pages/status.rs:1509 #: crates/pdfs-gui/src/app/pages/status.rs:1510
#: crates/pdfs-gui/src/app/pages/status.rs:1516 #: crates/pdfs-gui/src/app/pages/status.rs:1517
msgid "Couldn't make it online only" msgid "Couldn't make it online only"
msgstr "Impossibile renderlo solo online" msgstr "Impossibile renderlo solo online"
#: crates/pdfs-gui/src/app/pages/status.rs:1533 #: crates/pdfs-gui/src/app/pages/status.rs:1534
msgid "Show fewer" msgid "Show fewer"
msgstr "Mostra meno" msgstr "Mostra meno"
#: crates/pdfs-gui/src/app/pages/status.rs:1536 #: crates/pdfs-gui/src/app/pages/status.rs:1537
#, rust-format #, rust-format
msgid "Show all {n} file" msgid "Show all {n} file"
msgid_plural "Show all {n} files" msgid_plural "Show all {n} files"
@@ -5244,7 +5319,7 @@ msgstr "Cestino svuotato"
msgid "Couldn't empty the trash" msgid "Couldn't empty the trash"
msgstr "Impossibile svuotare il cestino" msgstr "Impossibile svuotare il cestino"
#: crates/pdfs-gui/src/app/pages/verify.rs:57 #: crates/pdfs-gui/src/app/pages/verify.rs:162
msgid "Verification" msgid "Verification"
msgstr "Verifica" msgstr "Verifica"
@@ -5876,40 +5951,45 @@ msgstr[1] "↑ {n} in caricamento"
msgid "Proton Drive isn't responding" msgid "Proton Drive isn't responding"
msgstr "Proton Drive non risponde" msgstr "Proton Drive non risponde"
#: crates/pdfs-gui/src/main.rs:172 #: crates/pdfs-gui/src/main.rs:173
#, rust-format
msgid "Signed in as {username} — locked"
msgstr "Accesso effettuato come {username} — bloccato"
#: crates/pdfs-gui/src/main.rs:181
#, rust-format #, rust-format
msgid "Signed in as {username} — not connected" msgid "Signed in as {username} — not connected"
msgstr "Accesso effettuato come {username} — non connesso" msgstr "Accesso effettuato come {username} — non connesso"
#: crates/pdfs-gui/src/main.rs:177 #: crates/pdfs-gui/src/main.rs:186
msgid "Not signed in" msgid "Not signed in"
msgstr "Accesso non eseguito" msgstr "Accesso non eseguito"
#. Translators: {error} is an error message, in English. #. Translators: {error} is an error message, in English.
#: crates/pdfs-gui/src/main.rs:180 #: crates/pdfs-gui/src/main.rs:189
#, rust-format #, rust-format
msgid "Error: {error}" msgid "Error: {error}"
msgstr "Errore: {error}" msgstr "Errore: {error}"
#: crates/pdfs-gui/src/main.rs:201 #: crates/pdfs-gui/src/main.rs:210
msgid "Proton Drive sent an unexpected reply" msgid "Proton Drive sent an unexpected reply"
msgstr "Proton Drive ha inviato una risposta inattesa" msgstr "Proton Drive ha inviato una risposta inattesa"
#: crates/pdfs-gui/src/main.rs:246 #: crates/pdfs-gui/src/main.rs:255
#, rust-format #, rust-format
msgid "Up to date in {mountpoint} ({n} pinned)" msgid "Up to date in {mountpoint} ({n} pinned)"
msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)"
msgstr[0] "Aggiornato in {mountpoint} ({n} fissato)" msgstr[0] "Aggiornato in {mountpoint} ({n} fissato)"
msgstr[1] "Aggiornato in {mountpoint} ({n} fissati)" msgstr[1] "Aggiornato in {mountpoint} ({n} fissati)"
#: crates/pdfs-gui/src/main.rs:253 #: crates/pdfs-gui/src/main.rs:262
#, rust-format #, rust-format
msgid "Syncing — {queued} ({n} pinned)" msgid "Syncing — {queued} ({n} pinned)"
msgid_plural "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)"
msgstr[0] "Sincronizzazione: {queued} ({n} fissato)" msgstr[0] "Sincronizzazione: {queued} ({n} fissato)"
msgstr[1] "Sincronizzazione: {queued} ({n} fissati)" msgstr[1] "Sincronizzazione: {queued} ({n} fissati)"
#: crates/pdfs-gui/src/main.rs:260 #: crates/pdfs-gui/src/main.rs:269
#, rust-format #, rust-format
msgid "Offline — cached files only ({n} pinned)" msgid "Offline — cached files only ({n} pinned)"
msgid_plural "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)"
@@ -5917,52 +5997,52 @@ msgstr[0] "Offline: solo file in cache ({n} fissato)"
msgstr[1] "Offline: solo file in cache ({n} fissati)" msgstr[1] "Offline: solo file in cache ({n} fissati)"
#. Translators: {queued} is a queue summary such as "3 uploads queued". #. Translators: {queued} is a queue summary such as "3 uploads queued".
#: crates/pdfs-gui/src/main.rs:266 #: crates/pdfs-gui/src/main.rs:275
#, rust-format #, rust-format
msgid "Offline — {queued}" msgid "Offline — {queued}"
msgstr "Offline: {queued}" msgstr "Offline: {queued}"
#. Translators: a menu item that opens the list of failing changes. #. Translators: a menu item that opens the list of failing changes.
#: crates/pdfs-gui/src/main.rs:296 #: crates/pdfs-gui/src/main.rs:305
#, rust-format #, rust-format
msgid "{n} issue — View" msgid "{n} issue — View"
msgid_plural "{n} issues — View" msgid_plural "{n} issues — View"
msgstr[0] "{n} problema: visualizza" msgstr[0] "{n} problema: visualizza"
msgstr[1] "{n} problemi: visualizza" msgstr[1] "{n} problemi: visualizza"
#: crates/pdfs-gui/src/main.rs:402 #: crates/pdfs-gui/src/main.rs:411
msgid "Sign In…" msgid "Sign In…"
msgstr "Accedi…" msgstr "Accedi…"
#: crates/pdfs-gui/src/main.rs:403 #: crates/pdfs-gui/src/main.rs:412
msgid "Open Proton Drive" msgid "Open Proton Drive"
msgstr "Apri Proton Drive" msgstr "Apri Proton Drive"
#: crates/pdfs-gui/src/main.rs:409 #: crates/pdfs-gui/src/main.rs:418
msgid "Open Folder" msgid "Open Folder"
msgstr "Apri cartella" msgstr "Apri cartella"
#: crates/pdfs-gui/src/main.rs:421 #: crates/pdfs-gui/src/main.rs:430
msgid "Resume Sync" msgid "Resume Sync"
msgstr "Riprendi sincronizzazione" msgstr "Riprendi sincronizzazione"
#: crates/pdfs-gui/src/main.rs:423 #: crates/pdfs-gui/src/main.rs:432
msgid "Pause Sync" msgid "Pause Sync"
msgstr "Metti in pausa sincronizzazione" msgstr "Metti in pausa sincronizzazione"
#: crates/pdfs-gui/src/main.rs:442 #: crates/pdfs-gui/src/main.rs:451
msgid "Connect" msgid "Connect"
msgstr "Connetti" msgstr "Connetti"
#: crates/pdfs-gui/src/main.rs:449 #: crates/pdfs-gui/src/main.rs:458
msgid "Hide Tray Icon" msgid "Hide Tray Icon"
msgstr "Nascondi icona nell'area di notifica" msgstr "Nascondi icona nell'area di notifica"
#: crates/pdfs-gui/src/main.rs:453 #: crates/pdfs-gui/src/main.rs:462
msgid "Stop Proton Drive…" msgid "Stop Proton Drive…"
msgstr "Arresta Proton Drive…" msgstr "Arresta Proton Drive…"
#: crates/pdfs-gui/src/main.rs:457 #: crates/pdfs-gui/src/main.rs:466
msgid "Quit" msgid "Quit"
msgstr "Esci" msgstr "Esci"
+218 -141
View File
@@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: proton-drive-linux 2.1.0\n" "Project-Id-Version: proton-drive-linux 2.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-10-05 01:01+0200\n" "POT-Creation-Date: 2026-10-08 10:15+0200\n"
"PO-Revision-Date: 2026-09-23 12:00+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n"
"Last-Translator: Proton Drive for Linux contributors\n" "Last-Translator: Proton Drive for Linux contributors\n"
"Language-Team: Dutch\n" "Language-Team: Dutch\n"
@@ -57,7 +57,7 @@ msgstr "%-d %b %Y"
msgid "%A, %B %-d, %Y, %H:%M" msgid "%A, %B %-d, %Y, %H:%M"
msgstr "%A %-d %B %Y, %H:%M" msgstr "%A %-d %B %Y, %H:%M"
#: crates/pdfs-gui/src/app/main.rs:743 #: crates/pdfs-gui/src/app/main.rs:760
#: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539
#: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:454
#: crates/pdfs-gui/src/app/pages/browser.rs:819 #: crates/pdfs-gui/src/app/pages/browser.rs:819
@@ -70,91 +70,91 @@ msgstr "%A %-d %B %Y, %H:%M"
msgid "My files" msgid "My files"
msgstr "Mijn bestanden" msgstr "Mijn bestanden"
#: crates/pdfs-gui/src/app/main.rs:746 crates/pdfs-gui/src/app/main.rs:1390 #: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407
#: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/albums.rs:638
#: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/photos.rs:842
#: crates/pdfs-gui/src/app/pages/places.rs:441 #: crates/pdfs-gui/src/app/pages/places.rs:441
msgid "Photos" msgid "Photos"
msgstr "Foto's" msgstr "Foto's"
#: crates/pdfs-gui/src/app/main.rs:751 #: crates/pdfs-gui/src/app/main.rs:768
#: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:168
#: crates/pdfs-gui/src/app/pages/shared.rs:482 #: crates/pdfs-gui/src/app/pages/shared.rs:482
msgid "Shared with me" msgid "Shared with me"
msgstr "Gedeeld met mij" msgstr "Gedeeld met mij"
#: crates/pdfs-gui/src/app/main.rs:756 #: crates/pdfs-gui/src/app/main.rs:773
#: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120
msgid "Shared by me" msgid "Shared by me"
msgstr "Gedeeld door mij" msgstr "Gedeeld door mij"
#: crates/pdfs-gui/src/app/main.rs:759 #: crates/pdfs-gui/src/app/main.rs:776
#: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:140
#: crates/pdfs-gui/src/app/pages/devices.rs:481 #: crates/pdfs-gui/src/app/pages/devices.rs:481
msgid "Computers" msgid "Computers"
msgstr "Computers" msgstr "Computers"
#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/main.rs:777
#: crates/pdfs-gui/src/app/pages/trash.rs:101 #: crates/pdfs-gui/src/app/pages/trash.rs:101
msgid "Trash" msgid "Trash"
msgstr "Prullenbak" msgstr "Prullenbak"
#. Translators: sidebar entry for the page listing this computer's synced folders. #. Translators: sidebar entry for the page listing this computer's synced folders.
#: crates/pdfs-gui/src/app/main.rs:762 #: crates/pdfs-gui/src/app/main.rs:779
msgid "Sync" msgid "Sync"
msgstr "Synchronisatie" msgstr "Synchronisatie"
#. Translators: a failed action's toast. {what} says what failed, such as #. Translators: a failed action's toast. {what} says what failed, such as
#. "Couldn't rename"; {detail} is the reason, usually untranslated text #. "Couldn't rename"; {detail} is the reason, usually untranslated text
#. from the mount service. #. from the mount service.
#: crates/pdfs-gui/src/app/main.rs:1079 #: crates/pdfs-gui/src/app/main.rs:1096
#, rust-format #, rust-format
msgid "{what}: {detail}" msgid "{what}: {detail}"
msgstr "{what}: {detail}" msgstr "{what}: {detail}"
#: crates/pdfs-gui/src/app/main.rs:1098 #: crates/pdfs-gui/src/app/main.rs:1115
msgid "You're offline" msgid "You're offline"
msgstr "Je bent offline" msgstr "Je bent offline"
#: crates/pdfs-gui/src/app/main.rs:1099 #: crates/pdfs-gui/src/app/main.rs:1116
msgid "That's not there any more" msgid "That's not there any more"
msgstr "Dat bestaat niet meer" msgstr "Dat bestaat niet meer"
#: crates/pdfs-gui/src/app/main.rs:1100 #: crates/pdfs-gui/src/app/main.rs:1117
msgid "You don't have access to that" msgid "You don't have access to that"
msgstr "Je hebt daar geen toegang toe" msgstr "Je hebt daar geen toegang toe"
#: crates/pdfs-gui/src/app/main.rs:1101 #: crates/pdfs-gui/src/app/main.rs:1118
msgid "Something changed this first" msgid "Something changed this first"
msgstr "Er is eerst iets anders gewijzigd" msgstr "Er is eerst iets anders gewijzigd"
#: crates/pdfs-gui/src/app/main.rs:1102 #: crates/pdfs-gui/src/app/main.rs:1119
msgid "Your Proton Drive is full" msgid "Your Proton Drive is full"
msgstr "Je Proton Drive is vol" msgstr "Je Proton Drive is vol"
#: crates/pdfs-gui/src/app/main.rs:1143 crates/pdfs-gui/src/app/main.rs:1372 #: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389
#: crates/pdfs-gui/src/app/pages/status.rs:385 #: crates/pdfs-gui/src/app/pages/status.rs:385
msgid "Preferences" msgid "Preferences"
msgstr "Voorkeuren" msgstr "Voorkeuren"
#: crates/pdfs-gui/src/app/main.rs:1144 #: crates/pdfs-gui/src/app/main.rs:1161
#: crates/pdfs-gui/src/app/widgets/compat.rs:262 #: crates/pdfs-gui/src/app/widgets/compat.rs:262
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "Sneltoetsen" msgstr "Sneltoetsen"
#: crates/pdfs-gui/src/app/main.rs:1146 #: crates/pdfs-gui/src/app/main.rs:1163
msgid "About Proton Drive for Linux" msgid "About Proton Drive for Linux"
msgstr "Over Proton Drive for Linux" msgstr "Over Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1151 #: crates/pdfs-gui/src/app/main.rs:1168
msgid "Main menu" msgid "Main menu"
msgstr "Hoofdmenu" msgstr "Hoofdmenu"
#: crates/pdfs-gui/src/app/main.rs:1207 #: crates/pdfs-gui/src/app/main.rs:1224
msgid "Proton Drive for Linux" msgid "Proton Drive for Linux"
msgstr "Proton Drive for Linux" msgstr "Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1215 #: crates/pdfs-gui/src/app/main.rs:1232
msgid "" msgid ""
"Files-on-demand Proton Drive for the Linux desktop.\n" "Files-on-demand Proton Drive for the Linux desktop.\n"
"\n" "\n"
@@ -166,23 +166,23 @@ msgstr ""
"Onofficiële client — niet gelieerd aan, onderschreven door of ondersteund " "Onofficiële client — niet gelieerd aan, onderschreven door of ondersteund "
"door Proton AG." "door Proton AG."
#: crates/pdfs-gui/src/app/main.rs:1220 #: crates/pdfs-gui/src/app/main.rs:1237
msgid "Source Code" msgid "Source Code"
msgstr "Broncode" msgstr "Broncode"
#: crates/pdfs-gui/src/app/main.rs:1224 #: crates/pdfs-gui/src/app/main.rs:1241
msgid "Proton Terms of Service" msgid "Proton Terms of Service"
msgstr "Servicevoorwaarden van Proton" msgstr "Servicevoorwaarden van Proton"
#: crates/pdfs-gui/src/app/main.rs:1228 #: crates/pdfs-gui/src/app/main.rs:1245
msgid "Proton Privacy Policy" msgid "Proton Privacy Policy"
msgstr "Privacybeleid van Proton" msgstr "Privacybeleid van Proton"
#: crates/pdfs-gui/src/app/main.rs:1233 #: crates/pdfs-gui/src/app/main.rs:1250
msgid "Disclaimer" msgid "Disclaimer"
msgstr "Disclaimer" msgstr "Disclaimer"
#: crates/pdfs-gui/src/app/main.rs:1237 #: crates/pdfs-gui/src/app/main.rs:1254
msgid "" msgid ""
"This is an unofficial, community-built client. It is not affiliated with, " "This is an unofficial, community-built client. It is not affiliated with, "
"endorsed by, or supported by Proton AG.\n" "endorsed by, or supported by Proton AG.\n"
@@ -205,7 +205,7 @@ msgstr ""
"Deze software wordt geleverd “zoals ze is”, zonder enige garantie. Bewaar " "Deze software wordt geleverd “zoals ze is”, zonder enige garantie. Bewaar "
"een kopie van gegevens die je niet kwijt wilt raken." "een kopie van gegevens die je niet kwijt wilt raken."
#: crates/pdfs-gui/src/app/main.rs:1245 #: crates/pdfs-gui/src/app/main.rs:1262
msgid "" msgid ""
"Places are named with data from <a href=\"https://www.geonames.org/" "Places are named with data from <a href=\"https://www.geonames.org/"
"\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/"
@@ -215,7 +215,7 @@ msgstr ""
"\">GeoNames</a>, gelicentieerd onder <a href=\"https://creativecommons.org/" "\">GeoNames</a>, gelicentieerd onder <a href=\"https://creativecommons.org/"
"licenses/by/4.0/\">CC BY 4.0</a>." "licenses/by/4.0/\">CC BY 4.0</a>."
#: crates/pdfs-gui/src/app/main.rs:1253 #: crates/pdfs-gui/src/app/main.rs:1270
msgid "" msgid ""
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/" "The places map is drawn from <a href=\"https://www.naturalearthdata.com/"
"\">Natural Earth</a> land outlines and country borders, which are in the " "\">Natural Earth</a> land outlines and country borders, which are in the "
@@ -225,7 +225,7 @@ msgstr ""
"href=\"https://www.naturalearthdata.com/\">Natural Earth</a>, die in het " "href=\"https://www.naturalearthdata.com/\">Natural Earth</a>, die in het "
"publieke domein vallen." "publieke domein vallen."
#: crates/pdfs-gui/src/app/main.rs:1261 #: crates/pdfs-gui/src/app/main.rs:1278
msgid "" msgid ""
"The street map, when turned on, comes from <a href=\"https://openfreemap.org/" "The street map, when turned on, comes from <a href=\"https://openfreemap.org/"
"\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/" "\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/"
@@ -241,15 +241,15 @@ msgstr ""
#. Translators: replace with your name(s) and email address(es), one #. Translators: replace with your name(s) and email address(es), one
#. per line, to be credited in the About dialog. #. per line, to be credited in the About dialog.
#: crates/pdfs-gui/src/app/main.rs:1272 #: crates/pdfs-gui/src/app/main.rs:1289
msgid "translator-credits" msgid "translator-credits"
msgstr "Bijdragers aan Proton Drive for Linux" msgstr "Bijdragers aan Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1310 #: crates/pdfs-gui/src/app/main.rs:1327
msgid "Stop Proton Drive?" msgid "Stop Proton Drive?"
msgstr "Proton Drive stoppen?" msgstr "Proton Drive stoppen?"
#: crates/pdfs-gui/src/app/main.rs:1311 #: crates/pdfs-gui/src/app/main.rs:1328
msgid "" msgid ""
"My files goes offline and nothing syncs until you connect again. Proton " "My files goes offline and nothing syncs until you connect again. Proton "
"Drive starts again at your next login." "Drive starts again at your next login."
@@ -257,7 +257,7 @@ msgstr ""
"Mijn bestanden gaat offline en er wordt niets gesynchroniseerd tot je weer " "Mijn bestanden gaat offline en er wordt niets gesynchroniseerd tot je weer "
"verbinding maakt. Proton Drive start opnieuw bij je volgende aanmelding." "verbinding maakt. Proton Drive start opnieuw bij je volgende aanmelding."
#: crates/pdfs-gui/src/app/main.rs:1313 crates/pdfs-gui/src/app/main.rs:1588 #: crates/pdfs-gui/src/app/main.rs:1330 crates/pdfs-gui/src/app/main.rs:1605
#: crates/pdfs-gui/src/app/pages/albums.rs:243 #: crates/pdfs-gui/src/app/pages/albums.rs:243
#: crates/pdfs-gui/src/app/pages/albums.rs:428 #: crates/pdfs-gui/src/app/pages/albums.rs:428
#: crates/pdfs-gui/src/app/pages/browser.rs:1892 #: crates/pdfs-gui/src/app/pages/browser.rs:1892
@@ -272,7 +272,8 @@ msgstr ""
#: crates/pdfs-gui/src/app/pages/locations.rs:1013 #: crates/pdfs-gui/src/app/pages/locations.rs:1013
#: crates/pdfs-gui/src/app/pages/locations.rs:1488 #: crates/pdfs-gui/src/app/pages/locations.rs:1488
#: crates/pdfs-gui/src/app/pages/locations.rs:1538 #: crates/pdfs-gui/src/app/pages/locations.rs:1538
#: crates/pdfs-gui/src/app/pages/login.rs:312 #: crates/pdfs-gui/src/app/pages/login.rs:393
#: crates/pdfs-gui/src/app/pages/login.rs:507
#: crates/pdfs-gui/src/app/pages/photos.rs:2055 #: crates/pdfs-gui/src/app/pages/photos.rs:2055
#: crates/pdfs-gui/src/app/pages/shared.rs:946 #: crates/pdfs-gui/src/app/pages/shared.rs:946
#: crates/pdfs-gui/src/app/pages/shared.rs:1027 #: crates/pdfs-gui/src/app/pages/shared.rs:1027
@@ -287,30 +288,30 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Annuleren" msgstr "Annuleren"
#: crates/pdfs-gui/src/app/main.rs:1314 #: crates/pdfs-gui/src/app/main.rs:1331
msgctxt "verb" msgctxt "verb"
msgid "Stop" msgid "Stop"
msgstr "Stoppen" msgstr "Stoppen"
#: crates/pdfs-gui/src/app/main.rs:1331 #: crates/pdfs-gui/src/app/main.rs:1348
msgid "Couldn't stop Proton Drive" msgid "Couldn't stop Proton Drive"
msgstr "Kon Proton Drive niet stoppen" msgstr "Kon Proton Drive niet stoppen"
#: crates/pdfs-gui/src/app/main.rs:1332 #: crates/pdfs-gui/src/app/main.rs:1349
msgid "The Proton Drive service did not stop." msgid "The Proton Drive service did not stop."
msgstr "De Proton Drive-service is niet gestopt." msgstr "De Proton Drive-service is niet gestopt."
#: crates/pdfs-gui/src/app/main.rs:1368 #: crates/pdfs-gui/src/app/main.rs:1385
#: crates/pdfs-gui/src/app/pages/status.rs:254 #: crates/pdfs-gui/src/app/pages/status.rs:254
msgid "General" msgid "General"
msgstr "Algemeen" msgstr "Algemeen"
#: crates/pdfs-gui/src/app/main.rs:1370 #: crates/pdfs-gui/src/app/main.rs:1387
#: crates/pdfs-gui/src/app/pages/browser.rs:247 #: crates/pdfs-gui/src/app/pages/browser.rs:247
msgid "Search Drive" msgid "Search Drive"
msgstr "Drive doorzoeken" msgstr "Drive doorzoeken"
#: crates/pdfs-gui/src/app/main.rs:1371 #: crates/pdfs-gui/src/app/main.rs:1388
#: crates/pdfs-gui/src/app/pages/browser.rs:957 #: crates/pdfs-gui/src/app/pages/browser.rs:957
#: crates/pdfs-gui/src/app/pages/devices.rs:554 #: crates/pdfs-gui/src/app/pages/devices.rs:554
#: crates/pdfs-gui/src/app/pages/shared.rs:586 #: crates/pdfs-gui/src/app/pages/shared.rs:586
@@ -319,38 +320,38 @@ msgstr "Drive doorzoeken"
msgid "Refresh" msgid "Refresh"
msgstr "Verversen" msgstr "Verversen"
#: crates/pdfs-gui/src/app/main.rs:1373 #: crates/pdfs-gui/src/app/main.rs:1390
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "Sneltoetsen" msgstr "Sneltoetsen"
#: crates/pdfs-gui/src/app/main.rs:1377 #: crates/pdfs-gui/src/app/main.rs:1394
msgid "Files" msgid "Files"
msgstr "Bestanden" msgstr "Bestanden"
#: crates/pdfs-gui/src/app/main.rs:1379 #: crates/pdfs-gui/src/app/main.rs:1396
#: crates/pdfs-gui/src/app/pages/browser.rs:294 #: crates/pdfs-gui/src/app/pages/browser.rs:294
msgid "New folder" msgid "New folder"
msgstr "Nieuwe map" msgstr "Nieuwe map"
#: crates/pdfs-gui/src/app/main.rs:1380 #: crates/pdfs-gui/src/app/main.rs:1397
#: crates/pdfs-gui/src/app/pages/browser.rs:290 #: crates/pdfs-gui/src/app/pages/browser.rs:290
msgid "Upload files" msgid "Upload files"
msgstr "Bestanden uploaden" msgstr "Bestanden uploaden"
#: crates/pdfs-gui/src/app/main.rs:1381 #: crates/pdfs-gui/src/app/main.rs:1398
msgid "Back / forward" msgid "Back / forward"
msgstr "Terug / vooruit" msgstr "Terug / vooruit"
#: crates/pdfs-gui/src/app/main.rs:1382 #: crates/pdfs-gui/src/app/main.rs:1399
#: crates/pdfs-gui/src/app/pages/browser.rs:1943 #: crates/pdfs-gui/src/app/pages/browser.rs:1943
msgid "Parent folder" msgid "Parent folder"
msgstr "Bovenliggende map" msgstr "Bovenliggende map"
#: crates/pdfs-gui/src/app/main.rs:1383 #: crates/pdfs-gui/src/app/main.rs:1400
msgid "Grid / list" msgid "Grid / list"
msgstr "Raster / lijst" msgstr "Raster / lijst"
#: crates/pdfs-gui/src/app/main.rs:1384 #: crates/pdfs-gui/src/app/main.rs:1401
#: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/albums.rs:298
#: crates/pdfs-gui/src/app/pages/browser.rs:1871 #: crates/pdfs-gui/src/app/pages/browser.rs:1871
#: crates/pdfs-gui/src/app/pages/browser.rs:1893 #: crates/pdfs-gui/src/app/pages/browser.rs:1893
@@ -359,7 +360,7 @@ msgstr "Raster / lijst"
msgid "Rename" msgid "Rename"
msgstr "Hernoemen" msgstr "Hernoemen"
#: crates/pdfs-gui/src/app/main.rs:1385 crates/pdfs-gui/src/app/main.rs:1405 #: crates/pdfs-gui/src/app/main.rs:1402 crates/pdfs-gui/src/app/main.rs:1422
#: crates/pdfs-gui/src/app/pages/browser.rs:357 #: crates/pdfs-gui/src/app/pages/browser.rs:357
#: crates/pdfs-gui/src/app/pages/browser.rs:900 #: crates/pdfs-gui/src/app/pages/browser.rs:900
#: crates/pdfs-gui/src/app/pages/browser.rs:932 #: crates/pdfs-gui/src/app/pages/browser.rs:932
@@ -369,73 +370,73 @@ msgstr "Hernoemen"
msgid "Move to Trash" msgid "Move to Trash"
msgstr "Naar prullenbak verplaatsen" msgstr "Naar prullenbak verplaatsen"
#: crates/pdfs-gui/src/app/main.rs:1386 #: crates/pdfs-gui/src/app/main.rs:1403
msgid "Clear the selection" msgid "Clear the selection"
msgstr "Selectie wissen" msgstr "Selectie wissen"
#: crates/pdfs-gui/src/app/main.rs:1392 #: crates/pdfs-gui/src/app/main.rs:1409
msgid "Larger thumbnails" msgid "Larger thumbnails"
msgstr "Grotere miniaturen" msgstr "Grotere miniaturen"
#: crates/pdfs-gui/src/app/main.rs:1393 #: crates/pdfs-gui/src/app/main.rs:1410
msgid "Smaller thumbnails" msgid "Smaller thumbnails"
msgstr "Kleinere miniaturen" msgstr "Kleinere miniaturen"
#: crates/pdfs-gui/src/app/main.rs:1394 #: crates/pdfs-gui/src/app/main.rs:1411
msgid "Reset thumbnail size" msgid "Reset thumbnail size"
msgstr "Miniatuurgrootte herstellen" msgstr "Miniatuurgrootte herstellen"
#: crates/pdfs-gui/src/app/main.rs:1395 #: crates/pdfs-gui/src/app/main.rs:1412
msgid "Select all" msgid "Select all"
msgstr "Alles selecteren" msgstr "Alles selecteren"
#: crates/pdfs-gui/src/app/main.rs:1399 #: crates/pdfs-gui/src/app/main.rs:1416
msgid "Photo Viewer" msgid "Photo Viewer"
msgstr "Fotoweergave" msgstr "Fotoweergave"
#: crates/pdfs-gui/src/app/main.rs:1401 #: crates/pdfs-gui/src/app/main.rs:1418
msgid "Previous / next photo" msgid "Previous / next photo"
msgstr "Vorige / volgende foto" msgstr "Vorige / volgende foto"
#: crates/pdfs-gui/src/app/main.rs:1402 #: crates/pdfs-gui/src/app/main.rs:1419
msgid "First / last photo" msgid "First / last photo"
msgstr "Eerste / laatste foto" msgstr "Eerste / laatste foto"
#: crates/pdfs-gui/src/app/main.rs:1403 #: crates/pdfs-gui/src/app/main.rs:1420
msgid "Show details" msgid "Show details"
msgstr "Details tonen" msgstr "Details tonen"
#: crates/pdfs-gui/src/app/main.rs:1404 #: crates/pdfs-gui/src/app/main.rs:1421
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Volledig scherm" msgstr "Volledig scherm"
#: crates/pdfs-gui/src/app/main.rs:1406 #: crates/pdfs-gui/src/app/main.rs:1423
msgid "Close" msgid "Close"
msgstr "Sluiten" msgstr "Sluiten"
#: crates/pdfs-gui/src/app/main.rs:1496 #: crates/pdfs-gui/src/app/main.rs:1513
msgid "Select a single item to rename it" msgid "Select a single item to rename it"
msgstr "Selecteer één item om het te hernoemen" msgstr "Selecteer één item om het te hernoemen"
#: crates/pdfs-gui/src/app/main.rs:1656 #: crates/pdfs-gui/src/app/main.rs:1673
#: crates/pdfs-gui/src/app/pages/albums.rs:85 #: crates/pdfs-gui/src/app/pages/albums.rs:85
#: crates/pdfs-gui/src/app/pages/places.rs:104 #: crates/pdfs-gui/src/app/pages/places.rs:104
#: crates/pdfs-gui/src/app/pages/status.rs:1370 #: crates/pdfs-gui/src/app/pages/status.rs:1371
msgid "Not connected" msgid "Not connected"
msgstr "Niet verbonden" msgstr "Niet verbonden"
#: crates/pdfs-gui/src/app/main.rs:1657 #: crates/pdfs-gui/src/app/main.rs:1674
msgid "The Proton Drive service isn't running." msgid "The Proton Drive service isn't running."
msgstr "De Proton Drive-service draait niet." msgstr "De Proton Drive-service draait niet."
#: crates/pdfs-gui/src/app/main.rs:1665 #: crates/pdfs-gui/src/app/main.rs:1682
#: crates/pdfs-gui/src/app/pages/status.rs:336 #: crates/pdfs-gui/src/app/pages/status.rs:336
#: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595 #: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595
#: crates/pdfs-gui/src/main.rs:444 #: crates/pdfs-gui/src/main.rs:453
msgid "Connecting…" msgid "Connecting…"
msgstr "Verbinden…" msgstr "Verbinden…"
#: crates/pdfs-gui/src/app/main.rs:1666 #: crates/pdfs-gui/src/app/main.rs:1683
msgid "Waiting for the Proton Drive service to start." msgid "Waiting for the Proton Drive service to start."
msgstr "Wachten tot de Proton Drive-service is gestart." msgstr "Wachten tot de Proton Drive-service is gestart."
@@ -1282,7 +1283,7 @@ msgstr "Kon albums niet laden"
#: crates/pdfs-gui/src/app/pages/shared.rs:1006 #: crates/pdfs-gui/src/app/pages/shared.rs:1006
#: crates/pdfs-gui/src/app/pages/shared.rs:1122 #: crates/pdfs-gui/src/app/pages/shared.rs:1122
#: crates/pdfs-gui/src/app/pages/status.rs:750 #: crates/pdfs-gui/src/app/pages/status.rs:750
#: crates/pdfs-gui/src/app/pages/status.rs:1517 #: crates/pdfs-gui/src/app/pages/status.rs:1518
#: crates/pdfs-gui/src/app/pages/takeout.rs:424 #: crates/pdfs-gui/src/app/pages/takeout.rs:424
#: crates/pdfs-gui/src/app/pages/takeout.rs:617 #: crates/pdfs-gui/src/app/pages/takeout.rs:617
#: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438
@@ -1550,7 +1551,7 @@ msgid "Make available offline"
msgstr "Offline beschikbaar maken" msgstr "Offline beschikbaar maken"
#: crates/pdfs-gui/src/app/pages/browser.rs:347 #: crates/pdfs-gui/src/app/pages/browser.rs:347
#: crates/pdfs-gui/src/app/pages/status.rs:1493 #: crates/pdfs-gui/src/app/pages/status.rs:1494
msgid "Make online only" msgid "Make online only"
msgstr "Alleen online maken" msgstr "Alleen online maken"
@@ -2713,7 +2714,7 @@ msgid "Sync paused until you resume"
msgstr "Synchronisatie gepauzeerd totdat je hervat" msgstr "Synchronisatie gepauzeerd totdat je hervat"
#: crates/pdfs-gui/src/app/pages/locations.rs:529 #: crates/pdfs-gui/src/app/pages/locations.rs:529
#: crates/pdfs-gui/src/app/pages/status.rs:1323 crates/pdfs-gui/src/main.rs:237 #: crates/pdfs-gui/src/app/pages/status.rs:1324 crates/pdfs-gui/src/main.rs:246
msgid "Sync paused" msgid "Sync paused"
msgstr "Synchronisatie gepauzeerd" msgstr "Synchronisatie gepauzeerd"
@@ -3185,26 +3186,37 @@ msgstr "Voer je e-mailadres en wachtwoord in."
msgid "Signing in…" msgid "Signing in…"
msgstr "Aanmelden…" msgstr "Aanmelden…"
#: crates/pdfs-gui/src/app/pages/login.rs:155 #: crates/pdfs-gui/src/app/pages/login.rs:164
msgid "Complete the verification to continue…" msgid "Complete the verification to continue…"
msgstr "Voltooi de verificatie om verder te gaan…" msgstr "Voltooi de verificatie om verder te gaan…"
#: crates/pdfs-gui/src/app/pages/login.rs:165 #: crates/pdfs-gui/src/app/pages/login.rs:197
msgid "login cancelled" msgid "login cancelled"
msgstr "aanmelden geannuleerd" msgstr "aanmelden geannuleerd"
#: crates/pdfs-gui/src/app/pages/login.rs:198 #: crates/pdfs-gui/src/app/pages/login.rs:210
msgid ""
"The verification page couldn't be opened, so the sign-in was cancelled. Your "
"system may keep the web view's sandbox from starting; the troubleshooting "
"guide explains what to do."
msgstr ""
"De verificatiepagina kon niet worden geopend, dus het aanmelden is "
"geannuleerd. Mogelijk verhindert je systeem dat de sandbox van de "
"webweergave start; de handleiding voor probleemoplossing legt uit wat je "
"kunt doen."
#: crates/pdfs-gui/src/app/pages/login.rs:233
msgid "Too many sign-in attempts. Wait a few minutes and try again." msgid "Too many sign-in attempts. Wait a few minutes and try again."
msgstr "" msgstr ""
"Te veel aanmeldpogingen. Wacht een paar minuten en probeer het opnieuw." "Te veel aanmeldpogingen. Wacht een paar minuten en probeer het opnieuw."
#: crates/pdfs-gui/src/app/pages/login.rs:204 #: crates/pdfs-gui/src/app/pages/login.rs:239
msgid "Couldn't reach Proton. Check your internet connection and try again." msgid "Couldn't reach Proton. Check your internet connection and try again."
msgstr "" msgstr ""
"Kon Proton niet bereiken. Controleer je internetverbinding en probeer het " "Kon Proton niet bereiken. Controleer je internetverbinding en probeer het "
"opnieuw." "opnieuw."
#: crates/pdfs-gui/src/app/pages/login.rs:207 #: crates/pdfs-gui/src/app/pages/login.rs:242
msgid "" msgid ""
"Signed in, but the session couldn't be saved to the system keyring. Make " "Signed in, but the session couldn't be saved to the system keyring. Make "
"sure a keyring (GNOME Keyring or KWallet) is running and unlocked." "sure a keyring (GNOME Keyring or KWallet) is running and unlocked."
@@ -3213,11 +3225,19 @@ msgstr ""
"opgeslagen. Zorg dat een sleutelbos (GNOME Keyring of KWallet) actief en " "opgeslagen. Zorg dat een sleutelbos (GNOME Keyring of KWallet) actief en "
"ontgrendeld is." "ontgrendeld is."
#: crates/pdfs-gui/src/app/pages/login.rs:210 #: crates/pdfs-gui/src/app/pages/login.rs:245
msgid "Sign-in cancelled: no two-factor code was entered." msgid "Sign-in cancelled: no two-factor code was entered."
msgstr "Aanmelden geannuleerd: er is geen tweestapscode ingevoerd." msgstr "Aanmelden geannuleerd: er is geen tweestapscode ingevoerd."
#: crates/pdfs-gui/src/app/pages/login.rs:213 #: crates/pdfs-gui/src/app/pages/login.rs:248
msgid "Sign-in cancelled: no mailbox password was entered."
msgstr "Aanmelden geannuleerd: er is geen mailboxwachtwoord ingevoerd."
#: crates/pdfs-gui/src/app/pages/login.rs:251
msgid "That mailbox password didn't unlock your account."
msgstr "Dat mailboxwachtwoord heeft je account niet ontgrendeld."
#: crates/pdfs-gui/src/app/pages/login.rs:254
msgid "" msgid ""
"Sign-in cancelled: the verification wasn't completed. Sign in again to get a " "Sign-in cancelled: the verification wasn't completed. Sign in again to get a "
"new one." "new one."
@@ -3226,33 +3246,85 @@ msgstr ""
"om een nieuwe te krijgen." "om een nieuwe te krijgen."
#. Translators: {error} is an error message, usually in English. #. Translators: {error} is an error message, usually in English.
#: crates/pdfs-gui/src/app/pages/login.rs:218 #: crates/pdfs-gui/src/app/pages/login.rs:259
#, rust-format #, rust-format
msgid "Sign-in failed: {error}" msgid "Sign-in failed: {error}"
msgstr "Aanmelden mislukt: {error}" msgstr "Aanmelden mislukt: {error}"
#: crates/pdfs-gui/src/app/pages/login.rs:298 #: crates/pdfs-gui/src/app/pages/login.rs:369
msgid "That mailbox password didn't unlock your account. Try again."
msgstr ""
"Dat mailboxwachtwoord heeft je account niet ontgrendeld. Probeer het opnieuw."
#: crates/pdfs-gui/src/app/pages/login.rs:371
msgid ""
"This account has a separate mailbox password. Enter it to unlock your files."
msgstr ""
"Dit account heeft een apart mailboxwachtwoord. Voer het in om je bestanden "
"te ontgrendelen."
#: crates/pdfs-gui/src/app/pages/login.rs:374
msgid "Mailbox Password"
msgstr "Mailboxwachtwoord"
#: crates/pdfs-gui/src/app/pages/login.rs:380
msgid "Mailbox password"
msgstr "Mailboxwachtwoord"
#: crates/pdfs-gui/src/app/pages/login.rs:384
msgid "Keep unlocked on this computer"
msgstr "Ontgrendeld houden op deze computer"
#: crates/pdfs-gui/src/app/pages/login.rs:386
msgid ""
"Stores the unlock in the system keyring, so Proton Drive opens without "
"asking after every login. Otherwise you enter the password once per session."
msgstr ""
"Bewaart de ontgrendeling in de sleutelhanger van het systeem, zodat Proton "
"Drive na elke aanmelding opent zonder te vragen. Anders voer je het "
"wachtwoord één keer per sessie in."
#: crates/pdfs-gui/src/app/pages/login.rs:394
#: crates/pdfs-gui/src/app/pages/login.rs:447
msgid "Unlock"
msgstr "Ontgrendelen"
#: crates/pdfs-gui/src/app/pages/login.rs:446
msgid "Proton Drive is locked. Enter your mailbox password to unlock it."
msgstr ""
"Proton Drive is vergrendeld. Voer je mailboxwachtwoord in om het te "
"ontgrendelen."
#: crates/pdfs-gui/src/app/pages/login.rs:475
msgid "Unlocked."
msgstr "Ontgrendeld."
#: crates/pdfs-gui/src/app/pages/login.rs:480
msgid "Couldn't unlock"
msgstr "Ontgrendelen mislukt"
#: crates/pdfs-gui/src/app/pages/login.rs:493
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "Tweestapsverificatie" msgstr "Tweestapsverificatie"
#: crates/pdfs-gui/src/app/pages/login.rs:300 #: crates/pdfs-gui/src/app/pages/login.rs:495
msgid "" msgid ""
"Enter the code from your authenticator app, or one of your recovery codes." "Enter the code from your authenticator app, or one of your recovery codes."
msgstr "Voer de code uit je authenticator-app in, of een van je herstelcodes." msgstr "Voer de code uit je authenticator-app in, of een van je herstelcodes."
#: crates/pdfs-gui/src/app/pages/login.rs:306 #: crates/pdfs-gui/src/app/pages/login.rs:501
msgid "Authentication code" msgid "Authentication code"
msgstr "Verificatiecode" msgstr "Verificatiecode"
#: crates/pdfs-gui/src/app/pages/login.rs:313 #: crates/pdfs-gui/src/app/pages/login.rs:508
msgid "Confirm" msgid "Confirm"
msgstr "Bevestigen" msgstr "Bevestigen"
#: crates/pdfs-gui/src/app/pages/login.rs:341 #: crates/pdfs-gui/src/app/pages/login.rs:536
msgid "Sign Out?" msgid "Sign Out?"
msgstr "Afmelden?" msgstr "Afmelden?"
#: crates/pdfs-gui/src/app/pages/login.rs:343 #: crates/pdfs-gui/src/app/pages/login.rs:538
msgid "" msgid ""
"Proton Drive will disconnect and stop syncing until you sign in again. Files " "Proton Drive will disconnect and stop syncing until you sign in again. Files "
"already on this computer stay where they are." "already on this computer stay where they are."
@@ -3261,7 +3333,7 @@ msgstr ""
"je opnieuw aanmeldt. Bestanden die al op deze computer staan, blijven waar " "je opnieuw aanmeldt. Bestanden die al op deze computer staan, blijven waar "
"ze zijn." "ze zijn."
#: crates/pdfs-gui/src/app/pages/login.rs:345 #: crates/pdfs-gui/src/app/pages/login.rs:540
msgid "Sign Out" msgid "Sign Out"
msgstr "Afmelden" msgstr "Afmelden"
@@ -4620,40 +4692,40 @@ msgid "Restart Now"
msgstr "Nu opnieuw starten" msgstr "Nu opnieuw starten"
#. Translators: {size} is an amount of storage, such as "1.2 GiB". #. Translators: {size} is an amount of storage, such as "1.2 GiB".
#: crates/pdfs-gui/src/app/pages/status.rs:912 #: crates/pdfs-gui/src/app/pages/status.rs:913
#, rust-format #, rust-format
msgid "{size} used" msgid "{size} used"
msgstr "{size} gebruikt" msgstr "{size} gebruikt"
#: crates/pdfs-gui/src/app/pages/status.rs:923 #: crates/pdfs-gui/src/app/pages/status.rs:924
#, rust-format #, rust-format
msgid "{used} of {total} used ({percent}%)" msgid "{used} of {total} used ({percent}%)"
msgstr "{used} van {total} gebruikt ({percent}%)" msgstr "{used} van {total} gebruikt ({percent}%)"
#: crates/pdfs-gui/src/app/pages/status.rs:961 #: crates/pdfs-gui/src/app/pages/status.rs:962
msgid "Proton Drive connected" msgid "Proton Drive connected"
msgstr "Proton Drive verbonden" msgstr "Proton Drive verbonden"
#: crates/pdfs-gui/src/app/pages/status.rs:962 #: crates/pdfs-gui/src/app/pages/status.rs:963
msgid "Your files are available." msgid "Your files are available."
msgstr "Je bestanden zijn beschikbaar." msgstr "Je bestanden zijn beschikbaar."
#: crates/pdfs-gui/src/app/pages/status.rs:967 #: crates/pdfs-gui/src/app/pages/status.rs:968
msgid "Proton Drive disconnected" msgid "Proton Drive disconnected"
msgstr "Verbinding met Proton Drive verbroken" msgstr "Verbinding met Proton Drive verbroken"
#: crates/pdfs-gui/src/app/pages/status.rs:968 #: crates/pdfs-gui/src/app/pages/status.rs:969
msgid "" msgid ""
"The Proton Drive service stopped. Files aren't available until it restarts." "The Proton Drive service stopped. Files aren't available until it restarts."
msgstr "" msgstr ""
"De Proton Drive-service is gestopt. Bestanden zijn pas weer beschikbaar als " "De Proton Drive-service is gestopt. Bestanden zijn pas weer beschikbaar als "
"hij opnieuw start." "hij opnieuw start."
#: crates/pdfs-gui/src/app/pages/status.rs:1037 #: crates/pdfs-gui/src/app/pages/status.rs:1038
msgid "Sync complete" msgid "Sync complete"
msgstr "Synchronisatie voltooid" msgstr "Synchronisatie voltooid"
#: crates/pdfs-gui/src/app/pages/status.rs:1039 #: crates/pdfs-gui/src/app/pages/status.rs:1040
#, rust-format #, rust-format
msgid "{n} file finished transferring." msgid "{n} file finished transferring."
msgid_plural "{n} files finished transferring." msgid_plural "{n} files finished transferring."
@@ -4662,27 +4734,27 @@ msgstr[1] "{n} bestanden zijn klaar met overdragen."
#. Translators: a background job's progress. {title} is the job, such as #. Translators: a background job's progress. {title} is the job, such as
#. "Scanning Photos"; {done} and {total} are counts. #. "Scanning Photos"; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1117 #: crates/pdfs-gui/src/app/pages/status.rs:1118
#, rust-format #, rust-format
msgid "{title} — {done} of {total}" msgid "{title} — {done} of {total}"
msgstr "{title} — {done} van {total}" msgstr "{title} — {done} van {total}"
#. Translators: a background job's progress. {title} is the job, {detail} #. Translators: a background job's progress. {title} is the job, {detail}
#. what it is working on; {done} and {total} are counts. #. what it is working on; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1120 #: crates/pdfs-gui/src/app/pages/status.rs:1121
#, rust-format #, rust-format
msgid "{title} — {detail} ({done} of {total})" msgid "{title} — {detail} ({done} of {total})"
msgstr "{title} — {detail} ({done} van {total})" msgstr "{title} — {detail} ({done} van {total})"
#. Translators: a background job still running. {title} is the job. #. Translators: a background job still running. {title} is the job.
#: crates/pdfs-gui/src/app/pages/status.rs:1122 #: crates/pdfs-gui/src/app/pages/status.rs:1123
#, rust-format #, rust-format
msgid "{title}…" msgid "{title}…"
msgstr "{title}…" msgstr "{title}…"
#. Translators: a background job still running. {title} is the job, #. Translators: a background job still running. {title} is the job,
#. {detail} what it is working on. #. {detail} what it is working on.
#: crates/pdfs-gui/src/app/pages/status.rs:1125 #: crates/pdfs-gui/src/app/pages/status.rs:1126
#, rust-format #, rust-format
msgid "{title} — {detail}…" msgid "{title} — {detail}…"
msgstr "{title} — {detail}…" msgstr "{title} — {detail}…"
@@ -4690,7 +4762,7 @@ msgstr "{title} — {detail}…"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} the size moved so far and {speed} a size such as #. file name, {done} the size moved so far and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1154 #: crates/pdfs-gui/src/app/pages/status.rs:1155
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} ({speed}/s)" msgid "{arrow} {name} — {done} ({speed}/s)"
msgstr "{arrow} {name} — {done} ({speed}/s)" msgstr "{arrow} {name} — {done} ({speed}/s)"
@@ -4698,117 +4770,117 @@ msgstr "{arrow} {name} — {done} ({speed}/s)"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} and {total} sizes and {speed} a size such as #. file name, {done} and {total} sizes and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1162 #: crates/pdfs-gui/src/app/pages/status.rs:1163
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} of {total} ({speed}/s)" msgid "{arrow} {name} — {done} of {total} ({speed}/s)"
msgstr "{arrow} {name} — {done} van {total} ({speed}/s)" msgstr "{arrow} {name} — {done} van {total} ({speed}/s)"
#: crates/pdfs-gui/src/app/pages/status.rs:1254 #: crates/pdfs-gui/src/app/pages/status.rs:1255
#, rust-format #, rust-format
msgid "{used} cached — no limit set" msgid "{used} cached — no limit set"
msgstr "{used} in cache — geen limiet ingesteld" msgstr "{used} in cache — geen limiet ingesteld"
#: crates/pdfs-gui/src/app/pages/status.rs:1260 #: crates/pdfs-gui/src/app/pages/status.rs:1261
#, rust-format #, rust-format
msgid "{used} of {limit} used" msgid "{used} of {limit} used"
msgstr "{used} van {limit} gebruikt" msgstr "{used} van {limit} gebruikt"
#: crates/pdfs-gui/src/app/pages/status.rs:1328 #: crates/pdfs-gui/src/app/pages/status.rs:1329
#, rust-format #, rust-format
msgid "{queued} · resumes {time}" msgid "{queued} · resumes {time}"
msgstr "{queued} · hervat {time}" msgstr "{queued} · hervat {time}"
#. Translators: {time} is when syncing resumes, such as "at 14:30" #. Translators: {time} is when syncing resumes, such as "at 14:30"
#. or "Tue 09:00". #. or "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1333 #: crates/pdfs-gui/src/app/pages/status.rs:1334
#, rust-format #, rust-format
msgid "Resumes {time}" msgid "Resumes {time}"
msgstr "Hervat {time}" msgstr "Hervat {time}"
#: crates/pdfs-gui/src/app/pages/status.rs:1335 #: crates/pdfs-gui/src/app/pages/status.rs:1336
msgid "Until you resume" msgid "Until you resume"
msgstr "Totdat je hervat" msgstr "Totdat je hervat"
#: crates/pdfs-gui/src/app/pages/status.rs:1342 crates/pdfs-gui/src/main.rs:239 #: crates/pdfs-gui/src/app/pages/status.rs:1343 crates/pdfs-gui/src/main.rs:248
#, rust-format #, rust-format
msgid "{n} change needs attention" msgid "{n} change needs attention"
msgid_plural "{n} changes need attention" msgid_plural "{n} changes need attention"
msgstr[0] "{n} wijziging vereist aandacht" msgstr[0] "{n} wijziging vereist aandacht"
msgstr[1] "{n} wijzigingen vereisen aandacht" msgstr[1] "{n} wijzigingen vereisen aandacht"
#: crates/pdfs-gui/src/app/pages/status.rs:1352 #: crates/pdfs-gui/src/app/pages/status.rs:1353
#: crates/pdfs-gui/src/app/widgets/details.rs:108 #: crates/pdfs-gui/src/app/widgets/details.rs:108
msgctxt "state" msgctxt "state"
msgid "Offline" msgid "Offline"
msgstr "Offline" msgstr "Offline"
#: crates/pdfs-gui/src/app/pages/status.rs:1353 #: crates/pdfs-gui/src/app/pages/status.rs:1354
msgid "Cached files only" msgid "Cached files only"
msgstr "Alleen bestanden in cache" msgstr "Alleen bestanden in cache"
#: crates/pdfs-gui/src/app/pages/status.rs:1358 #: crates/pdfs-gui/src/app/pages/status.rs:1359
msgctxt "state" msgctxt "state"
msgid "Syncing" msgid "Syncing"
msgstr "Synchroniseren" msgstr "Synchroniseren"
#: crates/pdfs-gui/src/app/pages/status.rs:1364 #: crates/pdfs-gui/src/app/pages/status.rs:1365
msgid "Up to date" msgid "Up to date"
msgstr "Bijgewerkt" msgstr "Bijgewerkt"
#: crates/pdfs-gui/src/app/pages/status.rs:1371 #: crates/pdfs-gui/src/app/pages/status.rs:1372
#: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622 #: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622
msgid "Proton Drive isn't running" msgid "Proton Drive isn't running"
msgstr "Proton Drive draait niet" msgstr "Proton Drive draait niet"
#: crates/pdfs-gui/src/app/pages/status.rs:1376 #: crates/pdfs-gui/src/app/pages/status.rs:1377
msgid "Not responding" msgid "Not responding"
msgstr "Reageert niet" msgstr "Reageert niet"
#: crates/pdfs-gui/src/app/pages/status.rs:1377 #: crates/pdfs-gui/src/app/pages/status.rs:1378
msgid "Proton Drive is taking too long to answer" msgid "Proton Drive is taking too long to answer"
msgstr "Proton Drive doet er te lang over om te antwoorden" msgstr "Proton Drive doet er te lang over om te antwoorden"
#. Translators: stands in for a time that could not be read, as in #. Translators: stands in for a time that could not be read, as in
#. "Resumes later". #. "Resumes later".
#: crates/pdfs-gui/src/app/pages/status.rs:1415 #: crates/pdfs-gui/src/app/pages/status.rs:1416
#: crates/pdfs-gui/src/app/pages/status.rs:1430 #: crates/pdfs-gui/src/app/pages/status.rs:1431
msgid "later" msgid "later"
msgstr "later" msgstr "later"
#. Translators: strftime format for a time later today, such as "at 14:30". #. Translators: strftime format for a time later today, such as "at 14:30".
#: crates/pdfs-gui/src/app/pages/status.rs:1419 #: crates/pdfs-gui/src/app/pages/status.rs:1420
msgid "at %H:%M" msgid "at %H:%M"
msgstr "om %H:%M" msgstr "om %H:%M"
#. Translators: strftime format for a time within the next week, such as #. Translators: strftime format for a time within the next week, such as
#. "Tue 09:00". #. "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1423 #: crates/pdfs-gui/src/app/pages/status.rs:1424
msgid "%a %H:%M" msgid "%a %H:%M"
msgstr "%a %H:%M" msgstr "%a %H:%M"
#. Translators: strftime format for a date further ahead, such as "23 Sep". #. Translators: strftime format for a date further ahead, such as "23 Sep".
#: crates/pdfs-gui/src/app/pages/status.rs:1426 #: crates/pdfs-gui/src/app/pages/status.rs:1427
msgid "%e %b" msgid "%e %b"
msgstr "%e %b" msgstr "%e %b"
#: crates/pdfs-gui/src/app/pages/status.rs:1456 #: crates/pdfs-gui/src/app/pages/status.rs:1457
msgid "No files kept offline" msgid "No files kept offline"
msgstr "Geen offline bewaarde bestanden" msgstr "Geen offline bewaarde bestanden"
#: crates/pdfs-gui/src/app/pages/status.rs:1458 #: crates/pdfs-gui/src/app/pages/status.rs:1459
msgid "Right-click a file and choose “Make available offline”." msgid "Right-click a file and choose “Make available offline”."
msgstr "Klik met rechts op een bestand en kies “Offline beschikbaar maken”." msgstr "Klik met rechts op een bestand en kies “Offline beschikbaar maken”."
#: crates/pdfs-gui/src/app/pages/status.rs:1509 #: crates/pdfs-gui/src/app/pages/status.rs:1510
#: crates/pdfs-gui/src/app/pages/status.rs:1516 #: crates/pdfs-gui/src/app/pages/status.rs:1517
msgid "Couldn't make it online only" msgid "Couldn't make it online only"
msgstr "Kon het niet alleen online maken" msgstr "Kon het niet alleen online maken"
#: crates/pdfs-gui/src/app/pages/status.rs:1533 #: crates/pdfs-gui/src/app/pages/status.rs:1534
msgid "Show fewer" msgid "Show fewer"
msgstr "Minder tonen" msgstr "Minder tonen"
#: crates/pdfs-gui/src/app/pages/status.rs:1536 #: crates/pdfs-gui/src/app/pages/status.rs:1537
#, rust-format #, rust-format
msgid "Show all {n} file" msgid "Show all {n} file"
msgid_plural "Show all {n} files" msgid_plural "Show all {n} files"
@@ -5237,7 +5309,7 @@ msgstr "Prullenbak geleegd"
msgid "Couldn't empty the trash" msgid "Couldn't empty the trash"
msgstr "Kon de prullenbak niet legen" msgstr "Kon de prullenbak niet legen"
#: crates/pdfs-gui/src/app/pages/verify.rs:57 #: crates/pdfs-gui/src/app/pages/verify.rs:162
msgid "Verification" msgid "Verification"
msgstr "Verificatie" msgstr "Verificatie"
@@ -5868,40 +5940,45 @@ msgstr[1] "↑ {n} worden geüpload"
msgid "Proton Drive isn't responding" msgid "Proton Drive isn't responding"
msgstr "Proton Drive reageert niet" msgstr "Proton Drive reageert niet"
#: crates/pdfs-gui/src/main.rs:172 #: crates/pdfs-gui/src/main.rs:173
#, rust-format
msgid "Signed in as {username} — locked"
msgstr "Aangemeld als {username} — vergrendeld"
#: crates/pdfs-gui/src/main.rs:181
#, rust-format #, rust-format
msgid "Signed in as {username} — not connected" msgid "Signed in as {username} — not connected"
msgstr "Aangemeld als {username} — niet verbonden" msgstr "Aangemeld als {username} — niet verbonden"
#: crates/pdfs-gui/src/main.rs:177 #: crates/pdfs-gui/src/main.rs:186
msgid "Not signed in" msgid "Not signed in"
msgstr "Niet aangemeld" msgstr "Niet aangemeld"
#. Translators: {error} is an error message, in English. #. Translators: {error} is an error message, in English.
#: crates/pdfs-gui/src/main.rs:180 #: crates/pdfs-gui/src/main.rs:189
#, rust-format #, rust-format
msgid "Error: {error}" msgid "Error: {error}"
msgstr "Fout: {error}" msgstr "Fout: {error}"
#: crates/pdfs-gui/src/main.rs:201 #: crates/pdfs-gui/src/main.rs:210
msgid "Proton Drive sent an unexpected reply" msgid "Proton Drive sent an unexpected reply"
msgstr "Proton Drive gaf een onverwacht antwoord" msgstr "Proton Drive gaf een onverwacht antwoord"
#: crates/pdfs-gui/src/main.rs:246 #: crates/pdfs-gui/src/main.rs:255
#, rust-format #, rust-format
msgid "Up to date in {mountpoint} ({n} pinned)" msgid "Up to date in {mountpoint} ({n} pinned)"
msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)"
msgstr[0] "Bijgewerkt in {mountpoint} ({n} vastgezet)" msgstr[0] "Bijgewerkt in {mountpoint} ({n} vastgezet)"
msgstr[1] "Bijgewerkt in {mountpoint} ({n} vastgezet)" msgstr[1] "Bijgewerkt in {mountpoint} ({n} vastgezet)"
#: crates/pdfs-gui/src/main.rs:253 #: crates/pdfs-gui/src/main.rs:262
#, rust-format #, rust-format
msgid "Syncing — {queued} ({n} pinned)" msgid "Syncing — {queued} ({n} pinned)"
msgid_plural "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)"
msgstr[0] "Synchroniseren — {queued} ({n} vastgezet)" msgstr[0] "Synchroniseren — {queued} ({n} vastgezet)"
msgstr[1] "Synchroniseren — {queued} ({n} vastgezet)" msgstr[1] "Synchroniseren — {queued} ({n} vastgezet)"
#: crates/pdfs-gui/src/main.rs:260 #: crates/pdfs-gui/src/main.rs:269
#, rust-format #, rust-format
msgid "Offline — cached files only ({n} pinned)" msgid "Offline — cached files only ({n} pinned)"
msgid_plural "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)"
@@ -5909,52 +5986,52 @@ msgstr[0] "Offline — alleen bestanden in cache ({n} vastgezet)"
msgstr[1] "Offline — alleen bestanden in cache ({n} vastgezet)" msgstr[1] "Offline — alleen bestanden in cache ({n} vastgezet)"
#. Translators: {queued} is a queue summary such as "3 uploads queued". #. Translators: {queued} is a queue summary such as "3 uploads queued".
#: crates/pdfs-gui/src/main.rs:266 #: crates/pdfs-gui/src/main.rs:275
#, rust-format #, rust-format
msgid "Offline — {queued}" msgid "Offline — {queued}"
msgstr "Offline — {queued}" msgstr "Offline — {queued}"
#. Translators: a menu item that opens the list of failing changes. #. Translators: a menu item that opens the list of failing changes.
#: crates/pdfs-gui/src/main.rs:296 #: crates/pdfs-gui/src/main.rs:305
#, rust-format #, rust-format
msgid "{n} issue — View" msgid "{n} issue — View"
msgid_plural "{n} issues — View" msgid_plural "{n} issues — View"
msgstr[0] "{n} probleem — Bekijken" msgstr[0] "{n} probleem — Bekijken"
msgstr[1] "{n} problemen — Bekijken" msgstr[1] "{n} problemen — Bekijken"
#: crates/pdfs-gui/src/main.rs:402 #: crates/pdfs-gui/src/main.rs:411
msgid "Sign In…" msgid "Sign In…"
msgstr "Aanmelden…" msgstr "Aanmelden…"
#: crates/pdfs-gui/src/main.rs:403 #: crates/pdfs-gui/src/main.rs:412
msgid "Open Proton Drive" msgid "Open Proton Drive"
msgstr "Proton Drive openen" msgstr "Proton Drive openen"
#: crates/pdfs-gui/src/main.rs:409 #: crates/pdfs-gui/src/main.rs:418
msgid "Open Folder" msgid "Open Folder"
msgstr "Map openen" msgstr "Map openen"
#: crates/pdfs-gui/src/main.rs:421 #: crates/pdfs-gui/src/main.rs:430
msgid "Resume Sync" msgid "Resume Sync"
msgstr "Synchronisatie hervatten" msgstr "Synchronisatie hervatten"
#: crates/pdfs-gui/src/main.rs:423 #: crates/pdfs-gui/src/main.rs:432
msgid "Pause Sync" msgid "Pause Sync"
msgstr "Synchronisatie pauzeren" msgstr "Synchronisatie pauzeren"
#: crates/pdfs-gui/src/main.rs:442 #: crates/pdfs-gui/src/main.rs:451
msgid "Connect" msgid "Connect"
msgstr "Verbinden" msgstr "Verbinden"
#: crates/pdfs-gui/src/main.rs:449 #: crates/pdfs-gui/src/main.rs:458
msgid "Hide Tray Icon" msgid "Hide Tray Icon"
msgstr "Pictogram in systeemvak verbergen" msgstr "Pictogram in systeemvak verbergen"
#: crates/pdfs-gui/src/main.rs:453 #: crates/pdfs-gui/src/main.rs:462
msgid "Stop Proton Drive…" msgid "Stop Proton Drive…"
msgstr "Proton Drive stoppen…" msgstr "Proton Drive stoppen…"
#: crates/pdfs-gui/src/main.rs:457 #: crates/pdfs-gui/src/main.rs:466
msgid "Quit" msgid "Quit"
msgstr "Afsluiten" msgstr "Afsluiten"
+207 -142
View File
@@ -6,9 +6,9 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: proton-drive-linux 2.10.0\n" "Project-Id-Version: proton-drive-linux 3.1.1\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-10-05 01:01+0200\n" "POT-Creation-Date: 2026-10-08 10:15+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -60,7 +60,7 @@ msgstr ""
msgid "%A, %B %-d, %Y, %H:%M" msgid "%A, %B %-d, %Y, %H:%M"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:743 #: crates/pdfs-gui/src/app/main.rs:760
#: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539
#: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:454
#: crates/pdfs-gui/src/app/pages/browser.rs:819 #: crates/pdfs-gui/src/app/pages/browser.rs:819
@@ -73,91 +73,91 @@ msgstr ""
msgid "My files" msgid "My files"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:746 crates/pdfs-gui/src/app/main.rs:1390 #: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407
#: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/albums.rs:638
#: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/photos.rs:842
#: crates/pdfs-gui/src/app/pages/places.rs:441 #: crates/pdfs-gui/src/app/pages/places.rs:441
msgid "Photos" msgid "Photos"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:751 #: crates/pdfs-gui/src/app/main.rs:768
#: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:168
#: crates/pdfs-gui/src/app/pages/shared.rs:482 #: crates/pdfs-gui/src/app/pages/shared.rs:482
msgid "Shared with me" msgid "Shared with me"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:756 #: crates/pdfs-gui/src/app/main.rs:773
#: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120
msgid "Shared by me" msgid "Shared by me"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:759 #: crates/pdfs-gui/src/app/main.rs:776
#: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:140
#: crates/pdfs-gui/src/app/pages/devices.rs:481 #: crates/pdfs-gui/src/app/pages/devices.rs:481
msgid "Computers" msgid "Computers"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/main.rs:777
#: crates/pdfs-gui/src/app/pages/trash.rs:101 #: crates/pdfs-gui/src/app/pages/trash.rs:101
msgid "Trash" msgid "Trash"
msgstr "" msgstr ""
#. Translators: sidebar entry for the page listing this computer's synced folders. #. Translators: sidebar entry for the page listing this computer's synced folders.
#: crates/pdfs-gui/src/app/main.rs:762 #: crates/pdfs-gui/src/app/main.rs:779
msgid "Sync" msgid "Sync"
msgstr "" msgstr ""
#. Translators: a failed action's toast. {what} says what failed, such as #. Translators: a failed action's toast. {what} says what failed, such as
#. "Couldn't rename"; {detail} is the reason, usually untranslated text #. "Couldn't rename"; {detail} is the reason, usually untranslated text
#. from the mount service. #. from the mount service.
#: crates/pdfs-gui/src/app/main.rs:1079 #: crates/pdfs-gui/src/app/main.rs:1096
#, rust-format #, rust-format
msgid "{what}: {detail}" msgid "{what}: {detail}"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1098 #: crates/pdfs-gui/src/app/main.rs:1115
msgid "You're offline" msgid "You're offline"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1099 #: crates/pdfs-gui/src/app/main.rs:1116
msgid "That's not there any more" msgid "That's not there any more"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1100 #: crates/pdfs-gui/src/app/main.rs:1117
msgid "You don't have access to that" msgid "You don't have access to that"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1101 #: crates/pdfs-gui/src/app/main.rs:1118
msgid "Something changed this first" msgid "Something changed this first"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1102 #: crates/pdfs-gui/src/app/main.rs:1119
msgid "Your Proton Drive is full" msgid "Your Proton Drive is full"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1143 crates/pdfs-gui/src/app/main.rs:1372 #: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389
#: crates/pdfs-gui/src/app/pages/status.rs:385 #: crates/pdfs-gui/src/app/pages/status.rs:385
msgid "Preferences" msgid "Preferences"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1144 #: crates/pdfs-gui/src/app/main.rs:1161
#: crates/pdfs-gui/src/app/widgets/compat.rs:262 #: crates/pdfs-gui/src/app/widgets/compat.rs:262
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1146 #: crates/pdfs-gui/src/app/main.rs:1163
msgid "About Proton Drive for Linux" msgid "About Proton Drive for Linux"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1151 #: crates/pdfs-gui/src/app/main.rs:1168
msgid "Main menu" msgid "Main menu"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1207 #: crates/pdfs-gui/src/app/main.rs:1224
msgid "Proton Drive for Linux" msgid "Proton Drive for Linux"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1215 #: crates/pdfs-gui/src/app/main.rs:1232
msgid "" msgid ""
"Files-on-demand Proton Drive for the Linux desktop.\n" "Files-on-demand Proton Drive for the Linux desktop.\n"
"\n" "\n"
@@ -165,23 +165,23 @@ msgid ""
"AG." "AG."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1220 #: crates/pdfs-gui/src/app/main.rs:1237
msgid "Source Code" msgid "Source Code"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1224 #: crates/pdfs-gui/src/app/main.rs:1241
msgid "Proton Terms of Service" msgid "Proton Terms of Service"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1228 #: crates/pdfs-gui/src/app/main.rs:1245
msgid "Proton Privacy Policy" msgid "Proton Privacy Policy"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1233 #: crates/pdfs-gui/src/app/main.rs:1250
msgid "Disclaimer" msgid "Disclaimer"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1237 #: crates/pdfs-gui/src/app/main.rs:1254
msgid "" msgid ""
"This is an unofficial, community-built client. It is not affiliated with, " "This is an unofficial, community-built client. It is not affiliated with, "
"endorsed by, or supported by Proton AG.\n" "endorsed by, or supported by Proton AG.\n"
@@ -194,21 +194,21 @@ msgid ""
"of any data you cannot afford to lose." "of any data you cannot afford to lose."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1245 #: crates/pdfs-gui/src/app/main.rs:1262
msgid "" msgid ""
"Places are named with data from <a href=\"https://www.geonames.org/" "Places are named with data from <a href=\"https://www.geonames.org/"
"\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/"
"licenses/by/4.0/\">CC BY 4.0</a>." "licenses/by/4.0/\">CC BY 4.0</a>."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1253 #: crates/pdfs-gui/src/app/main.rs:1270
msgid "" msgid ""
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/" "The places map is drawn from <a href=\"https://www.naturalearthdata.com/"
"\">Natural Earth</a> land outlines and country borders, which are in the " "\">Natural Earth</a> land outlines and country borders, which are in the "
"public domain." "public domain."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1261 #: crates/pdfs-gui/src/app/main.rs:1278
msgid "" msgid ""
"The street map, when turned on, comes from <a href=\"https://openfreemap.org/" "The street map, when turned on, comes from <a href=\"https://openfreemap.org/"
"\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/" "\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/"
@@ -219,21 +219,21 @@ msgstr ""
#. Translators: replace with your name(s) and email address(es), one #. Translators: replace with your name(s) and email address(es), one
#. per line, to be credited in the About dialog. #. per line, to be credited in the About dialog.
#: crates/pdfs-gui/src/app/main.rs:1272 #: crates/pdfs-gui/src/app/main.rs:1289
msgid "translator-credits" msgid "translator-credits"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1310 #: crates/pdfs-gui/src/app/main.rs:1327
msgid "Stop Proton Drive?" msgid "Stop Proton Drive?"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1311 #: crates/pdfs-gui/src/app/main.rs:1328
msgid "" msgid ""
"My files goes offline and nothing syncs until you connect again. Proton " "My files goes offline and nothing syncs until you connect again. Proton "
"Drive starts again at your next login." "Drive starts again at your next login."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1313 crates/pdfs-gui/src/app/main.rs:1588 #: crates/pdfs-gui/src/app/main.rs:1330 crates/pdfs-gui/src/app/main.rs:1605
#: crates/pdfs-gui/src/app/pages/albums.rs:243 #: crates/pdfs-gui/src/app/pages/albums.rs:243
#: crates/pdfs-gui/src/app/pages/albums.rs:428 #: crates/pdfs-gui/src/app/pages/albums.rs:428
#: crates/pdfs-gui/src/app/pages/browser.rs:1892 #: crates/pdfs-gui/src/app/pages/browser.rs:1892
@@ -248,7 +248,8 @@ msgstr ""
#: crates/pdfs-gui/src/app/pages/locations.rs:1013 #: crates/pdfs-gui/src/app/pages/locations.rs:1013
#: crates/pdfs-gui/src/app/pages/locations.rs:1488 #: crates/pdfs-gui/src/app/pages/locations.rs:1488
#: crates/pdfs-gui/src/app/pages/locations.rs:1538 #: crates/pdfs-gui/src/app/pages/locations.rs:1538
#: crates/pdfs-gui/src/app/pages/login.rs:312 #: crates/pdfs-gui/src/app/pages/login.rs:393
#: crates/pdfs-gui/src/app/pages/login.rs:507
#: crates/pdfs-gui/src/app/pages/photos.rs:2055 #: crates/pdfs-gui/src/app/pages/photos.rs:2055
#: crates/pdfs-gui/src/app/pages/shared.rs:946 #: crates/pdfs-gui/src/app/pages/shared.rs:946
#: crates/pdfs-gui/src/app/pages/shared.rs:1027 #: crates/pdfs-gui/src/app/pages/shared.rs:1027
@@ -263,30 +264,30 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1314 #: crates/pdfs-gui/src/app/main.rs:1331
msgctxt "verb" msgctxt "verb"
msgid "Stop" msgid "Stop"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1331 #: crates/pdfs-gui/src/app/main.rs:1348
msgid "Couldn't stop Proton Drive" msgid "Couldn't stop Proton Drive"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1332 #: crates/pdfs-gui/src/app/main.rs:1349
msgid "The Proton Drive service did not stop." msgid "The Proton Drive service did not stop."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1368 #: crates/pdfs-gui/src/app/main.rs:1385
#: crates/pdfs-gui/src/app/pages/status.rs:254 #: crates/pdfs-gui/src/app/pages/status.rs:254
msgid "General" msgid "General"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1370 #: crates/pdfs-gui/src/app/main.rs:1387
#: crates/pdfs-gui/src/app/pages/browser.rs:247 #: crates/pdfs-gui/src/app/pages/browser.rs:247
msgid "Search Drive" msgid "Search Drive"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1371 #: crates/pdfs-gui/src/app/main.rs:1388
#: crates/pdfs-gui/src/app/pages/browser.rs:957 #: crates/pdfs-gui/src/app/pages/browser.rs:957
#: crates/pdfs-gui/src/app/pages/devices.rs:554 #: crates/pdfs-gui/src/app/pages/devices.rs:554
#: crates/pdfs-gui/src/app/pages/shared.rs:586 #: crates/pdfs-gui/src/app/pages/shared.rs:586
@@ -295,38 +296,38 @@ msgstr ""
msgid "Refresh" msgid "Refresh"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1373 #: crates/pdfs-gui/src/app/main.rs:1390
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1377 #: crates/pdfs-gui/src/app/main.rs:1394
msgid "Files" msgid "Files"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1379 #: crates/pdfs-gui/src/app/main.rs:1396
#: crates/pdfs-gui/src/app/pages/browser.rs:294 #: crates/pdfs-gui/src/app/pages/browser.rs:294
msgid "New folder" msgid "New folder"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1380 #: crates/pdfs-gui/src/app/main.rs:1397
#: crates/pdfs-gui/src/app/pages/browser.rs:290 #: crates/pdfs-gui/src/app/pages/browser.rs:290
msgid "Upload files" msgid "Upload files"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1381 #: crates/pdfs-gui/src/app/main.rs:1398
msgid "Back / forward" msgid "Back / forward"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1382 #: crates/pdfs-gui/src/app/main.rs:1399
#: crates/pdfs-gui/src/app/pages/browser.rs:1943 #: crates/pdfs-gui/src/app/pages/browser.rs:1943
msgid "Parent folder" msgid "Parent folder"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1383 #: crates/pdfs-gui/src/app/main.rs:1400
msgid "Grid / list" msgid "Grid / list"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1384 #: crates/pdfs-gui/src/app/main.rs:1401
#: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/albums.rs:298
#: crates/pdfs-gui/src/app/pages/browser.rs:1871 #: crates/pdfs-gui/src/app/pages/browser.rs:1871
#: crates/pdfs-gui/src/app/pages/browser.rs:1893 #: crates/pdfs-gui/src/app/pages/browser.rs:1893
@@ -335,7 +336,7 @@ msgstr ""
msgid "Rename" msgid "Rename"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1385 crates/pdfs-gui/src/app/main.rs:1405 #: crates/pdfs-gui/src/app/main.rs:1402 crates/pdfs-gui/src/app/main.rs:1422
#: crates/pdfs-gui/src/app/pages/browser.rs:357 #: crates/pdfs-gui/src/app/pages/browser.rs:357
#: crates/pdfs-gui/src/app/pages/browser.rs:900 #: crates/pdfs-gui/src/app/pages/browser.rs:900
#: crates/pdfs-gui/src/app/pages/browser.rs:932 #: crates/pdfs-gui/src/app/pages/browser.rs:932
@@ -345,73 +346,73 @@ msgstr ""
msgid "Move to Trash" msgid "Move to Trash"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1386 #: crates/pdfs-gui/src/app/main.rs:1403
msgid "Clear the selection" msgid "Clear the selection"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1392 #: crates/pdfs-gui/src/app/main.rs:1409
msgid "Larger thumbnails" msgid "Larger thumbnails"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1393 #: crates/pdfs-gui/src/app/main.rs:1410
msgid "Smaller thumbnails" msgid "Smaller thumbnails"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1394 #: crates/pdfs-gui/src/app/main.rs:1411
msgid "Reset thumbnail size" msgid "Reset thumbnail size"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1395 #: crates/pdfs-gui/src/app/main.rs:1412
msgid "Select all" msgid "Select all"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1399 #: crates/pdfs-gui/src/app/main.rs:1416
msgid "Photo Viewer" msgid "Photo Viewer"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1401 #: crates/pdfs-gui/src/app/main.rs:1418
msgid "Previous / next photo" msgid "Previous / next photo"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1402 #: crates/pdfs-gui/src/app/main.rs:1419
msgid "First / last photo" msgid "First / last photo"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1403 #: crates/pdfs-gui/src/app/main.rs:1420
msgid "Show details" msgid "Show details"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1404 #: crates/pdfs-gui/src/app/main.rs:1421
msgid "Fullscreen" msgid "Fullscreen"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1406 #: crates/pdfs-gui/src/app/main.rs:1423
msgid "Close" msgid "Close"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1496 #: crates/pdfs-gui/src/app/main.rs:1513
msgid "Select a single item to rename it" msgid "Select a single item to rename it"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1656 #: crates/pdfs-gui/src/app/main.rs:1673
#: crates/pdfs-gui/src/app/pages/albums.rs:85 #: crates/pdfs-gui/src/app/pages/albums.rs:85
#: crates/pdfs-gui/src/app/pages/places.rs:104 #: crates/pdfs-gui/src/app/pages/places.rs:104
#: crates/pdfs-gui/src/app/pages/status.rs:1370 #: crates/pdfs-gui/src/app/pages/status.rs:1371
msgid "Not connected" msgid "Not connected"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1657 #: crates/pdfs-gui/src/app/main.rs:1674
msgid "The Proton Drive service isn't running." msgid "The Proton Drive service isn't running."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1665 #: crates/pdfs-gui/src/app/main.rs:1682
#: crates/pdfs-gui/src/app/pages/status.rs:336 #: crates/pdfs-gui/src/app/pages/status.rs:336
#: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595 #: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595
#: crates/pdfs-gui/src/main.rs:444 #: crates/pdfs-gui/src/main.rs:453
msgid "Connecting…" msgid "Connecting…"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/main.rs:1666 #: crates/pdfs-gui/src/app/main.rs:1683
msgid "Waiting for the Proton Drive service to start." msgid "Waiting for the Proton Drive service to start."
msgstr "" msgstr ""
@@ -1240,7 +1241,7 @@ msgstr ""
#: crates/pdfs-gui/src/app/pages/shared.rs:1006 #: crates/pdfs-gui/src/app/pages/shared.rs:1006
#: crates/pdfs-gui/src/app/pages/shared.rs:1122 #: crates/pdfs-gui/src/app/pages/shared.rs:1122
#: crates/pdfs-gui/src/app/pages/status.rs:750 #: crates/pdfs-gui/src/app/pages/status.rs:750
#: crates/pdfs-gui/src/app/pages/status.rs:1517 #: crates/pdfs-gui/src/app/pages/status.rs:1518
#: crates/pdfs-gui/src/app/pages/takeout.rs:424 #: crates/pdfs-gui/src/app/pages/takeout.rs:424
#: crates/pdfs-gui/src/app/pages/takeout.rs:617 #: crates/pdfs-gui/src/app/pages/takeout.rs:617
#: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438
@@ -1508,7 +1509,7 @@ msgid "Make available offline"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/browser.rs:347 #: crates/pdfs-gui/src/app/pages/browser.rs:347
#: crates/pdfs-gui/src/app/pages/status.rs:1493 #: crates/pdfs-gui/src/app/pages/status.rs:1494
msgid "Make online only" msgid "Make online only"
msgstr "" msgstr ""
@@ -2616,7 +2617,7 @@ msgid "Sync paused until you resume"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/locations.rs:529 #: crates/pdfs-gui/src/app/pages/locations.rs:529
#: crates/pdfs-gui/src/app/pages/status.rs:1323 crates/pdfs-gui/src/main.rs:237 #: crates/pdfs-gui/src/app/pages/status.rs:1324 crates/pdfs-gui/src/main.rs:246
msgid "Sync paused" msgid "Sync paused"
msgstr "" msgstr ""
@@ -3052,72 +3053,131 @@ msgstr ""
msgid "Signing in…" msgid "Signing in…"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:155 #: crates/pdfs-gui/src/app/pages/login.rs:164
msgid "Complete the verification to continue…" msgid "Complete the verification to continue…"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:165 #: crates/pdfs-gui/src/app/pages/login.rs:197
msgid "login cancelled" msgid "login cancelled"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:198 #: crates/pdfs-gui/src/app/pages/login.rs:210
msgid ""
"The verification page couldn't be opened, so the sign-in was cancelled. Your "
"system may keep the web view's sandbox from starting; the troubleshooting "
"guide explains what to do."
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:233
msgid "Too many sign-in attempts. Wait a few minutes and try again." msgid "Too many sign-in attempts. Wait a few minutes and try again."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:204 #: crates/pdfs-gui/src/app/pages/login.rs:239
msgid "Couldn't reach Proton. Check your internet connection and try again." msgid "Couldn't reach Proton. Check your internet connection and try again."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:207 #: crates/pdfs-gui/src/app/pages/login.rs:242
msgid "" msgid ""
"Signed in, but the session couldn't be saved to the system keyring. Make " "Signed in, but the session couldn't be saved to the system keyring. Make "
"sure a keyring (GNOME Keyring or KWallet) is running and unlocked." "sure a keyring (GNOME Keyring or KWallet) is running and unlocked."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:210 #: crates/pdfs-gui/src/app/pages/login.rs:245
msgid "Sign-in cancelled: no two-factor code was entered." msgid "Sign-in cancelled: no two-factor code was entered."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:213 #: crates/pdfs-gui/src/app/pages/login.rs:248
msgid "Sign-in cancelled: no mailbox password was entered."
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:251
msgid "That mailbox password didn't unlock your account."
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:254
msgid "" msgid ""
"Sign-in cancelled: the verification wasn't completed. Sign in again to get a " "Sign-in cancelled: the verification wasn't completed. Sign in again to get a "
"new one." "new one."
msgstr "" msgstr ""
#. Translators: {error} is an error message, usually in English. #. Translators: {error} is an error message, usually in English.
#: crates/pdfs-gui/src/app/pages/login.rs:218 #: crates/pdfs-gui/src/app/pages/login.rs:259
#, rust-format #, rust-format
msgid "Sign-in failed: {error}" msgid "Sign-in failed: {error}"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:298 #: crates/pdfs-gui/src/app/pages/login.rs:369
msgid "That mailbox password didn't unlock your account. Try again."
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:371
msgid ""
"This account has a separate mailbox password. Enter it to unlock your files."
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:374
msgid "Mailbox Password"
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:380
msgid "Mailbox password"
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:384
msgid "Keep unlocked on this computer"
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:386
msgid ""
"Stores the unlock in the system keyring, so Proton Drive opens without "
"asking after every login. Otherwise you enter the password once per session."
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:394
#: crates/pdfs-gui/src/app/pages/login.rs:447
msgid "Unlock"
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:446
msgid "Proton Drive is locked. Enter your mailbox password to unlock it."
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:475
msgid "Unlocked."
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:480
msgid "Couldn't unlock"
msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:493
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:300 #: crates/pdfs-gui/src/app/pages/login.rs:495
msgid "" msgid ""
"Enter the code from your authenticator app, or one of your recovery codes." "Enter the code from your authenticator app, or one of your recovery codes."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:306 #: crates/pdfs-gui/src/app/pages/login.rs:501
msgid "Authentication code" msgid "Authentication code"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:313 #: crates/pdfs-gui/src/app/pages/login.rs:508
msgid "Confirm" msgid "Confirm"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:341 #: crates/pdfs-gui/src/app/pages/login.rs:536
msgid "Sign Out?" msgid "Sign Out?"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:343 #: crates/pdfs-gui/src/app/pages/login.rs:538
msgid "" msgid ""
"Proton Drive will disconnect and stop syncing until you sign in again. Files " "Proton Drive will disconnect and stop syncing until you sign in again. Files "
"already on this computer stay where they are." "already on this computer stay where they are."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/login.rs:345 #: crates/pdfs-gui/src/app/pages/login.rs:540
msgid "Sign Out" msgid "Sign Out"
msgstr "" msgstr ""
@@ -4433,38 +4493,38 @@ msgid "Restart Now"
msgstr "" msgstr ""
#. Translators: {size} is an amount of storage, such as "1.2 GiB". #. Translators: {size} is an amount of storage, such as "1.2 GiB".
#: crates/pdfs-gui/src/app/pages/status.rs:912 #: crates/pdfs-gui/src/app/pages/status.rs:913
#, rust-format #, rust-format
msgid "{size} used" msgid "{size} used"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:923 #: crates/pdfs-gui/src/app/pages/status.rs:924
#, rust-format #, rust-format
msgid "{used} of {total} used ({percent}%)" msgid "{used} of {total} used ({percent}%)"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:961 #: crates/pdfs-gui/src/app/pages/status.rs:962
msgid "Proton Drive connected" msgid "Proton Drive connected"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:962 #: crates/pdfs-gui/src/app/pages/status.rs:963
msgid "Your files are available." msgid "Your files are available."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:967 #: crates/pdfs-gui/src/app/pages/status.rs:968
msgid "Proton Drive disconnected" msgid "Proton Drive disconnected"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:968 #: crates/pdfs-gui/src/app/pages/status.rs:969
msgid "" msgid ""
"The Proton Drive service stopped. Files aren't available until it restarts." "The Proton Drive service stopped. Files aren't available until it restarts."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1037 #: crates/pdfs-gui/src/app/pages/status.rs:1038
msgid "Sync complete" msgid "Sync complete"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1039 #: crates/pdfs-gui/src/app/pages/status.rs:1040
#, rust-format #, rust-format
msgid "{n} file finished transferring." msgid "{n} file finished transferring."
msgid_plural "{n} files finished transferring." msgid_plural "{n} files finished transferring."
@@ -4473,27 +4533,27 @@ msgstr[1] ""
#. Translators: a background job's progress. {title} is the job, such as #. Translators: a background job's progress. {title} is the job, such as
#. "Scanning Photos"; {done} and {total} are counts. #. "Scanning Photos"; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1117 #: crates/pdfs-gui/src/app/pages/status.rs:1118
#, rust-format #, rust-format
msgid "{title} — {done} of {total}" msgid "{title} — {done} of {total}"
msgstr "" msgstr ""
#. Translators: a background job's progress. {title} is the job, {detail} #. Translators: a background job's progress. {title} is the job, {detail}
#. what it is working on; {done} and {total} are counts. #. what it is working on; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1120 #: crates/pdfs-gui/src/app/pages/status.rs:1121
#, rust-format #, rust-format
msgid "{title} — {detail} ({done} of {total})" msgid "{title} — {detail} ({done} of {total})"
msgstr "" msgstr ""
#. Translators: a background job still running. {title} is the job. #. Translators: a background job still running. {title} is the job.
#: crates/pdfs-gui/src/app/pages/status.rs:1122 #: crates/pdfs-gui/src/app/pages/status.rs:1123
#, rust-format #, rust-format
msgid "{title}…" msgid "{title}…"
msgstr "" msgstr ""
#. Translators: a background job still running. {title} is the job, #. Translators: a background job still running. {title} is the job,
#. {detail} what it is working on. #. {detail} what it is working on.
#: crates/pdfs-gui/src/app/pages/status.rs:1125 #: crates/pdfs-gui/src/app/pages/status.rs:1126
#, rust-format #, rust-format
msgid "{title} — {detail}…" msgid "{title} — {detail}…"
msgstr "" msgstr ""
@@ -4501,7 +4561,7 @@ msgstr ""
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} the size moved so far and {speed} a size such as #. file name, {done} the size moved so far and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1154 #: crates/pdfs-gui/src/app/pages/status.rs:1155
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} ({speed}/s)" msgid "{arrow} {name} — {done} ({speed}/s)"
msgstr "" msgstr ""
@@ -4509,117 +4569,117 @@ msgstr ""
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} and {total} sizes and {speed} a size such as #. file name, {done} and {total} sizes and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1162 #: crates/pdfs-gui/src/app/pages/status.rs:1163
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} of {total} ({speed}/s)" msgid "{arrow} {name} — {done} of {total} ({speed}/s)"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1254 #: crates/pdfs-gui/src/app/pages/status.rs:1255
#, rust-format #, rust-format
msgid "{used} cached — no limit set" msgid "{used} cached — no limit set"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1260 #: crates/pdfs-gui/src/app/pages/status.rs:1261
#, rust-format #, rust-format
msgid "{used} of {limit} used" msgid "{used} of {limit} used"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1328 #: crates/pdfs-gui/src/app/pages/status.rs:1329
#, rust-format #, rust-format
msgid "{queued} · resumes {time}" msgid "{queued} · resumes {time}"
msgstr "" msgstr ""
#. Translators: {time} is when syncing resumes, such as "at 14:30" #. Translators: {time} is when syncing resumes, such as "at 14:30"
#. or "Tue 09:00". #. or "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1333 #: crates/pdfs-gui/src/app/pages/status.rs:1334
#, rust-format #, rust-format
msgid "Resumes {time}" msgid "Resumes {time}"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1335 #: crates/pdfs-gui/src/app/pages/status.rs:1336
msgid "Until you resume" msgid "Until you resume"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1342 crates/pdfs-gui/src/main.rs:239 #: crates/pdfs-gui/src/app/pages/status.rs:1343 crates/pdfs-gui/src/main.rs:248
#, rust-format #, rust-format
msgid "{n} change needs attention" msgid "{n} change needs attention"
msgid_plural "{n} changes need attention" msgid_plural "{n} changes need attention"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: crates/pdfs-gui/src/app/pages/status.rs:1352 #: crates/pdfs-gui/src/app/pages/status.rs:1353
#: crates/pdfs-gui/src/app/widgets/details.rs:108 #: crates/pdfs-gui/src/app/widgets/details.rs:108
msgctxt "state" msgctxt "state"
msgid "Offline" msgid "Offline"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1353 #: crates/pdfs-gui/src/app/pages/status.rs:1354
msgid "Cached files only" msgid "Cached files only"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1358 #: crates/pdfs-gui/src/app/pages/status.rs:1359
msgctxt "state" msgctxt "state"
msgid "Syncing" msgid "Syncing"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1364 #: crates/pdfs-gui/src/app/pages/status.rs:1365
msgid "Up to date" msgid "Up to date"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1371 #: crates/pdfs-gui/src/app/pages/status.rs:1372
#: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622 #: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622
msgid "Proton Drive isn't running" msgid "Proton Drive isn't running"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1376 #: crates/pdfs-gui/src/app/pages/status.rs:1377
msgid "Not responding" msgid "Not responding"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1377 #: crates/pdfs-gui/src/app/pages/status.rs:1378
msgid "Proton Drive is taking too long to answer" msgid "Proton Drive is taking too long to answer"
msgstr "" msgstr ""
#. Translators: stands in for a time that could not be read, as in #. Translators: stands in for a time that could not be read, as in
#. "Resumes later". #. "Resumes later".
#: crates/pdfs-gui/src/app/pages/status.rs:1415 #: crates/pdfs-gui/src/app/pages/status.rs:1416
#: crates/pdfs-gui/src/app/pages/status.rs:1430 #: crates/pdfs-gui/src/app/pages/status.rs:1431
msgid "later" msgid "later"
msgstr "" msgstr ""
#. Translators: strftime format for a time later today, such as "at 14:30". #. Translators: strftime format for a time later today, such as "at 14:30".
#: crates/pdfs-gui/src/app/pages/status.rs:1419 #: crates/pdfs-gui/src/app/pages/status.rs:1420
msgid "at %H:%M" msgid "at %H:%M"
msgstr "" msgstr ""
#. Translators: strftime format for a time within the next week, such as #. Translators: strftime format for a time within the next week, such as
#. "Tue 09:00". #. "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1423 #: crates/pdfs-gui/src/app/pages/status.rs:1424
msgid "%a %H:%M" msgid "%a %H:%M"
msgstr "" msgstr ""
#. Translators: strftime format for a date further ahead, such as "23 Sep". #. Translators: strftime format for a date further ahead, such as "23 Sep".
#: crates/pdfs-gui/src/app/pages/status.rs:1426 #: crates/pdfs-gui/src/app/pages/status.rs:1427
msgid "%e %b" msgid "%e %b"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1456 #: crates/pdfs-gui/src/app/pages/status.rs:1457
msgid "No files kept offline" msgid "No files kept offline"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1458 #: crates/pdfs-gui/src/app/pages/status.rs:1459
msgid "Right-click a file and choose “Make available offline”." msgid "Right-click a file and choose “Make available offline”."
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1509 #: crates/pdfs-gui/src/app/pages/status.rs:1510
#: crates/pdfs-gui/src/app/pages/status.rs:1516 #: crates/pdfs-gui/src/app/pages/status.rs:1517
msgid "Couldn't make it online only" msgid "Couldn't make it online only"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1533 #: crates/pdfs-gui/src/app/pages/status.rs:1534
msgid "Show fewer" msgid "Show fewer"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/status.rs:1536 #: crates/pdfs-gui/src/app/pages/status.rs:1537
#, rust-format #, rust-format
msgid "Show all {n} file" msgid "Show all {n} file"
msgid_plural "Show all {n} files" msgid_plural "Show all {n} files"
@@ -5023,7 +5083,7 @@ msgstr ""
msgid "Couldn't empty the trash" msgid "Couldn't empty the trash"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/app/pages/verify.rs:57 #: crates/pdfs-gui/src/app/pages/verify.rs:162
msgid "Verification" msgid "Verification"
msgstr "" msgstr ""
@@ -5644,40 +5704,45 @@ msgstr[1] ""
msgid "Proton Drive isn't responding" msgid "Proton Drive isn't responding"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:172 #: crates/pdfs-gui/src/main.rs:173
#, rust-format
msgid "Signed in as {username} — locked"
msgstr ""
#: crates/pdfs-gui/src/main.rs:181
#, rust-format #, rust-format
msgid "Signed in as {username} — not connected" msgid "Signed in as {username} — not connected"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:177 #: crates/pdfs-gui/src/main.rs:186
msgid "Not signed in" msgid "Not signed in"
msgstr "" msgstr ""
#. Translators: {error} is an error message, in English. #. Translators: {error} is an error message, in English.
#: crates/pdfs-gui/src/main.rs:180 #: crates/pdfs-gui/src/main.rs:189
#, rust-format #, rust-format
msgid "Error: {error}" msgid "Error: {error}"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:201 #: crates/pdfs-gui/src/main.rs:210
msgid "Proton Drive sent an unexpected reply" msgid "Proton Drive sent an unexpected reply"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:246 #: crates/pdfs-gui/src/main.rs:255
#, rust-format #, rust-format
msgid "Up to date in {mountpoint} ({n} pinned)" msgid "Up to date in {mountpoint} ({n} pinned)"
msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: crates/pdfs-gui/src/main.rs:253 #: crates/pdfs-gui/src/main.rs:262
#, rust-format #, rust-format
msgid "Syncing — {queued} ({n} pinned)" msgid "Syncing — {queued} ({n} pinned)"
msgid_plural "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: crates/pdfs-gui/src/main.rs:260 #: crates/pdfs-gui/src/main.rs:269
#, rust-format #, rust-format
msgid "Offline — cached files only ({n} pinned)" msgid "Offline — cached files only ({n} pinned)"
msgid_plural "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)"
@@ -5685,52 +5750,52 @@ msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#. Translators: {queued} is a queue summary such as "3 uploads queued". #. Translators: {queued} is a queue summary such as "3 uploads queued".
#: crates/pdfs-gui/src/main.rs:266 #: crates/pdfs-gui/src/main.rs:275
#, rust-format #, rust-format
msgid "Offline — {queued}" msgid "Offline — {queued}"
msgstr "" msgstr ""
#. Translators: a menu item that opens the list of failing changes. #. Translators: a menu item that opens the list of failing changes.
#: crates/pdfs-gui/src/main.rs:296 #: crates/pdfs-gui/src/main.rs:305
#, rust-format #, rust-format
msgid "{n} issue — View" msgid "{n} issue — View"
msgid_plural "{n} issues — View" msgid_plural "{n} issues — View"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: crates/pdfs-gui/src/main.rs:402 #: crates/pdfs-gui/src/main.rs:411
msgid "Sign In…" msgid "Sign In…"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:403 #: crates/pdfs-gui/src/main.rs:412
msgid "Open Proton Drive" msgid "Open Proton Drive"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:409 #: crates/pdfs-gui/src/main.rs:418
msgid "Open Folder" msgid "Open Folder"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:421 #: crates/pdfs-gui/src/main.rs:430
msgid "Resume Sync" msgid "Resume Sync"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:423 #: crates/pdfs-gui/src/main.rs:432
msgid "Pause Sync" msgid "Pause Sync"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:442 #: crates/pdfs-gui/src/main.rs:451
msgid "Connect" msgid "Connect"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:449 #: crates/pdfs-gui/src/main.rs:458
msgid "Hide Tray Icon" msgid "Hide Tray Icon"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:453 #: crates/pdfs-gui/src/main.rs:462
msgid "Stop Proton Drive…" msgid "Stop Proton Drive…"
msgstr "" msgstr ""
#: crates/pdfs-gui/src/main.rs:457 #: crates/pdfs-gui/src/main.rs:466
msgid "Quit" msgid "Quit"
msgstr "" msgstr ""
+217 -141
View File
@@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: proton-drive-linux 2.1.0\n" "Project-Id-Version: proton-drive-linux 2.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-10-05 01:01+0200\n" "POT-Creation-Date: 2026-10-08 10:15+0200\n"
"PO-Revision-Date: 2026-09-23 12:00+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n"
"Last-Translator: Proton Drive for Linux contributors\n" "Last-Translator: Proton Drive for Linux contributors\n"
"Language-Team: Polish\n" "Language-Team: Polish\n"
@@ -59,7 +59,7 @@ msgstr "%-d %b %Y"
msgid "%A, %B %-d, %Y, %H:%M" msgid "%A, %B %-d, %Y, %H:%M"
msgstr "%A, %-d %B %Y, %H:%M" msgstr "%A, %-d %B %Y, %H:%M"
#: crates/pdfs-gui/src/app/main.rs:743 #: crates/pdfs-gui/src/app/main.rs:760
#: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539
#: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:454
#: crates/pdfs-gui/src/app/pages/browser.rs:819 #: crates/pdfs-gui/src/app/pages/browser.rs:819
@@ -72,91 +72,91 @@ msgstr "%A, %-d %B %Y, %H:%M"
msgid "My files" msgid "My files"
msgstr "Moje pliki" msgstr "Moje pliki"
#: crates/pdfs-gui/src/app/main.rs:746 crates/pdfs-gui/src/app/main.rs:1390 #: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407
#: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/albums.rs:638
#: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/photos.rs:842
#: crates/pdfs-gui/src/app/pages/places.rs:441 #: crates/pdfs-gui/src/app/pages/places.rs:441
msgid "Photos" msgid "Photos"
msgstr "Zdjęcia" msgstr "Zdjęcia"
#: crates/pdfs-gui/src/app/main.rs:751 #: crates/pdfs-gui/src/app/main.rs:768
#: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:168
#: crates/pdfs-gui/src/app/pages/shared.rs:482 #: crates/pdfs-gui/src/app/pages/shared.rs:482
msgid "Shared with me" msgid "Shared with me"
msgstr "Udostępnione mi" msgstr "Udostępnione mi"
#: crates/pdfs-gui/src/app/main.rs:756 #: crates/pdfs-gui/src/app/main.rs:773
#: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120
msgid "Shared by me" msgid "Shared by me"
msgstr "Udostępnione przeze mnie" msgstr "Udostępnione przeze mnie"
#: crates/pdfs-gui/src/app/main.rs:759 #: crates/pdfs-gui/src/app/main.rs:776
#: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:140
#: crates/pdfs-gui/src/app/pages/devices.rs:481 #: crates/pdfs-gui/src/app/pages/devices.rs:481
msgid "Computers" msgid "Computers"
msgstr "Komputery" msgstr "Komputery"
#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/main.rs:777
#: crates/pdfs-gui/src/app/pages/trash.rs:101 #: crates/pdfs-gui/src/app/pages/trash.rs:101
msgid "Trash" msgid "Trash"
msgstr "Kosz" msgstr "Kosz"
#. Translators: sidebar entry for the page listing this computer's synced folders. #. Translators: sidebar entry for the page listing this computer's synced folders.
#: crates/pdfs-gui/src/app/main.rs:762 #: crates/pdfs-gui/src/app/main.rs:779
msgid "Sync" msgid "Sync"
msgstr "Synchronizacja" msgstr "Synchronizacja"
#. Translators: a failed action's toast. {what} says what failed, such as #. Translators: a failed action's toast. {what} says what failed, such as
#. "Couldn't rename"; {detail} is the reason, usually untranslated text #. "Couldn't rename"; {detail} is the reason, usually untranslated text
#. from the mount service. #. from the mount service.
#: crates/pdfs-gui/src/app/main.rs:1079 #: crates/pdfs-gui/src/app/main.rs:1096
#, rust-format #, rust-format
msgid "{what}: {detail}" msgid "{what}: {detail}"
msgstr "{what}: {detail}" msgstr "{what}: {detail}"
#: crates/pdfs-gui/src/app/main.rs:1098 #: crates/pdfs-gui/src/app/main.rs:1115
msgid "You're offline" msgid "You're offline"
msgstr "Brak połączenia z siecią" msgstr "Brak połączenia z siecią"
#: crates/pdfs-gui/src/app/main.rs:1099 #: crates/pdfs-gui/src/app/main.rs:1116
msgid "That's not there any more" msgid "That's not there any more"
msgstr "Ten element już nie istnieje" msgstr "Ten element już nie istnieje"
#: crates/pdfs-gui/src/app/main.rs:1100 #: crates/pdfs-gui/src/app/main.rs:1117
msgid "You don't have access to that" msgid "You don't have access to that"
msgstr "Brak dostępu do tego elementu" msgstr "Brak dostępu do tego elementu"
#: crates/pdfs-gui/src/app/main.rs:1101 #: crates/pdfs-gui/src/app/main.rs:1118
msgid "Something changed this first" msgid "Something changed this first"
msgstr "Coś zmieniło ten element wcześniej" msgstr "Coś zmieniło ten element wcześniej"
#: crates/pdfs-gui/src/app/main.rs:1102 #: crates/pdfs-gui/src/app/main.rs:1119
msgid "Your Proton Drive is full" msgid "Your Proton Drive is full"
msgstr "Proton Drive jest pełny" msgstr "Proton Drive jest pełny"
#: crates/pdfs-gui/src/app/main.rs:1143 crates/pdfs-gui/src/app/main.rs:1372 #: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389
#: crates/pdfs-gui/src/app/pages/status.rs:385 #: crates/pdfs-gui/src/app/pages/status.rs:385
msgid "Preferences" msgid "Preferences"
msgstr "Preferencje" msgstr "Preferencje"
#: crates/pdfs-gui/src/app/main.rs:1144 #: crates/pdfs-gui/src/app/main.rs:1161
#: crates/pdfs-gui/src/app/widgets/compat.rs:262 #: crates/pdfs-gui/src/app/widgets/compat.rs:262
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "Skróty klawiszowe" msgstr "Skróty klawiszowe"
#: crates/pdfs-gui/src/app/main.rs:1146 #: crates/pdfs-gui/src/app/main.rs:1163
msgid "About Proton Drive for Linux" msgid "About Proton Drive for Linux"
msgstr "O programie Proton Drive for Linux" msgstr "O programie Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1151 #: crates/pdfs-gui/src/app/main.rs:1168
msgid "Main menu" msgid "Main menu"
msgstr "Menu główne" msgstr "Menu główne"
#: crates/pdfs-gui/src/app/main.rs:1207 #: crates/pdfs-gui/src/app/main.rs:1224
msgid "Proton Drive for Linux" msgid "Proton Drive for Linux"
msgstr "Proton Drive for Linux" msgstr "Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1215 #: crates/pdfs-gui/src/app/main.rs:1232
msgid "" msgid ""
"Files-on-demand Proton Drive for the Linux desktop.\n" "Files-on-demand Proton Drive for the Linux desktop.\n"
"\n" "\n"
@@ -168,23 +168,23 @@ msgstr ""
"Nieoficjalny klient — niepowiązany z Proton AG, niepopierany ani " "Nieoficjalny klient — niepowiązany z Proton AG, niepopierany ani "
"niewspierany przez tę firmę." "niewspierany przez tę firmę."
#: crates/pdfs-gui/src/app/main.rs:1220 #: crates/pdfs-gui/src/app/main.rs:1237
msgid "Source Code" msgid "Source Code"
msgstr "Kod źródłowy" msgstr "Kod źródłowy"
#: crates/pdfs-gui/src/app/main.rs:1224 #: crates/pdfs-gui/src/app/main.rs:1241
msgid "Proton Terms of Service" msgid "Proton Terms of Service"
msgstr "Warunki korzystania z usług Proton" msgstr "Warunki korzystania z usług Proton"
#: crates/pdfs-gui/src/app/main.rs:1228 #: crates/pdfs-gui/src/app/main.rs:1245
msgid "Proton Privacy Policy" msgid "Proton Privacy Policy"
msgstr "Polityka prywatności Proton" msgstr "Polityka prywatności Proton"
#: crates/pdfs-gui/src/app/main.rs:1233 #: crates/pdfs-gui/src/app/main.rs:1250
msgid "Disclaimer" msgid "Disclaimer"
msgstr "Zastrzeżenia" msgstr "Zastrzeżenia"
#: crates/pdfs-gui/src/app/main.rs:1237 #: crates/pdfs-gui/src/app/main.rs:1254
msgid "" msgid ""
"This is an unofficial, community-built client. It is not affiliated with, " "This is an unofficial, community-built client. It is not affiliated with, "
"endorsed by, or supported by Proton AG.\n" "endorsed by, or supported by Proton AG.\n"
@@ -208,7 +208,7 @@ msgstr ""
"gwarancji. Zachowaj kopię danych, na których utratę nie możesz sobie " "gwarancji. Zachowaj kopię danych, na których utratę nie możesz sobie "
"pozwolić." "pozwolić."
#: crates/pdfs-gui/src/app/main.rs:1245 #: crates/pdfs-gui/src/app/main.rs:1262
msgid "" msgid ""
"Places are named with data from <a href=\"https://www.geonames.org/" "Places are named with data from <a href=\"https://www.geonames.org/"
"\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/"
@@ -218,7 +218,7 @@ msgstr ""
"\">GeoNames</a> udostępnionych na licencji <a href=\"https://" "\">GeoNames</a> udostępnionych na licencji <a href=\"https://"
"creativecommons.org/licenses/by/4.0/\">CC BY 4.0</a>." "creativecommons.org/licenses/by/4.0/\">CC BY 4.0</a>."
#: crates/pdfs-gui/src/app/main.rs:1253 #: crates/pdfs-gui/src/app/main.rs:1270
msgid "" msgid ""
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/" "The places map is drawn from <a href=\"https://www.naturalearthdata.com/"
"\">Natural Earth</a> land outlines and country borders, which are in the " "\">Natural Earth</a> land outlines and country borders, which are in the "
@@ -228,7 +228,7 @@ msgstr ""
"www.naturalearthdata.com/\">Natural Earth</a>, które należą do domeny " "www.naturalearthdata.com/\">Natural Earth</a>, które należą do domeny "
"publicznej." "publicznej."
#: crates/pdfs-gui/src/app/main.rs:1261 #: crates/pdfs-gui/src/app/main.rs:1278
msgid "" msgid ""
"The street map, when turned on, comes from <a href=\"https://openfreemap.org/" "The street map, when turned on, comes from <a href=\"https://openfreemap.org/"
"\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/" "\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/"
@@ -244,15 +244,15 @@ msgstr ""
#. Translators: replace with your name(s) and email address(es), one #. Translators: replace with your name(s) and email address(es), one
#. per line, to be credited in the About dialog. #. per line, to be credited in the About dialog.
#: crates/pdfs-gui/src/app/main.rs:1272 #: crates/pdfs-gui/src/app/main.rs:1289
msgid "translator-credits" msgid "translator-credits"
msgstr "Współtwórcy programu Proton Drive for Linux" msgstr "Współtwórcy programu Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1310 #: crates/pdfs-gui/src/app/main.rs:1327
msgid "Stop Proton Drive?" msgid "Stop Proton Drive?"
msgstr "Zatrzymać Proton Drive?" msgstr "Zatrzymać Proton Drive?"
#: crates/pdfs-gui/src/app/main.rs:1311 #: crates/pdfs-gui/src/app/main.rs:1328
msgid "" msgid ""
"My files goes offline and nothing syncs until you connect again. Proton " "My files goes offline and nothing syncs until you connect again. Proton "
"Drive starts again at your next login." "Drive starts again at your next login."
@@ -261,7 +261,7 @@ msgstr ""
"ponownego połączenia. Proton Drive uruchomi się ponownie przy następnym " "ponownego połączenia. Proton Drive uruchomi się ponownie przy następnym "
"logowaniu." "logowaniu."
#: crates/pdfs-gui/src/app/main.rs:1313 crates/pdfs-gui/src/app/main.rs:1588 #: crates/pdfs-gui/src/app/main.rs:1330 crates/pdfs-gui/src/app/main.rs:1605
#: crates/pdfs-gui/src/app/pages/albums.rs:243 #: crates/pdfs-gui/src/app/pages/albums.rs:243
#: crates/pdfs-gui/src/app/pages/albums.rs:428 #: crates/pdfs-gui/src/app/pages/albums.rs:428
#: crates/pdfs-gui/src/app/pages/browser.rs:1892 #: crates/pdfs-gui/src/app/pages/browser.rs:1892
@@ -276,7 +276,8 @@ msgstr ""
#: crates/pdfs-gui/src/app/pages/locations.rs:1013 #: crates/pdfs-gui/src/app/pages/locations.rs:1013
#: crates/pdfs-gui/src/app/pages/locations.rs:1488 #: crates/pdfs-gui/src/app/pages/locations.rs:1488
#: crates/pdfs-gui/src/app/pages/locations.rs:1538 #: crates/pdfs-gui/src/app/pages/locations.rs:1538
#: crates/pdfs-gui/src/app/pages/login.rs:312 #: crates/pdfs-gui/src/app/pages/login.rs:393
#: crates/pdfs-gui/src/app/pages/login.rs:507
#: crates/pdfs-gui/src/app/pages/photos.rs:2055 #: crates/pdfs-gui/src/app/pages/photos.rs:2055
#: crates/pdfs-gui/src/app/pages/shared.rs:946 #: crates/pdfs-gui/src/app/pages/shared.rs:946
#: crates/pdfs-gui/src/app/pages/shared.rs:1027 #: crates/pdfs-gui/src/app/pages/shared.rs:1027
@@ -291,30 +292,30 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Anuluj" msgstr "Anuluj"
#: crates/pdfs-gui/src/app/main.rs:1314 #: crates/pdfs-gui/src/app/main.rs:1331
msgctxt "verb" msgctxt "verb"
msgid "Stop" msgid "Stop"
msgstr "Zatrzymaj" msgstr "Zatrzymaj"
#: crates/pdfs-gui/src/app/main.rs:1331 #: crates/pdfs-gui/src/app/main.rs:1348
msgid "Couldn't stop Proton Drive" msgid "Couldn't stop Proton Drive"
msgstr "Nie można zatrzymać Proton Drive" msgstr "Nie można zatrzymać Proton Drive"
#: crates/pdfs-gui/src/app/main.rs:1332 #: crates/pdfs-gui/src/app/main.rs:1349
msgid "The Proton Drive service did not stop." msgid "The Proton Drive service did not stop."
msgstr "Usługa Proton Drive nie zatrzymała się." msgstr "Usługa Proton Drive nie zatrzymała się."
#: crates/pdfs-gui/src/app/main.rs:1368 #: crates/pdfs-gui/src/app/main.rs:1385
#: crates/pdfs-gui/src/app/pages/status.rs:254 #: crates/pdfs-gui/src/app/pages/status.rs:254
msgid "General" msgid "General"
msgstr "Ogólne" msgstr "Ogólne"
#: crates/pdfs-gui/src/app/main.rs:1370 #: crates/pdfs-gui/src/app/main.rs:1387
#: crates/pdfs-gui/src/app/pages/browser.rs:247 #: crates/pdfs-gui/src/app/pages/browser.rs:247
msgid "Search Drive" msgid "Search Drive"
msgstr "Przeszukaj Drive" msgstr "Przeszukaj Drive"
#: crates/pdfs-gui/src/app/main.rs:1371 #: crates/pdfs-gui/src/app/main.rs:1388
#: crates/pdfs-gui/src/app/pages/browser.rs:957 #: crates/pdfs-gui/src/app/pages/browser.rs:957
#: crates/pdfs-gui/src/app/pages/devices.rs:554 #: crates/pdfs-gui/src/app/pages/devices.rs:554
#: crates/pdfs-gui/src/app/pages/shared.rs:586 #: crates/pdfs-gui/src/app/pages/shared.rs:586
@@ -323,38 +324,38 @@ msgstr "Przeszukaj Drive"
msgid "Refresh" msgid "Refresh"
msgstr "Odśwież" msgstr "Odśwież"
#: crates/pdfs-gui/src/app/main.rs:1373 #: crates/pdfs-gui/src/app/main.rs:1390
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "Skróty klawiszowe" msgstr "Skróty klawiszowe"
#: crates/pdfs-gui/src/app/main.rs:1377 #: crates/pdfs-gui/src/app/main.rs:1394
msgid "Files" msgid "Files"
msgstr "Pliki" msgstr "Pliki"
#: crates/pdfs-gui/src/app/main.rs:1379 #: crates/pdfs-gui/src/app/main.rs:1396
#: crates/pdfs-gui/src/app/pages/browser.rs:294 #: crates/pdfs-gui/src/app/pages/browser.rs:294
msgid "New folder" msgid "New folder"
msgstr "Nowy katalog" msgstr "Nowy katalog"
#: crates/pdfs-gui/src/app/main.rs:1380 #: crates/pdfs-gui/src/app/main.rs:1397
#: crates/pdfs-gui/src/app/pages/browser.rs:290 #: crates/pdfs-gui/src/app/pages/browser.rs:290
msgid "Upload files" msgid "Upload files"
msgstr "Wyślij pliki" msgstr "Wyślij pliki"
#: crates/pdfs-gui/src/app/main.rs:1381 #: crates/pdfs-gui/src/app/main.rs:1398
msgid "Back / forward" msgid "Back / forward"
msgstr "Wstecz/dalej" msgstr "Wstecz/dalej"
#: crates/pdfs-gui/src/app/main.rs:1382 #: crates/pdfs-gui/src/app/main.rs:1399
#: crates/pdfs-gui/src/app/pages/browser.rs:1943 #: crates/pdfs-gui/src/app/pages/browser.rs:1943
msgid "Parent folder" msgid "Parent folder"
msgstr "Katalog nadrzędny" msgstr "Katalog nadrzędny"
#: crates/pdfs-gui/src/app/main.rs:1383 #: crates/pdfs-gui/src/app/main.rs:1400
msgid "Grid / list" msgid "Grid / list"
msgstr "Siatka/lista" msgstr "Siatka/lista"
#: crates/pdfs-gui/src/app/main.rs:1384 #: crates/pdfs-gui/src/app/main.rs:1401
#: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/albums.rs:298
#: crates/pdfs-gui/src/app/pages/browser.rs:1871 #: crates/pdfs-gui/src/app/pages/browser.rs:1871
#: crates/pdfs-gui/src/app/pages/browser.rs:1893 #: crates/pdfs-gui/src/app/pages/browser.rs:1893
@@ -363,7 +364,7 @@ msgstr "Siatka/lista"
msgid "Rename" msgid "Rename"
msgstr "Zmień nazwę" msgstr "Zmień nazwę"
#: crates/pdfs-gui/src/app/main.rs:1385 crates/pdfs-gui/src/app/main.rs:1405 #: crates/pdfs-gui/src/app/main.rs:1402 crates/pdfs-gui/src/app/main.rs:1422
#: crates/pdfs-gui/src/app/pages/browser.rs:357 #: crates/pdfs-gui/src/app/pages/browser.rs:357
#: crates/pdfs-gui/src/app/pages/browser.rs:900 #: crates/pdfs-gui/src/app/pages/browser.rs:900
#: crates/pdfs-gui/src/app/pages/browser.rs:932 #: crates/pdfs-gui/src/app/pages/browser.rs:932
@@ -373,73 +374,73 @@ msgstr "Zmień nazwę"
msgid "Move to Trash" msgid "Move to Trash"
msgstr "Przenieś do kosza" msgstr "Przenieś do kosza"
#: crates/pdfs-gui/src/app/main.rs:1386 #: crates/pdfs-gui/src/app/main.rs:1403
msgid "Clear the selection" msgid "Clear the selection"
msgstr "Wyczyść zaznaczenie" msgstr "Wyczyść zaznaczenie"
#: crates/pdfs-gui/src/app/main.rs:1392 #: crates/pdfs-gui/src/app/main.rs:1409
msgid "Larger thumbnails" msgid "Larger thumbnails"
msgstr "Większe miniatury" msgstr "Większe miniatury"
#: crates/pdfs-gui/src/app/main.rs:1393 #: crates/pdfs-gui/src/app/main.rs:1410
msgid "Smaller thumbnails" msgid "Smaller thumbnails"
msgstr "Mniejsze miniatury" msgstr "Mniejsze miniatury"
#: crates/pdfs-gui/src/app/main.rs:1394 #: crates/pdfs-gui/src/app/main.rs:1411
msgid "Reset thumbnail size" msgid "Reset thumbnail size"
msgstr "Przywróć rozmiar miniatur" msgstr "Przywróć rozmiar miniatur"
#: crates/pdfs-gui/src/app/main.rs:1395 #: crates/pdfs-gui/src/app/main.rs:1412
msgid "Select all" msgid "Select all"
msgstr "Zaznacz wszystko" msgstr "Zaznacz wszystko"
#: crates/pdfs-gui/src/app/main.rs:1399 #: crates/pdfs-gui/src/app/main.rs:1416
msgid "Photo Viewer" msgid "Photo Viewer"
msgstr "Przeglądarka zdjęć" msgstr "Przeglądarka zdjęć"
#: crates/pdfs-gui/src/app/main.rs:1401 #: crates/pdfs-gui/src/app/main.rs:1418
msgid "Previous / next photo" msgid "Previous / next photo"
msgstr "Poprzednie/następne zdjęcie" msgstr "Poprzednie/następne zdjęcie"
#: crates/pdfs-gui/src/app/main.rs:1402 #: crates/pdfs-gui/src/app/main.rs:1419
msgid "First / last photo" msgid "First / last photo"
msgstr "Pierwsze/ostatnie zdjęcie" msgstr "Pierwsze/ostatnie zdjęcie"
#: crates/pdfs-gui/src/app/main.rs:1403 #: crates/pdfs-gui/src/app/main.rs:1420
msgid "Show details" msgid "Show details"
msgstr "Wyświetl szczegóły" msgstr "Wyświetl szczegóły"
#: crates/pdfs-gui/src/app/main.rs:1404 #: crates/pdfs-gui/src/app/main.rs:1421
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Pełny ekran" msgstr "Pełny ekran"
#: crates/pdfs-gui/src/app/main.rs:1406 #: crates/pdfs-gui/src/app/main.rs:1423
msgid "Close" msgid "Close"
msgstr "Zamknij" msgstr "Zamknij"
#: crates/pdfs-gui/src/app/main.rs:1496 #: crates/pdfs-gui/src/app/main.rs:1513
msgid "Select a single item to rename it" msgid "Select a single item to rename it"
msgstr "Należy zaznaczyć jeden element, aby zmienić jego nazwę" msgstr "Należy zaznaczyć jeden element, aby zmienić jego nazwę"
#: crates/pdfs-gui/src/app/main.rs:1656 #: crates/pdfs-gui/src/app/main.rs:1673
#: crates/pdfs-gui/src/app/pages/albums.rs:85 #: crates/pdfs-gui/src/app/pages/albums.rs:85
#: crates/pdfs-gui/src/app/pages/places.rs:104 #: crates/pdfs-gui/src/app/pages/places.rs:104
#: crates/pdfs-gui/src/app/pages/status.rs:1370 #: crates/pdfs-gui/src/app/pages/status.rs:1371
msgid "Not connected" msgid "Not connected"
msgstr "Brak połączenia" msgstr "Brak połączenia"
#: crates/pdfs-gui/src/app/main.rs:1657 #: crates/pdfs-gui/src/app/main.rs:1674
msgid "The Proton Drive service isn't running." msgid "The Proton Drive service isn't running."
msgstr "Usługa Proton Drive nie jest uruchomiona." msgstr "Usługa Proton Drive nie jest uruchomiona."
#: crates/pdfs-gui/src/app/main.rs:1665 #: crates/pdfs-gui/src/app/main.rs:1682
#: crates/pdfs-gui/src/app/pages/status.rs:336 #: crates/pdfs-gui/src/app/pages/status.rs:336
#: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595 #: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595
#: crates/pdfs-gui/src/main.rs:444 #: crates/pdfs-gui/src/main.rs:453
msgid "Connecting…" msgid "Connecting…"
msgstr "Łączenie…" msgstr "Łączenie…"
#: crates/pdfs-gui/src/app/main.rs:1666 #: crates/pdfs-gui/src/app/main.rs:1683
msgid "Waiting for the Proton Drive service to start." msgid "Waiting for the Proton Drive service to start."
msgstr "Oczekiwanie na uruchomienie usługi Proton Drive." msgstr "Oczekiwanie na uruchomienie usługi Proton Drive."
@@ -1313,7 +1314,7 @@ msgstr "Nie można wczytać albumów"
#: crates/pdfs-gui/src/app/pages/shared.rs:1006 #: crates/pdfs-gui/src/app/pages/shared.rs:1006
#: crates/pdfs-gui/src/app/pages/shared.rs:1122 #: crates/pdfs-gui/src/app/pages/shared.rs:1122
#: crates/pdfs-gui/src/app/pages/status.rs:750 #: crates/pdfs-gui/src/app/pages/status.rs:750
#: crates/pdfs-gui/src/app/pages/status.rs:1517 #: crates/pdfs-gui/src/app/pages/status.rs:1518
#: crates/pdfs-gui/src/app/pages/takeout.rs:424 #: crates/pdfs-gui/src/app/pages/takeout.rs:424
#: crates/pdfs-gui/src/app/pages/takeout.rs:617 #: crates/pdfs-gui/src/app/pages/takeout.rs:617
#: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438
@@ -1586,7 +1587,7 @@ msgid "Make available offline"
msgstr "Oznacz jako dostępne offline" msgstr "Oznacz jako dostępne offline"
#: crates/pdfs-gui/src/app/pages/browser.rs:347 #: crates/pdfs-gui/src/app/pages/browser.rs:347
#: crates/pdfs-gui/src/app/pages/status.rs:1493 #: crates/pdfs-gui/src/app/pages/status.rs:1494
msgid "Make online only" msgid "Make online only"
msgstr "Oznacz jako tylko online" msgstr "Oznacz jako tylko online"
@@ -2770,7 +2771,7 @@ msgid "Sync paused until you resume"
msgstr "Wstrzymano synchronizację do czasu wznowienia" msgstr "Wstrzymano synchronizację do czasu wznowienia"
#: crates/pdfs-gui/src/app/pages/locations.rs:529 #: crates/pdfs-gui/src/app/pages/locations.rs:529
#: crates/pdfs-gui/src/app/pages/status.rs:1323 crates/pdfs-gui/src/main.rs:237 #: crates/pdfs-gui/src/app/pages/status.rs:1324 crates/pdfs-gui/src/main.rs:246
msgid "Sync paused" msgid "Sync paused"
msgstr "Wstrzymano synchronizację" msgstr "Wstrzymano synchronizację"
@@ -3242,26 +3243,36 @@ msgstr "Proszę podać adres e-mail i hasło."
msgid "Signing in…" msgid "Signing in…"
msgstr "Logowanie…" msgstr "Logowanie…"
#: crates/pdfs-gui/src/app/pages/login.rs:155 #: crates/pdfs-gui/src/app/pages/login.rs:164
msgid "Complete the verification to continue…" msgid "Complete the verification to continue…"
msgstr "Proszę ukończyć weryfikację, aby kontynuować…" msgstr "Proszę ukończyć weryfikację, aby kontynuować…"
#: crates/pdfs-gui/src/app/pages/login.rs:165 #: crates/pdfs-gui/src/app/pages/login.rs:197
msgid "login cancelled" msgid "login cancelled"
msgstr "anulowano logowanie" msgstr "anulowano logowanie"
#: crates/pdfs-gui/src/app/pages/login.rs:198 #: crates/pdfs-gui/src/app/pages/login.rs:210
msgid ""
"The verification page couldn't be opened, so the sign-in was cancelled. Your "
"system may keep the web view's sandbox from starting; the troubleshooting "
"guide explains what to do."
msgstr ""
"Nie można otworzyć strony weryfikacji, więc logowanie zostało anulowane. "
"System może blokować uruchomienie piaskownicy widoku internetowego; poradnik "
"rozwiązywania problemów wyjaśnia, co zrobić."
#: crates/pdfs-gui/src/app/pages/login.rs:233
msgid "Too many sign-in attempts. Wait a few minutes and try again." msgid "Too many sign-in attempts. Wait a few minutes and try again."
msgstr "" msgstr ""
"Za dużo prób logowania. Proszę odczekać kilka minut i spróbować ponownie." "Za dużo prób logowania. Proszę odczekać kilka minut i spróbować ponownie."
#: crates/pdfs-gui/src/app/pages/login.rs:204 #: crates/pdfs-gui/src/app/pages/login.rs:239
msgid "Couldn't reach Proton. Check your internet connection and try again." msgid "Couldn't reach Proton. Check your internet connection and try again."
msgstr "" msgstr ""
"Nie można połączyć się z Proton. Proszę sprawdzić połączenie z Internetem i " "Nie można połączyć się z Proton. Proszę sprawdzić połączenie z Internetem i "
"spróbować ponownie." "spróbować ponownie."
#: crates/pdfs-gui/src/app/pages/login.rs:207 #: crates/pdfs-gui/src/app/pages/login.rs:242
msgid "" msgid ""
"Signed in, but the session couldn't be saved to the system keyring. Make " "Signed in, but the session couldn't be saved to the system keyring. Make "
"sure a keyring (GNOME Keyring or KWallet) is running and unlocked." "sure a keyring (GNOME Keyring or KWallet) is running and unlocked."
@@ -3270,11 +3281,19 @@ msgstr ""
"upewnić, że baza kluczy (GNOME Keyring lub KWallet) jest uruchomiona i " "upewnić, że baza kluczy (GNOME Keyring lub KWallet) jest uruchomiona i "
"odblokowana." "odblokowana."
#: crates/pdfs-gui/src/app/pages/login.rs:210 #: crates/pdfs-gui/src/app/pages/login.rs:245
msgid "Sign-in cancelled: no two-factor code was entered." msgid "Sign-in cancelled: no two-factor code was entered."
msgstr "Anulowano logowanie: nie wpisano kodu uwierzytelniania dwuetapowego." msgstr "Anulowano logowanie: nie wpisano kodu uwierzytelniania dwuetapowego."
#: crates/pdfs-gui/src/app/pages/login.rs:213 #: crates/pdfs-gui/src/app/pages/login.rs:248
msgid "Sign-in cancelled: no mailbox password was entered."
msgstr "Anulowano logowanie: nie wpisano hasła skrzynki pocztowej."
#: crates/pdfs-gui/src/app/pages/login.rs:251
msgid "That mailbox password didn't unlock your account."
msgstr "To hasło skrzynki pocztowej nie odblokowało Twojego konta."
#: crates/pdfs-gui/src/app/pages/login.rs:254
msgid "" msgid ""
"Sign-in cancelled: the verification wasn't completed. Sign in again to get a " "Sign-in cancelled: the verification wasn't completed. Sign in again to get a "
"new one." "new one."
@@ -3283,35 +3302,87 @@ msgstr ""
"ponownie, aby otrzymać nową." "ponownie, aby otrzymać nową."
#. Translators: {error} is an error message, usually in English. #. Translators: {error} is an error message, usually in English.
#: crates/pdfs-gui/src/app/pages/login.rs:218 #: crates/pdfs-gui/src/app/pages/login.rs:259
#, rust-format #, rust-format
msgid "Sign-in failed: {error}" msgid "Sign-in failed: {error}"
msgstr "Logowanie się nie powiodło: {error}" msgstr "Logowanie się nie powiodło: {error}"
#: crates/pdfs-gui/src/app/pages/login.rs:298 #: crates/pdfs-gui/src/app/pages/login.rs:369
msgid "That mailbox password didn't unlock your account. Try again."
msgstr ""
"To hasło skrzynki pocztowej nie odblokowało Twojego konta. Spróbuj ponownie."
#: crates/pdfs-gui/src/app/pages/login.rs:371
msgid ""
"This account has a separate mailbox password. Enter it to unlock your files."
msgstr ""
"To konto ma osobne hasło skrzynki pocztowej. Wpisz je, aby odblokować swoje "
"pliki."
#: crates/pdfs-gui/src/app/pages/login.rs:374
msgid "Mailbox Password"
msgstr "Hasło skrzynki pocztowej"
#: crates/pdfs-gui/src/app/pages/login.rs:380
msgid "Mailbox password"
msgstr "Hasło skrzynki pocztowej"
#: crates/pdfs-gui/src/app/pages/login.rs:384
msgid "Keep unlocked on this computer"
msgstr "Pozostaw odblokowane na tym komputerze"
#: crates/pdfs-gui/src/app/pages/login.rs:386
msgid ""
"Stores the unlock in the system keyring, so Proton Drive opens without "
"asking after every login. Otherwise you enter the password once per session."
msgstr ""
"Zapisuje odblokowanie w systemowym breloku kluczy, aby Proton Drive otwierał "
"się bez pytania po każdym logowaniu. W przeciwnym razie wpisujesz hasło raz "
"na sesję."
#: crates/pdfs-gui/src/app/pages/login.rs:394
#: crates/pdfs-gui/src/app/pages/login.rs:447
msgid "Unlock"
msgstr "Odblokuj"
#: crates/pdfs-gui/src/app/pages/login.rs:446
msgid "Proton Drive is locked. Enter your mailbox password to unlock it."
msgstr ""
"Proton Drive jest zablokowany. Wpisz hasło skrzynki pocztowej, aby go "
"odblokować."
#: crates/pdfs-gui/src/app/pages/login.rs:475
msgid "Unlocked."
msgstr "Odblokowano."
#: crates/pdfs-gui/src/app/pages/login.rs:480
msgid "Couldn't unlock"
msgstr "Nie udało się odblokować"
#: crates/pdfs-gui/src/app/pages/login.rs:493
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "Uwierzytelnianie dwuskładnikowe" msgstr "Uwierzytelnianie dwuskładnikowe"
#: crates/pdfs-gui/src/app/pages/login.rs:300 #: crates/pdfs-gui/src/app/pages/login.rs:495
msgid "" msgid ""
"Enter the code from your authenticator app, or one of your recovery codes." "Enter the code from your authenticator app, or one of your recovery codes."
msgstr "" msgstr ""
"Proszę wpisać kod z aplikacji uwierzytelniającej lub jeden z kodów " "Proszę wpisać kod z aplikacji uwierzytelniającej lub jeden z kodów "
"odzyskiwania." "odzyskiwania."
#: crates/pdfs-gui/src/app/pages/login.rs:306 #: crates/pdfs-gui/src/app/pages/login.rs:501
msgid "Authentication code" msgid "Authentication code"
msgstr "Kod uwierzytelniania" msgstr "Kod uwierzytelniania"
#: crates/pdfs-gui/src/app/pages/login.rs:313 #: crates/pdfs-gui/src/app/pages/login.rs:508
msgid "Confirm" msgid "Confirm"
msgstr "Potwierdź" msgstr "Potwierdź"
#: crates/pdfs-gui/src/app/pages/login.rs:341 #: crates/pdfs-gui/src/app/pages/login.rs:536
msgid "Sign Out?" msgid "Sign Out?"
msgstr "Wylogować się?" msgstr "Wylogować się?"
#: crates/pdfs-gui/src/app/pages/login.rs:343 #: crates/pdfs-gui/src/app/pages/login.rs:538
msgid "" msgid ""
"Proton Drive will disconnect and stop syncing until you sign in again. Files " "Proton Drive will disconnect and stop syncing until you sign in again. Files "
"already on this computer stay where they are." "already on this computer stay where they are."
@@ -3319,7 +3390,7 @@ msgstr ""
"Proton Drive rozłączy się i przestanie synchronizować do czasu ponownego " "Proton Drive rozłączy się i przestanie synchronizować do czasu ponownego "
"zalogowania. Pliki znajdujące się już na tym komputerze pozostaną na miejscu." "zalogowania. Pliki znajdujące się już na tym komputerze pozostaną na miejscu."
#: crates/pdfs-gui/src/app/pages/login.rs:345 #: crates/pdfs-gui/src/app/pages/login.rs:540
msgid "Sign Out" msgid "Sign Out"
msgstr "Wyloguj się" msgstr "Wyloguj się"
@@ -4689,40 +4760,40 @@ msgid "Restart Now"
msgstr "Uruchom ponownie teraz" msgstr "Uruchom ponownie teraz"
#. Translators: {size} is an amount of storage, such as "1.2 GiB". #. Translators: {size} is an amount of storage, such as "1.2 GiB".
#: crates/pdfs-gui/src/app/pages/status.rs:912 #: crates/pdfs-gui/src/app/pages/status.rs:913
#, rust-format #, rust-format
msgid "{size} used" msgid "{size} used"
msgstr "Użyto {size}" msgstr "Użyto {size}"
#: crates/pdfs-gui/src/app/pages/status.rs:923 #: crates/pdfs-gui/src/app/pages/status.rs:924
#, rust-format #, rust-format
msgid "{used} of {total} used ({percent}%)" msgid "{used} of {total} used ({percent}%)"
msgstr "Użyto {used} z {total} ({percent}%)" msgstr "Użyto {used} z {total} ({percent}%)"
#: crates/pdfs-gui/src/app/pages/status.rs:961 #: crates/pdfs-gui/src/app/pages/status.rs:962
msgid "Proton Drive connected" msgid "Proton Drive connected"
msgstr "Połączono z Proton Drive" msgstr "Połączono z Proton Drive"
#: crates/pdfs-gui/src/app/pages/status.rs:962 #: crates/pdfs-gui/src/app/pages/status.rs:963
msgid "Your files are available." msgid "Your files are available."
msgstr "Twoje pliki są dostępne." msgstr "Twoje pliki są dostępne."
#: crates/pdfs-gui/src/app/pages/status.rs:967 #: crates/pdfs-gui/src/app/pages/status.rs:968
msgid "Proton Drive disconnected" msgid "Proton Drive disconnected"
msgstr "Rozłączono z Proton Drive" msgstr "Rozłączono z Proton Drive"
#: crates/pdfs-gui/src/app/pages/status.rs:968 #: crates/pdfs-gui/src/app/pages/status.rs:969
msgid "" msgid ""
"The Proton Drive service stopped. Files aren't available until it restarts." "The Proton Drive service stopped. Files aren't available until it restarts."
msgstr "" msgstr ""
"Usługa Proton Drive zatrzymała się. Pliki będą niedostępne do jej ponownego " "Usługa Proton Drive zatrzymała się. Pliki będą niedostępne do jej ponownego "
"uruchomienia." "uruchomienia."
#: crates/pdfs-gui/src/app/pages/status.rs:1037 #: crates/pdfs-gui/src/app/pages/status.rs:1038
msgid "Sync complete" msgid "Sync complete"
msgstr "Ukończono synchronizację" msgstr "Ukończono synchronizację"
#: crates/pdfs-gui/src/app/pages/status.rs:1039 #: crates/pdfs-gui/src/app/pages/status.rs:1040
#, rust-format #, rust-format
msgid "{n} file finished transferring." msgid "{n} file finished transferring."
msgid_plural "{n} files finished transferring." msgid_plural "{n} files finished transferring."
@@ -4732,27 +4803,27 @@ msgstr[2] "Ukończono przesyłanie {n} plików."
#. Translators: a background job's progress. {title} is the job, such as #. Translators: a background job's progress. {title} is the job, such as
#. "Scanning Photos"; {done} and {total} are counts. #. "Scanning Photos"; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1117 #: crates/pdfs-gui/src/app/pages/status.rs:1118
#, rust-format #, rust-format
msgid "{title} — {done} of {total}" msgid "{title} — {done} of {total}"
msgstr "{title} — {done} z {total}" msgstr "{title} — {done} z {total}"
#. Translators: a background job's progress. {title} is the job, {detail} #. Translators: a background job's progress. {title} is the job, {detail}
#. what it is working on; {done} and {total} are counts. #. what it is working on; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1120 #: crates/pdfs-gui/src/app/pages/status.rs:1121
#, rust-format #, rust-format
msgid "{title} — {detail} ({done} of {total})" msgid "{title} — {detail} ({done} of {total})"
msgstr "{title} — {detail} ({done} z {total})" msgstr "{title} — {detail} ({done} z {total})"
#. Translators: a background job still running. {title} is the job. #. Translators: a background job still running. {title} is the job.
#: crates/pdfs-gui/src/app/pages/status.rs:1122 #: crates/pdfs-gui/src/app/pages/status.rs:1123
#, rust-format #, rust-format
msgid "{title}…" msgid "{title}…"
msgstr "{title}…" msgstr "{title}…"
#. Translators: a background job still running. {title} is the job, #. Translators: a background job still running. {title} is the job,
#. {detail} what it is working on. #. {detail} what it is working on.
#: crates/pdfs-gui/src/app/pages/status.rs:1125 #: crates/pdfs-gui/src/app/pages/status.rs:1126
#, rust-format #, rust-format
msgid "{title} — {detail}…" msgid "{title} — {detail}…"
msgstr "{title} — {detail}…" msgstr "{title} — {detail}…"
@@ -4760,7 +4831,7 @@ msgstr "{title} — {detail}…"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} the size moved so far and {speed} a size such as #. file name, {done} the size moved so far and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1154 #: crates/pdfs-gui/src/app/pages/status.rs:1155
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} ({speed}/s)" msgid "{arrow} {name} — {done} ({speed}/s)"
msgstr "{arrow} {name} — {done} ({speed}/s)" msgstr "{arrow} {name} — {done} ({speed}/s)"
@@ -4768,38 +4839,38 @@ msgstr "{arrow} {name} — {done} ({speed}/s)"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} and {total} sizes and {speed} a size such as #. file name, {done} and {total} sizes and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1162 #: crates/pdfs-gui/src/app/pages/status.rs:1163
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} of {total} ({speed}/s)" msgid "{arrow} {name} — {done} of {total} ({speed}/s)"
msgstr "{arrow} {name} — {done} z {total} ({speed}/s)" msgstr "{arrow} {name} — {done} z {total} ({speed}/s)"
#: crates/pdfs-gui/src/app/pages/status.rs:1254 #: crates/pdfs-gui/src/app/pages/status.rs:1255
#, rust-format #, rust-format
msgid "{used} cached — no limit set" msgid "{used} cached — no limit set"
msgstr "{used} w pamięci podręcznej — bez limitu" msgstr "{used} w pamięci podręcznej — bez limitu"
#: crates/pdfs-gui/src/app/pages/status.rs:1260 #: crates/pdfs-gui/src/app/pages/status.rs:1261
#, rust-format #, rust-format
msgid "{used} of {limit} used" msgid "{used} of {limit} used"
msgstr "Użyto {used} z {limit}" msgstr "Użyto {used} z {limit}"
#: crates/pdfs-gui/src/app/pages/status.rs:1328 #: crates/pdfs-gui/src/app/pages/status.rs:1329
#, rust-format #, rust-format
msgid "{queued} · resumes {time}" msgid "{queued} · resumes {time}"
msgstr "{queued} · wznowienie {time}" msgstr "{queued} · wznowienie {time}"
#. Translators: {time} is when syncing resumes, such as "at 14:30" #. Translators: {time} is when syncing resumes, such as "at 14:30"
#. or "Tue 09:00". #. or "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1333 #: crates/pdfs-gui/src/app/pages/status.rs:1334
#, rust-format #, rust-format
msgid "Resumes {time}" msgid "Resumes {time}"
msgstr "Wznowienie {time}" msgstr "Wznowienie {time}"
#: crates/pdfs-gui/src/app/pages/status.rs:1335 #: crates/pdfs-gui/src/app/pages/status.rs:1336
msgid "Until you resume" msgid "Until you resume"
msgstr "Do czasu wznowienia" msgstr "Do czasu wznowienia"
#: crates/pdfs-gui/src/app/pages/status.rs:1342 crates/pdfs-gui/src/main.rs:239 #: crates/pdfs-gui/src/app/pages/status.rs:1343 crates/pdfs-gui/src/main.rs:248
#, rust-format #, rust-format
msgid "{n} change needs attention" msgid "{n} change needs attention"
msgid_plural "{n} changes need attention" msgid_plural "{n} changes need attention"
@@ -4807,81 +4878,81 @@ msgstr[0] "{n} zmiana wymaga uwagi"
msgstr[1] "{n} zmiany wymagają uwagi" msgstr[1] "{n} zmiany wymagają uwagi"
msgstr[2] "{n} zmian wymaga uwagi" msgstr[2] "{n} zmian wymaga uwagi"
#: crates/pdfs-gui/src/app/pages/status.rs:1352 #: crates/pdfs-gui/src/app/pages/status.rs:1353
#: crates/pdfs-gui/src/app/widgets/details.rs:108 #: crates/pdfs-gui/src/app/widgets/details.rs:108
msgctxt "state" msgctxt "state"
msgid "Offline" msgid "Offline"
msgstr "Offline" msgstr "Offline"
#: crates/pdfs-gui/src/app/pages/status.rs:1353 #: crates/pdfs-gui/src/app/pages/status.rs:1354
msgid "Cached files only" msgid "Cached files only"
msgstr "Tylko pliki w pamięci podręcznej" msgstr "Tylko pliki w pamięci podręcznej"
#: crates/pdfs-gui/src/app/pages/status.rs:1358 #: crates/pdfs-gui/src/app/pages/status.rs:1359
msgctxt "state" msgctxt "state"
msgid "Syncing" msgid "Syncing"
msgstr "Synchronizowanie" msgstr "Synchronizowanie"
#: crates/pdfs-gui/src/app/pages/status.rs:1364 #: crates/pdfs-gui/src/app/pages/status.rs:1365
msgid "Up to date" msgid "Up to date"
msgstr "Aktualne" msgstr "Aktualne"
#: crates/pdfs-gui/src/app/pages/status.rs:1371 #: crates/pdfs-gui/src/app/pages/status.rs:1372
#: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622 #: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622
msgid "Proton Drive isn't running" msgid "Proton Drive isn't running"
msgstr "Proton Drive nie jest uruchomiony" msgstr "Proton Drive nie jest uruchomiony"
#: crates/pdfs-gui/src/app/pages/status.rs:1376 #: crates/pdfs-gui/src/app/pages/status.rs:1377
msgid "Not responding" msgid "Not responding"
msgstr "Nie odpowiada" msgstr "Nie odpowiada"
#: crates/pdfs-gui/src/app/pages/status.rs:1377 #: crates/pdfs-gui/src/app/pages/status.rs:1378
msgid "Proton Drive is taking too long to answer" msgid "Proton Drive is taking too long to answer"
msgstr "Proton Drive zbyt długo nie odpowiada" msgstr "Proton Drive zbyt długo nie odpowiada"
#. Translators: stands in for a time that could not be read, as in #. Translators: stands in for a time that could not be read, as in
#. "Resumes later". #. "Resumes later".
#: crates/pdfs-gui/src/app/pages/status.rs:1415 #: crates/pdfs-gui/src/app/pages/status.rs:1416
#: crates/pdfs-gui/src/app/pages/status.rs:1430 #: crates/pdfs-gui/src/app/pages/status.rs:1431
msgid "later" msgid "later"
msgstr "później" msgstr "później"
#. Translators: strftime format for a time later today, such as "at 14:30". #. Translators: strftime format for a time later today, such as "at 14:30".
#: crates/pdfs-gui/src/app/pages/status.rs:1419 #: crates/pdfs-gui/src/app/pages/status.rs:1420
msgid "at %H:%M" msgid "at %H:%M"
msgstr "o %H:%M" msgstr "o %H:%M"
#. Translators: strftime format for a time within the next week, such as #. Translators: strftime format for a time within the next week, such as
#. "Tue 09:00". #. "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1423 #: crates/pdfs-gui/src/app/pages/status.rs:1424
msgid "%a %H:%M" msgid "%a %H:%M"
msgstr "%a %H:%M" msgstr "%a %H:%M"
#. Translators: strftime format for a date further ahead, such as "23 Sep". #. Translators: strftime format for a date further ahead, such as "23 Sep".
#: crates/pdfs-gui/src/app/pages/status.rs:1426 #: crates/pdfs-gui/src/app/pages/status.rs:1427
msgid "%e %b" msgid "%e %b"
msgstr "%-d %b" msgstr "%-d %b"
#: crates/pdfs-gui/src/app/pages/status.rs:1456 #: crates/pdfs-gui/src/app/pages/status.rs:1457
msgid "No files kept offline" msgid "No files kept offline"
msgstr "Brak plików dostępnych offline" msgstr "Brak plików dostępnych offline"
#: crates/pdfs-gui/src/app/pages/status.rs:1458 #: crates/pdfs-gui/src/app/pages/status.rs:1459
msgid "Right-click a file and choose “Make available offline”." msgid "Right-click a file and choose “Make available offline”."
msgstr "" msgstr ""
"Należy kliknąć plik prawym przyciskiem myszy i wybrać „Oznacz jako dostępne " "Należy kliknąć plik prawym przyciskiem myszy i wybrać „Oznacz jako dostępne "
"offline”." "offline”."
#: crates/pdfs-gui/src/app/pages/status.rs:1509 #: crates/pdfs-gui/src/app/pages/status.rs:1510
#: crates/pdfs-gui/src/app/pages/status.rs:1516 #: crates/pdfs-gui/src/app/pages/status.rs:1517
msgid "Couldn't make it online only" msgid "Couldn't make it online only"
msgstr "Nie można oznaczyć jako tylko online" msgstr "Nie można oznaczyć jako tylko online"
#: crates/pdfs-gui/src/app/pages/status.rs:1533 #: crates/pdfs-gui/src/app/pages/status.rs:1534
msgid "Show fewer" msgid "Show fewer"
msgstr "Wyświetl mniej" msgstr "Wyświetl mniej"
#: crates/pdfs-gui/src/app/pages/status.rs:1536 #: crates/pdfs-gui/src/app/pages/status.rs:1537
#, rust-format #, rust-format
msgid "Show all {n} file" msgid "Show all {n} file"
msgid_plural "Show all {n} files" msgid_plural "Show all {n} files"
@@ -5320,7 +5391,7 @@ msgstr "Opróżniono kosz"
msgid "Couldn't empty the trash" msgid "Couldn't empty the trash"
msgstr "Nie można opróżnić kosza" msgstr "Nie można opróżnić kosza"
#: crates/pdfs-gui/src/app/pages/verify.rs:57 #: crates/pdfs-gui/src/app/pages/verify.rs:162
msgid "Verification" msgid "Verification"
msgstr "Weryfikacja" msgstr "Weryfikacja"
@@ -5959,26 +6030,31 @@ msgstr[2] "↑ wysyłanie: {n}"
msgid "Proton Drive isn't responding" msgid "Proton Drive isn't responding"
msgstr "Proton Drive nie odpowiada" msgstr "Proton Drive nie odpowiada"
#: crates/pdfs-gui/src/main.rs:172 #: crates/pdfs-gui/src/main.rs:173
#, rust-format
msgid "Signed in as {username} — locked"
msgstr "Zalogowano jako {username} — zablokowane"
#: crates/pdfs-gui/src/main.rs:181
#, rust-format #, rust-format
msgid "Signed in as {username} — not connected" msgid "Signed in as {username} — not connected"
msgstr "Zalogowano jako {username} — brak połączenia" msgstr "Zalogowano jako {username} — brak połączenia"
#: crates/pdfs-gui/src/main.rs:177 #: crates/pdfs-gui/src/main.rs:186
msgid "Not signed in" msgid "Not signed in"
msgstr "Nie zalogowano" msgstr "Nie zalogowano"
#. Translators: {error} is an error message, in English. #. Translators: {error} is an error message, in English.
#: crates/pdfs-gui/src/main.rs:180 #: crates/pdfs-gui/src/main.rs:189
#, rust-format #, rust-format
msgid "Error: {error}" msgid "Error: {error}"
msgstr "Błąd: {error}" msgstr "Błąd: {error}"
#: crates/pdfs-gui/src/main.rs:201 #: crates/pdfs-gui/src/main.rs:210
msgid "Proton Drive sent an unexpected reply" msgid "Proton Drive sent an unexpected reply"
msgstr "Proton Drive wysłał nieoczekiwaną odpowiedź" msgstr "Proton Drive wysłał nieoczekiwaną odpowiedź"
#: crates/pdfs-gui/src/main.rs:246 #: crates/pdfs-gui/src/main.rs:255
#, rust-format #, rust-format
msgid "Up to date in {mountpoint} ({n} pinned)" msgid "Up to date in {mountpoint} ({n} pinned)"
msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)"
@@ -5986,7 +6062,7 @@ msgstr[0] "Aktualne w {mountpoint} ({n} przypięty)"
msgstr[1] "Aktualne w {mountpoint} ({n} przypięte)" msgstr[1] "Aktualne w {mountpoint} ({n} przypięte)"
msgstr[2] "Aktualne w {mountpoint} ({n} przypiętych)" msgstr[2] "Aktualne w {mountpoint} ({n} przypiętych)"
#: crates/pdfs-gui/src/main.rs:253 #: crates/pdfs-gui/src/main.rs:262
#, rust-format #, rust-format
msgid "Syncing — {queued} ({n} pinned)" msgid "Syncing — {queued} ({n} pinned)"
msgid_plural "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)"
@@ -5994,7 +6070,7 @@ msgstr[0] "Synchronizowanie — {queued} (przypięte: {n})"
msgstr[1] "Synchronizowanie — {queued} (przypięte: {n})" msgstr[1] "Synchronizowanie — {queued} (przypięte: {n})"
msgstr[2] "Synchronizowanie — {queued} (przypięte: {n})" msgstr[2] "Synchronizowanie — {queued} (przypięte: {n})"
#: crates/pdfs-gui/src/main.rs:260 #: crates/pdfs-gui/src/main.rs:269
#, rust-format #, rust-format
msgid "Offline — cached files only ({n} pinned)" msgid "Offline — cached files only ({n} pinned)"
msgid_plural "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)"
@@ -6003,13 +6079,13 @@ msgstr[1] "Offline — tylko pliki w pamięci podręcznej (przypięte: {n})"
msgstr[2] "Offline — tylko pliki w pamięci podręcznej (przypięte: {n})" msgstr[2] "Offline — tylko pliki w pamięci podręcznej (przypięte: {n})"
#. Translators: {queued} is a queue summary such as "3 uploads queued". #. Translators: {queued} is a queue summary such as "3 uploads queued".
#: crates/pdfs-gui/src/main.rs:266 #: crates/pdfs-gui/src/main.rs:275
#, rust-format #, rust-format
msgid "Offline — {queued}" msgid "Offline — {queued}"
msgstr "Offline — {queued}" msgstr "Offline — {queued}"
#. Translators: a menu item that opens the list of failing changes. #. Translators: a menu item that opens the list of failing changes.
#: crates/pdfs-gui/src/main.rs:296 #: crates/pdfs-gui/src/main.rs:305
#, rust-format #, rust-format
msgid "{n} issue — View" msgid "{n} issue — View"
msgid_plural "{n} issues — View" msgid_plural "{n} issues — View"
@@ -6017,39 +6093,39 @@ msgstr[0] "{n} problem — wyświetl"
msgstr[1] "{n} problemy — wyświetl" msgstr[1] "{n} problemy — wyświetl"
msgstr[2] "{n} problemów — wyświetl" msgstr[2] "{n} problemów — wyświetl"
#: crates/pdfs-gui/src/main.rs:402 #: crates/pdfs-gui/src/main.rs:411
msgid "Sign In…" msgid "Sign In…"
msgstr "Zaloguj się…" msgstr "Zaloguj się…"
#: crates/pdfs-gui/src/main.rs:403 #: crates/pdfs-gui/src/main.rs:412
msgid "Open Proton Drive" msgid "Open Proton Drive"
msgstr "Otwórz Proton Drive" msgstr "Otwórz Proton Drive"
#: crates/pdfs-gui/src/main.rs:409 #: crates/pdfs-gui/src/main.rs:418
msgid "Open Folder" msgid "Open Folder"
msgstr "Otwórz katalog" msgstr "Otwórz katalog"
#: crates/pdfs-gui/src/main.rs:421 #: crates/pdfs-gui/src/main.rs:430
msgid "Resume Sync" msgid "Resume Sync"
msgstr "Wznów synchronizację" msgstr "Wznów synchronizację"
#: crates/pdfs-gui/src/main.rs:423 #: crates/pdfs-gui/src/main.rs:432
msgid "Pause Sync" msgid "Pause Sync"
msgstr "Wstrzymaj synchronizację" msgstr "Wstrzymaj synchronizację"
#: crates/pdfs-gui/src/main.rs:442 #: crates/pdfs-gui/src/main.rs:451
msgid "Connect" msgid "Connect"
msgstr "Połącz" msgstr "Połącz"
#: crates/pdfs-gui/src/main.rs:449 #: crates/pdfs-gui/src/main.rs:458
msgid "Hide Tray Icon" msgid "Hide Tray Icon"
msgstr "Ukryj ikonę w obszarze powiadamiania" msgstr "Ukryj ikonę w obszarze powiadamiania"
#: crates/pdfs-gui/src/main.rs:453 #: crates/pdfs-gui/src/main.rs:462
msgid "Stop Proton Drive…" msgid "Stop Proton Drive…"
msgstr "Zatrzymaj Proton Drive…" msgstr "Zatrzymaj Proton Drive…"
#: crates/pdfs-gui/src/main.rs:457 #: crates/pdfs-gui/src/main.rs:466
msgid "Quit" msgid "Quit"
msgstr "Zakończ" msgstr "Zakończ"
+217 -141
View File
@@ -5,7 +5,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: proton-drive-linux 2.1.0\n" "Project-Id-Version: proton-drive-linux 2.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-10-05 01:01+0200\n" "POT-Creation-Date: 2026-10-08 10:15+0200\n"
"PO-Revision-Date: 2026-09-23 12:00+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n"
"Last-Translator: Proton Drive for Linux contributors\n" "Last-Translator: Proton Drive for Linux contributors\n"
"Language-Team: Portuguese (Brazil)\n" "Language-Team: Portuguese (Brazil)\n"
@@ -57,7 +57,7 @@ msgstr "%-d de %b de %Y"
msgid "%A, %B %-d, %Y, %H:%M" msgid "%A, %B %-d, %Y, %H:%M"
msgstr "%A, %-d de %B de %Y, %H:%M" msgstr "%A, %-d de %B de %Y, %H:%M"
#: crates/pdfs-gui/src/app/main.rs:743 #: crates/pdfs-gui/src/app/main.rs:760
#: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539
#: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:454
#: crates/pdfs-gui/src/app/pages/browser.rs:819 #: crates/pdfs-gui/src/app/pages/browser.rs:819
@@ -70,91 +70,91 @@ msgstr "%A, %-d de %B de %Y, %H:%M"
msgid "My files" msgid "My files"
msgstr "Meus arquivos" msgstr "Meus arquivos"
#: crates/pdfs-gui/src/app/main.rs:746 crates/pdfs-gui/src/app/main.rs:1390 #: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407
#: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/albums.rs:638
#: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/photos.rs:842
#: crates/pdfs-gui/src/app/pages/places.rs:441 #: crates/pdfs-gui/src/app/pages/places.rs:441
msgid "Photos" msgid "Photos"
msgstr "Fotos" msgstr "Fotos"
#: crates/pdfs-gui/src/app/main.rs:751 #: crates/pdfs-gui/src/app/main.rs:768
#: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:168
#: crates/pdfs-gui/src/app/pages/shared.rs:482 #: crates/pdfs-gui/src/app/pages/shared.rs:482
msgid "Shared with me" msgid "Shared with me"
msgstr "Compartilhados comigo" msgstr "Compartilhados comigo"
#: crates/pdfs-gui/src/app/main.rs:756 #: crates/pdfs-gui/src/app/main.rs:773
#: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120
msgid "Shared by me" msgid "Shared by me"
msgstr "Compartilhados por mim" msgstr "Compartilhados por mim"
#: crates/pdfs-gui/src/app/main.rs:759 #: crates/pdfs-gui/src/app/main.rs:776
#: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:140
#: crates/pdfs-gui/src/app/pages/devices.rs:481 #: crates/pdfs-gui/src/app/pages/devices.rs:481
msgid "Computers" msgid "Computers"
msgstr "Computadores" msgstr "Computadores"
#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/main.rs:777
#: crates/pdfs-gui/src/app/pages/trash.rs:101 #: crates/pdfs-gui/src/app/pages/trash.rs:101
msgid "Trash" msgid "Trash"
msgstr "Lixeira" msgstr "Lixeira"
#. Translators: sidebar entry for the page listing this computer's synced folders. #. Translators: sidebar entry for the page listing this computer's synced folders.
#: crates/pdfs-gui/src/app/main.rs:762 #: crates/pdfs-gui/src/app/main.rs:779
msgid "Sync" msgid "Sync"
msgstr "Sincronização" msgstr "Sincronização"
#. Translators: a failed action's toast. {what} says what failed, such as #. Translators: a failed action's toast. {what} says what failed, such as
#. "Couldn't rename"; {detail} is the reason, usually untranslated text #. "Couldn't rename"; {detail} is the reason, usually untranslated text
#. from the mount service. #. from the mount service.
#: crates/pdfs-gui/src/app/main.rs:1079 #: crates/pdfs-gui/src/app/main.rs:1096
#, rust-format #, rust-format
msgid "{what}: {detail}" msgid "{what}: {detail}"
msgstr "{what}: {detail}" msgstr "{what}: {detail}"
#: crates/pdfs-gui/src/app/main.rs:1098 #: crates/pdfs-gui/src/app/main.rs:1115
msgid "You're offline" msgid "You're offline"
msgstr "Você está offline" msgstr "Você está offline"
#: crates/pdfs-gui/src/app/main.rs:1099 #: crates/pdfs-gui/src/app/main.rs:1116
msgid "That's not there any more" msgid "That's not there any more"
msgstr "Isso não existe mais" msgstr "Isso não existe mais"
#: crates/pdfs-gui/src/app/main.rs:1100 #: crates/pdfs-gui/src/app/main.rs:1117
msgid "You don't have access to that" msgid "You don't have access to that"
msgstr "Você não tem acesso a isso" msgstr "Você não tem acesso a isso"
#: crates/pdfs-gui/src/app/main.rs:1101 #: crates/pdfs-gui/src/app/main.rs:1118
msgid "Something changed this first" msgid "Something changed this first"
msgstr "Algo alterou isso antes" msgstr "Algo alterou isso antes"
#: crates/pdfs-gui/src/app/main.rs:1102 #: crates/pdfs-gui/src/app/main.rs:1119
msgid "Your Proton Drive is full" msgid "Your Proton Drive is full"
msgstr "Seu Proton Drive está cheio" msgstr "Seu Proton Drive está cheio"
#: crates/pdfs-gui/src/app/main.rs:1143 crates/pdfs-gui/src/app/main.rs:1372 #: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389
#: crates/pdfs-gui/src/app/pages/status.rs:385 #: crates/pdfs-gui/src/app/pages/status.rs:385
msgid "Preferences" msgid "Preferences"
msgstr "Preferências" msgstr "Preferências"
#: crates/pdfs-gui/src/app/main.rs:1144 #: crates/pdfs-gui/src/app/main.rs:1161
#: crates/pdfs-gui/src/app/widgets/compat.rs:262 #: crates/pdfs-gui/src/app/widgets/compat.rs:262
msgid "Keyboard Shortcuts" msgid "Keyboard Shortcuts"
msgstr "Atalhos de teclado" msgstr "Atalhos de teclado"
#: crates/pdfs-gui/src/app/main.rs:1146 #: crates/pdfs-gui/src/app/main.rs:1163
msgid "About Proton Drive for Linux" msgid "About Proton Drive for Linux"
msgstr "Sobre o Proton Drive for Linux" msgstr "Sobre o Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1151 #: crates/pdfs-gui/src/app/main.rs:1168
msgid "Main menu" msgid "Main menu"
msgstr "Menu principal" msgstr "Menu principal"
#: crates/pdfs-gui/src/app/main.rs:1207 #: crates/pdfs-gui/src/app/main.rs:1224
msgid "Proton Drive for Linux" msgid "Proton Drive for Linux"
msgstr "Proton Drive for Linux" msgstr "Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1215 #: crates/pdfs-gui/src/app/main.rs:1232
msgid "" msgid ""
"Files-on-demand Proton Drive for the Linux desktop.\n" "Files-on-demand Proton Drive for the Linux desktop.\n"
"\n" "\n"
@@ -165,23 +165,23 @@ msgstr ""
"\n" "\n"
"Cliente não oficial — sem afiliação, endosso ou suporte da Proton AG." "Cliente não oficial — sem afiliação, endosso ou suporte da Proton AG."
#: crates/pdfs-gui/src/app/main.rs:1220 #: crates/pdfs-gui/src/app/main.rs:1237
msgid "Source Code" msgid "Source Code"
msgstr "Código-fonte" msgstr "Código-fonte"
#: crates/pdfs-gui/src/app/main.rs:1224 #: crates/pdfs-gui/src/app/main.rs:1241
msgid "Proton Terms of Service" msgid "Proton Terms of Service"
msgstr "Termos de Serviço da Proton" msgstr "Termos de Serviço da Proton"
#: crates/pdfs-gui/src/app/main.rs:1228 #: crates/pdfs-gui/src/app/main.rs:1245
msgid "Proton Privacy Policy" msgid "Proton Privacy Policy"
msgstr "Política de Privacidade da Proton" msgstr "Política de Privacidade da Proton"
#: crates/pdfs-gui/src/app/main.rs:1233 #: crates/pdfs-gui/src/app/main.rs:1250
msgid "Disclaimer" msgid "Disclaimer"
msgstr "Aviso legal" msgstr "Aviso legal"
#: crates/pdfs-gui/src/app/main.rs:1237 #: crates/pdfs-gui/src/app/main.rs:1254
msgid "" msgid ""
"This is an unofficial, community-built client. It is not affiliated with, " "This is an unofficial, community-built client. It is not affiliated with, "
"endorsed by, or supported by Proton AG.\n" "endorsed by, or supported by Proton AG.\n"
@@ -204,7 +204,7 @@ msgstr ""
"Este software é fornecido “no estado em que se encontra”, sem garantia de " "Este software é fornecido “no estado em que se encontra”, sem garantia de "
"qualquer tipo. Mantenha uma cópia de todos os dados que você não pode perder." "qualquer tipo. Mantenha uma cópia de todos os dados que você não pode perder."
#: crates/pdfs-gui/src/app/main.rs:1245 #: crates/pdfs-gui/src/app/main.rs:1262
msgid "" msgid ""
"Places are named with data from <a href=\"https://www.geonames.org/" "Places are named with data from <a href=\"https://www.geonames.org/"
"\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licensed under <a href=\"https://creativecommons.org/"
@@ -214,7 +214,7 @@ msgstr ""
"\">GeoNames</a>, licenciados sob <a href=\"https://creativecommons.org/" "\">GeoNames</a>, licenciados sob <a href=\"https://creativecommons.org/"
"licenses/by/4.0/\">CC BY 4.0</a>." "licenses/by/4.0/\">CC BY 4.0</a>."
#: crates/pdfs-gui/src/app/main.rs:1253 #: crates/pdfs-gui/src/app/main.rs:1270
msgid "" msgid ""
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/" "The places map is drawn from <a href=\"https://www.naturalearthdata.com/"
"\">Natural Earth</a> land outlines and country borders, which are in the " "\">Natural Earth</a> land outlines and country borders, which are in the "
@@ -224,7 +224,7 @@ msgstr ""
"fronteiras de países do <a href=\"https://www.naturalearthdata.com/" "fronteiras de países do <a href=\"https://www.naturalearthdata.com/"
"\">Natural Earth</a>, que são de domínio público." "\">Natural Earth</a>, que são de domínio público."
#: crates/pdfs-gui/src/app/main.rs:1261 #: crates/pdfs-gui/src/app/main.rs:1278
msgid "" msgid ""
"The street map, when turned on, comes from <a href=\"https://openfreemap.org/" "The street map, when turned on, comes from <a href=\"https://openfreemap.org/"
"\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/" "\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/"
@@ -240,15 +240,15 @@ msgstr ""
#. Translators: replace with your name(s) and email address(es), one #. Translators: replace with your name(s) and email address(es), one
#. per line, to be credited in the About dialog. #. per line, to be credited in the About dialog.
#: crates/pdfs-gui/src/app/main.rs:1272 #: crates/pdfs-gui/src/app/main.rs:1289
msgid "translator-credits" msgid "translator-credits"
msgstr "Colaboradores do Proton Drive for Linux" msgstr "Colaboradores do Proton Drive for Linux"
#: crates/pdfs-gui/src/app/main.rs:1310 #: crates/pdfs-gui/src/app/main.rs:1327
msgid "Stop Proton Drive?" msgid "Stop Proton Drive?"
msgstr "Parar o Proton Drive?" msgstr "Parar o Proton Drive?"
#: crates/pdfs-gui/src/app/main.rs:1311 #: crates/pdfs-gui/src/app/main.rs:1328
msgid "" msgid ""
"My files goes offline and nothing syncs until you connect again. Proton " "My files goes offline and nothing syncs until you connect again. Proton "
"Drive starts again at your next login." "Drive starts again at your next login."
@@ -256,7 +256,7 @@ msgstr ""
"Meus arquivos fica offline e nada é sincronizado até você se conectar " "Meus arquivos fica offline e nada é sincronizado até você se conectar "
"novamente. O Proton Drive será iniciado de novo no seu próximo login." "novamente. O Proton Drive será iniciado de novo no seu próximo login."
#: crates/pdfs-gui/src/app/main.rs:1313 crates/pdfs-gui/src/app/main.rs:1588 #: crates/pdfs-gui/src/app/main.rs:1330 crates/pdfs-gui/src/app/main.rs:1605
#: crates/pdfs-gui/src/app/pages/albums.rs:243 #: crates/pdfs-gui/src/app/pages/albums.rs:243
#: crates/pdfs-gui/src/app/pages/albums.rs:428 #: crates/pdfs-gui/src/app/pages/albums.rs:428
#: crates/pdfs-gui/src/app/pages/browser.rs:1892 #: crates/pdfs-gui/src/app/pages/browser.rs:1892
@@ -271,7 +271,8 @@ msgstr ""
#: crates/pdfs-gui/src/app/pages/locations.rs:1013 #: crates/pdfs-gui/src/app/pages/locations.rs:1013
#: crates/pdfs-gui/src/app/pages/locations.rs:1488 #: crates/pdfs-gui/src/app/pages/locations.rs:1488
#: crates/pdfs-gui/src/app/pages/locations.rs:1538 #: crates/pdfs-gui/src/app/pages/locations.rs:1538
#: crates/pdfs-gui/src/app/pages/login.rs:312 #: crates/pdfs-gui/src/app/pages/login.rs:393
#: crates/pdfs-gui/src/app/pages/login.rs:507
#: crates/pdfs-gui/src/app/pages/photos.rs:2055 #: crates/pdfs-gui/src/app/pages/photos.rs:2055
#: crates/pdfs-gui/src/app/pages/shared.rs:946 #: crates/pdfs-gui/src/app/pages/shared.rs:946
#: crates/pdfs-gui/src/app/pages/shared.rs:1027 #: crates/pdfs-gui/src/app/pages/shared.rs:1027
@@ -286,30 +287,30 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
#: crates/pdfs-gui/src/app/main.rs:1314 #: crates/pdfs-gui/src/app/main.rs:1331
msgctxt "verb" msgctxt "verb"
msgid "Stop" msgid "Stop"
msgstr "Parar" msgstr "Parar"
#: crates/pdfs-gui/src/app/main.rs:1331 #: crates/pdfs-gui/src/app/main.rs:1348
msgid "Couldn't stop Proton Drive" msgid "Couldn't stop Proton Drive"
msgstr "Não foi possível parar o Proton Drive" msgstr "Não foi possível parar o Proton Drive"
#: crates/pdfs-gui/src/app/main.rs:1332 #: crates/pdfs-gui/src/app/main.rs:1349
msgid "The Proton Drive service did not stop." msgid "The Proton Drive service did not stop."
msgstr "O serviço do Proton Drive não parou." msgstr "O serviço do Proton Drive não parou."
#: crates/pdfs-gui/src/app/main.rs:1368 #: crates/pdfs-gui/src/app/main.rs:1385
#: crates/pdfs-gui/src/app/pages/status.rs:254 #: crates/pdfs-gui/src/app/pages/status.rs:254
msgid "General" msgid "General"
msgstr "Geral" msgstr "Geral"
#: crates/pdfs-gui/src/app/main.rs:1370 #: crates/pdfs-gui/src/app/main.rs:1387
#: crates/pdfs-gui/src/app/pages/browser.rs:247 #: crates/pdfs-gui/src/app/pages/browser.rs:247
msgid "Search Drive" msgid "Search Drive"
msgstr "Pesquisar no Drive" msgstr "Pesquisar no Drive"
#: crates/pdfs-gui/src/app/main.rs:1371 #: crates/pdfs-gui/src/app/main.rs:1388
#: crates/pdfs-gui/src/app/pages/browser.rs:957 #: crates/pdfs-gui/src/app/pages/browser.rs:957
#: crates/pdfs-gui/src/app/pages/devices.rs:554 #: crates/pdfs-gui/src/app/pages/devices.rs:554
#: crates/pdfs-gui/src/app/pages/shared.rs:586 #: crates/pdfs-gui/src/app/pages/shared.rs:586
@@ -318,38 +319,38 @@ msgstr "Pesquisar no Drive"
msgid "Refresh" msgid "Refresh"
msgstr "Atualizar" msgstr "Atualizar"
#: crates/pdfs-gui/src/app/main.rs:1373 #: crates/pdfs-gui/src/app/main.rs:1390
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "Atalhos de teclado" msgstr "Atalhos de teclado"
#: crates/pdfs-gui/src/app/main.rs:1377 #: crates/pdfs-gui/src/app/main.rs:1394
msgid "Files" msgid "Files"
msgstr "Arquivos" msgstr "Arquivos"
#: crates/pdfs-gui/src/app/main.rs:1379 #: crates/pdfs-gui/src/app/main.rs:1396
#: crates/pdfs-gui/src/app/pages/browser.rs:294 #: crates/pdfs-gui/src/app/pages/browser.rs:294
msgid "New folder" msgid "New folder"
msgstr "Nova pasta" msgstr "Nova pasta"
#: crates/pdfs-gui/src/app/main.rs:1380 #: crates/pdfs-gui/src/app/main.rs:1397
#: crates/pdfs-gui/src/app/pages/browser.rs:290 #: crates/pdfs-gui/src/app/pages/browser.rs:290
msgid "Upload files" msgid "Upload files"
msgstr "Enviar arquivos" msgstr "Enviar arquivos"
#: crates/pdfs-gui/src/app/main.rs:1381 #: crates/pdfs-gui/src/app/main.rs:1398
msgid "Back / forward" msgid "Back / forward"
msgstr "Voltar / avançar" msgstr "Voltar / avançar"
#: crates/pdfs-gui/src/app/main.rs:1382 #: crates/pdfs-gui/src/app/main.rs:1399
#: crates/pdfs-gui/src/app/pages/browser.rs:1943 #: crates/pdfs-gui/src/app/pages/browser.rs:1943
msgid "Parent folder" msgid "Parent folder"
msgstr "Pasta superior" msgstr "Pasta superior"
#: crates/pdfs-gui/src/app/main.rs:1383 #: crates/pdfs-gui/src/app/main.rs:1400
msgid "Grid / list" msgid "Grid / list"
msgstr "Grade / lista" msgstr "Grade / lista"
#: crates/pdfs-gui/src/app/main.rs:1384 #: crates/pdfs-gui/src/app/main.rs:1401
#: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/albums.rs:298
#: crates/pdfs-gui/src/app/pages/browser.rs:1871 #: crates/pdfs-gui/src/app/pages/browser.rs:1871
#: crates/pdfs-gui/src/app/pages/browser.rs:1893 #: crates/pdfs-gui/src/app/pages/browser.rs:1893
@@ -358,7 +359,7 @@ msgstr "Grade / lista"
msgid "Rename" msgid "Rename"
msgstr "Renomear" msgstr "Renomear"
#: crates/pdfs-gui/src/app/main.rs:1385 crates/pdfs-gui/src/app/main.rs:1405 #: crates/pdfs-gui/src/app/main.rs:1402 crates/pdfs-gui/src/app/main.rs:1422
#: crates/pdfs-gui/src/app/pages/browser.rs:357 #: crates/pdfs-gui/src/app/pages/browser.rs:357
#: crates/pdfs-gui/src/app/pages/browser.rs:900 #: crates/pdfs-gui/src/app/pages/browser.rs:900
#: crates/pdfs-gui/src/app/pages/browser.rs:932 #: crates/pdfs-gui/src/app/pages/browser.rs:932
@@ -368,73 +369,73 @@ msgstr "Renomear"
msgid "Move to Trash" msgid "Move to Trash"
msgstr "Mover para a lixeira" msgstr "Mover para a lixeira"
#: crates/pdfs-gui/src/app/main.rs:1386 #: crates/pdfs-gui/src/app/main.rs:1403
msgid "Clear the selection" msgid "Clear the selection"
msgstr "Limpar a seleção" msgstr "Limpar a seleção"
#: crates/pdfs-gui/src/app/main.rs:1392 #: crates/pdfs-gui/src/app/main.rs:1409
msgid "Larger thumbnails" msgid "Larger thumbnails"
msgstr "Miniaturas maiores" msgstr "Miniaturas maiores"
#: crates/pdfs-gui/src/app/main.rs:1393 #: crates/pdfs-gui/src/app/main.rs:1410
msgid "Smaller thumbnails" msgid "Smaller thumbnails"
msgstr "Miniaturas menores" msgstr "Miniaturas menores"
#: crates/pdfs-gui/src/app/main.rs:1394 #: crates/pdfs-gui/src/app/main.rs:1411
msgid "Reset thumbnail size" msgid "Reset thumbnail size"
msgstr "Redefinir tamanho das miniaturas" msgstr "Redefinir tamanho das miniaturas"
#: crates/pdfs-gui/src/app/main.rs:1395 #: crates/pdfs-gui/src/app/main.rs:1412
msgid "Select all" msgid "Select all"
msgstr "Selecionar tudo" msgstr "Selecionar tudo"
#: crates/pdfs-gui/src/app/main.rs:1399 #: crates/pdfs-gui/src/app/main.rs:1416
msgid "Photo Viewer" msgid "Photo Viewer"
msgstr "Visualizador de fotos" msgstr "Visualizador de fotos"
#: crates/pdfs-gui/src/app/main.rs:1401 #: crates/pdfs-gui/src/app/main.rs:1418
msgid "Previous / next photo" msgid "Previous / next photo"
msgstr "Foto anterior / próxima" msgstr "Foto anterior / próxima"
#: crates/pdfs-gui/src/app/main.rs:1402 #: crates/pdfs-gui/src/app/main.rs:1419
msgid "First / last photo" msgid "First / last photo"
msgstr "Primeira / última foto" msgstr "Primeira / última foto"
#: crates/pdfs-gui/src/app/main.rs:1403 #: crates/pdfs-gui/src/app/main.rs:1420
msgid "Show details" msgid "Show details"
msgstr "Mostrar detalhes" msgstr "Mostrar detalhes"
#: crates/pdfs-gui/src/app/main.rs:1404 #: crates/pdfs-gui/src/app/main.rs:1421
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Tela cheia" msgstr "Tela cheia"
#: crates/pdfs-gui/src/app/main.rs:1406 #: crates/pdfs-gui/src/app/main.rs:1423
msgid "Close" msgid "Close"
msgstr "Fechar" msgstr "Fechar"
#: crates/pdfs-gui/src/app/main.rs:1496 #: crates/pdfs-gui/src/app/main.rs:1513
msgid "Select a single item to rename it" msgid "Select a single item to rename it"
msgstr "Selecione um único item para renomeá-lo" msgstr "Selecione um único item para renomeá-lo"
#: crates/pdfs-gui/src/app/main.rs:1656 #: crates/pdfs-gui/src/app/main.rs:1673
#: crates/pdfs-gui/src/app/pages/albums.rs:85 #: crates/pdfs-gui/src/app/pages/albums.rs:85
#: crates/pdfs-gui/src/app/pages/places.rs:104 #: crates/pdfs-gui/src/app/pages/places.rs:104
#: crates/pdfs-gui/src/app/pages/status.rs:1370 #: crates/pdfs-gui/src/app/pages/status.rs:1371
msgid "Not connected" msgid "Not connected"
msgstr "Não conectado" msgstr "Não conectado"
#: crates/pdfs-gui/src/app/main.rs:1657 #: crates/pdfs-gui/src/app/main.rs:1674
msgid "The Proton Drive service isn't running." msgid "The Proton Drive service isn't running."
msgstr "O serviço do Proton Drive não está em execução." msgstr "O serviço do Proton Drive não está em execução."
#: crates/pdfs-gui/src/app/main.rs:1665 #: crates/pdfs-gui/src/app/main.rs:1682
#: crates/pdfs-gui/src/app/pages/status.rs:336 #: crates/pdfs-gui/src/app/pages/status.rs:336
#: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595 #: crates/pdfs-gui/src/hud/mod.rs:274 crates/pdfs-gui/src/hud/mod.rs:595
#: crates/pdfs-gui/src/main.rs:444 #: crates/pdfs-gui/src/main.rs:453
msgid "Connecting…" msgid "Connecting…"
msgstr "Conectando…" msgstr "Conectando…"
#: crates/pdfs-gui/src/app/main.rs:1666 #: crates/pdfs-gui/src/app/main.rs:1683
msgid "Waiting for the Proton Drive service to start." msgid "Waiting for the Proton Drive service to start."
msgstr "Aguardando o serviço do Proton Drive iniciar." msgstr "Aguardando o serviço do Proton Drive iniciar."
@@ -1283,7 +1284,7 @@ msgstr "Não foi possível carregar os álbuns"
#: crates/pdfs-gui/src/app/pages/shared.rs:1006 #: crates/pdfs-gui/src/app/pages/shared.rs:1006
#: crates/pdfs-gui/src/app/pages/shared.rs:1122 #: crates/pdfs-gui/src/app/pages/shared.rs:1122
#: crates/pdfs-gui/src/app/pages/status.rs:750 #: crates/pdfs-gui/src/app/pages/status.rs:750
#: crates/pdfs-gui/src/app/pages/status.rs:1517 #: crates/pdfs-gui/src/app/pages/status.rs:1518
#: crates/pdfs-gui/src/app/pages/takeout.rs:424 #: crates/pdfs-gui/src/app/pages/takeout.rs:424
#: crates/pdfs-gui/src/app/pages/takeout.rs:617 #: crates/pdfs-gui/src/app/pages/takeout.rs:617
#: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:438
@@ -1551,7 +1552,7 @@ msgid "Make available offline"
msgstr "Disponibilizar offline" msgstr "Disponibilizar offline"
#: crates/pdfs-gui/src/app/pages/browser.rs:347 #: crates/pdfs-gui/src/app/pages/browser.rs:347
#: crates/pdfs-gui/src/app/pages/status.rs:1493 #: crates/pdfs-gui/src/app/pages/status.rs:1494
msgid "Make online only" msgid "Make online only"
msgstr "Tornar somente online" msgstr "Tornar somente online"
@@ -2708,7 +2709,7 @@ msgid "Sync paused until you resume"
msgstr "Sincronização pausada até você retomar" msgstr "Sincronização pausada até você retomar"
#: crates/pdfs-gui/src/app/pages/locations.rs:529 #: crates/pdfs-gui/src/app/pages/locations.rs:529
#: crates/pdfs-gui/src/app/pages/status.rs:1323 crates/pdfs-gui/src/main.rs:237 #: crates/pdfs-gui/src/app/pages/status.rs:1324 crates/pdfs-gui/src/main.rs:246
msgid "Sync paused" msgid "Sync paused"
msgstr "Sincronização pausada" msgstr "Sincronização pausada"
@@ -3177,25 +3178,35 @@ msgstr "Digite seu e-mail e senha."
msgid "Signing in…" msgid "Signing in…"
msgstr "Entrando…" msgstr "Entrando…"
#: crates/pdfs-gui/src/app/pages/login.rs:155 #: crates/pdfs-gui/src/app/pages/login.rs:164
msgid "Complete the verification to continue…" msgid "Complete the verification to continue…"
msgstr "Conclua a verificação para continuar…" msgstr "Conclua a verificação para continuar…"
#: crates/pdfs-gui/src/app/pages/login.rs:165 #: crates/pdfs-gui/src/app/pages/login.rs:197
msgid "login cancelled" msgid "login cancelled"
msgstr "login cancelado" msgstr "login cancelado"
#: crates/pdfs-gui/src/app/pages/login.rs:198 #: crates/pdfs-gui/src/app/pages/login.rs:210
msgid ""
"The verification page couldn't be opened, so the sign-in was cancelled. Your "
"system may keep the web view's sandbox from starting; the troubleshooting "
"guide explains what to do."
msgstr ""
"Não foi possível abrir a página de verificação, então a entrada foi "
"cancelada. Seu sistema pode estar impedindo que a sandbox da visualização "
"web inicie; o guia de solução de problemas explica o que fazer."
#: crates/pdfs-gui/src/app/pages/login.rs:233
msgid "Too many sign-in attempts. Wait a few minutes and try again." msgid "Too many sign-in attempts. Wait a few minutes and try again."
msgstr "Muitas tentativas de entrar. Aguarde alguns minutos e tente novamente." msgstr "Muitas tentativas de entrar. Aguarde alguns minutos e tente novamente."
#: crates/pdfs-gui/src/app/pages/login.rs:204 #: crates/pdfs-gui/src/app/pages/login.rs:239
msgid "Couldn't reach Proton. Check your internet connection and try again." msgid "Couldn't reach Proton. Check your internet connection and try again."
msgstr "" msgstr ""
"Não foi possível acessar o Proton. Verifique sua conexão com a internet e " "Não foi possível acessar o Proton. Verifique sua conexão com a internet e "
"tente novamente." "tente novamente."
#: crates/pdfs-gui/src/app/pages/login.rs:207 #: crates/pdfs-gui/src/app/pages/login.rs:242
msgid "" msgid ""
"Signed in, but the session couldn't be saved to the system keyring. Make " "Signed in, but the session couldn't be saved to the system keyring. Make "
"sure a keyring (GNOME Keyring or KWallet) is running and unlocked." "sure a keyring (GNOME Keyring or KWallet) is running and unlocked."
@@ -3204,11 +3215,19 @@ msgstr ""
"Certifique-se de que um chaveiro (GNOME Keyring ou KWallet) esteja em " "Certifique-se de que um chaveiro (GNOME Keyring ou KWallet) esteja em "
"execução e desbloqueado." "execução e desbloqueado."
#: crates/pdfs-gui/src/app/pages/login.rs:210 #: crates/pdfs-gui/src/app/pages/login.rs:245
msgid "Sign-in cancelled: no two-factor code was entered." msgid "Sign-in cancelled: no two-factor code was entered."
msgstr "Entrada cancelada: nenhum código de dois fatores foi digitado." msgstr "Entrada cancelada: nenhum código de dois fatores foi digitado."
#: crates/pdfs-gui/src/app/pages/login.rs:213 #: crates/pdfs-gui/src/app/pages/login.rs:248
msgid "Sign-in cancelled: no mailbox password was entered."
msgstr "Entrada cancelada: nenhuma senha da caixa de correio foi digitada."
#: crates/pdfs-gui/src/app/pages/login.rs:251
msgid "That mailbox password didn't unlock your account."
msgstr "Essa senha da caixa de correio não desbloqueou sua conta."
#: crates/pdfs-gui/src/app/pages/login.rs:254
msgid "" msgid ""
"Sign-in cancelled: the verification wasn't completed. Sign in again to get a " "Sign-in cancelled: the verification wasn't completed. Sign in again to get a "
"new one." "new one."
@@ -3217,35 +3236,87 @@ msgstr ""
"receber uma nova." "receber uma nova."
#. Translators: {error} is an error message, usually in English. #. Translators: {error} is an error message, usually in English.
#: crates/pdfs-gui/src/app/pages/login.rs:218 #: crates/pdfs-gui/src/app/pages/login.rs:259
#, rust-format #, rust-format
msgid "Sign-in failed: {error}" msgid "Sign-in failed: {error}"
msgstr "Falha ao entrar: {error}" msgstr "Falha ao entrar: {error}"
#: crates/pdfs-gui/src/app/pages/login.rs:298 #: crates/pdfs-gui/src/app/pages/login.rs:369
msgid "That mailbox password didn't unlock your account. Try again."
msgstr ""
"Essa senha da caixa de correio não desbloqueou sua conta. Tente novamente."
#: crates/pdfs-gui/src/app/pages/login.rs:371
msgid ""
"This account has a separate mailbox password. Enter it to unlock your files."
msgstr ""
"Esta conta tem uma senha da caixa de correio separada. Digite-a para "
"desbloquear seus arquivos."
#: crates/pdfs-gui/src/app/pages/login.rs:374
msgid "Mailbox Password"
msgstr "Senha da caixa de correio"
#: crates/pdfs-gui/src/app/pages/login.rs:380
msgid "Mailbox password"
msgstr "Senha da caixa de correio"
#: crates/pdfs-gui/src/app/pages/login.rs:384
msgid "Keep unlocked on this computer"
msgstr "Manter desbloqueado neste computador"
#: crates/pdfs-gui/src/app/pages/login.rs:386
msgid ""
"Stores the unlock in the system keyring, so Proton Drive opens without "
"asking after every login. Otherwise you enter the password once per session."
msgstr ""
"Guarda o desbloqueio no chaveiro do sistema, para que o Proton Drive abra "
"sem perguntar após cada login. Caso contrário, você digita a senha uma vez "
"por sessão."
#: crates/pdfs-gui/src/app/pages/login.rs:394
#: crates/pdfs-gui/src/app/pages/login.rs:447
msgid "Unlock"
msgstr "Desbloquear"
#: crates/pdfs-gui/src/app/pages/login.rs:446
msgid "Proton Drive is locked. Enter your mailbox password to unlock it."
msgstr ""
"O Proton Drive está bloqueado. Digite a senha da caixa de correio para "
"desbloqueá-lo."
#: crates/pdfs-gui/src/app/pages/login.rs:475
msgid "Unlocked."
msgstr "Desbloqueado."
#: crates/pdfs-gui/src/app/pages/login.rs:480
msgid "Couldn't unlock"
msgstr "Não foi possível desbloquear"
#: crates/pdfs-gui/src/app/pages/login.rs:493
msgid "Two-Factor Authentication" msgid "Two-Factor Authentication"
msgstr "Autenticação de dois fatores" msgstr "Autenticação de dois fatores"
#: crates/pdfs-gui/src/app/pages/login.rs:300 #: crates/pdfs-gui/src/app/pages/login.rs:495
msgid "" msgid ""
"Enter the code from your authenticator app, or one of your recovery codes." "Enter the code from your authenticator app, or one of your recovery codes."
msgstr "" msgstr ""
"Digite o código do seu aplicativo autenticador ou um dos seus códigos de " "Digite o código do seu aplicativo autenticador ou um dos seus códigos de "
"recuperação." "recuperação."
#: crates/pdfs-gui/src/app/pages/login.rs:306 #: crates/pdfs-gui/src/app/pages/login.rs:501
msgid "Authentication code" msgid "Authentication code"
msgstr "Código de autenticação" msgstr "Código de autenticação"
#: crates/pdfs-gui/src/app/pages/login.rs:313 #: crates/pdfs-gui/src/app/pages/login.rs:508
msgid "Confirm" msgid "Confirm"
msgstr "Confirmar" msgstr "Confirmar"
#: crates/pdfs-gui/src/app/pages/login.rs:341 #: crates/pdfs-gui/src/app/pages/login.rs:536
msgid "Sign Out?" msgid "Sign Out?"
msgstr "Sair?" msgstr "Sair?"
#: crates/pdfs-gui/src/app/pages/login.rs:343 #: crates/pdfs-gui/src/app/pages/login.rs:538
msgid "" msgid ""
"Proton Drive will disconnect and stop syncing until you sign in again. Files " "Proton Drive will disconnect and stop syncing until you sign in again. Files "
"already on this computer stay where they are." "already on this computer stay where they are."
@@ -3253,7 +3324,7 @@ msgstr ""
"O Proton Drive será desconectado e deixará de sincronizar até você entrar " "O Proton Drive será desconectado e deixará de sincronizar até você entrar "
"novamente. Os arquivos que já estão neste computador permanecem onde estão." "novamente. Os arquivos que já estão neste computador permanecem onde estão."
#: crates/pdfs-gui/src/app/pages/login.rs:345 #: crates/pdfs-gui/src/app/pages/login.rs:540
msgid "Sign Out" msgid "Sign Out"
msgstr "Sair" msgstr "Sair"
@@ -4604,40 +4675,40 @@ msgid "Restart Now"
msgstr "Reiniciar agora" msgstr "Reiniciar agora"
#. Translators: {size} is an amount of storage, such as "1.2 GiB". #. Translators: {size} is an amount of storage, such as "1.2 GiB".
#: crates/pdfs-gui/src/app/pages/status.rs:912 #: crates/pdfs-gui/src/app/pages/status.rs:913
#, rust-format #, rust-format
msgid "{size} used" msgid "{size} used"
msgstr "{size} usados" msgstr "{size} usados"
#: crates/pdfs-gui/src/app/pages/status.rs:923 #: crates/pdfs-gui/src/app/pages/status.rs:924
#, rust-format #, rust-format
msgid "{used} of {total} used ({percent}%)" msgid "{used} of {total} used ({percent}%)"
msgstr "{used} de {total} usados ({percent}%)" msgstr "{used} de {total} usados ({percent}%)"
#: crates/pdfs-gui/src/app/pages/status.rs:961 #: crates/pdfs-gui/src/app/pages/status.rs:962
msgid "Proton Drive connected" msgid "Proton Drive connected"
msgstr "Proton Drive conectado" msgstr "Proton Drive conectado"
#: crates/pdfs-gui/src/app/pages/status.rs:962 #: crates/pdfs-gui/src/app/pages/status.rs:963
msgid "Your files are available." msgid "Your files are available."
msgstr "Seus arquivos estão disponíveis." msgstr "Seus arquivos estão disponíveis."
#: crates/pdfs-gui/src/app/pages/status.rs:967 #: crates/pdfs-gui/src/app/pages/status.rs:968
msgid "Proton Drive disconnected" msgid "Proton Drive disconnected"
msgstr "Proton Drive desconectado" msgstr "Proton Drive desconectado"
#: crates/pdfs-gui/src/app/pages/status.rs:968 #: crates/pdfs-gui/src/app/pages/status.rs:969
msgid "" msgid ""
"The Proton Drive service stopped. Files aren't available until it restarts." "The Proton Drive service stopped. Files aren't available until it restarts."
msgstr "" msgstr ""
"O serviço do Proton Drive parou. Os arquivos ficarão indisponíveis até ele " "O serviço do Proton Drive parou. Os arquivos ficarão indisponíveis até ele "
"reiniciar." "reiniciar."
#: crates/pdfs-gui/src/app/pages/status.rs:1037 #: crates/pdfs-gui/src/app/pages/status.rs:1038
msgid "Sync complete" msgid "Sync complete"
msgstr "Sincronização concluída" msgstr "Sincronização concluída"
#: crates/pdfs-gui/src/app/pages/status.rs:1039 #: crates/pdfs-gui/src/app/pages/status.rs:1040
#, rust-format #, rust-format
msgid "{n} file finished transferring." msgid "{n} file finished transferring."
msgid_plural "{n} files finished transferring." msgid_plural "{n} files finished transferring."
@@ -4646,27 +4717,27 @@ msgstr[1] "{n} arquivos terminaram de ser transferidos."
#. Translators: a background job's progress. {title} is the job, such as #. Translators: a background job's progress. {title} is the job, such as
#. "Scanning Photos"; {done} and {total} are counts. #. "Scanning Photos"; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1117 #: crates/pdfs-gui/src/app/pages/status.rs:1118
#, rust-format #, rust-format
msgid "{title} — {done} of {total}" msgid "{title} — {done} of {total}"
msgstr "{title} — {done} de {total}" msgstr "{title} — {done} de {total}"
#. Translators: a background job's progress. {title} is the job, {detail} #. Translators: a background job's progress. {title} is the job, {detail}
#. what it is working on; {done} and {total} are counts. #. what it is working on; {done} and {total} are counts.
#: crates/pdfs-gui/src/app/pages/status.rs:1120 #: crates/pdfs-gui/src/app/pages/status.rs:1121
#, rust-format #, rust-format
msgid "{title} — {detail} ({done} of {total})" msgid "{title} — {detail} ({done} of {total})"
msgstr "{title} — {detail} ({done} de {total})" msgstr "{title} — {detail} ({done} de {total})"
#. Translators: a background job still running. {title} is the job. #. Translators: a background job still running. {title} is the job.
#: crates/pdfs-gui/src/app/pages/status.rs:1122 #: crates/pdfs-gui/src/app/pages/status.rs:1123
#, rust-format #, rust-format
msgid "{title}…" msgid "{title}…"
msgstr "{title}…" msgstr "{title}…"
#. Translators: a background job still running. {title} is the job, #. Translators: a background job still running. {title} is the job,
#. {detail} what it is working on. #. {detail} what it is working on.
#: crates/pdfs-gui/src/app/pages/status.rs:1125 #: crates/pdfs-gui/src/app/pages/status.rs:1126
#, rust-format #, rust-format
msgid "{title} — {detail}…" msgid "{title} — {detail}…"
msgstr "{title} — {detail}…" msgstr "{title} — {detail}…"
@@ -4674,7 +4745,7 @@ msgstr "{title} — {detail}…"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} the size moved so far and {speed} a size such as #. file name, {done} the size moved so far and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1154 #: crates/pdfs-gui/src/app/pages/status.rs:1155
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} ({speed}/s)" msgid "{arrow} {name} — {done} ({speed}/s)"
msgstr "{arrow} {name} — {done} ({speed}/s)" msgstr "{arrow} {name} — {done} ({speed}/s)"
@@ -4682,118 +4753,118 @@ msgstr "{arrow} {name} — {done} ({speed}/s)"
#. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the #. Translators: a file being transferred. {arrow} is ↑ or ↓, {name} the
#. file name, {done} and {total} sizes and {speed} a size such as #. file name, {done} and {total} sizes and {speed} a size such as
#. "1.2 MiB"; "/s" means per second. #. "1.2 MiB"; "/s" means per second.
#: crates/pdfs-gui/src/app/pages/status.rs:1162 #: crates/pdfs-gui/src/app/pages/status.rs:1163
#, rust-format #, rust-format
msgid "{arrow} {name} — {done} of {total} ({speed}/s)" msgid "{arrow} {name} — {done} of {total} ({speed}/s)"
msgstr "{arrow} {name} — {done} de {total} ({speed}/s)" msgstr "{arrow} {name} — {done} de {total} ({speed}/s)"
#: crates/pdfs-gui/src/app/pages/status.rs:1254 #: crates/pdfs-gui/src/app/pages/status.rs:1255
#, rust-format #, rust-format
msgid "{used} cached — no limit set" msgid "{used} cached — no limit set"
msgstr "{used} em cache — sem limite definido" msgstr "{used} em cache — sem limite definido"
#: crates/pdfs-gui/src/app/pages/status.rs:1260 #: crates/pdfs-gui/src/app/pages/status.rs:1261
#, rust-format #, rust-format
msgid "{used} of {limit} used" msgid "{used} of {limit} used"
msgstr "{used} de {limit} usados" msgstr "{used} de {limit} usados"
#: crates/pdfs-gui/src/app/pages/status.rs:1328 #: crates/pdfs-gui/src/app/pages/status.rs:1329
#, rust-format #, rust-format
msgid "{queued} · resumes {time}" msgid "{queued} · resumes {time}"
msgstr "{queued} · retoma {time}" msgstr "{queued} · retoma {time}"
#. Translators: {time} is when syncing resumes, such as "at 14:30" #. Translators: {time} is when syncing resumes, such as "at 14:30"
#. or "Tue 09:00". #. or "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1333 #: crates/pdfs-gui/src/app/pages/status.rs:1334
#, rust-format #, rust-format
msgid "Resumes {time}" msgid "Resumes {time}"
msgstr "Retoma {time}" msgstr "Retoma {time}"
#: crates/pdfs-gui/src/app/pages/status.rs:1335 #: crates/pdfs-gui/src/app/pages/status.rs:1336
msgid "Until you resume" msgid "Until you resume"
msgstr "Até você retomar" msgstr "Até você retomar"
#: crates/pdfs-gui/src/app/pages/status.rs:1342 crates/pdfs-gui/src/main.rs:239 #: crates/pdfs-gui/src/app/pages/status.rs:1343 crates/pdfs-gui/src/main.rs:248
#, rust-format #, rust-format
msgid "{n} change needs attention" msgid "{n} change needs attention"
msgid_plural "{n} changes need attention" msgid_plural "{n} changes need attention"
msgstr[0] "{n} alteração requer atenção" msgstr[0] "{n} alteração requer atenção"
msgstr[1] "{n} alterações requerem atenção" msgstr[1] "{n} alterações requerem atenção"
#: crates/pdfs-gui/src/app/pages/status.rs:1352 #: crates/pdfs-gui/src/app/pages/status.rs:1353
#: crates/pdfs-gui/src/app/widgets/details.rs:108 #: crates/pdfs-gui/src/app/widgets/details.rs:108
msgctxt "state" msgctxt "state"
msgid "Offline" msgid "Offline"
msgstr "Offline" msgstr "Offline"
#: crates/pdfs-gui/src/app/pages/status.rs:1353 #: crates/pdfs-gui/src/app/pages/status.rs:1354
msgid "Cached files only" msgid "Cached files only"
msgstr "Somente arquivos em cache" msgstr "Somente arquivos em cache"
#: crates/pdfs-gui/src/app/pages/status.rs:1358 #: crates/pdfs-gui/src/app/pages/status.rs:1359
msgctxt "state" msgctxt "state"
msgid "Syncing" msgid "Syncing"
msgstr "Sincronizando" msgstr "Sincronizando"
#: crates/pdfs-gui/src/app/pages/status.rs:1364 #: crates/pdfs-gui/src/app/pages/status.rs:1365
msgid "Up to date" msgid "Up to date"
msgstr "Atualizado" msgstr "Atualizado"
#: crates/pdfs-gui/src/app/pages/status.rs:1371 #: crates/pdfs-gui/src/app/pages/status.rs:1372
#: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622 #: crates/pdfs-gui/src/hud/mod.rs:608 crates/pdfs-gui/src/hud/mod.rs:622
msgid "Proton Drive isn't running" msgid "Proton Drive isn't running"
msgstr "O Proton Drive não está em execução" msgstr "O Proton Drive não está em execução"
#: crates/pdfs-gui/src/app/pages/status.rs:1376 #: crates/pdfs-gui/src/app/pages/status.rs:1377
msgid "Not responding" msgid "Not responding"
msgstr "Não está respondendo" msgstr "Não está respondendo"
#: crates/pdfs-gui/src/app/pages/status.rs:1377 #: crates/pdfs-gui/src/app/pages/status.rs:1378
msgid "Proton Drive is taking too long to answer" msgid "Proton Drive is taking too long to answer"
msgstr "O Proton Drive está demorando demais para responder" msgstr "O Proton Drive está demorando demais para responder"
#. Translators: stands in for a time that could not be read, as in #. Translators: stands in for a time that could not be read, as in
#. "Resumes later". #. "Resumes later".
#: crates/pdfs-gui/src/app/pages/status.rs:1415 #: crates/pdfs-gui/src/app/pages/status.rs:1416
#: crates/pdfs-gui/src/app/pages/status.rs:1430 #: crates/pdfs-gui/src/app/pages/status.rs:1431
msgid "later" msgid "later"
msgstr "mais tarde" msgstr "mais tarde"
#. Translators: strftime format for a time later today, such as "at 14:30". #. Translators: strftime format for a time later today, such as "at 14:30".
#: crates/pdfs-gui/src/app/pages/status.rs:1419 #: crates/pdfs-gui/src/app/pages/status.rs:1420
msgid "at %H:%M" msgid "at %H:%M"
msgstr "às %H:%M" msgstr "às %H:%M"
#. Translators: strftime format for a time within the next week, such as #. Translators: strftime format for a time within the next week, such as
#. "Tue 09:00". #. "Tue 09:00".
#: crates/pdfs-gui/src/app/pages/status.rs:1423 #: crates/pdfs-gui/src/app/pages/status.rs:1424
msgid "%a %H:%M" msgid "%a %H:%M"
msgstr "%a às %H:%M" msgstr "%a às %H:%M"
#. Translators: strftime format for a date further ahead, such as "23 Sep". #. Translators: strftime format for a date further ahead, such as "23 Sep".
#: crates/pdfs-gui/src/app/pages/status.rs:1426 #: crates/pdfs-gui/src/app/pages/status.rs:1427
msgid "%e %b" msgid "%e %b"
msgstr "em %-d de %b" msgstr "em %-d de %b"
#: crates/pdfs-gui/src/app/pages/status.rs:1456 #: crates/pdfs-gui/src/app/pages/status.rs:1457
msgid "No files kept offline" msgid "No files kept offline"
msgstr "Nenhum arquivo mantido offline" msgstr "Nenhum arquivo mantido offline"
#: crates/pdfs-gui/src/app/pages/status.rs:1458 #: crates/pdfs-gui/src/app/pages/status.rs:1459
msgid "Right-click a file and choose “Make available offline”." msgid "Right-click a file and choose “Make available offline”."
msgstr "" msgstr ""
"Clique com o botão direito em um arquivo e escolha “Disponibilizar offline”." "Clique com o botão direito em um arquivo e escolha “Disponibilizar offline”."
#: crates/pdfs-gui/src/app/pages/status.rs:1509 #: crates/pdfs-gui/src/app/pages/status.rs:1510
#: crates/pdfs-gui/src/app/pages/status.rs:1516 #: crates/pdfs-gui/src/app/pages/status.rs:1517
msgid "Couldn't make it online only" msgid "Couldn't make it online only"
msgstr "Não foi possível torná-lo somente online" msgstr "Não foi possível torná-lo somente online"
#: crates/pdfs-gui/src/app/pages/status.rs:1533 #: crates/pdfs-gui/src/app/pages/status.rs:1534
msgid "Show fewer" msgid "Show fewer"
msgstr "Mostrar menos" msgstr "Mostrar menos"
#: crates/pdfs-gui/src/app/pages/status.rs:1536 #: crates/pdfs-gui/src/app/pages/status.rs:1537
#, rust-format #, rust-format
msgid "Show all {n} file" msgid "Show all {n} file"
msgid_plural "Show all {n} files" msgid_plural "Show all {n} files"
@@ -5223,7 +5294,7 @@ msgstr "Lixeira esvaziada"
msgid "Couldn't empty the trash" msgid "Couldn't empty the trash"
msgstr "Não foi possível esvaziar a lixeira" msgstr "Não foi possível esvaziar a lixeira"
#: crates/pdfs-gui/src/app/pages/verify.rs:57 #: crates/pdfs-gui/src/app/pages/verify.rs:162
msgid "Verification" msgid "Verification"
msgstr "Verificação" msgstr "Verificação"
@@ -5855,40 +5926,45 @@ msgstr[1] "↑ {n} enviando"
msgid "Proton Drive isn't responding" msgid "Proton Drive isn't responding"
msgstr "O Proton Drive não está respondendo" msgstr "O Proton Drive não está respondendo"
#: crates/pdfs-gui/src/main.rs:172 #: crates/pdfs-gui/src/main.rs:173
#, rust-format
msgid "Signed in as {username} — locked"
msgstr "Conectado como {username} — bloqueado"
#: crates/pdfs-gui/src/main.rs:181
#, rust-format #, rust-format
msgid "Signed in as {username} — not connected" msgid "Signed in as {username} — not connected"
msgstr "Conectado como {username} — sem conexão" msgstr "Conectado como {username} — sem conexão"
#: crates/pdfs-gui/src/main.rs:177 #: crates/pdfs-gui/src/main.rs:186
msgid "Not signed in" msgid "Not signed in"
msgstr "Não conectado" msgstr "Não conectado"
#. Translators: {error} is an error message, in English. #. Translators: {error} is an error message, in English.
#: crates/pdfs-gui/src/main.rs:180 #: crates/pdfs-gui/src/main.rs:189
#, rust-format #, rust-format
msgid "Error: {error}" msgid "Error: {error}"
msgstr "Erro: {error}" msgstr "Erro: {error}"
#: crates/pdfs-gui/src/main.rs:201 #: crates/pdfs-gui/src/main.rs:210
msgid "Proton Drive sent an unexpected reply" msgid "Proton Drive sent an unexpected reply"
msgstr "O Proton Drive enviou uma resposta inesperada" msgstr "O Proton Drive enviou uma resposta inesperada"
#: crates/pdfs-gui/src/main.rs:246 #: crates/pdfs-gui/src/main.rs:255
#, rust-format #, rust-format
msgid "Up to date in {mountpoint} ({n} pinned)" msgid "Up to date in {mountpoint} ({n} pinned)"
msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)"
msgstr[0] "Atualizado em {mountpoint} ({n} fixado)" msgstr[0] "Atualizado em {mountpoint} ({n} fixado)"
msgstr[1] "Atualizado em {mountpoint} ({n} fixados)" msgstr[1] "Atualizado em {mountpoint} ({n} fixados)"
#: crates/pdfs-gui/src/main.rs:253 #: crates/pdfs-gui/src/main.rs:262
#, rust-format #, rust-format
msgid "Syncing — {queued} ({n} pinned)" msgid "Syncing — {queued} ({n} pinned)"
msgid_plural "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)"
msgstr[0] "Sincronizando — {queued} ({n} fixado)" msgstr[0] "Sincronizando — {queued} ({n} fixado)"
msgstr[1] "Sincronizando — {queued} ({n} fixados)" msgstr[1] "Sincronizando — {queued} ({n} fixados)"
#: crates/pdfs-gui/src/main.rs:260 #: crates/pdfs-gui/src/main.rs:269
#, rust-format #, rust-format
msgid "Offline — cached files only ({n} pinned)" msgid "Offline — cached files only ({n} pinned)"
msgid_plural "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)"
@@ -5896,52 +5972,52 @@ msgstr[0] "Offline — somente arquivos em cache ({n} fixado)"
msgstr[1] "Offline — somente arquivos em cache ({n} fixados)" msgstr[1] "Offline — somente arquivos em cache ({n} fixados)"
#. Translators: {queued} is a queue summary such as "3 uploads queued". #. Translators: {queued} is a queue summary such as "3 uploads queued".
#: crates/pdfs-gui/src/main.rs:266 #: crates/pdfs-gui/src/main.rs:275
#, rust-format #, rust-format
msgid "Offline — {queued}" msgid "Offline — {queued}"
msgstr "Offline — {queued}" msgstr "Offline — {queued}"
#. Translators: a menu item that opens the list of failing changes. #. Translators: a menu item that opens the list of failing changes.
#: crates/pdfs-gui/src/main.rs:296 #: crates/pdfs-gui/src/main.rs:305
#, rust-format #, rust-format
msgid "{n} issue — View" msgid "{n} issue — View"
msgid_plural "{n} issues — View" msgid_plural "{n} issues — View"
msgstr[0] "{n} problema — Ver" msgstr[0] "{n} problema — Ver"
msgstr[1] "{n} problemas — Ver" msgstr[1] "{n} problemas — Ver"
#: crates/pdfs-gui/src/main.rs:402 #: crates/pdfs-gui/src/main.rs:411
msgid "Sign In…" msgid "Sign In…"
msgstr "Entrar…" msgstr "Entrar…"
#: crates/pdfs-gui/src/main.rs:403 #: crates/pdfs-gui/src/main.rs:412
msgid "Open Proton Drive" msgid "Open Proton Drive"
msgstr "Abrir o Proton Drive" msgstr "Abrir o Proton Drive"
#: crates/pdfs-gui/src/main.rs:409 #: crates/pdfs-gui/src/main.rs:418
msgid "Open Folder" msgid "Open Folder"
msgstr "Abrir pasta" msgstr "Abrir pasta"
#: crates/pdfs-gui/src/main.rs:421 #: crates/pdfs-gui/src/main.rs:430
msgid "Resume Sync" msgid "Resume Sync"
msgstr "Retomar sincronização" msgstr "Retomar sincronização"
#: crates/pdfs-gui/src/main.rs:423 #: crates/pdfs-gui/src/main.rs:432
msgid "Pause Sync" msgid "Pause Sync"
msgstr "Pausar sincronização" msgstr "Pausar sincronização"
#: crates/pdfs-gui/src/main.rs:442 #: crates/pdfs-gui/src/main.rs:451
msgid "Connect" msgid "Connect"
msgstr "Conectar" msgstr "Conectar"
#: crates/pdfs-gui/src/main.rs:449 #: crates/pdfs-gui/src/main.rs:458
msgid "Hide Tray Icon" msgid "Hide Tray Icon"
msgstr "Ocultar ícone da bandeja" msgstr "Ocultar ícone da bandeja"
#: crates/pdfs-gui/src/main.rs:453 #: crates/pdfs-gui/src/main.rs:462
msgid "Stop Proton Drive…" msgid "Stop Proton Drive…"
msgstr "Parar o Proton Drive…" msgstr "Parar o Proton Drive…"
#: crates/pdfs-gui/src/main.rs:457 #: crates/pdfs-gui/src/main.rs:466
msgid "Quit" msgid "Quit"
msgstr "Sair" msgstr "Sair"
+6 -5
View File
@@ -1410,10 +1410,11 @@ def test_throughput(ctx: Context) -> None:
on the network. PDFS_ACCEPTANCE_MIN_MIBPS and PDFS_ACCEPTANCE_MIN_OPS move on the network. PDFS_ACCEPTANCE_MIN_MIBPS and PDFS_ACCEPTANCE_MIN_OPS move
the floors; 0 turns one off. the floors; 0 turns one off.
Metadata is the exception. On a Drive mount each create and each unlink Metadata has a lower default floor on a FUSE target: 3 operations a
waits for Drive before it returns (about 350 ms for a create), so 20 second. A local-first mount records each create and unlink locally and
operations a second is out of reach there by design. The default floor for clears it easily, but with `local_first` off each one waits for Drive
a FUSE target is 3, which still catches a second round trip per operation. before it returns (about 350 ms for a create), and 20 a second is out of
reach. The floor still catches a second round trip per operation.
""" """
min_rate = float(os.environ.get("PDFS_ACCEPTANCE_MIN_MIBPS", "10")) min_rate = float(os.environ.get("PDFS_ACCEPTANCE_MIN_MIBPS", "10"))
default_ops = "3" if is_fuse(ctx.root) else "20" default_ops = "3" if is_fuse(ctx.root) else "20"
@@ -2344,7 +2345,7 @@ def _shared_root(daemon: Daemon, mount: Path, role: str) -> Path:
Skips rather than fails when the account has no such share: the role cases Skips rather than fails when the account has no such share: the role cases
need a *second* account to have shared something at that role, which most need a *second* account to have shared something at that role, which most
runs will not have (mount-architecture.md §7). runs will not have.
""" """
entries = [e for e in _shared_with_me(daemon) if e.get("role") == role] entries = [e for e in _shared_with_me(daemon) if e.get("role") == role]
if not entries: if not entries: