mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a888e0bb28 | ||
|
|
c7fc85e943 | ||
|
|
5bfae0fee1 | ||
|
|
edd62c5f7a | ||
|
|
5c1530b4b9 | ||
|
|
ce324510de | ||
|
|
274bfd0ca2 | ||
|
|
b8cfd30ff3 | ||
|
|
9fa88ff397 | ||
|
|
f105159186 | ||
|
|
a14a63c94c | ||
|
|
ed3477d1b3 | ||
|
|
aa384ae2eb | ||
|
|
10c69b0d64 | ||
|
|
39be354bd2 | ||
|
|
2cca8afba9 | ||
|
|
6e9beb4b16 | ||
|
|
63484bf4b6 | ||
|
|
37c66b7220 | ||
|
|
41b6edbecb |
No files matched your search
@@ -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'
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ scriptable CLI.
|
|||||||
[](https://aur.archlinux.org/packages/proton-drive-for-linux)
|
[](https://aur.archlinux.org/packages/proton-drive-for-linux)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://www.rust-lang.org)
|
[](https://www.rust-lang.org)
|
||||||
|
[](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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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]
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -102,5 +102,5 @@ impl Db {
|
|||||||
Ok(seen)
|
Ok(seen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- device sync (devices.md) -----------------------------------------
|
// ---- device sync -----------------------------------------
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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,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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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());
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,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,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;
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large.
Load diff
+3676
-3184
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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
@@ -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
@@ -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).
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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`;
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 363 KiB |
@@ -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
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 ""
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in new issue
Block a user