Remove archived plan documents from tracking

Moved to .trash/ for reference - no longer needed in source tree.
This commit is contained in:
lashman
2026-03-01 12:44:35 +02:00
parent 7e55d5796f
commit da9568df61
14 changed files with 0 additions and 10338 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
# 20 Improvements Plan
## Batch 1: Low-risk code quality (no behavior change)
1. Wrap all hardcoded English strings in i18n()
2. Replace OnceCell.get().expect() with safe getters
3. Extract common async-toast-refresh helper
4. Log silently swallowed errors
## Batch 2: Performance
6. Async database initialization with loading screen
7. Batch CSS provider registration for letter-circle icons
8. Lazy-load detail view tabs
18. Rate-limit background analysis spawns
## Batch 3: UX
9. Progress indicator during background analysis
10. Multi-file drop and file picker support
12. Sort options in library view
15. Keyboard shortcut Ctrl+O for Add app
17. Validate scan directories exist before scanning
## Batch 4: Robustness
5. Add database migration tests
13. Confirmation before closing during active analysis
16. Graceful handling of corrupt/locked database
## Batch 5: Accessibility & Features
11. Remember detail view active tab
14. Announce analysis completion to screen readers
19. Custom launch arguments
20. Export/import app library

View File

@@ -1,183 +0,0 @@
# AppImage Comprehensive Metadata Extraction and Display
## Goal
Extract ALL available metadata from AppImage files - from the oldest Type 1 format to the newest Type 2 with AppStream XML - and display it comprehensively in the overview tab of the detail view.
## Background: All AppImage Metadata Sources
### 1. ELF Binary Header (Type 1 and Type 2)
- Magic bytes at offset 8: `AI\x01` (Type 1) or `AI\x02` (Type 2)
- Architecture from `e_machine` at offset 18
### 2. Type 1: ISO 9660 Volume Descriptor
- Update info at fixed offset 33651 (512 bytes)
### 3. Type 2: ELF Sections
- `.upd_info` (1024 bytes) - update transport (zsync, GitHub releases, etc.)
- `.sha256_sig` (1024 bytes) - GPG digital signature
- `.sig_key` (8192 bytes) - public key for signature verification
### 4. Desktop Entry File (.desktop)
Standard freedesktop fields:
- `Name`, `GenericName`, `Comment`, `Icon`, `Exec`, `Categories`
- `Keywords`, `MimeType`, `StartupWMClass`, `Terminal`
- `Actions` with `[Desktop Action <name>]` sections
- AppImage-specific: `X-AppImage-Version`, `X-AppImage-Name`, `X-AppImage-Arch`
### 5. AppStream / AppData XML (richest source)
Located at `usr/share/metainfo/*.xml` or `usr/share/appdata/*.xml`:
- `<id>` - reverse-DNS identifier
- `<name>` - localized app name
- `<summary>` - one-line description
- `<description>` - full rich-text description
- `<developer>` - developer/organization
- `<project_license>` - SPDX license
- `<project_group>` - umbrella project (GNOME, KDE, etc.)
- `<url type="...">` - homepage, bugtracker, donation, help, vcs-browser, contribute
- `<keywords>` - search terms
- `<categories>` - menu categories
- `<screenshots>` - screenshot URLs with captions
- `<releases>` - version history with dates and changelogs
- `<content_rating type="oars-1.1">` - age rating
- `<provides>` - MIME types, binaries, D-Bus interfaces
- `<branding>` - theme colors
### 6. Icons (already handled)
- `.DirIcon`, root-level PNG/SVG, `usr/share/icons/` hierarchy
### 7. Digital Signatures (Type 2)
- GPG signature in `.sha256_sig` ELF section
- Verifiable with embedded public key
## Approach
Parse everything at analysis time and store in the database (Approach A). This matches the existing architecture where `run_background_analysis()` populates the DB and the UI reads from `AppImageRecord`.
## Database Schema (Migration v9)
16 new columns on `appimages`:
| Column | Type | Default | Description |
|--------|------|---------|-------------|
| `appstream_id` | TEXT | NULL | Reverse-DNS ID (e.g. `org.kde.krita`) |
| `appstream_description` | TEXT | NULL | Rich description (paragraphs joined with newlines) |
| `generic_name` | TEXT | NULL | Generic descriptor ("Web Browser") |
| `license` | TEXT | NULL | SPDX license expression |
| `homepage_url` | TEXT | NULL | Project website |
| `bugtracker_url` | TEXT | NULL | Bug reporting URL |
| `donation_url` | TEXT | NULL | Donation page |
| `help_url` | TEXT | NULL | Documentation URL |
| `vcs_url` | TEXT | NULL | Source code URL |
| `keywords` | TEXT | NULL | Comma-separated keywords |
| `mime_types` | TEXT | NULL | Semicolon-separated MIME types |
| `content_rating` | TEXT | NULL | Summarized OARS rating |
| `project_group` | TEXT | NULL | Umbrella project |
| `release_history` | TEXT | NULL | JSON array of recent releases |
| `desktop_actions` | TEXT | NULL | JSON array of desktop actions |
| `has_signature` | INTEGER | 0 | Whether AppImage has GPG signature |
## New Module: AppStream XML Parser
**File:** `src/core/appstream.rs`
Uses `quick-xml` crate (pure Rust, lightweight).
Key types:
```
AppStreamMetadata
id: Option<String>
name: Option<String>
summary: Option<String>
description: Option<String>
developer: Option<String>
project_license: Option<String>
project_group: Option<String>
urls: HashMap<String, String>
keywords: Vec<String>
categories: Vec<String>
content_rating_summary: Option<String>
releases: Vec<ReleaseInfo>
mime_types: Vec<String>
ReleaseInfo
version: String
date: Option<String>
description: Option<String>
```
Parser function: `parse_appstream_file(path: &Path) -> Option<AppStreamMetadata>`
- Walks XML events, extracts all fields
- Strips HTML from `<description>` (joins `<p>` with newlines, `<li>` with bullets)
- Caps releases at 10 most recent
- Summarizes OARS content rating into a single label
## Extended Desktop Entry Parsing
Update `DesktopEntryFields` in `inspector.rs`:
- Add `generic_name`, `keywords`, `mime_types`, `terminal`, `actions`, `x_appimage_name`
- Parse `[Desktop Action <name>]` sections for action names and exec commands
## Inspector + Analysis Pipeline
1. `AppImageMetadata` struct gains all new fields
2. `inspect_appimage()` looks for AppStream XML after extraction, parses it, merges into metadata (AppStream takes priority for overlapping fields)
3. `run_background_analysis()` stores new fields via `db.update_appstream_metadata()`
4. Signature detection: read ELF `.sha256_sig` section, check if non-empty
## Overview Tab UI Layout
Groups in order (each only shown when data exists):
### About (new)
- App ID, Generic name, Developer, License, Project group
### Description (new)
- Full multi-paragraph AppStream description
### Links (new)
- Homepage, Bug tracker, Source code, Documentation, Donate
- Each row clickable via `gtk::UriLauncher`
### Updates (existing, unchanged)
### Release History (new)
- Recent releases with version, date, description
- Uses `adw::ExpanderRow` for entries with descriptions
### Usage (existing, unchanged)
### Capabilities (new)
- Keywords, MIME types, Content rating, Desktop actions
### File Information (existing, extended)
- Add "Signature: Signed / Not signed" row
## Dependencies
Add to `Cargo.toml`:
```toml
quick-xml = "0.37"
```
## Files Modified
| File | Changes |
|------|---------|
| `Cargo.toml` | Add `quick-xml` |
| `src/core/mod.rs` | Add `pub mod appstream;` |
| `src/core/appstream.rs` | **New** - AppStream XML parser |
| `src/core/database.rs` | Migration v9, new columns, `update_appstream_metadata()` |
| `src/core/inspector.rs` | Extended desktop parsing, AppStream integration, signature detection |
| `src/core/analysis.rs` | Store new metadata fields |
| `src/ui/detail_view.rs` | Redesigned overview tab with all new groups |
## Verification
1. `cargo build` compiles without errors
2. AppImages with AppStream XML show full metadata (developer, license, URLs, releases)
3. AppImages without AppStream XML still show desktop entry fields (graceful degradation)
4. URL links open in browser
5. Release history is scrollable/expandable
6. Empty groups are hidden
7. Re-scanning an app picks up newly available metadata

File diff suppressed because it is too large Load Diff

View File

