From a888e0bb28a94173de4e142be9e2b1eb871c7dee Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Thu, 8 Oct 2026 10:25:14 +0200 Subject: [PATCH] fix(gui): run the CAPTCHA page in its own process (B195) On secureblue, WebKit cannot start the sandbox for the verification page: bubblewrap is kept from the journal socket, xdg-dbus-proxy fails, and WebKit ends its process with g_error. The webview lived in pdfs-app, so the whole app crashed after the sign-in (#29). - gui: `pdfs-app --human-verification` shows the page in its own window, reads the URL on stdin and prints the token on stdout; its logs go to stderr - gui: a child that dies or fails without a token ends the sign-in with a message that the page could not be opened, instead of "cancelled" - i18n: translate the new message in every catalog - docs: B195 in the bug ledger, the cause and a one-time workaround in TROUBLESHOOTING.md, the child process in ARCHITECTURE.md, changelog --- crates/pdfs-gui/src/app/main.rs | 18 +- crates/pdfs-gui/src/app/pages/login.rs | 19 +- crates/pdfs-gui/src/app/pages/verify.rs | 208 ++++++++++++++++---- docs/ARCHITECTURE.md | 9 +- docs/BUGS.md | 33 +++- docs/CHANGELOG.md | 5 + docs/TROUBLESHOOTING.md | 14 ++ po/de.po | 237 ++++++++++++----------- po/en_GB.po | 205 ++++++++++---------- po/es.po | 211 ++++++++++---------- po/fr.po | 243 ++++++++++++----------- po/it.po | 244 +++++++++++++----------- po/nl.po | 237 ++++++++++++----------- po/pdfs.pot | 197 ++++++++++--------- po/pl.po | 236 ++++++++++++----------- po/pt_BR.po | 236 ++++++++++++----------- 16 files changed, 1355 insertions(+), 997 deletions(-) diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index 4fa54e7..9bf08a5 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -202,12 +202,20 @@ impl Ui { } fn main() -> glib::ExitCode { - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), - ) - .init(); + let verify = std::env::args().nth(1).as_deref() == Some(VERIFY_ARG); + let logs = tracing_subscriber::fmt().with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + ); + // 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(); + if verify { + return run_verification_window(); + } // The tray launches the app with arguments ("--page locations", // "--confirm-stop"), which a running instance must receive too. diff --git a/crates/pdfs-gui/src/app/pages/login.rs b/crates/pdfs-gui/src/app/pages/login.rs index 31d406d..c87fdbb 100644 --- a/crates/pdfs-gui/src/app/pages/login.rs +++ b/crates/pdfs-gui/src/app/pages/login.rs @@ -150,15 +150,27 @@ pub(crate) fn wire_login(ui: &Rc) { }); // 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 verification_failed = Rc::new(Cell::new(false)); + let hv_failed = verification_failed.clone(); glib::spawn_future_local(async move { if let Ok((url, token_tx)) = hv_req_rx.recv().await { ui_hv .login .login_status .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), + } } }); @@ -194,6 +206,9 @@ pub(crate) fn wire_login(ui: &Rc) { let _ = gio::spawn_blocking(service::enable_start).await; 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), } }); diff --git a/crates/pdfs-gui/src/app/pages/verify.rs b/crates/pdfs-gui/src/app/pages/verify.rs index bc31fe1..1b55f93 100644 --- a/crates/pdfs-gui/src/app/pages/verify.rs +++ b/crates/pdfs-gui/src/app/pages/verify.rs @@ -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 //! 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 //! browser because it reports completion by posting a message to its host, not //! 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 std::io::{BufRead, BufReader, Write as _}; +use std::process::{ExitStatus, Stdio}; 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. /// /// 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`. -/// -/// Closing the dialog without solving drops the sender, which the waiting login -/// worker reads as a cancelled sign-in — the same contract as the 2FA prompt. -pub(crate) fn prompt_human_verification( - ui: &Rc, - url: &str, - token_tx: std::sync::mpsc::Sender, -) { +/// How the verification window ended. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum Verification { + Solved(String), + /// Closed without solving. + Cancelled, + /// Never came up, or died: most likely WebKit could not start its sandbox. + 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 { + 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 { + 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, 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(); content.add_script(&webkit6::UserScript::new( BRIDGE, @@ -53,24 +154,19 @@ pub(crate) fn prompt_human_verification( .hexpand(true) .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")) - .content_width(420) - .content_height(560) + .icon_name(APP_ID) + .default_width(420) + .default_height(560) + .content(&body) .build(); - let header = adw::HeaderBar::new(); - 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(); + let app = app.downgrade(); content.connect_script_message_received(Some("hv"), move |_, value| { let Some(token) = extract_token(&value.to_str()) else { // 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. return; }; - if let Some(tx) = tx.borrow_mut().take() { - let _ = tx.send(token); + let mut stdout = std::io::stdout().lock(); + 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() { - dlg.close(); + if let Some(app) = app.upgrade() { + 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); - - let parent = ui.login.login_button.root().and_downcast::(); - // 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()); + window.present(); } /// Pull the verification token out of a message posted by the verification page. @@ -133,7 +221,12 @@ fn extract_token(raw: &str) -> Option { #[cfg(test)] 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] fn a_completion_message_yields_its_token() { @@ -163,4 +256,39 @@ mod tests { 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()) + ); + } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e71a462..a0ea309 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -542,7 +542,7 @@ sequenceDiagram actor User participant UI as pdfs-app participant Auth as pdfs-core auth - participant Web as WebKitGTK dialog + participant Web as pdfs-app --human-verification participant API as Proton API User->>UI: email and password @@ -550,9 +550,9 @@ sequenceDiagram Auth->>API: SRP handshake API-->>Auth: 9001, challenge URL Auth-->>UI: HumanVerificationRequired - UI->>Web: load challenge + UI->>Web: challenge URL (stdin) User->>Web: solve it - Web-->>UI: token (postMessage) + Web-->>UI: token (stdout) UI->>Auth: login_verified(token) Auth->>API: new SRP handshake with the token API-->>Auth: session @@ -561,6 +561,9 @@ sequenceDiagram - The challenge used up the first SRP handshake, so the retry starts a new one with the token attached. - The page may post the token as an object or as a JSON string; both are accepted. +- The page runs in a WebKitGTK view inside a child `pdfs-app`. WebKit aborts its process when its + sandbox cannot start, as on hardened systems such as secureblue (B195). In the child, that ends + the verification, and the app says why, instead of closing the app. - The CLI has no web view. It fails with a message to sign in once with `pdfs-app`; the stored session then works for the CLI and the service. diff --git a/docs/BUGS.md b/docs/BUGS.md index 1317e94..9cf6517 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -47,7 +47,7 @@ original wording. it in the index in the same change. ```markdown -### B195 — What the user sees, in one line +### B196 — What the user sees, in one line **Status:** Open **Found:** 2026-10-06, how it was found @@ -100,6 +100,7 @@ a live account since. | Entry | Title | |---|---| +| [B195](#b195--the-app-crashes-when-proton-asks-for-a-captcha-on-secureblue) | The app crashes when Proton asks for a CAPTCHA on secureblue | | [B193](#b193--deleting-a-file-whose-create-failed-can-trash-a-file-of-the-users-on-drive) | Deleting a file whose create failed can trash a file of the user's on Drive | | [B191](#b191--a-file-deleted-after-its-create-failed-and-was-written-again-can-stay-on-drive) | A file deleted after its create failed and was written again can stay on Drive | | [B190](#b190--a-file-saved-just-before-the-daemon-stops-can-keep-its-old-content-on-drive) | A file saved just before the daemon stops can keep its old content on Drive | @@ -297,6 +298,36 @@ a live account since. Newest first. +### B195 — The app crashes when Proton asks for a CAPTCHA on secureblue + +**Status:** Fixed (unverified). Nobody has run the fix on secureblue yet. +**Found:** 2026-10-07, [#29](https://github.com/narrrl/proton-drive-linux/issues/29). On +secureblue 44 (a hardened Fedora Atomic), `pdfs-app` aborted a few seconds after the sign-in, with +`bwrap: Can't get type of source /run/systemd/journal/socket: Permission denied` and +`Failed to fully launch dbus-proxy`. +**Where:** `crates/pdfs-gui/src/app/pages/verify.rs`, `crates/pdfs-gui/src/app/pages/login.rs` + +**Repro:** Sign in on secureblue from a network Proton asks a CAPTCHA for, such as a VPN. + +**Cause.** The CAPTCHA page runs in a WebKitGTK view (B8). Before it loads anything, WebKit starts +its sandbox: bubblewrap, with the journal socket bound in, and `xdg-dbus-proxy`. secureblue keeps +bubblewrap from the socket, the proxy does not start, and WebKit ends its process with `g_error`. +The view was in the app's own process, so the whole app went down, and nothing in our code can +catch that. + +**Fix.** The view runs in a child, `pdfs-app --human-verification`, that reads the URL on stdin +and prints the token on stdout. A child that exits without a token after a crash or a non-zero +status ends the sign-in with a message that says the page could not be opened. TROUBLESHOOTING.md +gives the cause and a one-time workaround. The sandbox is not switched off by the app: users choose +a hardened system for that isolation. + +**Test:** unit tests `a_window_that_died_fails`, `a_window_closed_without_a_token_cancels`, +`a_token_counts_even_if_the_window_died_after_it` and `the_token_is_found_among_other_output`. +On 2026-10-08 the child ran under real WebKit on Arch against a local page that posts a completion, +and handed back its token. + +--- + ### B194 — On a fresh install the tray exits and the Takeout page cannot remember its archives **Status:** Fixed. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5f33256..97184ac 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,11 @@ scratch (user data in `staging/` and `recovery/` is never touched by this). ## [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. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index e733661..aab2f5c 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -72,6 +72,20 @@ pdfs status 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 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 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. diff --git a/po/de.po b/po/de.po index 17dcc1b..d7f77f7 100644 --- a/po/de.po +++ b/po/de.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-07 02:39+0200\n" +"POT-Creation-Date: 2026-10-08 10:15+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: German\n" @@ -57,7 +57,7 @@ msgstr "%-d. %b %Y" msgid "%A, %B %-d, %Y, %H:%M" msgstr "%A, %-d. %B %Y, %H:%M" -#: crates/pdfs-gui/src/app/main.rs:747 +#: crates/pdfs-gui/src/app/main.rs:760 #: 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:819 @@ -70,91 +70,91 @@ msgstr "%A, %-d. %B %Y, %H:%M" msgid "My files" msgstr "Meine Dateien" -#: crates/pdfs-gui/src/app/main.rs:750 crates/pdfs-gui/src/app/main.rs:1394 +#: 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/photos.rs:842 #: crates/pdfs-gui/src/app/pages/places.rs:441 msgid "Photos" msgstr "Fotos" -#: crates/pdfs-gui/src/app/main.rs:755 +#: 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:482 msgid "Shared with me" msgstr "Mit mir geteilt" -#: crates/pdfs-gui/src/app/main.rs:760 +#: crates/pdfs-gui/src/app/main.rs:773 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 msgid "Shared by me" msgstr "Von mir geteilt" -#: crates/pdfs-gui/src/app/main.rs:763 +#: 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:481 msgid "Computers" msgstr "Computer" -#: crates/pdfs-gui/src/app/main.rs:764 +#: crates/pdfs-gui/src/app/main.rs:777 #: crates/pdfs-gui/src/app/pages/trash.rs:101 msgid "Trash" msgstr "Papierkorb" #. Translators: sidebar entry for the page listing this computer's synced folders. -#: crates/pdfs-gui/src/app/main.rs:766 +#: crates/pdfs-gui/src/app/main.rs:779 msgid "Sync" msgstr "Synchronisierung" #. Translators: a failed action's toast. {what} says what failed, such as #. "Couldn't rename"; {detail} is the reason, usually untranslated text #. from the mount service. -#: crates/pdfs-gui/src/app/main.rs:1083 +#: crates/pdfs-gui/src/app/main.rs:1096 #, rust-format msgid "{what}: {detail}" msgstr "{what}: {detail}" -#: crates/pdfs-gui/src/app/main.rs:1102 +#: crates/pdfs-gui/src/app/main.rs:1115 msgid "You're offline" msgstr "Keine Internetverbindung" -#: crates/pdfs-gui/src/app/main.rs:1103 +#: crates/pdfs-gui/src/app/main.rs:1116 msgid "That's not there any more" msgstr "Das ist nicht mehr vorhanden" -#: crates/pdfs-gui/src/app/main.rs:1104 +#: crates/pdfs-gui/src/app/main.rs:1117 msgid "You don't have access to that" msgstr "Kein Zugriff darauf" -#: crates/pdfs-gui/src/app/main.rs:1105 +#: crates/pdfs-gui/src/app/main.rs:1118 msgid "Something changed this first" msgstr "Etwas anderes hat dies zuerst geändert" -#: crates/pdfs-gui/src/app/main.rs:1106 +#: crates/pdfs-gui/src/app/main.rs:1119 msgid "Your Proton Drive is full" msgstr "Das Proton Drive ist voll" -#: crates/pdfs-gui/src/app/main.rs:1147 crates/pdfs-gui/src/app/main.rs:1376 +#: 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 msgid "Preferences" msgstr "Einstellungen" -#: crates/pdfs-gui/src/app/main.rs:1148 +#: crates/pdfs-gui/src/app/main.rs:1161 #: crates/pdfs-gui/src/app/widgets/compat.rs:262 msgid "Keyboard Shortcuts" msgstr "Tastenkürzel" -#: crates/pdfs-gui/src/app/main.rs:1150 +#: crates/pdfs-gui/src/app/main.rs:1163 msgid "About Proton Drive for Linux" msgstr "Info zu Proton Drive for Linux" -#: crates/pdfs-gui/src/app/main.rs:1155 +#: crates/pdfs-gui/src/app/main.rs:1168 msgid "Main menu" msgstr "Hauptmenü" -#: crates/pdfs-gui/src/app/main.rs:1211 +#: crates/pdfs-gui/src/app/main.rs:1224 msgid "Proton Drive for Linux" msgstr "Proton Drive for Linux" -#: crates/pdfs-gui/src/app/main.rs:1219 +#: crates/pdfs-gui/src/app/main.rs:1232 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -166,23 +166,23 @@ msgstr "" "Inoffizieller Client – nicht mit Proton AG verbunden, von Proton AG " "unterstützt oder befürwortet." -#: crates/pdfs-gui/src/app/main.rs:1224 +#: crates/pdfs-gui/src/app/main.rs:1237 msgid "Source Code" msgstr "Quellcode" -#: crates/pdfs-gui/src/app/main.rs:1228 +#: crates/pdfs-gui/src/app/main.rs:1241 msgid "Proton Terms of Service" msgstr "Nutzungsbedingungen von Proton" -#: crates/pdfs-gui/src/app/main.rs:1232 +#: crates/pdfs-gui/src/app/main.rs:1245 msgid "Proton Privacy Policy" msgstr "Datenschutzerklärung von Proton" -#: crates/pdfs-gui/src/app/main.rs:1237 +#: crates/pdfs-gui/src/app/main.rs:1250 msgid "Disclaimer" msgstr "Haftungsausschluss" -#: crates/pdfs-gui/src/app/main.rs:1241 +#: crates/pdfs-gui/src/app/main.rs:1254 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -207,7 +207,7 @@ msgstr "" "bereitgestellt. Daten, deren Verlust nicht hinnehmbar ist, sollten " "zusätzlich gesichert werden." -#: crates/pdfs-gui/src/app/main.rs:1249 +#: crates/pdfs-gui/src/app/main.rs:1262 msgid "" "Places are named with data from GeoNames, licensed under GeoNames, lizenziert unter CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -227,7 +227,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth gezeichnet, die " "gemeinfrei sind." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, licensed under CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -224,7 +224,7 @@ msgstr "" "\">Natural Earth land outlines and country borders, which are in the " "public domain." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, con licencia CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -225,7 +225,7 @@ msgstr "" "fronteras de países de Natural " "Earth, que son de dominio público." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, sous licence CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -226,7 +226,7 @@ msgstr "" "frontières des pays de Natural " "Earth, qui sont dans le domaine public." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, con licenza CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -224,7 +224,7 @@ msgstr "" "confini nazionali di Natural " "Earth, che sono di pubblico dominio." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, gelicentieerd onder CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -225,7 +225,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth, die in het " "publieke domein vallen." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the \n" "Language-Team: LANGUAGE \n" @@ -60,7 +60,7 @@ msgstr "" msgid "%A, %B %-d, %Y, %H:%M" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:747 +#: crates/pdfs-gui/src/app/main.rs:760 #: 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:819 @@ -73,91 +73,91 @@ msgstr "" msgid "My files" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:750 crates/pdfs-gui/src/app/main.rs:1394 +#: 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/photos.rs:842 #: crates/pdfs-gui/src/app/pages/places.rs:441 msgid "Photos" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:755 +#: 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:482 msgid "Shared with me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:760 +#: crates/pdfs-gui/src/app/main.rs:773 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 msgid "Shared by me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:763 +#: 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:481 msgid "Computers" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:764 +#: crates/pdfs-gui/src/app/main.rs:777 #: crates/pdfs-gui/src/app/pages/trash.rs:101 msgid "Trash" msgstr "" #. Translators: sidebar entry for the page listing this computer's synced folders. -#: crates/pdfs-gui/src/app/main.rs:766 +#: crates/pdfs-gui/src/app/main.rs:779 msgid "Sync" msgstr "" #. Translators: a failed action's toast. {what} says what failed, such as #. "Couldn't rename"; {detail} is the reason, usually untranslated text #. from the mount service. -#: crates/pdfs-gui/src/app/main.rs:1083 +#: crates/pdfs-gui/src/app/main.rs:1096 #, rust-format msgid "{what}: {detail}" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1102 +#: crates/pdfs-gui/src/app/main.rs:1115 msgid "You're offline" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1103 +#: crates/pdfs-gui/src/app/main.rs:1116 msgid "That's not there any more" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1104 +#: crates/pdfs-gui/src/app/main.rs:1117 msgid "You don't have access to that" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1105 +#: crates/pdfs-gui/src/app/main.rs:1118 msgid "Something changed this first" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1106 +#: crates/pdfs-gui/src/app/main.rs:1119 msgid "Your Proton Drive is full" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1147 crates/pdfs-gui/src/app/main.rs:1376 +#: 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 msgid "Preferences" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1148 +#: crates/pdfs-gui/src/app/main.rs:1161 #: crates/pdfs-gui/src/app/widgets/compat.rs:262 msgid "Keyboard Shortcuts" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1150 +#: crates/pdfs-gui/src/app/main.rs:1163 msgid "About Proton Drive for Linux" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1155 +#: crates/pdfs-gui/src/app/main.rs:1168 msgid "Main menu" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1211 +#: crates/pdfs-gui/src/app/main.rs:1224 msgid "Proton Drive for Linux" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1219 +#: crates/pdfs-gui/src/app/main.rs:1232 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -165,23 +165,23 @@ msgid "" "AG." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1224 +#: crates/pdfs-gui/src/app/main.rs:1237 msgid "Source Code" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1228 +#: crates/pdfs-gui/src/app/main.rs:1241 msgid "Proton Terms of Service" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1232 +#: crates/pdfs-gui/src/app/main.rs:1245 msgid "Proton Privacy Policy" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1237 +#: crates/pdfs-gui/src/app/main.rs:1250 msgid "Disclaimer" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1241 +#: crates/pdfs-gui/src/app/main.rs:1254 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -194,21 +194,21 @@ msgid "" "of any data you cannot afford to lose." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1249 +#: crates/pdfs-gui/src/app/main.rs:1262 msgid "" "Places are named with data from GeoNames, licensed under CC BY 4.0." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " "public domain." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames udostępnionych na licencji CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -228,7 +228,7 @@ msgstr "" "www.naturalearthdata.com/\">Natural Earth, które należą do domeny " "publicznej." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, licenciados sob CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -224,7 +224,7 @@ msgstr "" "fronteiras de países do Natural Earth, que são de domínio público." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the