mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
fix(gui): decode viewer photos and read their EXIF off the main thread
The viewer decoded each full-size photo, read its EXIF and stat'ed it on the GTK main thread, freezing the app for a large photo. That now runs in gio::spawn_blocking, and a result for a photo the user has left is dropped (B242).
This commit is contained in:
1 parent
99707c48fa
commit
6ec8043934
2 files changed
+59
-12
No files matched your search
@@ -275,14 +275,42 @@ pub(crate) fn load_photo(ui: &Rc<Ui>, viewer: &Rc<Viewer>, uid: String) {
|
|||||||
viewer.status.set_label(message);
|
viewer.status.set_label(message);
|
||||||
viewer.status.set_visible(true);
|
viewer.status.set_visible(true);
|
||||||
};
|
};
|
||||||
match result {
|
let path = match result {
|
||||||
Ok(Ok(Response::FilePath { path })) if is_video => {
|
Ok(Ok(Response::FilePath { path })) => path,
|
||||||
|
Ok(Ok(Response::Error { message, .. })) => return fail(&message),
|
||||||
|
Ok(Ok(_)) => return fail(&gettext("Unexpected reply from the Proton Drive service.")),
|
||||||
|
Ok(Err(_)) | Err(_) => return fail(&gettext("Couldn't reach Proton Drive.")),
|
||||||
|
};
|
||||||
|
// Decoding a full-size photo and reading its EXIF take a good part of
|
||||||
|
// a second for a large one, which froze the whole app on the main
|
||||||
|
// thread. Done on a worker; a reply for a photo the user has since
|
||||||
|
// left is dropped like the request's own.
|
||||||
|
let loaded = {
|
||||||
|
let path = path.clone();
|
||||||
|
gio::spawn_blocking(move || {
|
||||||
|
let size = std::fs::metadata(&path).ok().map(|meta| meta.len());
|
||||||
|
if is_video {
|
||||||
|
return (size, None, ExifInfo::default());
|
||||||
|
}
|
||||||
|
let texture = gtk4::gdk::Texture::from_filename(&path);
|
||||||
|
(size, Some(texture), read_exif(&path))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
if *viewer.uid.borrow() != uid {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok((size, texture, info)) = loaded else {
|
||||||
|
return fail(&gettext("Couldn't render this photo."));
|
||||||
|
};
|
||||||
|
match texture {
|
||||||
|
None => {
|
||||||
viewer.video.set_filename(Some(&path));
|
viewer.video.set_filename(Some(&path));
|
||||||
viewer.media.set_visible_child_name("video");
|
viewer.media.set_visible_child_name("video");
|
||||||
*viewer.path.borrow_mut() = Some(path.clone());
|
*viewer.path.borrow_mut() = Some(path.clone());
|
||||||
show_info(&viewer, &path, ExifInfo::default());
|
show_info(&viewer, size, ExifInfo::default());
|
||||||
}
|
}
|
||||||
Ok(Ok(Response::FilePath { path })) => match gtk4::gdk::Texture::from_filename(&path) {
|
Some(texture) => match texture {
|
||||||
Ok(texture) => {
|
Ok(texture) => {
|
||||||
// Zoomed in on the thumbnail: keep the photo the size it is
|
// Zoomed in on the thumbnail: keep the photo the size it is
|
||||||
// on screen, now drawn from the full-size pixels.
|
// on screen, now drawn from the full-size pixels.
|
||||||
@@ -297,16 +325,13 @@ pub(crate) fn load_photo(ui: &Rc<Ui>, viewer: &Rc<Viewer>, uid: String) {
|
|||||||
zoom_to(&viewer, Some(zoom), None);
|
zoom_to(&viewer, Some(zoom), None);
|
||||||
}
|
}
|
||||||
*viewer.path.borrow_mut() = Some(path.clone());
|
*viewer.path.borrow_mut() = Some(path.clone());
|
||||||
show_info(&viewer, &path, read_exif(&path));
|
show_info(&viewer, size, info);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to load texture for {path}: {e}");
|
tracing::error!("Failed to load texture for {path}: {e}");
|
||||||
fail(&gettext("Couldn't render this photo."));
|
fail(&gettext("Couldn't render this photo."));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Ok(Ok(Response::Error { message, .. })) => fail(&message),
|
|
||||||
Ok(Ok(_)) => fail(&gettext("Unexpected reply from the Proton Drive service.")),
|
|
||||||
Ok(Err(_)) | Err(_) => fail(&gettext("Couldn't reach Proton Drive.")),
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -452,16 +477,16 @@ pub(crate) fn clear_info(viewer: &Rc<Viewer>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fill the details drawer for the photo now on screen: its own file facts
|
/// Fill the details drawer for the photo now on screen: its own file facts
|
||||||
/// (size, dimensions) plus whatever EXIF it carries.
|
/// (`size`, dimensions) plus whatever EXIF it carries.
|
||||||
pub(crate) fn show_info(viewer: &Rc<Viewer>, path: &str, info: ExifInfo) {
|
pub(crate) fn show_info(viewer: &Rc<Viewer>, size: Option<u64>, info: ExifInfo) {
|
||||||
while let Some(row) = viewer.info_rows.first_child() {
|
while let Some(row) = viewer.info_rows.first_child() {
|
||||||
viewer.info_rows.remove(&row);
|
viewer.info_rows.remove(&row);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut fields: Vec<(String, String)> = Vec::new();
|
let mut fields: Vec<(String, String)> = Vec::new();
|
||||||
if let Ok(meta) = std::fs::metadata(path) {
|
if let Some(size) = size {
|
||||||
// Translators: label of a photo's file size in the details panel.
|
// Translators: label of a photo's file size in the details panel.
|
||||||
fields.push((pgettext("property", "Size"), human_bytes(meta.len())));
|
fields.push((pgettext("property", "Size"), human_bytes(size)));
|
||||||
}
|
}
|
||||||
fields.extend(info.fields);
|
fields.extend(info.fields);
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ a live account since.
|
|||||||
|
|
||||||
| Entry | Title |
|
| Entry | Title |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| [B242](#b242--opening-a-large-photo-freezes-the-app) | Opening a large photo freezes the app |
|
||||||
| [B241](#b241--every-closed-photo-viewer-stays-in-memory-with-its-photo) | Every closed photo viewer stays in memory with its photo |
|
| [B241](#b241--every-closed-photo-viewer-stays-in-memory-with-its-photo) | Every closed photo viewer stays in memory with its photo |
|
||||||
| [B240](#b240--pdfs-mount-can-be-core-dumped-with-the-keys-in-memory) | pdfs mount can be core-dumped with the keys in memory |
|
| [B240](#b240--pdfs-mount-can-be-core-dumped-with-the-keys-in-memory) | pdfs mount can be core-dumped with the keys in memory |
|
||||||
| [B239](#b239----wait-reports-success-for-imports-and-re-dates-that-failed) | --wait reports success for imports and re-dates that failed |
|
| [B239](#b239----wait-reports-success-for-imports-and-re-dates-that-failed) | --wait reports success for imports and re-dates that failed |
|
||||||
@@ -344,6 +345,27 @@ a live account since.
|
|||||||
|
|
||||||
Newest first.
|
Newest first.
|
||||||
|
|
||||||
|
### B242 — Opening a large photo freezes the app
|
||||||
|
|
||||||
|
**Status:** Fixed, not verified live
|
||||||
|
**Found:** 2026-10-16, architecture audit (`FINDINGS.md` 2.5.1).
|
||||||
|
**Where:** `load_photo` and `show_info` in `crates/pdfs-gui/src/app/pages/photo_viewer.rs`
|
||||||
|
|
||||||
|
**Repro:** Open a 50-megapixel photo in the viewer and press → repeatedly while each one loads.
|
||||||
|
|
||||||
|
**Cause.** When the full-size file arrived, the viewer decoded it with
|
||||||
|
`Texture::from_filename`, read its EXIF and stat'ed it on the GTK main thread. For a large photo
|
||||||
|
that took a good part of a second each time, and the whole app froze for it: no redraw, no
|
||||||
|
input, no other page.
|
||||||
|
|
||||||
|
**Fix.** The decode, the EXIF read and the stat run on a worker (`gio::spawn_blocking`). The
|
||||||
|
result is used only if the viewer still shows that photo; a reply for one the user has left is
|
||||||
|
dropped.
|
||||||
|
|
||||||
|
**Test:** none automated; the GUI tests run without a display.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### B241 — Every closed photo viewer stays in memory with its photo
|
### B241 — Every closed photo viewer stays in memory with its photo
|
||||||
|
|
||||||
**Status:** Fixed, not verified live
|
**Status:** Fixed, not verified live
|
||||||
|
|||||||
Reference in new issue
Block a user