Implement Driftwood AppImage manager - Phases 1 and 2
Phase 1 - Application scaffolding: - GTK4/libadwaita application window with AdwNavigationView - GSettings-backed window state persistence - GResource-compiled CSS and schema - Library view with grid/list toggle, search, sorting, filtering - Detail view with file info, desktop integration controls - Preferences window with scan directories, theme, behavior settings - CLI with list, scan, integrate, remove, clean, inspect commands - AppImage discovery, metadata extraction, desktop integration - Orphaned desktop entry detection and cleanup - AppImage packaging script Phase 2 - Intelligence layer: - Database schema v2 with migration for status tracking columns - FUSE detection engine (libfuse2/3, fusermount, /dev/fuse, AppImageLauncher) - Wayland awareness engine (session type, toolkit detection, XWayland) - Update info parsing from AppImage ELF sections (.upd_info) - GitHub/GitLab Releases API integration for update checking - Update download with progress tracking and atomic apply - Launch wrapper with FUSE auto-detection and usage tracking - Duplicate and multi-version detection with recommendations - Dashboard with system health, library stats, disk usage - Update check dialog (single and batch) - Duplicate resolution dialog - Status badges on library cards and detail view - Extended CLI: status, check-updates, duplicates, launch commands 49 tests passing across all modules.
This commit is contained in:
119
src/ui/app_card.rs
Normal file
119
src/ui/app_card.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use gtk::prelude::*;
|
||||
|
||||
use crate::core::database::AppImageRecord;
|
||||
use crate::core::fuse::FuseStatus;
|
||||
use crate::core::wayland::WaylandStatus;
|
||||
use super::widgets;
|
||||
|
||||
/// Build a grid card for an AppImage.
|
||||
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)
|
||||
.halign(gtk::Align::Center)
|
||||
.build();
|
||||
card.add_css_class("app-card");
|
||||
card.set_size_request(160, -1);
|
||||
|
||||
// Icon (48x48)
|
||||
let icon = if let Some(ref icon_path) = record.icon_path {
|
||||
let path = std::path::Path::new(icon_path);
|
||||
if path.exists() {
|
||||
let paintable = gtk::gdk::Texture::from_filename(path).ok();
|
||||
let image = gtk::Image::builder()
|
||||
.pixel_size(48)
|
||||
.build();
|
||||
if let Some(texture) = paintable {
|
||||
image.set_paintable(Some(&texture));
|
||||
} else {
|
||||
image.set_icon_name(Some("application-x-executable-symbolic"));
|
||||
}
|
||||
image
|
||||
} else {
|
||||
gtk::Image::builder()
|
||||
.icon_name("application-x-executable-symbolic")
|
||||
.pixel_size(48)
|
||||
.build()
|
||||
}
|
||||
} else {
|
||||
gtk::Image::builder()
|
||||
.icon_name("application-x-executable-symbolic")
|
||||
.pixel_size(48)
|
||||
.build()
|
||||
};
|
||||
|
||||
// App name
|
||||
let name = record.app_name.as_deref().unwrap_or(&record.filename);
|
||||
let name_label = gtk::Label::builder()
|
||||
.label(name)
|
||||
.css_classes(["app-card-name"])
|
||||
.ellipsize(gtk::pango::EllipsizeMode::End)
|
||||
.max_width_chars(18)
|
||||
.build();
|
||||
|
||||
// Version
|
||||
let version_text = record.app_version.as_deref().unwrap_or("");
|
||||
let version_label = gtk::Label::builder()
|
||||
.label(version_text)
|
||||
.css_classes(["app-card-version"])
|
||||
.ellipsize(gtk::pango::EllipsizeMode::End)
|
||||
.build();
|
||||
|
||||
card.append(&icon);
|
||||
card.append(&name_label);
|
||||
if !version_text.is_empty() {
|
||||
card.append(&version_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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Integration badge (only show if not integrated, to reduce clutter)
|
||||
if !record.integrated {
|
||||
badges.append(&widgets::status_badge("Not integrated", "neutral"));
|
||||
}
|
||||
|
||||
card.append(&badges);
|
||||
|
||||
let child = gtk::FlowBoxChild::builder()
|
||||
.child(&card)
|
||||
.build();
|
||||
|
||||
child
|
||||
}
|
||||
Reference in New Issue
Block a user