User-visible strings, doc comments and docs now say "Favorite" and "color", matching the `pdfs favorite` command; UI_UX_PLAN.md §8 records the choice. The changelog's 1.11.x and Unreleased entries said schema 30, but V31 (`photos.resolved_at`) shipped in 1.11.0. Adds the missing release links for 1.10.0 to 1.11.1.
23 KiB
UI/UX audit and improvement plan
Audit date: 2026-09-23. Scope: pdfs-gui (all pages, dialogs, photo viewer), pdfs-tray, and the
backend capabilities the UI would need. File references are to crates/pdfs-gui/src/app/ unless
stated otherwise.
1. Summary
The client is functionally rich but the UI is built page by page, and it shows:
- No sync control surface. There is no way to pause or resume sync, no list of what is stuck or
failing, and no conflict overview. The backend has no pause at all, no per-op listing, and no
per-file conflict record (conflicts exist only as
(sync-conflict <ts>)files plus activity rows). The GUI readsonline,pending_uploadsandpending_changesfromStatusand ignoresfailing_ops,failing_error,parked_uploadsandstaged_*. - Information architecture is scattered. Live transfers are a group on Settings while the Activity page is history only. Locations and Computers point at each other. Import lives under Settings but is launched from Gallery and returns to Settings. Sign-out, quota, mount status, pinned files, import and developer info all share the Settings page.
- Chrome is non-standard. Every page draws its own title and toolbar row under an almost empty
AdwHeaderBarwhose title is always "Proton Drive". Two stacked bars, wasted height, no current folder in the title. - Icons and wording drift. Upload uses
document-send-symbolic(reads as "send"), offline uses stars (reads as "favorite"), trash icons mean "stop syncing", "remove device" and "remove bookmark". "Keep offline" / "Unpin" / "Remove offline copy" / "Available offline" are one concept. "Computers" vs "device", "Gallery" vs "Photos", "mount service" vs "daemon" vs "Connect". - Theming is hardcoded.
PROTON_PURPLEoverrides the system accent; badge, viewer and tile colours are hex/rgba literals and ignore light, dark and high-contrast modes. - Several visible bugs (section 9).
2. Design principles
- Follow GNOME HIG and current libadwaita. The system has libadwaita 1.9; the crate is pinned to
feature
v1_5. Raise it to at leastv1_7to getadw::Spinner,ToggleGroup,InlineViewSwitcher,ButtonRow,WrapBox,BottomSheet, and (1.8)ShortcutsDialog. - Sync state is first class. The user must always see: up to date / syncing / paused / offline / needs attention, and be one click from pausing or from the list of problems.
- One term, one icon, one place per concept (glossary in section 8).
- Secondary and destructive actions go into menus. Rows get at most one visible suffix action
plus a
view-more-symbolicmenu. Destructive actions are red inside the menu and in the confirm dialog, not as filled buttons in headers. - Never block on the network visibly. Keep old content on screen while reloading, show a header spinner, never flash the whole page to a "Loading…" status page.
- Respect the system. Use the system accent by default (brand purple stays on login, app icon
and an opt-in toggle). Use named colours (
@success_color,@warning_color,@error_color).
3. New information architecture
Sidebar
Proton Drive (header: app name)
My files
Photos
Shared with me (badge: pending invitations)
Shared by me
Computers
Trash
─────────────
Sync (badge: issue count, icon reflects state)
───────────── footer
[●] Up to date · Pause ← sync status strip, click opens Sync
▓▓▓░░░ 269 GiB of 1.6 TiB ← quota
(avatar) user@proton.me ⋮ ← account menu: Preferences, Sign out…
- Remove sidebar rows: Locations (merged into Sync), Activity (becomes the Sync "History"
view), Settings (becomes an
AdwPreferencesDialog,Ctrl+,). - Rename Gallery to Photos and Shared to Shared by me.
- Primary menu: Preferences, Keyboard Shortcuts, About. The About dialog's
debug_infogets app version, user agent, daemon status and config path (replacing the "Developer" group).
Sync page (new, replaces Locations + Activity + Settings→Activity)
Top: an AdwBanner-style status card with state icon, one-line summary ("Syncing 12 files · 3.4
MB/s", "Paused until you resume", "2 files need attention") and one primary button: Pause /
Resume (split button with "Pause for 1 hour / until tomorrow / until resumed").
Below, an InlineViewSwitcher with four views:
- Issues (default when non-empty): conflicts, failing uploads (
last_error, attempts, next retry), folders inerror/conflictstate, parked uploads older than N minutes. Each row has a primary fix action (Resolve…, Retry now) and a menu (Show in folder, Discard change…). - Transfers: in-flight uploads and downloads with progress, speed, cancel; background jobs (thumbnail build, Takeout import, re-date) with their own cancel.
- Folders: the old Locations list. Row: path, mode as a dropdown suffix (Synced / On-demand, with
confirm when going on-demand deletes local copies), state subtitle,
Sync nowand a menu (Open, Change location…, Pause this folder, Stop syncing…). "Add folder…" as aButtonRowat the end. The primary mount appears first and is not removable. - History: the old Activity feed, with a kind filter, failures in
@error_color, rows activatable (open or reveal the target), and "Clear history".
Preferences dialog (slimmed Settings)
| Page | Contents |
|---|---|
| General | Start on login; Mount location (path + Change…); Notifications (on sync issues, on import done) |
| Storage | Cache usage bar; Cache budget (spin row, in GB, "Unlimited" switch instead of magic 0); Clear cache…; Offline files (count + "Manage" → Sync › Folders filter or a sub-page) |
| Sync | Ignore patterns (editable list); Conflict clean-up: Off / Report / Remove identical copies (conflict_sweep); later: bandwidth limits |
| Advanced | Open-with rules, search prompt config (only if kept in GUI) |
Moved out: account row and Sign out (sidebar account menu, with confirm), quota (sidebar footer), mount status (sidebar status strip), live transfers (Sync), Google Photos import (Photos page menu), version/user agent (About → Debug info).
4. Backend work required
Everything below is new; nothing like it exists today.
B-1 Pause and resume (global, then per folder)
Request::SetSyncPaused { paused: bool, until: Option<i64> }, persisted (config keysync_paused_untilor a DB setting) so a pause survives restarts;Response::Statusgainspausedandpaused_until.- Drain:
run_pending_drain(pdfs-fuse/src/drain.rs:265) already releases claimed ops while offline; gate onpausedat the same point and reuse the wake path. - Sync engine: add
SyncMsg::Pause/Resumeor gatereconcile_all/reconcile_folder; the 120 s poll and watcher keep recording, they just do not apply. - FUSE reads must keep working while paused (on-demand hydration is a read, not a sync); document that pause means "no uploads, no mirror reconcile".
- Per folder later:
sync_folder.pausedcolumn, skipped inreconcile_all. - Bandwidth limits later: token bucket in
CountingReader/CountingWriter(pdfs-fuse/src/transfers.rs).
B-2 Pending ops and transfers
Request::ListPendingOps→ id, kind, path/name, attempts,last_error(classified asErrorKind),next_attempt_at, parked flag, staged bytes.Request::RetryOp { id }(setnext_attempt_at = now, wake drain) andRetryAllFailing.Request::DiscardOp { id }for a create/revision whose bytes should be dropped. Must save the staged blob to a local recovery folder first; never silent data loss (seedocs/RECOVERY.md).TransferItemgains a stable id andRequest::CancelTransfer { id }.- Per-folder last error message on
SyncFolderInfo(today the text only exists in the activity feed).
B-3 Conflicts
Request::ListConflicts: walk nodes whose name parses withsweep.rsconflict_base_name, for the mount and every mirror folder. Return copy path, original path (if it exists), both sizes and mtimes, whether content is identical (size +content_sha1, as the sweep already proves), and which side is local vs remote when known.- Optionally a
conflicttable written bykeep_as_conflict_copy/preserve_conflict_copyso the origin (drain vs mirror, device) is recorded instead of inferred from names. Request::ResolveConflict { copy, keep: Original | Copy | Both }:- Original: trash the copy (re-verify like the enforcing sweep does).
- Copy: upload the copy's content as a new revision of the original, then trash the copy (the old content stays in version history, so this is reversible).
- Both: rename the copy to a user-chosen name, drop the
(sync-conflict …)stamp.
- Activity rows for each resolution.
5. Shell and visual polish (all pages)
- Per-page header bars. Give each stack page its own
adw::ToolbarView+adw::HeaderBar(or inject page widgets into the shared bar on page switch). Title = page name, or the current folder in My files. Page actions move into the header; the extra toolbar row disappears. - One page skeleton. Same margins everywhere (today 12 vs 18), same
Clamprule (lists that are settings-like clamp at 720, file/photo grids are full width), same status pages (compact,vexpand, crossfade), same Retry semantics (Retry reloads; "Start Proton Drive" is a separate, explicitly labelled button when the service is down). - Loading.
adw::Spinnerin the header; keep the old model until the new one lands; status page only on first load. - Theme. Drop the global
accent_*override; offer "Use Proton purple accent" in Preferences. Replace hex colours with named colours; test light, dark and high contrast. - Move the 70-line CSS string in
main.rs:232toresources/style.cssin the gresource (style-dark.cssif needed), so it can be edited and linted. - Shortcuts. Replace the hand-rolled cheatsheet with
adw::ShortcutsDialog(orGtkShortcutsWindowbefore 1.8) listing all bindings, including F5/Ctrl+R and the viewer keys. - Toasts vs dialogs. Dialogs report their own errors inline (versions, share, add bookmark); a
toast behind a modal is invisible. Pluralise properly (
ngettext-ready helpers, no "item(s)"). - i18n groundwork. Wrap user strings in a
gettext!-style macro now, even without translations, so later localisation is mechanical.
6. Page-by-page plan
My files
Header: [‹ ›] Path bar (breadcrumbs, scroll to end, Ctrl+L to edit) … [search] [view ▾] [+ New ▾]
- New ▾ (
list-add-symbolic, suggested): New folder (Ctrl+Shift+N), Upload files (Ctrl+U), Upload folder. Replaces three separate icons, fixes the "send" icon. - View ▾ split button: Grid/List (Ctrl+1/2), Sort by Name/Size/Modified, ascending/descending, Folders first, zoom slider. Persist choice (config or GSettings).
- Folder ⋮ menu (in header or background context menu): Refresh, Build thumbnails, Make folder available offline, Open in file manager, Share folder…. Build thumbnails leaves the main toolbar.
- Navigation. Back/forward history (Alt+←/→), Up (Alt+↑); searching then pressing Back clears the search consistently.
- Context menus as
gtk::PopoverMenufromgio::Menu+ actions: keyboard navigable, Menu / Shift+F10, same action set as details pane. Add a background menu. Order: Open, Open with…, Play (media) · Available offline (check item) · Share…, Copy link · Rename…, Move to…, Versions… · Move to Trash (destructive). - Offline state. One concept "Available offline", toggle semantics. Badges: cloud-only = no
badge or subtle
cloud-outline; downloaded = check; available offline = pin/folder-downloadstyle filled icon. Use named colours. Folders can be made available offline too, or the toggle is hidden for them (fixdetails.rs:259). - List view. Sortable, resizable columns: Name, Status, Size, Modified (date + time), and Location for search results. Rubberband selection. One selection model shared between views.
- Details pane. On wide windows a real side pane (breakpoint, not overlay); header bar with close; real thumbnail; sections: Info, Offline, Sharing (people count, link), Versions (latest 3 + "All versions…"). Same actions as the context menu.
- Selection bar (≥1 selected, not ≥2): count + size, Available offline, Move to…, Share (single), Move to Trash; Select all (Ctrl+A). Drop the confirm dialog for trash since Undo exists (trash is reversible), or keep confirm and drop Undo — not both.
- Drag and drop. Drop external files/folders to upload (reuse Takeout's
FileListtarget), drag the whole selection, highlight drop targets, breadcrumbs accept drops, Undo toast on move. - Move to… a folder-picker dialog (tree/list of Drive folders) instead of a free-text path.
- Rename pre-selects the stem, not the extension; drop the redundant "Rename “x”." body.
- Search. Scope toggle (This folder / Everywhere), "Showing first 200 results" note, Location
column, correct cached badges (search hits hardcode
cached: false). - Upload feedback. Toast "Uploading 3 items" with "View" → Sync › Transfers.
Photos (was Gallery)
- Header: title "Photos",
InlineViewSwitcherTimeline | Albums in the header, Select toggle,+ Upload(fix: useadw::ButtonContent, today.icon_name()overwrites.label()atphotos.rs:426-432), ⋮ menu: Import from Google Photos…, Refresh, Rebuild library. - Filters: a single filter dropdown/
ToggleGroup(All, Photos, Videos, RAW, Favorites) instead of two segmented rows where "Photos" appears twice. Counts follow the active filter. - Month scrubber on the right edge that jumps (not filters); the month dropdown becomes a real "Filter by date" if kept.
- Infinite scroll with prefetch (no "Load more" pill; page size ≥ 200; also trigger when the first page does not fill the viewport).
- Tiles:
ContentFit::Cover(crop) in justified rows, known aspect ratio from metadata to prevent reflow, placeholder as a tinted card of the final size, subtle hover (no 1.06 scale with heavy shadow on a 2 px gap grid), last row justified up to a max stretch. - Context menu on tiles: Open, Favorite, Add to album, Download/Save copy, Show in My files, Move to Trash. Selection: Shift range, Ctrl+A, bulk Favorite, Add to album, Download.
- Empty states per filter ("No favorites yet", "No videos in June 2024") instead of the global "No photos yet" with upload buttons.
- Import: runs show a banner on Photos ("Importing from Google Photos — 1,204 of 5,000 · View") and Back returns to where the user came from.
- Albums: Create, rename, delete, remove from album (not trash). Fix state bugs: filter row hidden on the album grid, subtitle count, scroll position kept when switching back.
- Viewer:
adw::Window(or full-window overlay), controls auto-hide after 2 s idle, zoom/pan (pinch, Ctrl+scroll, double-click), info panel beside the image instead of over the top bar, pages beyond the loaded 60, counter against the real total, videos play inline withgtk::Video, trash with the same confirm/undo rule as the grid, real filename in "Save a copy", success toast, "Open with…" app chooser, favorite tooltip reflects state, readable dates.
Shared with me / Shared by me
- Shared with me: header title shows the drill-down path, with back; invitations as a banner at the top ("2 invitations · Review") opening a list, Reject confirmed; bookmarks become a separate view or section via a switcher. Rows: Open, Save to My files, Available offline where supported.
- Shared by me: rows activatable (open), suffix "Manage" becomes a menu with Copy link, Manage access…, Stop sharing….
- Share dialog: inline validation, loading spinner, confirm "Remove access" and "Delete link", human role names, link expiry.
Computers
- Show the real device name for "This computer" with rename inline.
- Other computers: one suffix menu (Rename, Use this identity…, Remove…). Remove uses a destructive confirm that requires typing the device name since it deletes backups.
- "Restore folders" becomes a per-device action ("Restore to this computer…") with an inline-empty state when nothing is restorable, and moves the "Add folder" responsibility fully to Sync.
- Terminology: "computer" everywhere in UI copy.
Trash
- Same skeleton as other list pages (clamped
boxed-list), sort by deleted date, multi-select with Restore / Delete permanently, "Empty Trash…" moved into a header ⋮ menu or kept as a flat button with a destructive confirm, proper plurals, Retry on error.
Login
- Human error messages mapped from error kinds, spinner in the button, "Create account" and "Forgot password" links, recovery-code hint in 2FA, cancelling CAPTCHA returns to the form with a message.
7. Tray
- Symbolic icon variants:
pdfs-synced,pdfs-syncing,pdfs-paused,pdfs-offline,pdfs-attention; tooltip with the one-line state;NeedsAttentionstatus for issues. - Menu: status line · "N issues — View" (opens Sync › Issues) · Pause sync ▸ (1 h, until tomorrow, until resumed) / Resume · Open Proton Drive · Open folder · "Sign in…" when logged out (no "Connect") · Stop Proton Drive (confirm) · "Hide tray icon" instead of "Quit" (daemon keeps running and the label must not suggest otherwise).
8. Glossary (one term, one icon)
| Concept | UI term | Icon |
|---|---|---|
| Root of Drive | My files | folder-symbolic |
| Background service | Proton Drive (running / stopped) — never "daemon" or "mount service" in UI | — |
| Cloud-only file | Online only | none / cloud-outline |
| Pinned file | Available offline | pin/download-filled custom icon, not stars |
| Upload | Upload files / Upload folder | document-upload-style arrow up, custom pair |
| Folder sync mode | Synced / On-demand | — |
| Stop syncing a folder | Stop syncing… | media-playback-stop-symbolic in a menu, not trash |
| Remove a computer | Remove computer… | destructive, in menu |
| Conflict copy | Conflict | dialog-warning-symbolic + warning colour |
| Failure | Failed / Needs attention | dialog-error-symbolic + error colour |
| Pause | Pause sync / Resume sync | media-playback-pause-symbolic / -start- |
| Photos section | Photos | image-x-generic-symbolic |
| Sharing | Shared by me / Shared with me | distinct icons (emblem-shared / folder-publicshare) |
Spelling: US English throughout ("Favorite", "color"), matching the pdfs favorite command and
Proton's own apps. Title Case for
dialog headings and buttons in dialogs, Sentence case for menu items and rows (HIG).
9. Bugs found during the audit (quick wins)
- Photos Upload pill loses its label (
photos.rs:426-432); same for viewer "Show on map" (photo_viewer.rs:594-598). - Takeout Back always goes to Settings, sidebar highlights Settings when opened from Photos
(
main.rs:779,takeout.rs:131-136). - Thumbnail-build completion message computed then hidden (
browser.rs:1632-1667). - Search results show cached files as cloud-only (
browser.rs:2344). - Details "Available offline" switch shown for folders (
details.rs:259). - Grid and list keep separate selections; details pane can describe a hidden-view item.
- F2 silently does nothing with multiple items selected.
- Filter changes on the Albums grid swap to the timeline while the Albums toggle stays active
(
photos.rs:958-997). - Subtitle "60 photos" next to "All 1,416" (loaded count vs total,
photos.rs:2007-2015); viewer counter "12 of 60". - Viewer info panel covers Close/Trash/Save (
photo_viewer.rs:588vs648). - Locations mode switch and removal reload Computers, not Locations; a rejected mode switch stays
flipped until the next tick (
devices.rs:612,devices.rs:809,locations.rs:374). - Going on-demand deletes local copies without confirmation.
- Retry on Not-connected silently restarts the systemd unit; Locations' Retry does not.
- Toasts from Versions/Share dialogs land behind the modal.
- "Emptied trash —" dangling dash, "item(s)", "archive(s)", raw revision id as version title, raw lowercase link role.
- Bulk context menu shows a separator directly under the header when a folder is selected.
- Sign out, Reject invitation, Remove access, Delete link, tray Disconnect: no confirmation.
- Stale doc comments (
devices.rs:46-48,locations.rs:329-331,albums.rs:4,browser.rs:1155).
10. Phased roadmap
Each phase is shippable on its own. Estimates are rough, for one developer.
| Phase | Content | Crates | Size |
|---|---|---|---|
| 0 Quick wins | Section 9 bugs, glossary wording, icon swaps (upload, offline, stop syncing), confirmations, plurals | gui, tray | S (2–3 days) |
| 1 Shell | libadwaita feature bump, per-page header bars, sidebar restructure + footer (status, quota, account), Preferences dialog, About debug info, theme/colour cleanup, CSS to gresource, shortcuts dialog | gui | M (1 week) |
| 2 Sync hub + pause | B-1 global pause, B-2 list/retry pending ops, Sync page (status card, Issues, Transfers, Folders, History), tray states and pause menu, GUI uses failing_* / parked_* |
core, fuse, gui, tray, cli | L (1.5–2 weeks) |
| 3 Conflicts | B-3 list + resolve, Issues rows with a compare/resolve dialog (both versions side by side: size, date, device, preview for images/text), CLI pdfs conflicts |
core, fuse, gui, cli | M–L |
| 4 File browser | Header layout, New menu, sort/persisted view, PopoverMenu context menus, details side pane, DnD upload, folder picker for Move, search scope | gui | L |
| 5 Photos | Rename, header switcher, filters, infinite scroll + scrubber, tile layout, context menu, album management, viewer rewrite (auto-hide, zoom, inline video) | gui (+ core for album mutations) | L |
| 6 Rest | Shared pages, Computers, Trash, Login, per-folder pause, bandwidth limits, i18n wrapping | all | M |
Phase 2 is where the user-reported gaps (pause, resume, stuck items) get closed; phase 3 closes the conflict overview. Phases 0–1 fix most of the "unpolished" impression cheaply and should come first because later phases build on the new header/page skeleton.
Verification per phase
cargo fmt --all -- --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace.- Backend phases: DB migration tests in
crates/pdfs-core/src/db/tests.rs(paused state, conflict table if added), drain tests proving no op is claimed while paused, resolve-conflict tests proving the losing side stays recoverable (trash or version history). - GUI phases: before/after screenshots in
images/(light and dark), keyboard-only walkthrough of each page, narrow-window check (sidebar collapse breakpoint).