@@ -1,107 +0,0 @@
# Audit Fixes Design
## Goal
Fix all 29 findings from the full codebase audit, organized by severity tier with build verification between tiers.
## Approach
Fix by severity tier (Critical -> High -> Medium -> Low). Run `cargo build` after each tier to catch regressions early.
## Tier 1: Critical (5 items)
### #1 - security.rs: Fix unsquashfs argument order
`detect_version_from_binary` passes `appimage_path` after the extract pattern. unsquashfs expects the archive before patterns. Move `appimage_path` before the file pattern, remove the `-e` flag.
### #2 - integrator.rs: Quote Exec path in .desktop files
`Exec={exec} %U` breaks for paths with spaces. Change to `Exec="{exec}" %U`.
### #3 - duplicates.rs: Fix compare_versions total order
`compare_versions("1.0", "v1.0")` returns `Less` both ways (violates antisymmetry). Use `clean_version()` on both inputs for the equality check.
### #4 - inspector.rs: Chunk-based signature detection
`detect_signature` reads entire files (1.5GB+) into memory. Replace with `BufReader` reading 64KB chunks, scanning each for the signature bytes.
### #5 - updater.rs: Read only first 12 bytes in verify_appimage
Replace `fs::read(path)` with `File::open` + `read_exact` for just the ELF/AI magic bytes.
## Tier 2: High (6 items)
### #6 - database.rs: Handle NULL severity in CVE summaries
`get_cve_summary` and `get_all_cve_summary` fail on NULL severity. Change to `Option<String>`, default `None` to `"MEDIUM"`.
### #7 - inspector.rs: Fix deadlock in extract_metadata_files
Piped stderr + `.status()` can deadlock. Change to `Stdio::null()` since we don't use stderr.
### #8 - updater.rs: Fix glob_match edge case
After matching the last part with `ends_with`, reduce the search text before checking middle parts.
### #9 - backup.rs: Prevent archive filename collisions
Use relative paths from home directory instead of bare filenames, so two dirs with the same leaf name don't collide.
### #10 - launcher.rs: Async crash detection
Remove the 1.5s blocking sleep from `execute_appimage`. Return `Started` immediately with the `Child`. Callers (already async) handle crash detection by polling the child after a delay.
### #11 - launcher.rs: Drop stderr pipe on success
After returning `Started`, either drop `child.stderr` or use `Stdio::null()` for stderr to prevent pipe buffer deadlock on long-running apps.
## Tier 3: Medium (9 items)
### #12 - window.rs: Gate scan on auto-scan-on-startup
Wrap `self.trigger_scan()` in `if self.settings().boolean("auto-scan-on-startup")`.
### #13 - window.rs: Fix window size persistence
Change `self.default_size()` to `(self.width(), self.height())`.
### #14 - widgets.rs: Fix announce() for any container
Change `announce()` to not require a `gtk::Box` - use a more generic approach or fix callers to pass the correct widget type.
### #15 - detail_view.rs: Claim gesture in lightbox
Add `gesture.set_state(gtk::EventSequenceState::Claimed)` in the picture click handler.
### #16 - cli.rs: Use serde_json for JSON output
Replace hand-crafted `format!` JSON with `serde_json::json!()`.
### #17 - style.css: Remove dead @media blocks
Delete `@media (prefers-color-scheme: dark)` and `@media (prefers-contrast: more)` blocks. libadwaita named colors already adapt.
### #18 - gschema.xml + detail_view.rs: Wire detail-tab persistence
Save active tab on switch, restore on open.
### #19 - metainfo.xml: Remove invalid categories
Delete `<categories>` block (already in .desktop file, invalid in metainfo per AppStream spec).
### #20 - fuse.rs: Byte-level search
Replace `String::from_utf8_lossy().to_lowercase()` with direct byte-level case-insensitive search using `windows()`.
## Tier 4: Low (9 items)
### #21 - wayland.rs: Tighten env var detection
Remove `WAYLAND_DISPLAY` from fallback heuristic. Keep only `GDK_BACKEND` and `QT_QPA_PLATFORM`.
### #22 - inspector.rs: Add ELF magic validation
Check `\x7fELF` magic and endianness byte before parsing `e_machine`.
### #23 - updater.rs: Add timeout to extract_update_info_runtime
Add 5-second timeout to prevent indefinite blocking.
### #24 - launcher.rs: Handle quoted args
Use a shell-like tokenizer that respects double-quoted strings in `parse_launch_args`.
### #25 - (merged with #20)
### #26 - window.rs: Stop watcher timer on window destroy
Return `glib::ControlFlow::Break` when `window_weak.upgrade()` returns `None`.
### #27 - gschema.xml: Add choices/range constraints
Add `<choices>` to enumerated string keys, `<range>` to backup-retention-days.
### #28 - style.css: Remove unused CSS classes
Delete `.quick-action-pill`, `.badge-row`, `.detail-view-switcher`, base `.letter-icon`.
### #29 - style.css/app_card.rs: Fix status-ok/status-attention
Define CSS rules for these classes or remove the class additions from code.
## Verification
After each tier: `cargo build` with zero errors and zero warnings. After all tiers: manual app launch test.

File diff suppressed because it is too large Load Diff

View File

@@ -1,127 +0,0 @@
# Beginner-Friendly Copy Overhaul - Design
**Goal:** Rewrite all technical jargon in the detail view into plain language that newcomers can understand, while keeping technical details available in tooltips.
**Approach:** Friendly titles/subtitles for everyone, technical details on hover. Terminal commands stay copyable but with softer framing ("Install with one command" instead of showing the raw command as a subtitle).
**Scope:** detail_view.rs (all 4 tabs), security_report.rs, fuse_user_explanation(), wayland_user_explanation()
---
## Overview Tab
### About section
- "SPDX license identifier for this application" tooltip -> "The license that governs how this app can be used and shared."
- "Project group" title -> "Project"
- "Bug tracker" link label -> "Report a problem"
### File Information section
- "AppImage format" title -> "Package format"
- "Type 1 (ISO 9660) - older format, still widely supported" -> "Type 1 - older format, still widely supported"
- "Type 2 (SquashFS) - modern format, most common today" -> "Type 2 - modern, compressed format"
- Tooltip rewrite: "AppImages come in two formats. Type 1 is the older format. Type 2 is the current standard - it uses compression for smaller files and faster loading."
- "Executable" title -> "Ready to run"
- "Yes - this file has execute permission" -> "Yes - this file is ready to launch"
- "No - execute permission is missing. It will be set automatically when launched." -> "No - will be fixed automatically when launched"
- Tooltip: "Whether the file has the permissions needed to run. If not, Driftwood sets this up automatically the first time you launch it."
- "This AppImage contains a GPG signature" subtitle -> "Signed by the developer"
- Signature tooltip: "This app was signed by its developer, which helps verify it hasn't been tampered with since it was published."
- "Last scanned" -> "Last checked"
### Capabilities section
- Section title: "Capabilities" -> "Features"
- "Desktop actions" -> "Quick actions"
- Tooltip: "Additional actions available from the right-click menu when this app is added to your app menu."
- Content rating tooltip: "An age rating for the app's content, similar to game ratings."
### Updates section
- Tooltip about zsync/delta updates -> "Driftwood can check for newer versions of this app automatically. The developer has included information about where updates are published."
---
## System Tab
### Desktop Integration section
- Section title: "Desktop Integration" -> "App Menu"
- Description -> "Add this app to your launcher so you can find it like any other installed app."
- Switch subtitle: "Creates a .desktop entry and installs the app icon" -> "Creates a shortcut and installs the app icon"
- Switch tooltip: "This makes the app appear in your Activities menu and app launcher, just like a regular installed app. It creates a shortcut file and copies the app's icon to your system."
- "Desktop file" row title -> "Shortcut file"
### Compatibility section
- Description -> "How well this app works with your system. Most issues can be fixed with a quick install."
- "Wayland display" -> "Display compatibility"
- Wayland tooltip: "Wayland is the modern display system on Linux. Apps built for the older system (X11) still work, but native Wayland apps look sharper, especially on high-resolution screens."
- "Analyze toolkit" -> "Detect app framework"
- Subtitle: "Inspect bundled libraries to detect which UI toolkit this app uses" -> "Check which technology this app is built with"
- Tooltip: "Apps are built with different frameworks (like GTK, Qt, or Electron). Knowing the framework helps predict how well the app works with your display system."
- Post-analysis subtitle: "Detected: {toolkit} ({count} libraries scanned)" -> "Built with: {toolkit}"
- Error subtitle: "Analysis failed - the AppImage may not be mountable" -> "Analysis failed - could not read the app's contents"
- "Last observed protocol" -> "Last display mode"
- Tooltip: "How the app connected to your display the last time it was launched."
- "FUSE (filesystem)" -> "App mounting"
- FUSE tooltip: "FUSE lets apps like AppImages run directly without unpacking first. Without it, apps still work but take a little longer to start."
- "Launch method" -> "Startup method"
- Launch tooltip: "AppImages can start two ways: mounting (fast, instant startup) or unpacking to a temporary folder first (slower, but works everywhere). The method is chosen automatically based on your system."
### Wayland explanations (wayland_user_explanation)
- Native: "Fully compatible - the best experience on your system."
- XWayland: "Works through a compatibility layer. May appear slightly blurry on high-resolution screens."
- Possible: "Might work well. Try launching it to find out."
- X11Only: "Built for an older display system. It will run automatically, but you may notice minor visual quirks."
- Unknown: "Not yet determined. Launch the app or use 'Detect app framework' to check."
### FUSE explanations (fuse_user_explanation)
- FullyFunctional: "Everything is set up - apps start instantly."
- Fuse3Only: "A small system component is missing. Most apps will still work, but some may need it. Copy the install command to fix this."
- NoFusermount: "A system component is missing, so apps will take a little longer to start. They'll still work fine."
- NoDevFuse: "Your system doesn't support instant app mounting. Apps will unpack before starting, which takes a bit longer."
- MissingLibfuse2: "A small system component is needed for fast startup. Copy the install command to fix this."
### Sandboxing section
- Section title: "Sandboxing" -> "App Isolation"
- Description -> "Restrict what this app can access on your system for extra security."
- Switch title: "Firejail sandbox" -> "Isolate this app"
- Switch tooltip: "Sandboxing restricts what an app can access - files, network, devices, etc. Even if an app has a security issue, it can't freely access your personal data."
- Subtitle when available: "Isolate this app using Firejail. Current mode: {mode}" -> "Currently: {mode}"
- Subtitle when missing: "Firejail is not installed. Use the row below to copy the install command." -> "Not available yet. Install with one command using the button below."
- Install row: "Install Firejail" / "sudo apt install firejail" -> "Install app isolation" / "Install with one command"
---
## Security Tab
### Vulnerability Scanning section
- Section title: "Vulnerability Scanning" -> "Security Check"
- Description -> "Check this app for known security issues."
- "Bundled libraries" -> "Included components"
- Subtitle: "{N} libraries detected inside this AppImage" -> "{N} components found inside this app"
- Tooltip: "Apps bundle their own copies of system components (libraries). These can sometimes contain known security issues if they're outdated."
- Clean subtitle: "No known security issues found in the bundled libraries." -> "No known security issues found."
- Scan row: "Run security scan" / "Check bundled libraries against known CVE databases" -> "Run security check" / "Check for known security issues in this app"
- Scan tooltip: "This checks the components inside this app against a public database of known security issues to see if any are outdated or vulnerable."
- Error: "Scan failed - the AppImage may not be mountable" -> "Check failed - could not read the app's contents"
### Integrity section
- "SHA256 checksum" -> "File fingerprint"
- Tooltip: "A unique code (SHA256 checksum) generated from the file's contents. If the file changes in any way, this code changes too. You can compare it against the developer's published fingerprint to verify nothing was altered."
---
## Storage Tab
- Group description: "Config, cache, and data directories this app may have created." -> "Settings, cache, and data this app may have saved to your system."
- Search subtitle: "Search for config, cache, and data directories" -> "Search for files this app has saved"
- Empty result: "No associated data directories found" -> "No saved data found"
- "Path" -> "File location"
---
## Security Report page (security_report.rs)
- "Run a security scan to check bundled libraries for known vulnerabilities." -> "Run a security check to look for known issues in your apps."
- "No known vulnerabilities found in any bundled libraries." -> "No known security issues found in any of your apps."
- "Overall security status across all AppImages" -> "Overall security status across all your apps"
- Tooltip: "Common Vulnerabilities and Exposures found in bundled libraries" -> "Known security issues found in the components bundled inside your apps."
- Per-app description: "{N} CVE (vulnerability) records found" -> "{N} known security issues found"
- Individual CVE/library expander titles: keep as-is (technical detail layer)

