From 91fa1614ed8dc61b53c8c5f57fdd9f65d8d8d3f4 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 11 Oct 2026 21:20:51 +0200 Subject: [PATCH] fix(core,fuse,cli,gui): say when an upgrade left the old daemon running A package upgrade replaces the binaries but does not restart the user service, so the daemon keeps the old release and its bugs until the next login. On 3.2.0 the reporter of #33 still saw "Couldn't sync Documents: read .../snapshots/37: Permission denied", which only a daemon older than 3.2.0 can produce: since then the scan leaves an unreadable subfolder out and only an unreadable sync root fails the pass (B206). The daemon now reports its release in Status; one from before the field omits it, which reads as older. When the daemon is older than the front-end, the tray says "Update not finished - restart Proton Drive" and offers a restart, the sync card shows "Restart to finish updating" with a Restart button, and pdfs status and pdfs diagnose print the systemctl command. A newer daemon is not flagged, since then the front-end is the stale one. Refs #33 --- crates/pdfs-cli/src/main.rs | 28 +- crates/pdfs-core/src/control.rs | 47 ++ crates/pdfs-fuse/src/control.rs | 1 + crates/pdfs-fuse/src/events.rs | 1 + crates/pdfs-gui/src/app/main.rs | 1 + crates/pdfs-gui/src/app/pages/locations.rs | 22 +- crates/pdfs-gui/src/app/pages/status.rs | 25 + crates/pdfs-gui/src/main.rs | 41 +- docs/BUGS.md | 32 + docs/CHANGELOG.md | 5 + po/de.po | 815 ++++++++++---------- po/en_GB.po | 811 ++++++++++---------- po/es.po | 814 ++++++++++---------- po/fr.po | 815 ++++++++++---------- po/it.po | 814 ++++++++++---------- po/nl.po | 814 ++++++++++---------- po/pdfs.pot | 835 +++++++++++---------- po/pl.po | 815 ++++++++++---------- po/pt_BR.po | 814 ++++++++++---------- 19 files changed, 3988 insertions(+), 3562 deletions(-) diff --git a/crates/pdfs-cli/src/main.rs b/crates/pdfs-cli/src/main.rs index ccba21b..c4368e4 100644 --- a/crates/pdfs-cli/src/main.rs +++ b/crates/pdfs-cli/src/main.rs @@ -15,7 +15,7 @@ use pdfs_core::config::AppDirs; use pdfs_core::control::{ ConflictKeep, ErrorKind, MountKind, RefreshScope, Request as CtlRequest, Response as CtlResponse, RestoreItem as CtlRestoreItem, ShareEntryKind, SyncIssue, SyncPhase, - pending_summary, + daemon_outdated, pending_summary, }; use pdfs_core::db::Db; use pdfs_core::service; @@ -2026,6 +2026,7 @@ fn cmd_status() -> Result<()> { paused_until, session_expired, tokens_unsaved, + version, .. }) => { let state = match (online, paused) { @@ -2067,6 +2068,9 @@ fn cmd_status() -> Result<()> { "Keyring refuses the refreshed sign-in; unlock it, or a reboot needs `pdfs login`" ); } + if daemon_outdated(&version) { + println!("Daemon {}", outdated_daemon_note(&version)); + } } Ok(other) => println!("Mount: unexpected response {other:?}"), Err(_) => println!("Mount: not running."), @@ -2074,6 +2078,20 @@ fn cmd_status() -> Result<()> { Ok(()) } +/// What `pdfs status` and `pdfs diagnose` say about a daemon still running +/// an older release than this CLI. +fn outdated_daemon_note(version: &str) -> String { + let running = if version.is_empty() { + "an older release".to_string() + } else { + version.to_string() + }; + format!( + "still runs {running}, this is {}; run `systemctl --user restart proton-drive` to finish the update", + env!("CARGO_PKG_VERSION"), + ) +} + fn cmd_locations() -> Result<()> { let response = control_request(CtlRequest::ListLocations)?; if emit_json(&response)? { @@ -3444,9 +3462,17 @@ fn cmd_diagnose() -> Result<()> { tokens_unsaved, pending_uploads, pending_changes, + version, .. }) => { report.finding(DiagnoseLevel::Ok, " daemon responding", ""); + if daemon_outdated(&version) { + report.finding( + DiagnoseLevel::Warn, + " daemon version", + outdated_daemon_note(&version), + ); + } let live_type = mounted_fs_type(Path::new(&mountpoint)); let is_fuse = live_type .as_deref() diff --git a/crates/pdfs-core/src/control.rs b/crates/pdfs-core/src/control.rs index f2eebd4..190c5ea 100644 --- a/crates/pdfs-core/src/control.rs +++ b/crates/pdfs-core/src/control.rs @@ -1850,6 +1850,27 @@ pub fn pending_summary(uploads: u64, changes: u64) -> Option { } } +/// Whether the daemon that reported `version` in a [`Response::Status`] is an +/// older release than this build. Installing a package replaces the binaries +/// but leaves the running user service on the old one, which keeps its bugs +/// until it restarts, while the app and the CLI already describe the new +/// release (#33). Empty means a daemon from before the field, so older too. +/// +/// Only older counts. A newer daemon means this front-end is the one still +/// running from before the upgrade, and restarting the service would not help. +pub fn daemon_outdated(version: &str) -> bool { + fn parse(v: &str) -> Option<(u64, u64, u64)> { + let mut parts = v.split('.').map(|p| p.parse().ok()); + let parsed = (parts.next()??, parts.next()??, parts.next()??); + parts.next().is_none().then_some(parsed) + } + match (parse(version), parse(env!("CARGO_PKG_VERSION"))) { + (Some(running), Some(this)) => running < this, + (None, _) => version.is_empty(), + (Some(_), None) => false, + } +} + /// One worker thread in a [`Diagnostics`] report. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct WorkerState { @@ -1969,6 +1990,11 @@ pub enum Response { /// out. #[serde(default)] tokens_unsaved: bool, + /// The release of the daemon that answered. A package upgrade does not + /// restart the user service, so it can be older than the front-end + /// asking; daemons before 3.2.2 omit it, which reads as empty. + #[serde(default)] + version: String, }, /// A human-readable success message. Ok { message: String }, @@ -2612,6 +2638,27 @@ mod tests { ); } + /// #33: a daemon left running over an upgrade is named as older, also when + /// it predates the version field, but a newer one is not, since restarting + /// the service cannot bring it closer to this build. + #[test] + fn only_an_older_daemon_is_outdated() { + assert!(!daemon_outdated(env!("CARGO_PKG_VERSION"))); + assert!(daemon_outdated("")); + assert!(daemon_outdated("0.9.9")); + assert!(!daemon_outdated("999.0.0")); + assert!(!daemon_outdated("not-a-version")); + let old: Response = serde_json::from_str( + r#"{"Status":{"username":"u","mountpoint":"/m","pinned":0, + "used":0,"budget":0,"pins":[]}}"#, + ) + .unwrap(); + let Response::Status { version, .. } = old else { + panic!("not a status: {old:?}"); + }; + assert!(daemon_outdated(&version)); + } + /// The mutation requests must survive a line-delimited JSON round-trip, since /// that is exactly how they cross the control socket. #[test] diff --git a/crates/pdfs-fuse/src/control.rs b/crates/pdfs-fuse/src/control.rs index f112e14..e08b5a0 100644 --- a/crates/pdfs-fuse/src/control.rs +++ b/crates/pdfs-fuse/src/control.rs @@ -336,6 +336,7 @@ pub(crate) fn status_response(core: &Core, username: &str, mountpoint: &Path) -> online: core.online.load(Ordering::Relaxed), session_expired: core.session_expired(), tokens_unsaved: pdfs_core::auth::tokens_unsaved(), + version: env!("CARGO_PKG_VERSION").to_string(), pending_uploads: queued.uploads.max(0) as u64 + landing, pending_changes: queued.changes.max(0) as u64, } diff --git a/crates/pdfs-fuse/src/events.rs b/crates/pdfs-fuse/src/events.rs index 90c7462..a41601a 100644 --- a/crates/pdfs-fuse/src/events.rs +++ b/crates/pdfs-fuse/src/events.rs @@ -329,6 +329,7 @@ mod tests { online: true, session_expired: false, tokens_unsaved: false, + version: "3.2.2".into(), pending_uploads: 1, pending_changes: 0, }; diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index 8cb84b4..37f4378 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -581,6 +581,7 @@ fn build_window(app: &adw::Application) { pause: locations_widgets.pause.clone(), paused: Cell::new(false), sign_in: locations_widgets.card_sign_in.clone(), + restart: locations_widgets.card_restart.clone(), }, queue: QueueState { group: locations_widgets.queue_group.clone(), diff --git a/crates/pdfs-gui/src/app/pages/locations.rs b/crates/pdfs-gui/src/app/pages/locations.rs index 68e9967..fdf22fc 100644 --- a/crates/pdfs-gui/src/app/pages/locations.rs +++ b/crates/pdfs-gui/src/app/pages/locations.rs @@ -48,6 +48,8 @@ pub(crate) struct SyncCard { pub(crate) paused: Cell, /// Shown only while Proton refuses the session; opens the sign-in page. pub(crate) sign_in: gtk4::Button, + /// Shown only while the daemon is older than the app; restarts it. + pub(crate) restart: gtk4::Button, } /// What a folder row's controls are built from. The subtitle and progress bar @@ -111,6 +113,7 @@ pub(crate) struct LocationsWidgets { pub(crate) card_title: gtk4::Label, pub(crate) card_detail: gtk4::Label, pub(crate) card_sign_in: gtk4::Button, + pub(crate) card_restart: gtk4::Button, pub(crate) pause: adw::SplitButton, pub(crate) queue_group: adw::PreferencesGroup, pub(crate) retry_all: gtk4::Button, @@ -137,7 +140,7 @@ pub(crate) fn build_locations_page(history: >k4::Widget) -> (gtk4::Widget, Loc .build(); add_folder.add_css_class("flat"); let transfers_group = build_transfers_group(); - let (card, card_icon, card_title, card_detail, card_sign_in) = build_sync_card(); + let (card, card_icon, card_title, card_detail, card_sign_in, card_restart) = build_sync_card(); let pause = build_pause_button(); let (queue_group, retry_all) = build_queue_group(); let conflicts_group = adw::PreferencesGroup::builder() @@ -257,6 +260,7 @@ pub(crate) fn build_locations_page(history: >k4::Widget) -> (gtk4::Widget, Loc card_title, card_detail, card_sign_in, + card_restart, pause, queue_group, retry_all, @@ -299,6 +303,11 @@ pub(crate) fn wire_locations(ui: &Rc, retry: >k4::Button, add_folder: >k .card .sign_in .connect_clicked(move |_| show_login(&ui_sign_in)); + let ui_restart = ui.clone(); + ui.locations + .card + .restart + .connect_clicked(move |_| restart_service_then(&ui_restart, |_| {})); let ui_retry = ui.clone(); ui.locations .queue @@ -396,6 +405,7 @@ fn build_sync_card() -> ( gtk4::Label, gtk4::Label, gtk4::Button, + gtk4::Button, ) { let icon = gtk4::Image::builder() .icon_name("pdfs-sync-symbolic") @@ -430,10 +440,18 @@ fn build_sync_card() -> ( .build(); sign_in.add_css_class("pill"); sign_in.add_css_class("suggested-action"); + let restart = gtk4::Button::builder() + .label(gettext("Restart")) + .valign(gtk4::Align::Center) + .visible(false) + .build(); + restart.add_css_class("pill"); + restart.add_css_class("suggested-action"); card.append(&icon); card.append(&text); card.append(&sign_in); - (card, icon, title, detail, sign_in) + card.append(&restart); + (card, icon, title, detail, sign_in, restart) } /// Pause/Resume, with timed pauses in its menu. diff --git a/crates/pdfs-gui/src/app/pages/status.rs b/crates/pdfs-gui/src/app/pages/status.rs index 3930802..6eec4a3 100644 --- a/crates/pdfs-gui/src/app/pages/status.rs +++ b/crates/pdfs-gui/src/app/pages/status.rs @@ -1232,6 +1232,7 @@ pub(crate) fn paint_status(ui: &Rc, status: Response) { paused_until, session_expired, tokens_unsaved, + version, .. } = status else { @@ -1246,6 +1247,8 @@ pub(crate) fn paint_status(ui: &Rc, status: Response) { SyncState::SessionExpired { queued } } else if tokens_unsaved { SyncState::TokensUnsaved + } else if pdfs_core::control::daemon_outdated(&version) { + SyncState::Outdated { running: version } } else if paused { SyncState::Paused { until: paused_until, @@ -1317,6 +1320,12 @@ pub(crate) enum SyncState { /// The keyring refuses the session's refreshed tokens. Sync goes on, but /// a reboot before the keyring takes them signs the user out. TokensUnsaved, + /// The daemon is an older release than this window: an upgrade replaced + /// the files but left the service running, with the old release's bugs, + /// until it restarts (#33). `running` is empty when it is too old to say. + Outdated { + running: String, + }, /// The user paused syncing; nothing goes up until it resumes. Outranks /// everything else, because it is the reason for everything else. Paused { @@ -1352,6 +1361,7 @@ fn paint_sync_status(ui: &Rc, state: SyncState) { let paused = matches!(state, SyncState::Paused { .. }); let connected = !matches!(state, SyncState::Disconnected | SyncState::NotResponding); let expired = matches!(state, SyncState::SessionExpired { .. }); + let outdated = matches!(state, SyncState::Outdated { .. }); let (icon, class, title, detail) = match state { SyncState::SessionExpired { queued } => ( "dialog-password-symbolic", @@ -1374,6 +1384,20 @@ fn paint_sync_status(ui: &Rc, state: SyncState) { "Unlock your keyring, or the next reboot signs you out", )), ), + SyncState::Outdated { running } => ( + "dialog-warning-symbolic", + Some("warning"), + gettext("Restart to finish updating"), + Some(if running.is_empty() { + gettext("An older version of Proton Drive is still running") + } else { + // Translators: {version} is a release number such as "3.2.0". + gettext_f( + "Proton Drive {version} is still running", + &[("version", &running)], + ) + }), + ), SyncState::Paused { until, queued } => ( "media-playback-pause-symbolic", Some("warning"), @@ -1445,6 +1469,7 @@ fn paint_sync_status(ui: &Rc, state: SyncState) { image.add_css_class(class); } ui.locations.card.sign_in.set_visible(expired); + ui.locations.card.restart.set_visible(outdated); ui.status.status_title.set_label(&title); ui.status.status_detail.set_visible(detail.is_some()); ui.status diff --git a/crates/pdfs-gui/src/main.rs b/crates/pdfs-gui/src/main.rs index fd4b53a..8accc27 100644 --- a/crates/pdfs-gui/src/main.rs +++ b/crates/pdfs-gui/src/main.rs @@ -22,7 +22,8 @@ use ksni::{MenuItem, Status, ToolTip, Tray}; use pdfs_core::auth; use pdfs_core::config::AppDirs; use pdfs_core::control::{ - Event, Feed, JobItem, Request, Response, TransferDirection, TransferItem, follow, send, + Event, Feed, JobItem, Request, Response, TransferDirection, TransferItem, daemon_outdated, + follow, send, }; use pdfs_core::service; @@ -86,6 +87,9 @@ struct DriveState { paused: bool, /// Queued changes that keep failing, for the "N issues" item. failing: u64, + /// Whether the daemon is an older release than the tray, left running by + /// an upgrade; offers the restart that finishes it. + outdated: bool, } /// Summarise a work snapshot into one menu line, or empty when idle. A single @@ -166,6 +170,7 @@ fn poll_state(socket: &Path, default_mountpoint: &Path) -> DriveState { sync: String::new(), paused: false, failing: 0, + outdated: false, }, // No daemon: describe login state instead so the menu is still useful. Err(_) => { @@ -202,6 +207,7 @@ fn poll_state(socket: &Path, default_mountpoint: &Path) -> DriveState { sync: String::new(), paused: false, failing: 0, + outdated: false, } } } @@ -218,6 +224,7 @@ fn unexpected_reply(default_mountpoint: &Path) -> DriveState { sync: String::new(), paused: false, failing: 0, + outdated: false, } } @@ -239,6 +246,7 @@ fn status_state( paused, session_expired, tokens_unsaved, + version, .. } = status else { @@ -246,9 +254,10 @@ fn status_state( }; let (pinned, online, failing_ops, paused) = (*pinned, *online, *failing_ops, *paused); let queued = i18n::pending_summary(*pending_uploads, *pending_changes); + let outdated = daemon_outdated(version); let phase = if *session_expired { Phase::SessionExpired - } else if *tokens_unsaved { + } else if *tokens_unsaved || outdated { Phase::Attention } else { phase_of( @@ -265,6 +274,9 @@ fn status_state( _ if *session_expired => gettext("Session expired — sign in again"), // Sync goes on meanwhile, but a reboot ends it until the next login. _ if *tokens_unsaved => gettext("Sign-in not saved — unlock your keyring"), + // An upgrade replaced the files but not the running service, which + // keeps the old release's bugs until it restarts. + _ if outdated => gettext("Update not finished — restart Proton Drive"), _ if paused => gettext("Sync paused"), _ if failing_ops > 0 => ngettext_f( "{n} change needs attention", @@ -302,6 +314,7 @@ fn status_state( mountpoint: PathBuf::from(mountpoint), sync: sync_line(items, jobs), failing: failing_ops, + outdated, } } @@ -445,6 +458,13 @@ impl Tray for DriveTray { _ => items.push(item(&gettext("Open Proton Drive"), |_| open_manager(&[]))), } + if self.state.outdated { + items.push(item(&gettext("Restart Proton Drive"), |this| { + service::restart(); + this.state.line = gettext("Connecting…"); + })); + } + if self.state.mounted { items.push( StandardItem { @@ -626,9 +646,14 @@ mod tests { } fn status(extra: &str) -> Response { + status_from(env!("CARGO_PKG_VERSION"), extra) + } + + fn status_from(version: &str, extra: &str) -> Response { serde_json::from_str(&format!( r#"{{"Status":{{"username":"u","mountpoint":"/m","pinned":0,"used":0,"budget":0, - "pins":[],"online":false,"pending_uploads":3,"failing_ops":2,"paused":true{extra}}}}}"# + "pins":[],"online":false,"pending_uploads":3,"failing_ops":2,"paused":true, + "version":"{version}"{extra}}}}}"# )) .unwrap() } @@ -660,6 +685,16 @@ mod tests { assert_eq!(state.line, "Sign-in not saved — unlock your keyring"); } + /// #33: a daemon an upgrade left running on the old release is named, + /// above a pause, and the menu offers the restart that finishes it. + #[test] + fn a_daemon_older_than_the_tray_needs_a_restart() { + let state = status_state(&status_from("0.1.0", ""), &[], &[], Path::new("/m")); + assert_eq!(state.phase, Phase::Attention); + assert_eq!(state.line, "Update not finished — restart Proton Drive"); + assert!(state.outdated); + } + #[test] fn a_daemon_that_does_not_say_has_a_live_session() { let state = status_state(&status(""), &[], &[], Path::new("/m")); diff --git a/docs/BUGS.md b/docs/BUGS.md index 40fd438..a5d8069 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -100,6 +100,7 @@ a live account since. | Entry | Title | |---|---| +| [B206](#b206--an-upgrade-leaves-the-old-daemon-running-and-nothing-says-so) | An upgrade leaves the old daemon running, and nothing says so | | [B205](#b205--a-photo-upload-the-sdk-refuses-says-youre-offline) | A photo upload the SDK refuses says "You're offline" | | [B204](#b204--a-write-over-a-partial-edit-whose-base-was-never-read-fails-with-eio) | A write over a partial edit whose base was never read fails with `EIO` | | [B203](#b203--pdfs-unlock-leaves-a-running-daemon-a-spent-refresh-token) | `pdfs unlock` leaves a running daemon a spent refresh token | @@ -308,6 +309,37 @@ a live account since. Newest first. +### B206 — An upgrade leaves the old daemon running, and nothing says so + +**Status:** Fixed, not verified live +**Found:** 2026-10-11, issue [#33](https://github.com/narrrl/proton-drive-linux/issues/33). On +3.2.0 the reporter still saw `Couldn't sync Documents: Read …/snapshots/37: Permission denied`, +the B196 failure 3.2.0 fixed. Since 3.2.0 the scan leaves an unreadable subfolder out and +`walk_local_dir` can only fail the pass on the sync root, so that message comes from a daemon +older than 3.2.0. +**Where:** `Response::Status` and `daemon_outdated` in `crates/pdfs-core/src/control.rs`; +`status_response` in `crates/pdfs-fuse/src/control.rs`; the tray in `crates/pdfs-gui/src/main.rs`; +the sync card in `crates/pdfs-gui/src/app/pages/status.rs`; `pdfs status` and `pdfs diagnose` + +**Repro:** With the service running, install a newer package, then open the app. + +**Cause.** A package upgrade replaces the binaries but does not restart the user service, so the +running daemon keeps the old release and its bugs until the next login or a manual restart. The +app, the tray and the CLI start from the new binaries and could not tell, so they described the +new release while the old one did the work. + +**Fix.** The daemon reports its release in `Status`. A daemon from before the field omits it, +which reads as older. When the daemon is older than the front-end, the tray says "Update not +finished — restart Proton Drive" with a Restart Proton Drive item, the sync card says "Restart to +finish updating" with a Restart button, and `pdfs status` and `pdfs diagnose` print the +`systemctl --user restart proton-drive` command. A newer daemon is not flagged: then the +front-end is the stale one, and restarting the service would not help. + +**Test:** `control::tests::only_an_older_daemon_is_outdated`; +`tests::a_daemon_older_than_the_tray_needs_a_restart` in `pdfs-tray`. + +--- + ### B205 — A photo upload the SDK refuses says "You're offline" **Status:** Fixed, not verified live diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4993d88..e3c5eaa 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,6 +16,11 @@ scratch (user data in `staging/` and `recovery/` is never touched by this). connection now reads as offline. Other failures show what went wrong, and Upload is greyed out on an account without a photo library. ([#38](https://github.com/narrrl/proton-drive-linux/issues/38)) +- **After an upgrade, the old version kept running in the background, and nothing said so.** + Installing a package does not restart the service, so fixed bugs, such as an unreadable folder + stopping a synced folder, came back until the next login. The tray, the app, `pdfs status` and + `pdfs diagnose` now say when the service is older than the app, and the tray and the app offer + to restart it. ([#33](https://github.com/narrrl/proton-drive-linux/issues/33)) ## [3.2.1] — 2026-10-11 diff --git a/po/de.po b/po/de.po index c9e8ce9..ec1eb02 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-09 12:17+0200\n" +"POT-Creation-Date: 2026-10-11 21:12+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: German\n" @@ -31,7 +31,7 @@ msgstr[1] "vor {n} Min." #. Translators: strftime format for the time a photo was taken, shown on its tile, such as "14:05". #: crates/pdfs-gui/src/app/dates.rs:44 #: crates/pdfs-gui/src/app/pages/activity.rs:838 -#: crates/pdfs-gui/src/app/pages/photos.rs:2771 +#: crates/pdfs-gui/src/app/pages/photos.rs:2774 msgid "%H:%M" msgstr "%H:%M" @@ -57,86 +57,86 @@ 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:762 +#: crates/pdfs-gui/src/app/main.rs:764 #: 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 #: crates/pdfs-gui/src/app/pages/browser.rs:1217 #: crates/pdfs-gui/src/app/pages/browser.rs:2083 #: crates/pdfs-gui/src/app/pages/browser.rs:2160 -#: crates/pdfs-gui/src/app/pages/locations.rs:1647 +#: crates/pdfs-gui/src/app/pages/locations.rs:1666 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:252 #: crates/pdfs-gui/src/app/pages/trash.rs:191 crates/pdfs-gui/src/prompt.rs:171 msgid "My files" msgstr "Meine Dateien" -#: crates/pdfs-gui/src/app/main.rs:765 crates/pdfs-gui/src/app/main.rs:1396 +#: crates/pdfs-gui/src/app/main.rs:767 crates/pdfs-gui/src/app/main.rs:1398 #: 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:845 #: crates/pdfs-gui/src/app/pages/places.rs:441 msgid "Photos" msgstr "Fotos" -#: crates/pdfs-gui/src/app/main.rs:770 +#: crates/pdfs-gui/src/app/main.rs:772 #: 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:775 +#: crates/pdfs-gui/src/app/main.rs:777 #: 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:778 +#: crates/pdfs-gui/src/app/main.rs:780 #: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:485 msgid "Computers" msgstr "Computer" -#: crates/pdfs-gui/src/app/main.rs:779 +#: crates/pdfs-gui/src/app/main.rs:781 #: 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:781 +#: crates/pdfs-gui/src/app/main.rs:783 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:1097 +#: crates/pdfs-gui/src/app/main.rs:1099 #, rust-format msgid "{what}: {detail}" msgstr "{what}: {detail}" -#: crates/pdfs-gui/src/app/main.rs:1116 +#: crates/pdfs-gui/src/app/main.rs:1118 msgid "You're offline" msgstr "Keine Internetverbindung" -#: crates/pdfs-gui/src/app/main.rs:1117 +#: crates/pdfs-gui/src/app/main.rs:1119 msgid "That's not there any more" msgstr "Das ist nicht mehr vorhanden" -#: crates/pdfs-gui/src/app/main.rs:1118 +#: crates/pdfs-gui/src/app/main.rs:1120 msgid "You don't have access to that" msgstr "Kein Zugriff darauf" -#: crates/pdfs-gui/src/app/main.rs:1119 +#: crates/pdfs-gui/src/app/main.rs:1121 msgid "Something changed this first" msgstr "Etwas anderes hat dies zuerst geändert" -#: crates/pdfs-gui/src/app/main.rs:1120 +#: crates/pdfs-gui/src/app/main.rs:1122 msgid "Your Proton Drive is full" msgstr "Das Proton Drive ist voll" -#: crates/pdfs-gui/src/app/main.rs:1207 +#: crates/pdfs-gui/src/app/main.rs:1209 msgid "Proton Drive for Linux" msgstr "Proton Drive for Linux" -#: crates/pdfs-gui/src/app/main.rs:1215 +#: crates/pdfs-gui/src/app/main.rs:1217 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -148,23 +148,23 @@ msgstr "" "Inoffizieller Client – nicht mit Proton AG verbunden, von Proton AG " "unterstützt oder befürwortet." -#: crates/pdfs-gui/src/app/main.rs:1220 +#: crates/pdfs-gui/src/app/main.rs:1222 msgid "Source Code" msgstr "Quellcode" -#: crates/pdfs-gui/src/app/main.rs:1224 +#: crates/pdfs-gui/src/app/main.rs:1226 msgid "Proton Terms of Service" msgstr "Nutzungsbedingungen von Proton" -#: crates/pdfs-gui/src/app/main.rs:1228 +#: crates/pdfs-gui/src/app/main.rs:1230 msgid "Proton Privacy Policy" msgstr "Datenschutzerklärung von Proton" -#: crates/pdfs-gui/src/app/main.rs:1233 +#: crates/pdfs-gui/src/app/main.rs:1235 msgid "Disclaimer" msgstr "Haftungsausschluss" -#: crates/pdfs-gui/src/app/main.rs:1237 +#: crates/pdfs-gui/src/app/main.rs:1239 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -189,7 +189,7 @@ msgstr "" "bereitgestellt. Daten, deren Verlust nicht hinnehmbar ist, sollten " "zusätzlich gesichert werden." -#: crates/pdfs-gui/src/app/main.rs:1245 +#: crates/pdfs-gui/src/app/main.rs:1247 msgid "" "Places are named with data from GeoNames, licensed under GeoNames, lizenziert unter CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1253 +#: crates/pdfs-gui/src/app/main.rs:1255 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -209,7 +209,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth gezeichnet, die " "gemeinfrei sind." -#: crates/pdfs-gui/src/app/main.rs:1261 +#: crates/pdfs-gui/src/app/main.rs:1263 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:1253 +#: crates/pdfs-gui/src/app/main.rs:1255 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -206,7 +206,7 @@ msgstr "" "\">Natural Earth land outlines and country borders, which are in the " "public domain." -#: crates/pdfs-gui/src/app/main.rs:1261 +#: crates/pdfs-gui/src/app/main.rs:1263 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:1253 +#: crates/pdfs-gui/src/app/main.rs:1255 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "fronteras de países de Natural " "Earth, que son de dominio público." -#: crates/pdfs-gui/src/app/main.rs:1261 +#: crates/pdfs-gui/src/app/main.rs:1263 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:1253 +#: crates/pdfs-gui/src/app/main.rs:1255 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -208,7 +208,7 @@ msgstr "" "frontières des pays de Natural " "Earth, qui sont dans le domaine public." -#: crates/pdfs-gui/src/app/main.rs:1261 +#: crates/pdfs-gui/src/app/main.rs:1263 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:1253 +#: crates/pdfs-gui/src/app/main.rs:1255 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -206,7 +206,7 @@ msgstr "" "confini nazionali di Natural " "Earth, che sono di pubblico dominio." -#: crates/pdfs-gui/src/app/main.rs:1261 +#: crates/pdfs-gui/src/app/main.rs:1263 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:1253 +#: crates/pdfs-gui/src/app/main.rs:1255 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth, die in het " "publieke domein vallen." -#: crates/pdfs-gui/src/app/main.rs:1261 +#: crates/pdfs-gui/src/app/main.rs:1263 msgid "" "The street map, when turned on, comes from OpenFreeMap in the \n" "Language-Team: LANGUAGE \n" @@ -34,7 +34,7 @@ msgstr[1] "" #. Translators: strftime format for the time a photo was taken, shown on its tile, such as "14:05". #: crates/pdfs-gui/src/app/dates.rs:44 #: crates/pdfs-gui/src/app/pages/activity.rs:838 -#: crates/pdfs-gui/src/app/pages/photos.rs:2771 +#: crates/pdfs-gui/src/app/pages/photos.rs:2774 msgid "%H:%M" msgstr "" @@ -60,86 +60,86 @@ msgstr "" msgid "%A, %B %-d, %Y, %H:%M" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:762 +#: crates/pdfs-gui/src/app/main.rs:764 #: 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 #: crates/pdfs-gui/src/app/pages/browser.rs:1217 #: crates/pdfs-gui/src/app/pages/browser.rs:2083 #: crates/pdfs-gui/src/app/pages/browser.rs:2160 -#: crates/pdfs-gui/src/app/pages/locations.rs:1647 +#: crates/pdfs-gui/src/app/pages/locations.rs:1666 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:252 #: crates/pdfs-gui/src/app/pages/trash.rs:191 crates/pdfs-gui/src/prompt.rs:171 msgid "My files" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:765 crates/pdfs-gui/src/app/main.rs:1396 +#: crates/pdfs-gui/src/app/main.rs:767 crates/pdfs-gui/src/app/main.rs:1398 #: 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:845 #: crates/pdfs-gui/src/app/pages/places.rs:441 msgid "Photos" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:770 +#: crates/pdfs-gui/src/app/main.rs:772 #: 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:775 +#: crates/pdfs-gui/src/app/main.rs:777 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 msgid "Shared by me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:778 +#: crates/pdfs-gui/src/app/main.rs:780 #: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:485 msgid "Computers" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:779 +#: crates/pdfs-gui/src/app/main.rs:781 #: 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:781 +#: crates/pdfs-gui/src/app/main.rs:783 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:1097 +#: crates/pdfs-gui/src/app/main.rs:1099 #, rust-format msgid "{what}: {detail}" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1116 +#: crates/pdfs-gui/src/app/main.rs:1118 msgid "You're offline" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1117 +#: crates/pdfs-gui/src/app/main.rs:1119 msgid "That's not there any more" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1118 +#: crates/pdfs-gui/src/app/main.rs:1120 msgid "You don't have access to that" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1119 +#: crates/pdfs-gui/src/app/main.rs:1121 msgid "Something changed this first" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1120 +#: crates/pdfs-gui/src/app/main.rs:1122 msgid "Your Proton Drive is full" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1207 +#: crates/pdfs-gui/src/app/main.rs:1209 msgid "Proton Drive for Linux" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1215 +#: crates/pdfs-gui/src/app/main.rs:1217 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -147,23 +147,23 @@ msgid "" "AG." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1220 +#: crates/pdfs-gui/src/app/main.rs:1222 msgid "Source Code" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1224 +#: crates/pdfs-gui/src/app/main.rs:1226 msgid "Proton Terms of Service" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1228 +#: crates/pdfs-gui/src/app/main.rs:1230 msgid "Proton Privacy Policy" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1233 +#: crates/pdfs-gui/src/app/main.rs:1235 msgid "Disclaimer" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1237 +#: crates/pdfs-gui/src/app/main.rs:1239 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -176,21 +176,21 @@ msgid "" "of any data you cannot afford to lose." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1245 +#: crates/pdfs-gui/src/app/main.rs:1247 msgid "" "Places are named with data from GeoNames, licensed under CC BY 4.0." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1253 +#: crates/pdfs-gui/src/app/main.rs:1255 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:1261 +#: crates/pdfs-gui/src/app/main.rs:1263 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:1253 +#: crates/pdfs-gui/src/app/main.rs:1255 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -210,7 +210,7 @@ msgstr "" "www.naturalearthdata.com/\">Natural Earth, które należą do domeny " "publicznej." -#: crates/pdfs-gui/src/app/main.rs:1261 +#: crates/pdfs-gui/src/app/main.rs:1263 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:1253 +#: crates/pdfs-gui/src/app/main.rs:1255 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -206,7 +206,7 @@ msgstr "" "fronteiras de países do Natural Earth, que são de domínio público." -#: crates/pdfs-gui/src/app/main.rs:1261 +#: crates/pdfs-gui/src/app/main.rs:1263 msgid "" "The street map, when turned on, comes from OpenFreeMap in the