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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user