diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index c67232b..164410a 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -345,8 +345,12 @@ fn build_window(app: &adw::Application) { login_frame.add_top_bar(&{ let header = adw::HeaderBar::new(); header.set_show_title(false); + // The split is collapsed while signed out; going "back" would only + // uncover a sidebar with nowhere to go. + header.set_show_back_button(false); header }); + login_frame.add_top_bar(&login_widgets.keyring_banner); login_frame.set_content(Some(&login_page)); stack.add_named(&login_frame, Some("login")); stack.add_named(&browser_page, Some("browser")); @@ -383,13 +387,17 @@ fn build_window(app: &adw::Application) { let toasts = adw::ToastOverlay::new(); toasts.set_child(Some(&split)); + let (startup_session, keyring_error) = match read_session() { + Ok(session) => (session, None), + Err(e) => (None, Some(e)), + }; let ui = Rc::new(Ui { dirs, stack: stack.clone(), toasts: toasts.clone(), busy: Cell::new(0), opening: RefCell::new(HashSet::new()), - session: RefCell::new(auth::load().ok()), + session: RefCell::new(startup_session), unlock_offered: Cell::new(false), mounted: RefCell::new(false), sidebar: sidebar_list.clone(), @@ -648,6 +656,9 @@ fn build_window(app: &adw::Application) { }, }); wire_login(&ui); + if let Some(e) = keyring_error { + keyring_unreadable(&ui, &e); + } let ui_status = ui.clone(); main_widgets .status_button diff --git a/crates/pdfs-gui/src/app/pages/login.rs b/crates/pdfs-gui/src/app/pages/login.rs index 9c1151c..e7b0dc8 100644 --- a/crates/pdfs-gui/src/app/pages/login.rs +++ b/crates/pdfs-gui/src/app/pages/login.rs @@ -9,6 +9,9 @@ pub(crate) struct LoginState { /// Spins inside the Sign in button while a sign-in is running. pub(crate) login_spinner: Spinner, pub(crate) login_status: gtk4::Label, + /// Shown over the form while a saved sign-in exists but the keyring would + /// not hand it over; see [`keyring_unreadable`]. + pub(crate) keyring_banner: adw::Banner, } /// Where the login page's account links go. @@ -87,6 +90,12 @@ pub(crate) fn build_login_page() -> (gtk4::Widget, LoginState) { .child(&inner) .build(); let scroll = gtk4::ScrolledWindow::builder().child(&clamp).build(); + let keyring_banner = adw::Banner::builder() + .title(gettext( + "Couldn't read your saved sign-in. Unlock the system keyring, then try again.", + )) + .button_label(gettext("Retry")) + .build(); ( scroll.upcast(), @@ -96,10 +105,80 @@ pub(crate) fn build_login_page() -> (gtk4::Widget, LoginState) { login_button, login_spinner, login_status, + keyring_banner, }, ) } +/// The stored session, `None` when there is none, or the keyring's error when +/// it could not be read. A locked or absent keyring is not a sign-out: the +/// session is still saved, and signing in again would only replace it. +pub(crate) fn read_session() -> pdfs_core::Result> { + match auth::load() { + Ok(session) => Ok(Some(session)), + Err(pdfs_core::Error::NotLoggedIn) => Ok(None), + Err(e) => Err(e), + } +} + +/// How long to wait before each automatic retry of an unreadable keyring. A +/// keyring daemon started with the session is usually up within seconds. +const KEYRING_RETRIES: [u32; 4] = [2, 5, 10, 30]; + +/// The keyring refused to hand over the saved session. Keep the login page up, +/// but say why, and pick the session up as soon as the keyring answers: on its +/// own a few times, and whenever the user presses Retry. +pub(crate) fn keyring_unreadable(ui: &Rc, error: &pdfs_core::Error) { + tracing::warn!("cannot read the saved session: {error}"); + let banner = &ui.login.keyring_banner; + if banner.is_revealed() { + return; + } + banner.set_revealed(true); + let ui_retry = ui.clone(); + let handler = banner.connect_button_clicked(move |_| retry_session(&ui_retry)); + // Disconnected once the banner goes, so a later failure wires it afresh. + let handler = RefCell::new(Some(handler)); + banner.connect_revealed_notify(move |banner| { + if !banner.is_revealed() + && let Some(handler) = handler.borrow_mut().take() + { + banner.disconnect(handler); + } + }); + let ui = ui.clone(); + glib::spawn_future_local(async move { + for seconds in KEYRING_RETRIES { + glib::timeout_future_seconds(seconds).await; + if !ui.login.keyring_banner.is_revealed() { + return; + } + retry_session(&ui); + } + }); +} + +/// Read the saved session again after [`keyring_unreadable`]. Found: show the +/// signed-in window. Gone: the plain login page. Still unreadable: keep waiting. +fn retry_session(ui: &Rc) { + let ui = ui.clone(); + glib::spawn_future_local(async move { + let Ok(result) = gio::spawn_blocking(read_session).await else { + return; + }; + match result { + Ok(session) => { + ui.login.keyring_banner.set_revealed(false); + if session.is_some() && ui.session.borrow().is_none() { + *ui.session.borrow_mut() = session; + refresh(&ui); + } + } + Err(e) => tracing::debug!("the keyring still cannot be read: {e}"), + } + }); +} + /// Connect the sign-in button: read the fields, run [`auth::login`] on a worker /// thread, and report the outcome back on the main loop. pub(crate) fn wire_login(ui: &Rc) { @@ -202,10 +281,11 @@ pub(crate) fn wire_login(ui: &Rc) { // Read on a worker: a keyring that has to be unlocked first // answers only once the user has, and the window must not // freeze until then. - *ui.session.borrow_mut() = gio::spawn_blocking(|| auth::load().ok()) - .await - .ok() - .flatten(); + match gio::spawn_blocking(read_session).await { + Ok(Ok(session)) => *ui.session.borrow_mut() = session, + Ok(Err(e)) => keyring_unreadable(&ui, &e), + Err(_) => {} + } // Enable+start the mount service now that we have a session. let _ = gio::spawn_blocking(service::enable_start).await; refresh(&ui); diff --git a/docs/BUGS.md b/docs/BUGS.md index 6e2f8c1..92b99e3 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -100,6 +100,7 @@ a live account since. | Entry | Title | |---|---| +| [B253](#b253--the-app-shows-the-sign-in-page-when-the-keyring-is-locked) | The app shows the sign-in page when the keyring is locked | | [B252](#b252--computers-sharing-a-device-overwrite-each-others-profile) | Computers sharing a device overwrite each other's profile | | [B251](#b251--two-computers-can-make-the-same-conflict-copy-name) | Two computers can make the same conflict copy name | | [B250](#b250--a-revision-made-during-a-mirror-download-never-arrives) | A revision made during a mirror download never arrives | @@ -355,6 +356,31 @@ a live account since. Newest first. +### B253 — The app shows the sign-in page when the keyring is locked + +**Status:** Fixed, not verified live +**Found:** 2026-10-16, user report: "the login each time you reboot or logout and back in". +**Where:** `build_window` in `crates/pdfs-gui/src/app/main.rs`; `wire_login` in +`crates/pdfs-gui/src/app/pages/login.rs` + +**Repro:** Sign in, then start `pdfs-app` while the Secret Service is locked, not yet running, +or unreachable. The app shows the sign-in form as if nobody were signed in. The saved session +is still in the keyring, and the daemon waits for it. + +**Cause.** The app read the session with `auth::load().ok()`, which turns every keyring error +into "signed out". Only `NotLoggedIn` means that. The tray already told the two apart. + +**Fix.** `read_session` keeps `NotLoggedIn` apart from keyring errors. On an error the login +page shows a banner saying the keyring could not be read, with a Retry button. The app reads +the keyring again after 2, 5, 10 and 30 seconds and on Retry, and opens on Files once it +answers, without asking for a sign-in. A sign-in whose session cannot be read back gets the +same banner. + +**Test:** None automated; the GTK window has no test harness. Checked by hand with +`DBUS_SESSION_BUS_ADDRESS` pointing nowhere: the banner shows over the form. + +--- + ### B252 — Computers sharing a device overwrite each other's profile **Status:** Fixed, not verified live diff --git a/po/de.po b/po/de.po index e717399..778e824 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-16 16:02+0200\n" +"POT-Creation-Date: 2026-10-16 17:09+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:767 +#: crates/pdfs-gui/src/app/main.rs:849 #: 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:833 @@ -70,74 +70,74 @@ msgstr "%A, %-d. %B %Y, %H:%M" msgid "My files" msgstr "Meine Dateien" -#: crates/pdfs-gui/src/app/main.rs:770 crates/pdfs-gui/src/app/main.rs:1415 +#: crates/pdfs-gui/src/app/main.rs:852 crates/pdfs-gui/src/app/main.rs:1517 #: crates/pdfs-gui/src/app/pages/albums.rs:638 #: 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:775 +#: crates/pdfs-gui/src/app/main.rs:857 #: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:484 msgid "Shared with me" msgstr "Mit mir geteilt" -#: crates/pdfs-gui/src/app/main.rs:780 +#: crates/pdfs-gui/src/app/main.rs:862 #: 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:783 +#: crates/pdfs-gui/src/app/main.rs:865 #: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:493 msgid "Computers" msgstr "Computer" -#: crates/pdfs-gui/src/app/main.rs:784 +#: crates/pdfs-gui/src/app/main.rs:866 #: 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:786 +#: crates/pdfs-gui/src/app/main.rs:868 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:1112 +#: crates/pdfs-gui/src/app/main.rs:1214 #: crates/pdfs-gui/src/app/pages/takeout.rs:714 #, rust-format msgid "{what}: {detail}" msgstr "{what}: {detail}" -#: crates/pdfs-gui/src/app/main.rs:1135 +#: crates/pdfs-gui/src/app/main.rs:1237 msgid "You're offline" msgstr "Keine Internetverbindung" -#: crates/pdfs-gui/src/app/main.rs:1136 +#: crates/pdfs-gui/src/app/main.rs:1238 msgid "That's not there any more" msgstr "Das ist nicht mehr vorhanden" -#: crates/pdfs-gui/src/app/main.rs:1137 +#: crates/pdfs-gui/src/app/main.rs:1239 msgid "You don't have access to that" msgstr "Kein Zugriff darauf" -#: crates/pdfs-gui/src/app/main.rs:1138 +#: crates/pdfs-gui/src/app/main.rs:1240 msgid "Something changed this first" msgstr "Etwas anderes hat dies zuerst geändert" -#: crates/pdfs-gui/src/app/main.rs:1139 +#: crates/pdfs-gui/src/app/main.rs:1241 msgid "Your Proton Drive is full" msgstr "Das Proton Drive ist voll" -#: crates/pdfs-gui/src/app/main.rs:1226 +#: crates/pdfs-gui/src/app/main.rs:1328 msgid "Proton Drive for Linux" msgstr "Proton Drive for Linux" -#: crates/pdfs-gui/src/app/main.rs:1234 +#: crates/pdfs-gui/src/app/main.rs:1336 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -149,23 +149,23 @@ msgstr "" "Inoffizieller Client – nicht mit Proton AG verbunden, von Proton AG " "unterstützt oder befürwortet." -#: crates/pdfs-gui/src/app/main.rs:1239 +#: crates/pdfs-gui/src/app/main.rs:1341 msgid "Source Code" msgstr "Quellcode" -#: crates/pdfs-gui/src/app/main.rs:1243 +#: crates/pdfs-gui/src/app/main.rs:1345 msgid "Proton Terms of Service" msgstr "Nutzungsbedingungen von Proton" -#: crates/pdfs-gui/src/app/main.rs:1247 +#: crates/pdfs-gui/src/app/main.rs:1349 msgid "Proton Privacy Policy" msgstr "Datenschutzerklärung von Proton" -#: crates/pdfs-gui/src/app/main.rs:1252 +#: crates/pdfs-gui/src/app/main.rs:1354 msgid "Disclaimer" msgstr "Haftungsausschluss" -#: crates/pdfs-gui/src/app/main.rs:1256 +#: crates/pdfs-gui/src/app/main.rs:1358 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -190,7 +190,7 @@ msgstr "" "bereitgestellt. Daten, deren Verlust nicht hinnehmbar ist, sollten " "zusätzlich gesichert werden." -#: crates/pdfs-gui/src/app/main.rs:1264 +#: crates/pdfs-gui/src/app/main.rs:1366 msgid "" "Places are named with data from GeoNames, licensed under GeoNames, lizenziert unter CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1272 +#: crates/pdfs-gui/src/app/main.rs:1374 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -210,7 +210,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth gezeichnet, die " "gemeinfrei sind." -#: crates/pdfs-gui/src/app/main.rs:1280 +#: crates/pdfs-gui/src/app/main.rs:1382 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:1272 +#: crates/pdfs-gui/src/app/main.rs:1374 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "\">Natural Earth land outlines and country borders, which are in the " "public domain." -#: crates/pdfs-gui/src/app/main.rs:1280 +#: crates/pdfs-gui/src/app/main.rs:1382 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:1272 +#: crates/pdfs-gui/src/app/main.rs:1374 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -208,7 +208,7 @@ msgstr "" "fronteras de países de Natural " "Earth, que son de dominio público." -#: crates/pdfs-gui/src/app/main.rs:1280 +#: crates/pdfs-gui/src/app/main.rs:1382 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:1272 +#: crates/pdfs-gui/src/app/main.rs:1374 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -209,7 +209,7 @@ msgstr "" "frontières des pays de Natural " "Earth, qui sont dans le domaine public." -#: crates/pdfs-gui/src/app/main.rs:1280 +#: crates/pdfs-gui/src/app/main.rs:1382 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:1272 +#: crates/pdfs-gui/src/app/main.rs:1374 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "confini nazionali di Natural " "Earth, che sono di pubblico dominio." -#: crates/pdfs-gui/src/app/main.rs:1280 +#: crates/pdfs-gui/src/app/main.rs:1382 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:1272 +#: crates/pdfs-gui/src/app/main.rs:1374 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -208,7 +208,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth, die in het " "publieke domein vallen." -#: crates/pdfs-gui/src/app/main.rs:1280 +#: crates/pdfs-gui/src/app/main.rs:1382 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:767 +#: crates/pdfs-gui/src/app/main.rs:849 #: 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:833 @@ -73,74 +73,74 @@ msgstr "" msgid "My files" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:770 crates/pdfs-gui/src/app/main.rs:1415 +#: crates/pdfs-gui/src/app/main.rs:852 crates/pdfs-gui/src/app/main.rs:1517 #: crates/pdfs-gui/src/app/pages/albums.rs:638 #: 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:775 +#: crates/pdfs-gui/src/app/main.rs:857 #: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:484 msgid "Shared with me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:780 +#: crates/pdfs-gui/src/app/main.rs:862 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 msgid "Shared by me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:783 +#: crates/pdfs-gui/src/app/main.rs:865 #: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:493 msgid "Computers" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:784 +#: crates/pdfs-gui/src/app/main.rs:866 #: 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:786 +#: crates/pdfs-gui/src/app/main.rs:868 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:1112 +#: crates/pdfs-gui/src/app/main.rs:1214 #: crates/pdfs-gui/src/app/pages/takeout.rs:714 #, rust-format msgid "{what}: {detail}" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1135 +#: crates/pdfs-gui/src/app/main.rs:1237 msgid "You're offline" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1136 +#: crates/pdfs-gui/src/app/main.rs:1238 msgid "That's not there any more" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1137 +#: crates/pdfs-gui/src/app/main.rs:1239 msgid "You don't have access to that" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1138 +#: crates/pdfs-gui/src/app/main.rs:1240 msgid "Something changed this first" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1139 +#: crates/pdfs-gui/src/app/main.rs:1241 msgid "Your Proton Drive is full" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1226 +#: crates/pdfs-gui/src/app/main.rs:1328 msgid "Proton Drive for Linux" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1234 +#: crates/pdfs-gui/src/app/main.rs:1336 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -148,23 +148,23 @@ msgid "" "AG." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1239 +#: crates/pdfs-gui/src/app/main.rs:1341 msgid "Source Code" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1243 +#: crates/pdfs-gui/src/app/main.rs:1345 msgid "Proton Terms of Service" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1247 +#: crates/pdfs-gui/src/app/main.rs:1349 msgid "Proton Privacy Policy" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1252 +#: crates/pdfs-gui/src/app/main.rs:1354 msgid "Disclaimer" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1256 +#: crates/pdfs-gui/src/app/main.rs:1358 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -177,21 +177,21 @@ msgid "" "of any data you cannot afford to lose." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1264 +#: crates/pdfs-gui/src/app/main.rs:1366 msgid "" "Places are named with data from GeoNames, licensed under CC BY 4.0." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1272 +#: crates/pdfs-gui/src/app/main.rs:1374 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:1280 +#: crates/pdfs-gui/src/app/main.rs:1382 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:1272 +#: crates/pdfs-gui/src/app/main.rs:1374 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -211,7 +211,7 @@ msgstr "" "www.naturalearthdata.com/\">Natural Earth, które należą do domeny " "publicznej." -#: crates/pdfs-gui/src/app/main.rs:1280 +#: crates/pdfs-gui/src/app/main.rs:1382 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:1272 +#: crates/pdfs-gui/src/app/main.rs:1374 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "fronteiras de países do Natural Earth, que são de domínio público." -#: crates/pdfs-gui/src/app/main.rs:1280 +#: crates/pdfs-gui/src/app/main.rs:1382 msgid "" "The street map, when turned on, comes from OpenFreeMap in the