View File

@@ -1,679 +0,0 @@
# Driftwood Feature Roadmap - Design Document
**Goal:** Add 26 features to make Driftwood the definitive AppImage manager for Linux newcomers coming from Windows, covering the full lifecycle from discovery to clean uninstall.
**Architecture:** All features build on the existing GTK4/libadwaita/Rust stack. Every system modification (desktop files, icons, MIME associations, autostart entries) is tracked in a central `system_modifications` table and fully reversed on app removal. Features are ordered from simplest to most complex.
**Tech Stack:** Rust, gtk4-rs, libadwaita-rs, rusqlite, gio, notify crate, pkexec/polkit for privileged operations, XDG specs (Desktop Entry, Autostart, MIME Applications), AppImage feed.json catalog.
---
## Core Architecture: System Modification Tracking
Every feature that touches system files MUST use this tracking system. No exceptions.
### New Database Table
```sql
CREATE TABLE IF NOT EXISTS system_modifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
appimage_id INTEGER REFERENCES appimages(id) ON DELETE CASCADE,
mod_type TEXT NOT NULL,
file_path TEXT NOT NULL,
previous_value TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_system_mods_appimage
ON system_modifications(appimage_id);
```
**mod_type values:** `desktop_file`, `icon`, `autostart`, `mime_default`, `mime_entry`, `system_desktop`, `system_icon`, `system_binary`
### Core Functions (src/core/integrator.rs)
```rust
pub fn register_modification(db: &Database, appimage_id: i64, mod_type: &str, file_path: &str, previous_value: Option<&str>) -> Result<()>
pub fn undo_modification(db: &Database, mod_id: i64) -> Result<()>
pub fn undo_all_modifications(db: &Database, appimage_id: i64) -> Result<()>
```
### Uninstall Flow
1. Query `system_modifications` for the appimage_id
2. For each modification (in reverse order):
- `desktop_file` / `autostart` / `icon`: delete the file
- `mime_default`: restore previous_value via `xdg-mime default {previous_value} {mime_type}`
- `system_*`: delete via `pkexec rm`
3. Run `update-desktop-database ~/.local/share/applications/`
4. Run `gtk-update-icon-cache ~/.local/share/icons/hicolor/` (if icons were removed)
5. Optionally delete app data paths (with user confirmation)
6. Delete the AppImage file
7. Delete DB record (CASCADE handles system_modifications rows)
### CLI Purge Command
`driftwood purge` - removes ALL system modifications for ALL managed apps, for when Driftwood itself is being removed from the system.
---
## Feature Specifications (Sorted: Easiest to Most Complex)
---
### F1. Auto-Set Executable Permission on Existing Files
**Complexity:** Trivial (15 min)
**Problem:** Files already in scan directories skip chmod during drag-and-drop. Non-executable AppImages found during scan aren't fixed.
**Files:** `src/ui/drop_dialog.rs`, `src/core/discovery.rs`
**Changes:**
- `drop_dialog.rs`: In the `in_scan_dir` branch of `register_dropped_files()`, add permission check and fix:
```rust
if !std::os::unix::fs::PermissionsExt::mode(&metadata.permissions()) & 0o111 != 0 {
std::fs::set_permissions(&final_path, std::fs::Permissions::from_mode(0o755))?;
}
```
- `discovery.rs`: After scan finds a non-executable AppImage, auto-fix permissions and log it
---
### F2. Move-or-Copy Option for Drag-and-Drop (+ Keep in Place)
**Complexity:** Easy (30 min)
**Problem:** Always copies file, wasting disk. No option to keep in place.
**Files:** `src/ui/drop_dialog.rs`
**Changes:**
- Replace 3-button dialog with 4 response options:
- `"cancel"` - Cancel
- `"keep-in-place"` - Keep in place (register at current location, set executable, no copy)
- `"copy-only"` - Copy to Applications (current "Just add" behavior)
- `"copy-and-integrate"` - Copy & add to menu (current "Add to app menu", suggested/default)
- `register_dropped_files()` receives a new `copy_mode` enum: `KeepInPlace`, `CopyOnly`, `CopyAndIntegrate`
- `KeepInPlace`: set executable on original path, register in DB at original location, optionally integrate
- Dialog text updated: "Where should this AppImage live?"
---
### F3. Version Rollback
**Complexity:** Easy (1 hr)
**Problem:** No way to go back if an update breaks things.
**Files:** `src/core/updater.rs`, `src/ui/detail_view.rs`, `src/core/database.rs`
**Changes:**
- New DB column: `previous_version_path TEXT` on appimages
- In `updater.rs` update flow: before replacing, rename old to `{path}.prev` and store path in `previous_version_path`
- Detail view system tab: "Rollback to previous version" button (visible only when `previous_version_path` is set)
- Rollback: swap current and .prev files, update DB fields, re-run analysis
- Cleanup: delete .prev file when user explicitly confirms or after configurable retention
---
### F4. Source Tracking
**Complexity:** Easy (1 hr)
**Problem:** Users can't tell where an AppImage was downloaded from.
**Files:** `src/ui/detail_view.rs`, `src/core/database.rs`, `src/ui/drop_dialog.rs`
**Changes:**
- New DB column: `source_url TEXT` on appimages
- Auto-detect from `update_info` field (already stored): parse GitHub/GitLab URLs
- Display "Source: github.com/obsidianmd/obsidian" on detail view overview tab
- Drop dialog: optional "Where did you download this?" text field (pre-filled if detected)
- Catalog installs (F26): automatically set source_url from catalog entry
---
### F5. Launch Statistics Dashboard
**Complexity:** Easy (1-2 hrs)
**Problem:** Launch data is tracked but never shown.
**Files:** `src/ui/dashboard.rs`, `src/core/database.rs`
**Changes:**
- New DB queries:
- `get_top_launched(limit: i32) -> Vec<(String, u64)>` - most launched apps
- `get_launch_count_since(since: &str) -> u64` - total launches since date
- `get_recent_launches(limit: i32) -> Vec<(String, String)>` - recent launch events with timestamps
- Dashboard: new "Activity" section showing:
- Top 5 most-launched apps with launch counts
- "X launches this week" summary stat
- "Last launched: AppName, 2 hours ago"
---
### F6. Batch Operations
**Complexity:** Medium (2-3 hrs)
**Problem:** Can't select multiple AppImages for bulk actions.
**Files:** `src/ui/library_view.rs`, `src/ui/app_card.rs`, `src/window.rs`
**Changes:**
- Library view header: "Select" toggle button
- When active: app cards show checkboxes, bottom action bar slides up
- Action bar buttons: "Integrate" / "Remove Integration" / "Delete" / "Export"
- Each action confirms with count: "Integrate 5 AppImages?"
- Delete uses the full uninstall flow (F14/system_modifications cleanup)
- Selection state stored in a `HashSet<i64>` of record IDs on the LibraryView
---
### F7. Automatic Desktop Integration on Scan
**Complexity:** Easy (30 min)
**Problem:** Users forget to integrate after scanning.
**Files:** `src/window.rs`, `src/ui/preferences.rs`
**Changes:**
- GSettings key `auto-integrate` already exists (default false)
- Wire it up: after scan completes in `window.rs`, if setting is true, iterate newly discovered apps and call `integrator::integrate()` for each
- Register all created files via `register_modification()`
- Preferences: add toggle in Behavior page (already may be there, verify)
---
### F8. Autostart Manager
**Complexity:** Medium (2 hrs)
**Problem:** No way to set AppImages to start at login.
**Spec:** XDG Autostart - `.desktop` file in `~/.config/autostart/`
**Files:** `src/core/integrator.rs`, `src/ui/detail_view.rs`, `src/core/database.rs`
**Changes:**
- New DB column: `autostart INTEGER NOT NULL DEFAULT 0`
- New functions in `integrator.rs`:
```rust
pub fn enable_autostart(db: &Database, record: &AppImageRecord) -> Result<PathBuf>
pub fn disable_autostart(db: &Database, record_id: i64) -> Result<()>
```
- `enable_autostart`: creates `~/.config/autostart/driftwood-{id}.desktop` with:
```ini
[Desktop Entry]
Type=Application
Name={app_name}
Exec={appimage_path}
Icon={icon_path}
X-GNOME-Autostart-enabled=true
X-Driftwood-AppImage-ID={id}
```
- Registers modification with mod_type `autostart`
- Detail view system tab: "Start at login" switch row
- `disable_autostart`: deletes the file, removes from system_modifications
- Uninstall flow: handled by `undo_all_modifications()`
---
### F9. System Notification Integration
**Complexity:** Medium (2 hrs)
**Problem:** Toasts vanish, important events get missed.
**Files:** `src/core/notification.rs`, `src/window.rs`
**Changes:**
- Use `gio::Application::send_notification(id, notification)` for:
- App crash on launch (high priority)
- Updates available after background check (normal priority)
- Security vulnerabilities found (high priority if critical/high severity)
- Keep toasts for minor confirmations (copied, integrated, etc.)
- Notification click opens Driftwood and navigates to relevant view
- `notification.rs` already has the logic for CVE notifications - extend to use gio::Notification instead of/in addition to libnotify
---
### F10. Storage Dashboard per App
**Complexity:** Medium (2 hrs)
**Problem:** Users don't know total disk usage per app.
**Files:** `src/ui/detail_view.rs`, `src/core/footprint.rs`, `src/core/database.rs`
**Changes:**
- `footprint.rs`: new function `get_total_footprint(db: &Database, record_id: i64) -> FootprintSummary`
```rust
pub struct FootprintSummary {
pub binary_size: u64,
pub config_size: u64,
pub cache_size: u64,
pub data_size: u64,
pub state_size: u64,
pub total_size: u64,
}
```
- Detail view storage tab: visual breakdown with labeled size bars
- Each category shows path and size: "Config (~/.config/MyApp) - 12 MB"
- "Clean cache" button per category (deletes cache paths only)
- Library list view: optional "Total size" column
---
### F11. Background Update Checks
**Complexity:** Medium (2-3 hrs)
**Problem:** No automatic update awareness.
**Files:** `src/window.rs`, `src/ui/dashboard.rs`, `src/ui/library_view.rs`, GSettings schema
**Changes:**
- New GSettings key: `update-check-interval-hours` type i, default 24, range 1-168
- New GSettings key: `last-update-check` type s, default ''
- On startup: if `auto-check-updates` is true and enough time has passed, spawn background check
- Background check: iterate all apps with update_info, call `check_appimage_for_update()` per app
- Results: update `update_available` column in DB, send gio::Notification if updates found
- Dashboard: show "X updates available" with timestamp "Last checked: 2h ago"
- Library view: badge on apps with updates
- Preferences: toggle + interval dropdown
---
### F12. One-Click Update All
**Complexity:** Medium (3-4 hrs)
**Problem:** Can only update one app at a time.
**Files:** `src/ui/dashboard.rs`, new `src/ui/batch_update_dialog.rs`, `src/core/updater.rs`
**Changes:**
- Dashboard: "Update All (N)" button when updates are available
- Opens batch update dialog showing list of apps to update with checkboxes
- Progress: per-app progress bar, overall progress bar
- Each update: download new version, save old as .prev (F3 rollback), replace, re-analyze
- On completion: summary toast "Updated 5 apps successfully, 1 failed"
- Failed updates: show error per app, keep old version
- Cancel: stops remaining updates, already-updated apps stay updated
---
### F13. Full Uninstall with Data Cleanup
**Complexity:** Medium (3 hrs)
**Problem:** Deleting AppImage leaves config/cache/data behind.
**Files:** `src/ui/detail_view.rs`, new confirmation dialog logic, `src/core/footprint.rs`
**Changes:**
- Delete button in detail view triggers new uninstall flow:
1. Show dialog with FootprintSummary (from F10):
- "Delete MyApp?" with breakdown:
- [x] AppImage file (245 MB)
- [x] Configuration (~/.config/MyApp) - 12 MB
- [x] Cache (~/.cache/MyApp) - 89 MB
- [x] Data (~/.local/share/MyApp) - 1.2 GB
- Total: 1.5 GB will be freed
2. All checked by default, user can uncheck to keep data
3. On confirm:
- Call `undo_all_modifications()` (removes .desktop, icons, autostart, MIME defaults)
- Delete selected data paths
- Delete the AppImage file
- Remove DB record
- Batch delete (F6) uses same flow with aggregated summary
---
### F14. Theme/Icon Preview in Drop Dialog
**Complexity:** Medium (2 hrs)
**Problem:** Users don't see what the app looks like before integrating.
**Files:** `src/ui/drop_dialog.rs`, `src/core/inspector.rs`
**Changes:**
- New function in inspector: `extract_icon_fast(path: &Path) -> Option<PathBuf>`
- Runs `unsquashfs -l` to find icon file, then extracts just that one file
- Much faster than full inspection
- Drop dialog: after registration, show preview card:
- App icon (from fast extraction)
- App name (from filename initially, updated after full analysis)
- "This is how it will appear in your app menu"
- If icon extraction fails, show default AppImage icon
---
### F15. FUSE Fix Wizard
**Complexity:** Significant (4-5 hrs)
**Problem:** #1 support issue - FUSE not installed, AppImages won't run.
**Files:** `src/core/fuse.rs`, new `src/ui/fuse_wizard.rs`, `src/ui/dashboard.rs`, new `data/app.driftwood.Driftwood.policy`
**Changes:**
- New distro detection in `fuse.rs`:
```rust
pub struct DistroInfo {
pub id: String, // "ubuntu", "fedora", "arch", etc.
pub id_like: Vec<String>,
pub version_id: String,
}
pub fn detect_distro() -> Option<DistroInfo> // parses /etc/os-release
pub fn get_fuse_install_command(distro: &DistroInfo) -> Option<String>
```
- Install commands by distro family:
- Debian/Ubuntu: `apt install -y libfuse2t64` (24.04+) or `apt install -y libfuse2` (older)
- Fedora/RHEL: `dnf install -y fuse-libs`
- Arch/Manjaro: `pacman -S --noconfirm fuse2`
- openSUSE: `zypper install -y libfuse2`
- Polkit policy file (`data/app.driftwood.Driftwood.policy`):
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="app.driftwood.Driftwood.install-fuse">
<description>Install FUSE library for AppImage support</description>
<message>Authentication is required to install the FUSE library</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>
```
- Wizard dialog (NavigationPage-style multi-step):
1. "FUSE is not installed" - explanation of what FUSE is and why it's needed
2. "We detected {distro}. Install with: `{command}`" - shows exact command
3. "Install now" button runs `pkexec sh -c "{command}"` and shows output
4. Re-check: calls `detect_system_fuse()` and shows success/failure
- Dashboard: yellow banner when FUSE is missing with "Fix now" button
---
### F16. File Type Association Manager
**Complexity:** Significant (4 hrs)
**Problem:** AppImages don't register MIME types, so files don't open with them.
**Spec:** XDG MIME Applications - `~/.local/share/applications/mimeapps.list`
**Files:** `src/core/integrator.rs`, `src/ui/detail_view.rs`
**Changes:**
- `inspector.rs` already extracts desktop entry content including MimeType= field
- New function: `parse_mime_types(desktop_entry: &str) -> Vec<String>` - extracts MimeType= values
- When integrating: include `MimeType=` in generated .desktop file
- Detail view overview tab: "Supported file types" section listing MIME types
- Per-type toggle: "Set as default for .png files"
- Setting default:
1. Query current default: `xdg-mime query default {mime_type}`
2. Store in `system_modifications` with `previous_value` = current default
3. Set new: `xdg-mime default driftwood-{id}.desktop {mime_type}`
- Removing: restore previous default from `previous_value`
- Uninstall: handled by `undo_all_modifications()`
---
### F17. Taskbar/Panel Icon Fix (StartupWMClass)
**Complexity:** Significant (3 hrs)
**Problem:** Running AppImages show wrong/generic icon in taskbar.
**Files:** `src/core/integrator.rs`, `src/core/wayland.rs`, `src/ui/detail_view.rs`
**Changes:**
- During inspection: extract `StartupWMClass=` from embedded .desktop entry
- Store in new DB column: `startup_wm_class TEXT`
- When generating .desktop file for integration, include StartupWMClass if available
- After launch + Wayland analysis: read `/proc/{pid}/environ` for `GDK_BACKEND`, check window list via `xdotool` or `xprop` for WM_CLASS
- If WM_CLASS doesn't match StartupWMClass: log warning, offer to patch .desktop file
- Detail view system tab: show detected WM_CLASS, allow manual override
---
### F18. Download Verification Helper
**Complexity:** Significant (4 hrs)
**Problem:** No way to verify AppImage authenticity.
**Files:** New `src/core/verification.rs`, `src/ui/detail_view.rs`, `src/ui/drop_dialog.rs`
**Changes:**
- AppImage signature spec: GPG signature embedded at offset stored in ELF section
- New module `verification.rs`:
```rust
pub enum VerificationStatus {
SignedValid { signer: String },
SignedInvalid { reason: String },
Unsigned,
ChecksumMatch,
ChecksumMismatch,
NotChecked,
}
pub fn check_embedded_signature(path: &Path) -> VerificationStatus
pub fn verify_sha256(path: &Path, expected: &str) -> VerificationStatus
```
- `check_embedded_signature`: runs `unsquashfs -l` to check for `.sha256` or signature files, or uses the AppImage --appimage-signature flag
- Drop dialog: after adding, show verification badge
- Detail view: "Verification: Signed by ..." or "Unsigned - verify manually"
- Manual verification: paste SHA256 hash, compare with computed hash
- New DB column: `verification_status TEXT` on appimages
---
### F19. First-Run Permission Summary
**Complexity:** Medium (3 hrs)
**Problem:** Users don't know what an AppImage can access before running it.
**Files:** New `src/ui/permission_dialog.rs`, `src/ui/detail_view.rs`
**Changes:**
- Before first launch of any AppImage (check `launch_events` count = 0):
1. Show permission dialog:
- "MyApp will run with full access to your files and system"
- Icon + app name
- If `has_firejail()`: offer sandbox options
- "Run without restrictions" (default for now)
- "Run in Firejail sandbox (recommended)"
- "Don't show this again" checkbox (stored per-app in DB)
2. If user chooses Firejail: set `sandbox_mode = 'firejail'` in DB
- New DB column: `first_run_prompted INTEGER NOT NULL DEFAULT 0`
- Skip dialog if `first_run_prompted = 1`
- Preferences: global toggle to disable first-run prompts
---
### F20. Default App Selector
**Complexity:** Medium (3 hrs)
**Problem:** Can't set AppImage as default browser, mail client, etc.
**Files:** `src/ui/detail_view.rs`, `src/core/integrator.rs`
**Changes:**
- Detect app capabilities from categories:
- `WebBrowser` -> can be default web browser
- `Email` -> can be default email client
- `FileManager` -> can be default file manager
- `TerminalEmulator` -> can be default terminal
- Detail view overview tab: "Set as default" section (only shown for applicable categories)
- Setting defaults:
- Browser: `xdg-settings set default-web-browser driftwood-{id}.desktop`
- Email: `xdg-mime default driftwood-{id}.desktop x-scheme-handler/mailto`
- File manager: `xdg-mime default driftwood-{id}.desktop inode/directory`
- Before setting: query current default, store in `system_modifications` with `previous_value`
- Uninstall: restore previous defaults via `undo_all_modifications()`
- Requirements: app must be integrated first (needs .desktop file)
---
### F21. Multi-User / System-Wide Install
**Complexity:** Significant (4 hrs)
**Problem:** Can't install for all users on a shared computer.
**Files:** `src/core/integrator.rs`, `src/ui/detail_view.rs`, polkit policy
**Changes:**
- Reuses polkit policy from F15 (add new action `app.driftwood.Driftwood.system-install`)
- "Install system-wide" option in detail view (requires app to be integrated first)
- Flow:
1. `pkexec cp {appimage} /opt/driftwood-apps/{filename}`
2. `pkexec chmod 755 /opt/driftwood-apps/{filename}`
3. Generate system .desktop in `/usr/share/applications/driftwood-{id}.desktop`
4. `pkexec cp {desktop_file} /usr/share/applications/`
5. Copy icon to `/usr/share/icons/hicolor/` via pkexec
6. `pkexec update-desktop-database /usr/share/applications/`
- Register all paths as `system_desktop`, `system_icon`, `system_binary` in system_modifications
- Uninstall system-wide: `pkexec rm` for each tracked path
- DB flag: `system_wide INTEGER NOT NULL DEFAULT 0`
---
### F22. CLI Enhancements
**Complexity:** Medium (3 hrs)
**Problem:** Missing install-from-URL and update-all commands.
**Files:** `src/cli.rs`
**New commands:**
- `driftwood install <url>` - Download from URL, validate, move to ~/Applications, set executable, register, optionally integrate
- `driftwood update --all` - Check all apps, download and apply available updates
- `driftwood autostart <path> --enable/--disable` - Toggle autostart for an AppImage
- `driftwood purge` - Remove ALL system modifications for all managed apps (for Driftwood removal)
- `driftwood verify <path>` - Check embedded signature or compare SHA256
---
### F23. Portable Mode / USB Drive Support
**Complexity:** Major (6-8 hrs)
**Problem:** Can't manage AppImages on removable media.
**Files:** New `src/core/portable.rs`, `src/ui/library_view.rs`, `src/window.rs`, `src/core/database.rs`
**Changes:**
- New GSettings key: `watch-removable-media` type b, default false
- `portable.rs`:
```rust
pub fn detect_removable_mounts() -> Vec<MountInfo>
pub fn scan_mount_for_appimages(mount: &MountInfo) -> Vec<DiscoveredAppImage>
pub fn is_path_on_removable(path: &Path) -> bool
```
- Detection: parse `/proc/mounts` for removable media (type vfat, exfat, ntfs on /media/ or /run/media/)
- Alternative: use `gio::VolumeMonitor` to watch for mount/unmount events
- DB changes:
- New column: `is_portable INTEGER DEFAULT 0`
- New column: `mount_point TEXT`
- Library view: "Portable" filter/section showing apps on removable media
- When drive unmounts: grey out those apps, mark as unavailable
- When drive mounts: re-scan and refresh
- Portable apps skip the "copy to ~/Applications" step - they stay on the drive
- Integration: .desktop files use the full path (may break when unmounted - show warning)
---
### F24. "Similar to..." Recommendations
**Complexity:** Medium (3 hrs, depends on F26)
**Problem:** No app discovery within the tool.
**Files:** `src/ui/detail_view.rs`, `src/core/database.rs`
**Changes:**
- Requires catalog data from F26 (catalog_apps table populated)
- New DB query: `find_similar_apps(categories: &[String], exclude_id: i64, limit: i32) -> Vec<CatalogApp>`
- Matches on shared categories, weighted by specificity
- Detail view overview tab: "You might also like" section at the bottom
- Shows up to 5 catalog apps with icon, name, one-line description
- Click opens catalog detail page (F26)
- Falls back to nothing if catalog is empty
---
### F25. AppImageHub In-App Catalog Browser
**Complexity:** Major (8-12 hrs)
**Problem:** No way to discover and install new AppImages from within the app.
**Data source:** `https://appimage.github.io/feed.json` (~1500 apps)
**Files:** New `src/ui/catalog_view.rs`, new `src/ui/catalog_detail.rs`, `src/core/database.rs`, `src/window.rs`
**Architecture:**
#### Data Layer
- Fetch `feed.json` and parse into `catalog_apps` table (already exists in DB schema)
- Store in `catalog_sources` as source record
- Fields mapped from feed.json:
- `name` -> `name`
- `description` -> `description`
- `categories` -> `categories` (joined with ;)
- `authors[0].name` -> `author`
- `license` -> `license`
- `links` (type=GitHub) -> `repository_url`
- `links` (type=Download) -> `download_url`
- `icons[0]` -> `icon_url` (prefix with `https://appimage.github.io/database/`)
- `screenshots` -> `screenshots` (JSON array)
- Refresh: on first open, then daily if enabled
- New GSettings key: `catalog-last-refreshed` type s, default ''
#### Catalog Browse View (NavigationPage)
- Header: search bar + category filter chips
- Categories from feed: AudioVideo, Development, Education, Game, Graphics, Network, Office, Science, System, Utility
- Grid of catalog app cards (reuse app_card pattern):
- Icon (fetched from URL, cached locally)
- App name
- Short description (first line)
- Category badge
- Pagination: load 50 at a time, "Load more" button
- Search: filters by name and description (client-side, data is local)
#### Catalog App Detail (NavigationPage pushed on click)
- App icon (large)
- Name, author, license
- Full description
- Screenshots carousel (if available)
- "Install" button (suggested style)
- Source link (opens GitHub/website in browser)
#### Install Flow
1. User clicks "Install"
2. Resolve download URL:
- If `download_url` points to GitHub releases page: fetch latest release via GitHub API (`https://api.github.com/repos/{owner}/{repo}/releases/latest`), find .AppImage asset
- If direct link: use as-is
3. Download with progress bar (reqwest or gio file download)
4. Validate: check AppImage magic bytes
5. Move to ~/Applications, set executable
6. Register in DB with `source_url` set
7. Run full analysis pipeline
8. Optionally integrate (based on `auto-integrate` setting)
9. Navigate to the app's detail view in library
#### Navigation
- Main sidebar/navigation: add "Catalog" entry alongside Dashboard and Library
- Or: floating action button on library view "Browse catalog"
---
## New GSettings Keys Summary
| Key | Type | Default | Range/Choices | Feature |
|-----|------|---------|---------------|---------|
| `update-check-interval-hours` | i | 24 | 1-168 | F11 |
| `last-update-check` | s | '' | - | F11 |
| `catalog-last-refreshed` | s | '' | - | F25 |
| `watch-removable-media` | b | false | - | F23 |
| `show-first-run-prompt` | b | true | - | F19 |
## New Database Columns Summary
| Table | Column | Type | Default | Feature |
|-------|--------|------|---------|---------|
| appimages | previous_version_path | TEXT | NULL | F3 |
| appimages | source_url | TEXT | NULL | F4 |
| appimages | autostart | INTEGER | 0 | F8 |
| appimages | startup_wm_class | TEXT | NULL | F17 |
| appimages | verification_status | TEXT | NULL | F18 |
| appimages | first_run_prompted | INTEGER | 0 | F19 |
| appimages | system_wide | INTEGER | 0 | F21 |
| appimages | is_portable | INTEGER | 0 | F23 |
| appimages | mount_point | TEXT | NULL | F23 |
## New Files Summary
| File | Feature | Purpose |
|------|---------|---------|
| `src/ui/fuse_wizard.rs` | F15 | FUSE installation wizard dialog |
| `src/ui/batch_update_dialog.rs` | F12 | Batch update progress dialog |
| `src/ui/permission_dialog.rs` | F19 | First-run permission summary |
| `src/ui/catalog_view.rs` | F25 | Catalog browse/search page |
| `src/ui/catalog_detail.rs` | F25 | Catalog app detail page |
| `src/core/verification.rs` | F18 | Signature and checksum verification |
| `src/core/portable.rs` | F23 | Removable media detection and management |
| `data/app.driftwood.Driftwood.policy` | F15, F21 | Polkit policy for privileged operations |
## Implementation Order
The features are numbered F1-F25 in order of complexity. Implement sequentially - some later features depend on earlier ones:
- F25 (Catalog) depends on nothing but is largest
- F24 (Similar to) depends on F25
- F12 (Update All) depends on F11 (Background checks)
- F13 (Full Uninstall) depends on system_modifications table (core architecture)
- F20 (Default App) depends on F16 (MIME associations)
**Critical path:** Core architecture (system_modifications) -> F1-F7 quick wins -> F8-F14 medium features -> F15-F22 significant features -> F23-F25 major features

