Implement UI/UX overhaul - cards, list, tabbed detail, context menu

Card view: 200px cards with 72px icons, .title-3 names, version+size
combined line, single priority badge, libadwaita .card class replacing
custom .app-card CSS.

List view: 48px rounded icons, .rich-list class, structured two-line
subtitle (description + version/size), single priority badge.

Detail view: restructured into ViewStack/ViewSwitcher with 4 tabs
(Overview, System, Security, Storage). 96px hero banner with gradient
background. Rows distributed logically across tabs.

Context menu: right-click (GestureClick button 3) and long-press on
cards and list rows. Menu items: Launch, Check for Updates, Scan for
Vulnerabilities, Integrate/Remove Integration, Open Containing Folder,
Copy Path. All backed by parameterized window actions.

CSS: removed custom .app-card rules (replaced by .card), added
.icon-rounded for list icons, .detail-banner gradient, and
.detail-view-switcher positioning.
This commit is contained in:
lashman
2026-02-27 11:10:23 +02:00
parent 4f7d8560f1
commit 33cc8a757a
7 changed files with 2630 additions and 397 deletions

View File

@@ -11,22 +11,23 @@ pub fn build_app_card(record: &AppImageRecord) -> gtk::FlowBoxChild {
let card = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(6)
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.margin_top(14)
.margin_bottom(14)
.margin_start(14)
.margin_end(14)
.halign(gtk::Align::Center)
.build();
card.add_css_class("app-card");
card.set_size_request(180, -1);
card.add_css_class("card");
card.set_size_request(200, -1);
// Icon (64x64) with integration emblem overlay
// Icon (72x72) with integration emblem overlay
let name = record.app_name.as_deref().unwrap_or(&record.filename);
let icon_widget = widgets::app_icon(
record.icon_path.as_deref(),
name,
64,
72,
);
icon_widget.add_css_class("icon-dropshadow");
// If integrated, overlay a small checkmark emblem
if record.integrated {
@@ -46,75 +47,46 @@ pub fn build_app_card(record: &AppImageRecord) -> gtk::FlowBoxChild {
card.append(&icon_widget);
}
// App name - use libadwaita built-in heading class
// App name - .title-3 for more visual weight
let name_label = gtk::Label::builder()
.label(name)
.css_classes(["heading"])
.css_classes(["title-3"])
.ellipsize(gtk::pango::EllipsizeMode::End)
.max_width_chars(20)
.build();
// Version - use libadwaita built-in caption + dimmed
// Version + size combined on one line
let version_text = record.app_version.as_deref().unwrap_or("");
let version_label = gtk::Label::builder()
.label(version_text)
.css_classes(["caption", "dimmed"])
let size_text = widgets::format_size(record.size_bytes);
let meta_text = if version_text.is_empty() {
size_text
} else {
format!("{} - {}", version_text, size_text)
};
let meta_label = gtk::Label::builder()
.label(&meta_text)
.css_classes(["caption", "dimmed", "numeric"])
.ellipsize(gtk::pango::EllipsizeMode::End)
.build();
// File size as subtle caption
let size_text = widgets::format_size(record.size_bytes);
let size_label = gtk::Label::builder()
.label(&size_text)
.css_classes(["caption", "dimmed"])
.build();
card.append(&name_label);
if !version_text.is_empty() {
card.append(&version_label);
card.append(&meta_label);
// Single most important badge (priority: Update > FUSE issue > Wayland issue)
if let Some(badge) = build_priority_badge(record) {
let badge_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.halign(gtk::Align::Center)
.margin_top(4)
.build();
badge_box.append(&badge);
card.append(&badge_box);
}
card.append(&size_label);
// Status badges row
let badges = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(4)
.halign(gtk::Align::Center)
.build();
badges.add_css_class("badge-row");
// Wayland status badge
if let Some(ref ws) = record.wayland_status {
let status = WaylandStatus::from_str(ws);
if status != WaylandStatus::Unknown {
badges.append(&widgets::status_badge(status.label(), status.badge_class()));
}
}
// FUSE status badge
if let Some(ref fs) = record.fuse_status {
let status = FuseStatus::from_str(fs);
if !status.is_functional() {
badges.append(&widgets::status_badge(status.label(), status.badge_class()));
}
}
// Update available badge
if record.latest_version.is_some() {
if let (Some(ref latest), Some(ref current)) =
(&record.latest_version, &record.app_version)
{
if crate::core::updater::version_is_newer(latest, current) {
badges.append(&widgets::status_badge("Update", "info"));
}
}
}
card.append(&badges);
let child = gtk::FlowBoxChild::builder()
.child(&card)
.build();
child.add_css_class("activatable");
// Accessible label for screen readers
let accessible_name = build_accessible_label(record);
@@ -123,6 +95,35 @@ pub fn build_app_card(record: &AppImageRecord) -> gtk::FlowBoxChild {
child
}
/// Return the single most important badge for a card.
/// Priority: Update available > FUSE issue > Wayland issue.
pub fn build_priority_badge(record: &AppImageRecord) -> Option<gtk::Label> {
// 1. Update available (highest priority)
if let (Some(ref latest), Some(ref current)) = (&record.latest_version, &record.app_version) {
if crate::core::updater::version_is_newer(latest, current) {
return Some(widgets::status_badge("Update", "info"));
}
}
// 2. FUSE issue
if let Some(ref fs) = record.fuse_status {
let status = FuseStatus::from_str(fs);
if !status.is_functional() {
return Some(widgets::status_badge(status.label(), status.badge_class()));
}
}
// 3. Wayland issue (not Native or Unknown)
if let Some(ref ws) = record.wayland_status {
let status = WaylandStatus::from_str(ws);
if status != WaylandStatus::Unknown && status != WaylandStatus::Native {
return Some(widgets::status_badge(status.label(), status.badge_class()));
}
}
None
}
/// Build a descriptive accessible label for screen readers.
fn build_accessible_label(record: &AppImageRecord) -> String {
let name = record.app_name.as_deref().unwrap_or(&record.filename);