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