File diff suppressed because it is too large Load Diff

View File

@@ -1,289 +0,0 @@
# Driftwood UI/UX Overhaul Design
## Context
Driftwood is a GTK4/libadwaita AppImage manager. The current UI is functional but visually plain - cards look like basic boxes, the list view resembles a settings page, and the detail view is a wall of ActionRows with no hierarchy. This design overhauls all three views plus adds a right-click context menu to make Driftwood feel like a first-class GNOME app.
## Design Principles
- Use libadwaita's built-in style classes wherever possible instead of custom CSS
- Follow GNOME HIG spacing (6px grid, 12px padding baseline, 14px grid gaps)
- Only show the most important information at each level (card -> list -> detail = progressive disclosure)
- Actions belong in context menus and detail headers, not crammed into cards
---
## 1. Card View (Grid)
### Current State
- 180px wide cards with 64px icons
- All badges shown (Wayland, FUSE, Update, Integration)
- Custom `.app-card` CSS duplicating libadwaita's `.card` behavior
- FlowBox allows up to 6 columns (cards get too small)
### New Design
```
+----------------------------------+
| |
| [72px icon] |
| (icon-dropshadow) |
| |
| App Name |
| (.title-3) |
| |
| 1.2.3 - 45 MiB |
| (.caption .dimmed .numeric) |
| |
| [single most important badge] |
+----------------------------------+
200px wide, 14px internal padding
```
### Changes
- **Card width: 200px** (from 180px) for better breathing room
- **Icon size: 72px** (from 64px) with `.icon-dropshadow` class
- **App name: `.title-3`** (from `.heading`) for more visual weight
- **Version + size on one combined line** using `.caption .dimmed .numeric`
- **Single badge only** - show the most important status using priority: Update > FUSE issue > Wayland issue. Integration is already shown via the icon corner emblem
- **Replace custom `.app-card` CSS with libadwaita `.card` + `.activatable`** - native hover, active, dark mode, and contrast states for free
- **FlowBox max 4 columns** (from 6) so cards stay readable
- **14px row and column spacing** (matching GNOME Software)
- **Right-click context menu** on each card (see Section 5)
### Files Modified
- `src/ui/app_card.rs` - card construction, badge logic, CSS classes
- `data/resources/style.css` - remove `.app-card` rules, add new sizing
- `src/ui/library_view.rs` - FlowBox max_children_per_line, context menu wiring
---
## 2. List View
### Current State
- 40px icons, standard ActionRow
- Subtitle mashes version + size + description into one hyphenated string
- All badges shown in a suffix box
- Standard ListBox (no `.rich-list`)
### New Design
```
+--[48px icon]--+--Title--------------------------+--[badge]--[>]--+
| (rounded | App Name | |
| 8px clip) | Description or path (.dimmed) | [Update] |
| | 1.2.3 - 45 MiB (.caption) | |
+---------------+----------------------------------+---------------+
```
### Changes
- **Icon size: 48px** (from 40px) with `border-radius: 8px` and `overflow: hidden` for rounded clipping
- **Subtitle structured as two lines:**
- Line 1: Description snippet or file path (dimmed)
- Line 2: Version + size (caption, dimmed, numeric)
- Use `subtitle-lines(2)` on ActionRow to allow the two-line subtitle
- **`.rich-list` style class** on the ListBox for taller rows
- **Single badge suffix** - same priority logic as cards
- **Remove integration badge from suffix** - redundant with icon emblem
- **Right-click context menu** - same menu as card view
- **Navigate arrow stays** as rightmost suffix
### Files Modified
- `src/ui/library_view.rs` - list row construction, ListBox class, context menu
- `data/resources/style.css` - icon rounding class
---
## 3. Detail View (Tabbed)
### Current State
- 64px icon in banner
- Single scrolling page with 3 PreferencesGroups
- 20+ rows all visible at once
- No visual hierarchy between sections
### New Design
```
+--[< back]------- App Name --------[Update][Launch]--+
| |
| +--[96px icon]---+ App Name (.title-1) |
| | (icon- | 1.2.3 - x86_64 (.dimmed) |
| | dropshadow) | Short description (.body) |
| +----------------+ [Integrated] [Native Wayland] |
| |
| +---[Overview]--[System]--[Security]--[Storage]--+ |
| | | |
| | (active tab content below) | |
| | | |
| + + |
+------------------------------------------------------+
```
### Hero Banner
- **96px icon** (from 64px) with `.icon-dropshadow`
- **App name in `.title-1`** (stays as-is)
- **Subtle gradient background:** `linear-gradient(to bottom, alpha(@accent_bg_color, 0.08), transparent)` behind the banner area
- **Key badges inline** (stays as-is)
### Tab System
- **`adw::ViewStack`** contains four pages
- **`adw::ViewSwitcher`** with `.inline` style, placed between the banner and tab content (not in the header bar)
- Header bar stays clean with just back button, app name title, Update button, Launch button
### Tab 1: Overview (default)
Shows the most commonly needed information at a glance.
- Update method (update type or "no automatic updates")
- Update status (available/up-to-date) with version info
- Last checked date
- Total launches + last launched
- AppImage type + executable status
- File path with copy + open folder buttons
- First seen / last scanned dates
- Notes (if any)
### Tab 2: System
All system integration and compatibility information.
- Desktop integration switch
- Desktop file path (if integrated)
- Wayland compatibility row + badge
- Analyze toolkit button
- Runtime display protocol (if available)
- FUSE status + badge
- Launch method
- Firejail sandbox switch + install hint
### Tab 3: Security
Vulnerability scanning and integrity.
- Bundled libraries count
- Vulnerability summary with severity badge
- Scan button (with busy state)
- SHA256 checksum with copy button
### Tab 4: Storage
Disk usage and data discovery.
- AppImage file size
- Total disk footprint (if discovered)
- Discover data paths button
- Individual discovered paths with type icons, confidence badges, sizes
### Files Modified
- `src/ui/detail_view.rs` - major restructure: banner upgrade, ViewStack/ViewSwitcher, redistribute rows across 4 tab pages
- `data/resources/style.css` - banner gradient, ViewSwitcher positioning
---
## 4. CSS & Visual Polish
### Remove
- `.app-card` and `.app-card:hover` and `.app-card:active` rules (replaced by libadwaita `.card`)
- Dark mode `.app-card` overrides (handled by `.card` automatically)
- High contrast `.app-card` overrides (handled by `.card` automatically)
### Add
```css
/* Rounded icon clipping for list view */
.icon-rounded {
border-radius: 8px;
overflow: hidden;
}
/* Detail banner gradient wash */
.detail-banner {
padding: 18px 0;
background-image: linear-gradient(
to bottom,
alpha(@accent_bg_color, 0.08),
transparent
);
border-radius: 12px;
margin-bottom: 6px;
}
```
### Keep (unchanged)
- All status badge styling
- Integration emblem styling
- Letter-circle fallback icons
- All WCAG AAA styles (focus indicators, high contrast, reduced motion, target sizes)
- Compatibility warning banner
- Quick action pill styling
### Style Classes Used (libadwaita built-in)
- `.card` + `.activatable` on FlowBoxChild card boxes
- `.icon-dropshadow` on icons 48px+
- `.rich-list` on list view ListBox
- `.numeric` on version/size labels
- `.title-3` on card app names
- `.inline` on the detail ViewSwitcher
- `.property` on key-value ActionRows where subtitle is the main content (path, SHA256)
---
## 5. Right-Click Context Menu
### Design
A `GtkPopoverMenu` built from a `gio::Menu` model, attached to each FlowBoxChild (card) and ListBox row. Triggered by secondary click (button 3) or long-press on touch.
```
+---------------------------+
| Launch |
+---------------------------+
| Check for Updates |
| Scan for Vulnerabilities |
+---------------------------+
| Integrate / Remove |
| Open Containing Folder |
+---------------------------+
| Copy Path |
+---------------------------+
```
### Menu Items
| Label | Action | Notes |
|-------|--------|-------|
| Launch | `app.launch-appimage(id)` | Launches the AppImage |
| Check for Updates | `app.check-update(id)` | Triggers update check, shows toast with result |
| Scan for Vulnerabilities | `app.scan-security(id)` | Triggers security scan, shows toast |
| Integrate / Remove Integration | `app.toggle-integration(id)` | Label changes based on current state |
| Open Containing Folder | `app.open-folder(id)` | Opens file manager to the directory |
| Copy Path | `app.copy-path(id)` | Copies full path, shows toast |
### Implementation Approach
- Define actions at the window level with the record ID as parameter
- Build a `gio::Menu` with sections (separators between groups)
- Attach `GtkPopoverMenu` to each card/row
- Wire `GtkGestureClick` for button 3 (right-click) and `GtkGestureLongPress` for touch
- Update the "Integrate/Remove" label dynamically based on `record.integrated`
### Files Modified
- `src/window.rs` - define parameterized actions
- `src/ui/library_view.rs` - create menu model, attach to cards and rows
- `src/ui/app_card.rs` - gesture attachment on FlowBoxChild
---
## 6. Files Modified Summary
| File | Changes |
|------|---------|
| `src/ui/app_card.rs` | 72px icon, .title-3 name, single badge, .card class, gesture for context menu |
| `src/ui/library_view.rs` | FlowBox max 4 cols, .rich-list on ListBox, list row restructure, context menu creation and attachment |
| `src/ui/detail_view.rs` | 96px icon, ViewStack/ViewSwitcher tabs, redistribute rows into 4 tab pages, banner gradient |
| `src/window.rs` | Parameterized actions for context menu (launch, update, scan, integrate, open-folder, copy-path) |
| `data/resources/style.css` | Remove .app-card rules, add .icon-rounded, update .detail-banner with gradient, keep all WCAG styles |
| `src/ui/widgets.rs` | Minor - ensure icon helper supports .icon-dropshadow |
## Verification
After implementation:
1. `cargo build` - zero errors, zero warnings
2. `cargo test` - all 128+ tests pass
3. Visual verification of all three views in light + dark mode
4. Right-click context menu works on cards and list rows
5. Detail view tabs switch correctly, content is correctly distributed
6. Keyboard navigation: Tab through cards, Enter to open, Escape to go back
7. All WCAG AAA compliance preserved

