fixed some bugs

This commit is contained in:
nvrl committed 2026-07-18 19:24:11 +02:00
1 parent 7766628098
commit a90cf83ff5
39 files changed
+1573 -1285

No files matched your search

+7 -6
View File
@@ -39,11 +39,10 @@ use pdfs_core::auth;
use pdfs_core::config::AppDirs;
use pdfs_core::control::{
ErrorKind,
ActivityEntry, ActivityKind, BookmarkInfo, DeviceInfo, DirEntry, InvitationInfo, JobItem,
PhotoItem, PhotoKind, PublicLinkInfo, RefreshScope, Request, Response, SearchHit, ShareEntry,
ShareEntryKind, SharedItem, SyncFolderInfo, SyncPhase, SyncProgress, TransferDirection,
TransferItem, pending_summary, send,
ActivityEntry, ActivityKind, BookmarkInfo, DeviceInfo, DirEntry, ErrorKind, InvitationInfo,
JobItem, PhotoItem, PhotoKind, PublicLinkInfo, RefreshScope, Request, Response, SearchHit,
ShareEntry, ShareEntryKind, SharedItem, SyncFolderInfo, SyncPhase, SyncProgress,
TransferDirection, TransferItem, pending_summary, send,
};
use pdfs_core::service;
@@ -134,7 +133,8 @@ impl Ui {
/// The aspect ratio (w/h) to lay `uid`'s tile out at: the real one once its
/// thumbnail has been seen, otherwise [`RATIO_UNKNOWN`].
fn ratio(&self, uid: &str) -> f64 {
self.gallery.photo_ratio
self.gallery
.photo_ratio
.borrow()
.get(uid)
.copied()
@@ -809,6 +809,7 @@ fn error_headline(kind: ErrorKind, fallback: &str) -> &str {
ErrorKind::NotFound => "That's not there any more",
ErrorKind::Denied => "You don't have access to that",
ErrorKind::Conflict => "Something changed this first",
ErrorKind::Quota => "Your Proton Drive is full",
ErrorKind::Invalid | ErrorKind::Remote | ErrorKind::Internal => fallback,
}
}
+7 -1
View File
@@ -96,7 +96,13 @@ pub(crate) fn wire_activity(ui: &Rc<Ui>, retry: &gtk4::Button) {
}
/// Show a status page in place of the Activity list.
pub(crate) fn activity_status(ui: &Rc<Ui>, icon: &str, title: &str, description: &str, retry: bool) {
pub(crate) fn activity_status(
ui: &Rc<Ui>,
icon: &str,
title: &str,
description: &str,
retry: bool,
) {
ui.activity.status.set_icon_name(Some(icon));
ui.activity.status.set_title(title);
ui.activity.status.set_description(Some(description));
+20 -5
View File
@@ -609,7 +609,11 @@ pub(crate) fn stream_entry(ui: &Rc<Ui>, entry: &DirEntry) {
let mountpoint = ui.dirs.resolved_mountpoint(&ui.dirs.load_config());
let abs = mountpoint.join(&rel);
let Some(path) = abs.to_str() else {
toast_error(ui, "Couldn't play video", "The file path isn't valid UTF-8.");
toast_error(
ui,
"Couldn't play video",
"The file path isn't valid UTF-8.",
);
return;
};
toast(ui, &format!("Streaming “{}”…", entry.name));
@@ -744,7 +748,8 @@ pub(crate) fn repaint_crumb(ui: &Rc<Ui>, path: &str) {
} else {
path.split('/').collect()
};
ui.browser.crumb
ui.browser
.crumb
.append(&crumb_node(ui, "Proton Drive", "", segments.is_empty()));
let mut acc = String::new();
for (i, seg) in segments.iter().enumerate() {
@@ -1071,7 +1076,9 @@ pub(crate) fn start_upload(ui: &Rc<Ui>, sources: Vec<String>) {
};
toast(&ui, &what);
}
Ok(Ok(Response::Error { message, kind })) => toast_failure(&ui, "Couldn't upload", &message, kind),
Ok(Ok(Response::Error { message, kind })) => {
toast_failure(&ui, "Couldn't upload", &message, kind)
}
_ => toast_error(&ui, "Couldn't upload", "The mount service didn't respond."),
}
});
@@ -1237,7 +1244,11 @@ pub(crate) fn load_browser(ui: &Rc<Ui>) {
match result {
Ok(Ok(Response::Entries { entries })) => repaint_browser(&ui, &entries),
Ok(Ok(Response::Error { message, kind })) => browser_failed(&ui, &message, kind),
Ok(Ok(_)) => browser_failed(&ui, "Unexpected reply from the mount service.", ErrorKind::Internal),
Ok(Ok(_)) => browser_failed(
&ui,
"Unexpected reply from the mount service.",
ErrorKind::Internal,
),
Ok(Err(_)) | Err(_) => browser_unreachable(&ui),
}
});
@@ -1374,7 +1385,11 @@ pub(crate) fn run_search(ui: &Rc<Ui>, query: &str) {
match result {
Ok(Ok(Response::SearchResults { hits })) => repaint_search(&ui, &hits),
Ok(Ok(Response::Error { message, kind })) => browser_failed(&ui, &message, kind),
Ok(Ok(_)) => browser_failed(&ui, "Unexpected reply from the mount service.", ErrorKind::Internal),
Ok(Ok(_)) => browser_failed(
&ui,
"Unexpected reply from the mount service.",
ErrorKind::Internal,
),
Ok(Err(_)) | Err(_) => browser_unreachable(&ui),
}
});
+6 -1
View File
@@ -699,7 +699,12 @@ pub(crate) fn prompt_remove_device(ui: &Rc<Ui>, uid: &str, name: &str) {
}
/// Run a mutation raised from the Devices page and reload the page on success.
pub(crate) fn run_devices_mutation(ui: &Rc<Ui>, req: Request, done: &'static str, failed: &'static str) {
pub(crate) fn run_devices_mutation(
ui: &Rc<Ui>,
req: Request,
done: &'static str,
failed: &'static str,
) {
ui.busy_begin();
let rx = spawn_request(ui.dirs.control_socket(), req);
let ui = ui.clone();
+11 -4
View File
@@ -82,10 +82,12 @@ pub(crate) fn wire_login(ui: &Rc<Ui>) {
// Pressing Enter in either field submits the form, so signing in never needs
// a reach for the mouse. Both fields route to the same button.
let btn = ui.login.login_button.clone();
ui.login.email
ui.login
.email
.connect_entry_activated(move |_| btn.emit_clicked());
let btn = ui.login.login_button.clone();
ui.login.password
ui.login
.password
.connect_entry_activated(move |_| btn.emit_clicked());
let ui = ui.clone();
@@ -94,7 +96,9 @@ pub(crate) fn wire_login(ui: &Rc<Ui>) {
let username = ui.login.email.text().to_string();
let password = ui.login.password.text().to_string();
if username.is_empty() || password.is_empty() {
ui.login.login_status.set_text("Enter your email and password.");
ui.login
.login_status
.set_text("Enter your email and password.");
return;
}
@@ -129,7 +133,10 @@ pub(crate) fn wire_login(ui: &Rc<Ui>) {
service::enable_start();
refresh(&ui);
}
Err(e) => ui.login.login_status.set_text(&format!("Sign-in failed: {e}")),
Err(e) => ui
.login
.login_status
.set_text(&format!("Sign-in failed: {e}")),
}
});
});
@@ -322,7 +322,10 @@ pub(crate) fn navigate_photo(ui: &Rc<Ui>, viewer: &Rc<Viewer>, delta: i32) {
let renderable = |idx: u32| -> Option<PhotoItem> {
model
.item(idx)
.and_then(|obj| obj.downcast_ref::<BoxedAnyObject>().map(|b| b.borrow::<PhotoItem>().clone()))
.and_then(|obj| {
obj.downcast_ref::<BoxedAnyObject>()
.map(|b| b.borrow::<PhotoItem>().clone())
})
.filter(|photo| photo.kind != PhotoKind::Video)
};
let scan = |from: i32, step: i32| -> Option<(u32, PhotoItem)> {
@@ -564,7 +567,8 @@ pub(crate) fn open_photo_viewer(ui: &Rc<Ui>, initial_uid: String) {
let n = ui.gallery.model.n_items();
let initial_idx = find_photo_index(&ui.gallery.model, &initial_uid).unwrap_or(0);
let capture_time = ui
.gallery.model
.gallery
.model
.item(initial_idx)
.and_downcast::<BoxedAnyObject>()
.map_or(0, |boxed| boxed.borrow::<PhotoItem>().capture_time);
+41 -14
View File
@@ -356,8 +356,18 @@ pub(crate) fn month_range(year: i32, month: i32) -> Option<(i64, i64)> {
/// English month names, indexed 1..=12.
pub(crate) const MONTH_NAMES: [&str; 12] = [
"January", "February", "March", "April", "May", "June", "July", "August", "September",
"October", "November", "December",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
/// Rebuild the date-jump dropdown for the active kind: ask the daemon which
@@ -380,7 +390,10 @@ pub(crate) fn refresh_photo_months(ui: &Rc<Ui>) {
let mut labels = vec!["All dates".to_string()];
let mut ranges: Vec<Option<(i64, i64)>> = vec![None];
for m in months {
let name = MONTH_NAMES.get((m.month - 1) as usize).copied().unwrap_or("?");
let name = MONTH_NAMES
.get((m.month - 1) as usize)
.copied()
.unwrap_or("?");
labels.push(format!("{name} {} ({})", m.year, m.count));
ranges.push(month_range(m.year, m.month));
}
@@ -389,7 +402,8 @@ pub(crate) fn refresh_photo_months(ui: &Rc<Ui>) {
// Repopulating resets the selection to 0, which would otherwise fire the
// handler and reload; suppress that — the caller is already reloading.
ui.gallery.date_suppress.set(true);
ui.gallery.dates
ui.gallery
.dates
.set_model(Some(&gtk4::StringList::new(&label_refs)));
ui.gallery.dates.set_selected(0);
*ui.gallery.date_ranges.borrow_mut() = ranges;
@@ -444,7 +458,8 @@ pub(crate) fn wire_gallery(ui: &Rc<Ui>, list: &gtk4::ListView, scroll: &gtk4::Sc
// re-justify it in place, without rebuilding the ListStore (which would
// yank the scroll position back to the top).
ui_bind
.gallery.bound
.gallery
.bound
.borrow_mut()
.insert(item.position(), section);
});
@@ -456,7 +471,8 @@ pub(crate) fn wire_gallery(ui: &Rc<Ui>, list: &gtk4::ListView, scroll: &gtk4::Sc
factory.connect_unbind(move |_, item| {
let item = item.downcast_ref::<gtk4::ListItem>().unwrap();
ui_unbind
.gallery.bound
.gallery
.bound
.borrow_mut()
.remove(&item.position());
if let Some(obj) = item.item().and_downcast::<BoxedAnyObject>() {
@@ -562,7 +578,8 @@ pub(crate) fn wire_gallery(ui: &Rc<Ui>, list: &gtk4::ListView, scroll: &gtk4::Sc
return;
}
let range = ui_dates
.gallery.date_ranges
.gallery
.date_ranges
.borrow()
.get(dd.selected() as usize)
.copied()
@@ -742,7 +759,12 @@ pub(crate) fn plan_rows(ratios: &[f64], target: f64, width: f64) -> Vec<Vec<(i32
/// Size one row's tiles at `height`. A `justified` row is nudged so its widths
/// plus gaps hit `avail` exactly — rounding each width independently leaves a
/// few px of ragged right edge, so the last tile absorbs the remainder.
pub(crate) fn size_row(ratios: &[f64], height: f64, avail: f64, justified: bool) -> Vec<(i32, i32)> {
pub(crate) fn size_row(
ratios: &[f64],
height: f64,
avail: f64,
justified: bool,
) -> Vec<(i32, i32)> {
let height = height.round().max(1.0);
let mut sizes: Vec<(i32, i32)> = ratios
.iter()
@@ -896,13 +918,15 @@ pub(crate) fn want_thumb(ui: &Rc<Ui>, photo: &PhotoItem, picture: &gtk4::Picture
return;
}
ui.gallery.thumb_wanted
ui.gallery
.thumb_wanted
.borrow_mut()
.insert(photo.uid.clone(), picture.clone());
match photo.thumb_path.as_deref() {
Some(path) => {
ui.gallery.decode_queue
ui.gallery
.decode_queue
.borrow_mut()
.push_back((photo.uid.clone(), path.to_string()));
schedule_decode(ui);
@@ -1082,7 +1106,8 @@ pub(crate) fn schedule_relayout(ui: &Rc<Ui>) {
/// ListView binds them.
pub(crate) fn relayout_gallery(ui: &Rc<Ui>) {
let bound: Vec<(u32, gtk4::Box)> = ui
.gallery.bound
.gallery
.bound
.borrow()
.iter()
.map(|(pos, section)| (*pos, section.clone()))
@@ -1444,9 +1469,11 @@ pub(crate) fn play_video(ui: &Rc<Ui>, uid: String) {
"Couldn't open this video",
"Unexpected reply from the mount service.",
),
Ok(Err(_)) | Err(_) => {
toast_error(&ui, "Couldn't open this video", "Couldn't reach Proton Drive.")
}
Ok(Err(_)) | Err(_) => toast_error(
&ui,
"Couldn't open this video",
"Couldn't reach Proton Drive.",
),
}
});
}
+6 -1
View File
@@ -472,7 +472,12 @@ pub(crate) fn prompt_add_bookmark(ui: &Rc<Ui>) {
}
/// Run a mutation raised from the Shared page and reload the page on success.
pub(crate) fn run_shared_mutation(ui: &Rc<Ui>, req: Request, done: &'static str, failed: &'static str) {
pub(crate) fn run_shared_mutation(
ui: &Rc<Ui>,
req: Request,
done: &'static str,
failed: &'static str,
) {
ui.busy_begin();
let rx = spawn_request(ui.dirs.control_socket(), req);
let ui = ui.clone();
@@ -94,7 +94,13 @@ pub(crate) fn wire_shared_by_me(ui: &Rc<Ui>, retry: &gtk4::Button) {
}
/// Show a status page in place of the Shared (by-me) list.
pub(crate) fn shared_by_me_status(ui: &Rc<Ui>, icon: &str, title: &str, description: &str, retry: bool) {
pub(crate) fn shared_by_me_status(
ui: &Rc<Ui>,
icon: &str,
title: &str,
description: &str,
retry: bool,
) {
ui.shared_by_me.status.set_icon_name(Some(icon));
ui.shared_by_me.status.set_title(title);
ui.shared_by_me.status.set_description(Some(description));
+21 -6
View File
@@ -259,14 +259,20 @@ pub(crate) const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
/// read once from config / systemd here (the refresh loop owns only the live
/// mount + cache-usage read-out), with [`Ui::settings_suppress`] set around the
/// programmatic populate so the change handlers don't fire on it.
pub(crate) fn wire_settings(ui: &Rc<Ui>, purge_button: &gtk4::Button, mountpoint_button: &gtk4::Button) {
pub(crate) fn wire_settings(
ui: &Rc<Ui>,
purge_button: &gtk4::Button,
mountpoint_button: &gtk4::Button,
) {
let config = ui.dirs.load_config();
// Populate from persisted config + the systemd unit state, suppressed.
ui.status.settings_suppress.set(true);
ui.status.budget_row
ui.status
.budget_row
.set_value(config.resolved_cache_budget() as f64 / GIB);
ui.status.mountpoint_row
ui.status
.mountpoint_row
.set_subtitle(&ui.dirs.resolved_mountpoint(&config).display().to_string());
ui.status.autostart_row.set_active(service::is_enabled());
ui.status.settings_suppress.set(false);
@@ -337,7 +343,12 @@ pub(crate) fn wire_settings(ui: &Rc<Ui>, purge_button: &gtk4::Button, mountpoint
/// confirming with `done` or reporting the daemon's error under `failed`. Unlike
/// [`run_mutation`] there's no browser reload; the next refresh tick repaints the
/// cache read-out.
pub(crate) fn settings_request(ui: &Rc<Ui>, req: Request, done: &'static str, failed: &'static str) {
pub(crate) fn settings_request(
ui: &Rc<Ui>,
req: Request,
done: &'static str,
failed: &'static str,
) {
ui.busy_begin();
let rx = spawn_request(ui.dirs.control_socket(), req);
let ui = ui.clone();
@@ -586,7 +597,8 @@ pub(crate) fn repaint_transfers(ui: &Rc<Ui>, items: &[TransferItem], jobs: &[Job
.child(&row_box)
.build();
ui.status.transfers_group.add(&row);
ui.status.transfer_rows
ui.status
.transfer_rows
.borrow_mut()
.push(TransferRow { row, label, bar });
}
@@ -742,7 +754,10 @@ pub(crate) fn repaint_pins(ui: &Rc<Ui>, pins: &[pdfs_core::cache::Pin], mounted:
.subtitle("Right-click a file in the mount to keep it offline.")
.build();
ui.status.pins_group.add(&row);
ui.status.pin_rows.borrow_mut().push(PinRow { row, unpin: None });
ui.status
.pin_rows
.borrow_mut()
.push(PinRow { row, unpin: None });
return;
}
+24 -16
View File
@@ -147,7 +147,10 @@ pub(crate) fn build_details_pane() -> (gtk4::Widget, DetailsWidgets) {
/// wire its buttons back onto the entry it's showing.
pub(crate) fn wire_details(ui: &Rc<Ui>) {
// Both views share one model but have their own selection, so watch both.
for selection in [ui.details.grid_selection.clone(), ui.details.list_selection.clone()] {
for selection in [
ui.details.grid_selection.clone(),
ui.details.list_selection.clone(),
] {
let ui_sel = ui.clone();
selection.connect_selection_changed(move |sel, _, _| {
match entry_at(sel.model().as_ref(), sel.selected()) {
@@ -160,10 +163,12 @@ pub(crate) fn wire_details(ui: &Rc<Ui>) {
let ui_close = ui.clone();
ui.details.details.close_button.connect_clicked(move |_| {
ui_close
.details.grid_selection
.details
.grid_selection
.set_selected(gtk4::INVALID_LIST_POSITION);
ui_close
.details.list_selection
.details
.list_selection
.set_selected(gtk4::INVALID_LIST_POSITION);
hide_details(&ui_close);
});
@@ -193,19 +198,22 @@ pub(crate) fn wire_details(ui: &Rc<Ui>) {
}
});
let ui_pin = ui.clone();
ui.details.details.pin_row.connect_active_notify(move |row| {
if ui_pin.details.details_suppress.get() {
return;
}
let Some(entry) = ui_pin.details.details_entry.borrow().clone() else {
return;
};
// The switch reads the *desired* state; `toggle_pin` derives the request
// from the entry's current one, so only act when they actually differ.
if row.is_active() != entry.pinned {
toggle_pin(&ui_pin, &entry);
}
});
ui.details
.details
.pin_row
.connect_active_notify(move |row| {
if ui_pin.details.details_suppress.get() {
return;
}
let Some(entry) = ui_pin.details.details_entry.borrow().clone() else {
return;
};
// The switch reads the *desired* state; `toggle_pin` derives the request
// from the entry's current one, so only act when they actually differ.
if row.is_active() != entry.pinned {
toggle_pin(&ui_pin, &entry);
}
});
}
/// Reveal the details pane and paint it from `entry`.
@@ -400,7 +400,11 @@ pub(crate) fn repaint_share_link(state: &Rc<ShareDialog>, link: Option<&PublicLi
/// Create a public link, then reload the dialog so the copy/remove controls
/// replace the create form (and the freshly minted URL is shown).
pub(crate) fn share_dialog_create_link(state: &Rc<ShareDialog>, role: String, password: Option<String>) {
pub(crate) fn share_dialog_create_link(
state: &Rc<ShareDialog>,
role: String,
password: Option<String>,
) {
let rx = spawn_request(
state.ui.dirs.control_socket(),
Request::CreatePublicLink {
+1 -3
View File
@@ -840,9 +840,7 @@ impl Ui {
// so it lands on the final list, not a half-populated one.
if self.open_pending.get() && self.pending.get() == 0 {
self.open_pending.set(false);
if let (Some(index), Some(window)) =
(self.cursor.get(), self.window.borrow().clone())
{
if let (Some(index), Some(window)) = (self.cursor.get(), self.window.borrow().clone()) {
self.open(index, &window);
}
}