Files
proton-drive-linux/docs/UI_UX_PLAN.md
T
nvrl bd4f797d1f feat(gui): polish pass over wording, icons and responsiveness
The Sync page gives each folder one card with its state, a Synced /
Online only switch and a menu for the rest; switching either way asks
first and the download prompt shows the free space on the disk.

Enabling, disabling and restarting the Proton Drive service ran
systemctl on GTK's main thread and froze the window. They now run in
the background, and every Retry button goes through one shared restart
helper.

Ten icons are bundled under resources/icons for glyphs no stock theme
has or that read ambiguously (sync, share, people, link, menu, map and
so on). A new test fails when the app or search prompt asks for a
symbolic icon that is neither bundled nor on a list checked against
Adwaita 50, libadwaita and GTK, or when a bundled icon is missing from
the resource manifest. The tray's sync icon, which Adwaita 50 no longer
ships, is now view-refresh.

Wording follows one glossary, recorded in docs/TRANSLATING.md: "Proton
Drive service" instead of "mount service" or "daemon", "Online only"
instead of "On-demand", "Proton Drive folder" instead of "mountpoint".
Dialog headings and buttons use title case and page titles match the
sidebar. Dates go through one formatter (app/dates.rs) and sizes
through i18n::human_bytes, and the cache label is translated.

The status tick stops while the window is hidden or minimized and runs
every fifth tick while it is unfocused, refreshing as soon as it gains
focus. "Sync complete" is announced only for large batches and only
when the window isn't focused. Smaller fixes: Empty Trash hides when
there is nothing to empty, copy buttons confirm with a check mark, the
sidebar status spins until the first report, and Photos moves Import
and Find Duplicates into a menu beside a labelled Select toggle.

All catalogs are updated and complete.
2026-09-28 13:26:59 +02:00

355 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 reads `online`, `pending_uploads` and `pending_changes` from `Status` and ignores
`failing_ops`, `failing_error`, `parked_uploads` and `staged_*`.
- **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
`AdwHeaderBar` whose 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_PURPLE` overrides 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
1. **Follow GNOME HIG and current libadwaita.** The system has libadwaita 1.9; the crate is pinned to
feature `v1_5`. Raise it to at least `v1_7` to get `adw::Spinner`, `ToggleGroup`,
`InlineViewSwitcher`, `ButtonRow`, `WrapBox`, `BottomSheet`, and (1.8) `ShortcutsDialog`.
2. **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.
3. **One term, one icon, one place** per concept (glossary in section 8).
4. **Secondary and destructive actions go into menus.** Rows get at most one visible suffix action
plus a `view-more-symbolic` menu. Destructive actions are red inside the menu and in the confirm
dialog, not as filled buttons in headers.
5. **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.
6. **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_info` gets 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:
1. **Issues** (default when non-empty): conflicts, failing uploads (`last_error`, attempts, next
retry), folders in `error`/`conflict` state, parked uploads older than N minutes. Each row has a
primary fix action (Resolve…, Retry now) and a menu (Show in folder, Discard change…).
2. **Transfers**: in-flight uploads and downloads with progress, speed, cancel; background jobs
(thumbnail build, Takeout import, re-date) with their own cancel.
3. **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 now` and a menu (Open,
Change location…, Pause this folder, Stop syncing…). "Add folder…" as a `ButtonRow` at the end.
The primary mount appears first and is not removable.
4. **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 key
`sync_paused_until` or a DB setting) so a pause survives restarts; `Response::Status` gains
`paused` and `paused_until`.
- Drain: `run_pending_drain` (`pdfs-fuse/src/drain.rs:265`) already releases claimed ops while
offline; gate on `paused` at the same point and reuse the wake path.
- Sync engine: add `SyncMsg::Pause/Resume` or gate `reconcile_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.paused` column, skipped in `reconcile_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 as
`ErrorKind`), `next_attempt_at`, parked flag, staged bytes.
- `Request::RetryOp { id }` (set `next_attempt_at = now`, wake drain) and `RetryAllFailing`.
- `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 (see `docs/RECOVERY.md`).
- `TransferItem` gains a stable id and `Request::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 with `sweep.rs` `conflict_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 `conflict` table written by `keep_as_conflict_copy` / `preserve_conflict_copy` so 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 `Clamp` rule (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::Spinner` in 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:232` to `resources/style.css` in the gresource
(`style-dark.css` if needed), so it can be edited and linted.
- **Shortcuts.** Replace the hand-rolled cheatsheet with `adw::ShortcutsDialog` (or
`GtkShortcutsWindow` before 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::PopoverMenu` from `gio::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-download`
style filled icon. Use named colours. Folders can be made available offline too, or the toggle is
hidden for them (fix `details.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 `FileList` target), 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", `InlineViewSwitcher` Timeline | Albums in the header, Select toggle,
`+ Upload` (fix: use `adw::ButtonContent`, today `.icon_name()` overwrites `.label()` at
`photos.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 with
`gtk::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; `NeedsAttention` status 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 / Online only | — |
| Where My files appears on disk | Proton Drive folder — never "mountpoint" in UI | — |
| 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)
1. Photos Upload pill loses its label (`photos.rs:426-432`); same for viewer "Show on map"
(`photo_viewer.rs:594-598`).
2. Takeout Back always goes to Settings, sidebar highlights Settings when opened from Photos
(`main.rs:779`, `takeout.rs:131-136`).
3. Thumbnail-build completion message computed then hidden (`browser.rs:1632-1667`).
4. Search results show cached files as cloud-only (`browser.rs:2344`).
5. Details "Available offline" switch shown for folders (`details.rs:259`).
6. Grid and list keep separate selections; details pane can describe a hidden-view item.
7. F2 silently does nothing with multiple items selected.
8. Filter changes on the Albums grid swap to the timeline while the Albums toggle stays active
(`photos.rs:958-997`).
9. Subtitle "60 photos" next to "All 1,416" (loaded count vs total, `photos.rs:2007-2015`); viewer
counter "12 of 60".
10. Viewer info panel covers Close/Trash/Save (`photo_viewer.rs:588` vs `648`).
11. 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`).
12. Going on-demand deletes local copies without confirmation.
13. Retry on Not-connected silently restarts the systemd unit; Locations' Retry does not.
14. Toasts from Versions/Share dialogs land behind the modal.
15. "Emptied trash —" dangling dash, "item(s)", "archive(s)", raw revision id as version title,
raw lowercase link role.
16. Bulk context menu shows a separator directly under the header when a folder is selected.
17. Sign out, Reject invitation, Remove access, Delete link, tray Disconnect: no confirmation.
18. 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).