File diff suppressed because it is too large Load Diff

View File

@@ -1,246 +0,0 @@
# WCAG 2.2 AAA Compliance Design for Driftwood
## Context
Driftwood is a GTK4/libadwaita AppImage manager. The app already uses semantic libadwaita widgets (ActionRow, PreferencesGroup, SwitchRow, NavigationView) and has partial accessibility support: ~11 accessible labels, status badges with text+color, keyboard shortcuts, and reduced-motion CSS. However, it falls short of full WCAG 2.2 AAA compliance.
This design covers every change needed to achieve AAA compliance across all four WCAG principles: Perceivable, Operable, Understandable, and Robust.
## Approach: Hybrid Helpers + Direct Properties
Create centralized accessibility helper functions in `widgets.rs` for patterns that repeat (labeled buttons, described badges, live announcements), then add direct accessible properties for unique cases in each UI file. This keeps the code DRY while ensuring nothing is missed.
## Scope
**In scope:** All UI code in `src/ui/`, `src/window.rs`, and `data/resources/style.css`.
**Out of scope:** CLI (`src/cli.rs`), core backend modules, build system.
---
## 1. Perceivable (WCAG 1.x)
### 1.1 Non-text Content (1.1.1 - Level A)
**Current state:** 8 icon-only buttons, some with accessible labels, some without.
**Changes needed:**
- Add `update_property(&[AccessibleProperty::Label(...)])` to every icon-only button:
- `library_view.rs`: menu button, search button, grid button, list button
- `duplicate_dialog.rs`: delete button per row
- `widgets.rs`: copy button
- `preferences.rs`: remove directory button
- Add accessible descriptions to integration emblem overlay in `app_card.rs`
- Add accessible labels to all `gtk::Image` icons used as prefixes in rows (check icons in integration_dialog, category icons in cleanup_wizard)
### 1.2 Time-based Media - N/A (no audio/video)
### 1.3 Adaptable
**1.3.1 Info and Relationships (A):**
- Add `AccessibleRole::Group` to badge boxes in detail_view, library_view list rows, and app_card
- Add `AccessibleProperty::Label` to ListBox containers in library_view (list view), preferences (directory list, cleanup wizard lists)
- Add `AccessibleRelation::LabelledBy` connecting PreferencesGroup titles to their child row containers where applicable
**1.3.6 Identify Purpose (AAA):**
- Set `AccessibleRole::Banner` on the detail view banner
- Set `AccessibleRole::Navigation` on the NavigationView wrapper
- Set `AccessibleRole::Search` on the search bar
- Set `AccessibleRole::Status` on status badges
- Set `AccessibleRole::Main` on the main content area
### 1.4 Distinguishable
**1.4.6 Enhanced Contrast (AAA - 7:1 ratio):**
- Add a `prefers-contrast: more` media query in `style.css` for high-contrast mode
- In high-contrast mode: solid opaque borders on app cards (no alpha), bolder status badge colors, thicker focus rings (3px)
- Verify that libadwaita theme variables meet 7:1 in both light and dark mode (they do - libadwaita's named colors are designed for WCAG AA, and the `prefers-contrast: more` variant handles AAA)
**1.4.8 Visual Presentation (AAA):**
- Text is already relative-sized (em units)
- libadwaita handles line height and spacing according to system preferences
- No changes needed beyond ensuring we do not override user-configured text spacing
**1.4.11 Non-text Contrast (AA):**
- Increase focus ring width to 3px (currently 2px) in high-contrast mode
- Ensure status badge borders are visible at 3:1 ratio against their background
---
## 2. Operable (WCAG 2.x)
### 2.1 Keyboard Accessible
**2.1.3 Keyboard No Exception (AAA):**
- Verify all dashboard actionable rows are keyboard-activatable (they use `activatable: true` + `action_name`)
- Verify cleanup wizard checkboxes are keyboard-toggleable via `activatable_widget`
- Add keyboard shortcut Ctrl+Q for quit (already exists in GNOME via app.quit)
- Ensure delete button in duplicate_dialog can be reached via Tab
### 2.2 Enough Time
**2.2.4 Interruptions (AAA):**
- Toast notifications already have timeouts and can be dismissed
- No other auto-updating content exists
### 2.3 Seizures and Physical Reactions
**2.3.3 Animation from Interactions (AAA):**
- Expand `prefers-reduced-motion` CSS to cover ALL transitions:
- `navigation view` slide transitions (currently only covers `stack`)
- `adw::Dialog` presentation animations
- `flowboxchild` hover/active transitions
- Search bar reveal animation
- Spinner animations (already respected by adw::Spinner)
### 2.4 Navigable
**2.4.7 Focus Visible (AA) + 2.4.13 Focus Appearance (AAA):**
- Add focus-visible styles for ALL focusable elements, not just app cards:
- `button:focus-visible` - 3px solid outline with accent color
- `row:focus-visible` - highlight with outline
- `switch:focus-visible`
- `checkbutton:focus-visible`
- `searchentry:focus-visible`
- `comborow:focus-visible`
- `spinrow:focus-visible`
- `expander:focus-visible`
- Focus indicator must be at least 2px thick and have 3:1 contrast (AAA requires this)
**2.4.8 Location (AAA):**
- Update the window title dynamically to reflect current page: "Driftwood - Dashboard", "Driftwood - Security Report", "Driftwood - {App Name}"
- This is already partially done via NavigationPage titles; ensure the window's actual title property updates
**2.5.8 Target Size (AA):**
- Audit all clickable targets for minimum 24x24px
- The copy button, delete button (duplicate dialog), and remove directory button need `min-width: 24px; min-height: 24px` in CSS
- Status badges in actionable rows are not click targets (the row is), so this is fine
---
## 3. Understandable (WCAG 3.x)
### 3.1 Readable
**3.1.1 Language of Page (A):**
- The GTK accessible layer reads the locale from the system. No explicit action needed for a native desktop app.
**3.1.3 Unusual Words (AAA):**
- Add `tooltip_text` explanations for technical terms displayed in the UI:
- "FUSE" -> tooltip: "Filesystem in Userspace - required for mounting AppImages"
- "XWayland" -> tooltip: "X11 compatibility layer for Wayland desktops"
- "AppImage Type 1/2" -> tooltip: "Type 1 uses ISO9660, Type 2 uses SquashFS"
- "CVE" -> tooltip: "Common Vulnerabilities and Exposures - security vulnerability identifier"
- "SHA256" -> tooltip: "Cryptographic hash for verifying file integrity"
- "Firejail" -> tooltip: "Linux application sandboxing tool"
- "zsync" -> tooltip: "Efficient delta-update download protocol"
- "fusermount" -> tooltip: "User-space filesystem mount utility"
**3.1.4 Abbreviations (AAA):**
- Expand "CVE" to "CVE (vulnerability)" on first use in security report
- Expand "SHA256" to "SHA256 checksum" in detail view
**3.1.5 Reading Level (AAA):**
- Review all user-facing strings for plain language (most are already simple)
- Replace "No update information embedded" with "This app cannot check for updates automatically"
- Replace "AppImage does not contain update information" with "No automatic update support"
### 3.2 Predictable - Already compliant (no unexpected changes)
### 3.3 Input Assistance
**3.3.5 Help (AAA):**
- Add contextual descriptions to all PreferencesGroup widgets (already mostly done)
- Add `description` text to any PreferencesGroup missing it
**3.3.6 Error Prevention - All (AAA):**
- Destructive actions already have confirmation (confirm-before-delete setting, alert dialogs)
- Add confirmation to bulk "Remove All Suggested" in duplicate dialog (currently executes immediately)
- Add confirmation to "Clean Selected" in cleanup wizard (currently executes immediately)
---
## 4. Robust (WCAG 4.x)
### 4.1.2 Name, Role, Value (A)
**Accessible Names (all interactive elements):**
Every button, toggle, switch, row, and input must have an accessible name. The full list of items needing labels:
| Widget | File | Label to add |
|--------|------|-------------|
| Menu button | library_view.rs | "Main menu" |
| Search toggle | library_view.rs | "Toggle search" |
| Grid view toggle | library_view.rs | "Switch to grid view" |
| List view toggle | library_view.rs | "Switch to list view" |
| Copy button | widgets.rs | "Copy to clipboard" |
| Delete button | duplicate_dialog.rs | "Delete this AppImage" |
| Remove directory | preferences.rs | "Remove scan directory" |
| Close button | cleanup_wizard.rs | "Close dialog" |
| Clean Selected | cleanup_wizard.rs | "Clean selected items" |
| Remove All Suggested | duplicate_dialog.rs | "Remove all suggested duplicates" |
| Add Location | preferences.rs | "Add scan directory" |
| Scan Now | library_view.rs | "Scan for AppImages" |
| Preferences | library_view.rs | "Open preferences" |
**Accessible Roles:**
- `FlowBox` -> already has label
- `ListBox` (list view) -> add `AccessibleProperty::Label("AppImage library list")`
- `ListBox` (preferences directory list) -> add label "Scan directories"
- `ListBox` (cleanup items) -> add label per category
- `ListBox` (integration dialog identity) -> add label "Application details"
- `ListBox` (integration dialog actions) -> add label "Integration actions"
- Status badges -> add `AccessibleRole::Status` (or use `update_property` with `AccessibleProperty::Label`)
**Accessible States:**
- Switch rows -> GTK handles this automatically via `SwitchRow`
- Scan button -> add `AccessibleState::Busy` while scanning is in progress
- Security scan row -> add `AccessibleState::Busy` during scan
- Analyze toolkit row -> add `AccessibleState::Busy` during analysis
### 4.1.3 Status Messages (AA)
**Live region announcements for async operations:**
Create a helper function `announce(text: &str)` in widgets.rs that uses a hidden GTK Label with `AccessibleRole::Alert` to broadcast status changes to screen readers.
Operations needing announcements:
- Scan start ("Scanning for AppImages...")
- Scan complete ("{n} AppImages found, {m} new")
- Update check start/complete
- Security scan start/complete
- Cleanup analysis start/complete
- Search results count change ("{n} results" or "No results")
---
## Files Modified
| File | Changes |
|------|---------|
| `data/resources/style.css` | Focus indicators for all widgets, high-contrast media query, reduced-motion expansion, target size minimums |
| `src/ui/widgets.rs` | New `announce()` live region helper, accessible label on copy_button, accessible role on status badges |
| `src/ui/library_view.rs` | Accessible labels on all header buttons, list box label, search results announcement, accessible roles |
| `src/ui/app_card.rs` | Accessible description for emblem overlay |
| `src/ui/detail_view.rs` | Accessible roles on banner, busy states on async rows, tooltips for technical terms, plain-language rewrites |
| `src/ui/dashboard.rs` | Tooltips for technical terms, accessible labels on any unlabeled elements |
| `src/ui/duplicate_dialog.rs` | Accessible label on delete buttons, confirmation before bulk remove, list box labels |
| `src/ui/cleanup_wizard.rs` | Accessible labels on buttons, confirmation before cleanup, list box labels, busy announcement |
| `src/ui/preferences.rs` | Accessible label on remove button and add button, list box labels |
| `src/ui/security_report.rs` | Accessible labels, tooltips for CVE terms |
| `src/ui/integration_dialog.rs` | List box labels, accessible descriptions |
| `src/ui/update_dialog.rs` | Plain-language rewrites |
| `src/window.rs` | Window title updates per page, live announcements for scan/update/clean operations |
## Verification
After implementation:
1. `cargo build` - zero errors, zero warnings
2. `cargo test` - all tests pass
3. Run with Orca screen reader - verify every element is announced correctly
4. Tab through entire app - verify all elements have visible focus indicators
5. Set `GTK_THEME=Adwaita:dark` - verify dark mode focus/contrast
6. Set high-contrast theme - verify enhanced contrast mode
7. Set `GTK_DEBUG=interactive` - inspect accessible tree
8. Keyboard-only navigation test through every screen
9. Verify all targets meet 24x24px minimum
10. Verify reduced-motion disables all animations

File diff suppressed because it is too large Load Diff

View File

@@ -1,166 +0,0 @@
# Final UX Improvements Design: Tags, Export/Import, Changelog
## Overview
Three remaining UX improvements from the 30-item enhancement list:
- **#12** App grouping with tags + grouped library sections
- **#13** Unified export/import (shared core for CLI + GUI) with extended fields
- **#14** Bridge changelog gap (GitHub release notes -> release_history) + updates view preview
## #12: Tags + Grouped Library Sections
### Current State
- `tags: Option<String>` field exists in `AppImageRecord` (database.rs:50)
- `update_tags(id, tags)` DB method exists (database.rs:2069)
- No UI for viewing, editing, or filtering by tags
- Library view is flat (grid or list mode, no sections)
### Design
**Detail view - tag editor:**
- Add a "Tags" row in the detail view info section
- Display existing tags as pill-shaped chips with "x" remove buttons
- A "+" button at the end opens an inline text entry to add a new tag
- Tags stored as comma-separated string in the existing `tags` column
- On add/remove, call `db.update_tags()` immediately (no save button)
**Library view - tag filter chips:**
- Add a horizontal scrollable chip bar at the top of the library view (same pattern as catalog category chips)
- "All" chip selected by default, plus one chip per unique tag found across all apps
- Selecting a tag filters the view to only apps with that tag
- Chip bar is only visible when at least one app has tags
**Library view - grouped sections:**
- When no search is active and no tag filter is selected, group apps by their first tag under collapsible section headers
- Apps with no tags go under an "Uncategorized" section at the bottom
- Sort dropdown still works within each group
- In search mode or tag-filter mode, grouping is disabled (flat list)
**Data model:** No schema changes. Comma-separated tags in existing column.
### Files Modified
| File | Changes |
|------|---------|
| `src/ui/detail_view.rs` | Add tag editor chips row in info section |
| `src/ui/library_view.rs` | Add tag filter chip bar, grouped section headers |
| `src/core/database.rs` | Add `get_all_tags()` query to collect distinct tags |
## #13: Unified Export/Import (CLI + GUI)
### Current State
- CLI-only export/import in `cli.rs` (lines 887-1000+)
- JSON format v1 with limited fields: path, app_name, app_version, integrated, notes, categories
- No GUI file picker dialogs for export/import
- No shared module - logic is inline in CLI handlers
### Design
**Shared core module: `src/core/backup.rs`**
New module with two public functions:
```
pub fn export_app_list(db: &Database, path: &Path) -> Result<usize, BackupError>
pub fn import_app_list(db: &Database, path: &Path) -> Result<ImportResult, BackupError>
```
Where `ImportResult` contains:
- `matched: usize` - apps found and metadata merged
- `missing: Vec<String>` - app names/paths not found on disk
**JSON format v2:**
```json
{
"version": 2,
"exported_at": "2026-03-01T10:00:00Z",
"appimages": [
{
"path": "/home/user/Applications/MyApp.AppImage",
"app_name": "MyApp",
"app_version": "1.0.0",
"integrated": true,
"notes": "user notes",
"categories": "Graphics;Utility;",
"tags": "work,design",
"pinned": false,
"launch_args": "--no-sandbox",
"sandbox_mode": "permissive",
"autostart": false
}
]
}
```
Graceful v1 handling: missing fields default to None (not overwritten on import).
**Import merge semantics:**
- Match apps by path (exact match)
- For matched apps, merge each field: only overwrite if the import value is non-empty/non-null AND the existing value is empty/null (preserve user's current data)
- Tags: merge (union of existing + imported tags, no duplicates)
- For unmatched paths: collect into `missing` list
**CLI (cli.rs):**
- `cmd_export()` and `cmd_import()` become thin wrappers calling `backup::export_app_list()` / `backup::import_app_list()`
- Same CLI flags and output messages
**GUI (window.rs):**
- Add "Export app list" and "Import app list" items to the hamburger menu
- Export: `gtk::FileDialog` save picker, default filename `driftwood-apps.json`, calls `backup::export_app_list()`, toast on success
- Import: `gtk::FileDialog` open picker with `.json` filter, calls `backup::import_app_list()`, toast showing "Imported N apps" + dialog listing missing apps if any
- Refresh library view after import
### Files Modified
| File | Changes |
|------|---------|
| `src/core/backup.rs` | New module: export/import logic, JSON v2 format |
| `src/core/mod.rs` | Add `pub mod backup;` |
| `src/cli.rs` | Refactor cmd_export/cmd_import to call backup module |
| `src/window.rs` | Add menu items + file picker dialogs for export/import |
## #14: Bridge Changelog Gap + Updates View Preview
### Current State
- `release_history: Option<String>` field exists in AppImageRecord (JSON array)
- Detail view already renders release history when data exists (detail_view.rs:760-820)
- AppStream parsing populates release_history for local metainfo (rare)
- GitHub enrichment fetches release info but does NOT write to release_history
- Updates view shows update-available cards but no changelog preview
### Design
**Enrichment bridge (github_enrichment.rs):**
- After `enrich_app_release_info()` fetches the latest release, also fetch up to 10 recent releases from the GitHub releases API
- Convert each release to `ReleaseInfo { version, date, description }` format
- Serialize as JSON array and write to `release_history` column
- Only update if the field is currently empty (don't overwrite AppStream data which may be more authoritative)
- The existing detail view Release History section will then automatically display this data for all GitHub-enriched apps
**New DB method:**
- `update_release_history(id: i64, history_json: &str)` - simple UPDATE on the release_history column
**Updates view changelog preview (updates_view.rs):**
- Each update card gets an `adw::ExpanderRow` "What's new" section below the existing content
- If `release_history` is populated, find the entry matching `latest_version` and show its description
- If no matching entry or no release_history data: show "Release notes not available" in dim text
- Collapsed by default to keep the view compact
### Files Modified
| File | Changes |
|------|---------|
| `src/core/github_enrichment.rs` | Fetch recent releases, write to release_history |
| `src/core/database.rs` | Add `update_release_history()` method |
| `src/ui/updates_view.rs` | Add expandable "What's new" section to update cards |
## Implementation Order
1. **#14 first** - Smallest scope, self-contained enrichment + UI change
2. **#12 second** - Tags are self-contained, no dependency on other features
3. **#13 last** - Export should include tags (depends on #12 being functional)