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:
184
src/window.rs
184
src/window.rs
@@ -10,7 +10,11 @@ use crate::core::database::Database;
|
||||
use crate::core::discovery;
|
||||
use crate::core::fuse;
|
||||
use crate::core::inspector;
|
||||
use crate::core::integrator;
|
||||
use crate::core::launcher;
|
||||
use crate::core::orphan;
|
||||
use crate::core::security;
|
||||
use crate::core::updater;
|
||||
use crate::core::wayland;
|
||||
use crate::i18n::{i18n, ni18n_f};
|
||||
use crate::ui::cleanup_wizard;
|
||||
@@ -371,6 +375,186 @@ impl DriftwoodWindow {
|
||||
shortcuts_action,
|
||||
]);
|
||||
|
||||
// --- Context menu actions (parameterized with record ID) ---
|
||||
let param_type = Some(glib::VariantTy::INT64);
|
||||
|
||||
// Launch action
|
||||
let launch_action = gio::SimpleAction::new("launch-appimage", param_type);
|
||||
{
|
||||
let window_weak = self.downgrade();
|
||||
launch_action.connect_activate(move |_, param| {
|
||||
let Some(window) = window_weak.upgrade() else { return };
|
||||
let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
|
||||
let db = window.database().clone();
|
||||
if let Ok(Some(record)) = db.get_appimage_by_id(record_id) {
|
||||
let appimage_path = std::path::Path::new(&record.path);
|
||||
match launcher::launch_appimage(&db, record_id, appimage_path, "gui_context", &[], &[]) {
|
||||
launcher::LaunchResult::Started { child, method } => {
|
||||
log::info!("Context menu launched: {} (PID: {}, method: {})", record.path, child.id(), method.as_str());
|
||||
}
|
||||
launcher::LaunchResult::Failed(msg) => {
|
||||
log::error!("Failed to launch: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
self.add_action(&launch_action);
|
||||
|
||||
// Check for updates action (per-app)
|
||||
let check_update_action = gio::SimpleAction::new("check-update", param_type);
|
||||
{
|
||||
let window_weak = self.downgrade();
|
||||
check_update_action.connect_activate(move |_, param| {
|
||||
let Some(window) = window_weak.upgrade() else { return };
|
||||
let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
|
||||
let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let bg_db = Database::open().expect("DB open failed");
|
||||
if let Ok(Some(record)) = bg_db.get_appimage_by_id(record_id) {
|
||||
let appimage_path = std::path::Path::new(&record.path);
|
||||
if !appimage_path.exists() {
|
||||
return false;
|
||||
}
|
||||
let (_type_label, raw_info, check_result) = updater::check_appimage_for_update(
|
||||
appimage_path,
|
||||
record.app_version.as_deref(),
|
||||
);
|
||||
if raw_info.is_some() {
|
||||
bg_db.update_update_info(record_id, raw_info.as_deref(), None).ok();
|
||||
}
|
||||
if let Some(result) = check_result {
|
||||
if result.update_available {
|
||||
if let Some(ref version) = result.latest_version {
|
||||
bg_db.set_update_available(record_id, Some(version), result.download_url.as_deref()).ok();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
bg_db.clear_update_available(record_id).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}).await;
|
||||
match result {
|
||||
Ok(true) => toast_overlay.add_toast(adw::Toast::new("Update available!")),
|
||||
Ok(false) => toast_overlay.add_toast(adw::Toast::new("Already up to date")),
|
||||
Err(_) => toast_overlay.add_toast(adw::Toast::new("Update check failed")),
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
self.add_action(&check_update_action);
|
||||
|
||||
// Scan for vulnerabilities (per-app)
|
||||
let scan_security_action = gio::SimpleAction::new("scan-security", param_type);
|
||||
{
|
||||
let window_weak = self.downgrade();
|
||||
scan_security_action.connect_activate(move |_, param| {
|
||||
let Some(window) = window_weak.upgrade() else { return };
|
||||
let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
|
||||
let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
|
||||
glib::spawn_future_local(async move {
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let bg_db = Database::open().expect("DB open failed");
|
||||
if let Ok(Some(record)) = bg_db.get_appimage_by_id(record_id) {
|
||||
let appimage_path = std::path::Path::new(&record.path);
|
||||
let scan_result = security::scan_and_store(&bg_db, record_id, appimage_path);
|
||||
return Some(scan_result.total_cves());
|
||||
}
|
||||
None
|
||||
}).await;
|
||||
match result {
|
||||
Ok(Some(total)) => {
|
||||
if total == 0 {
|
||||
toast_overlay.add_toast(adw::Toast::new("No vulnerabilities found"));
|
||||
} else {
|
||||
let msg = format!("Found {} CVE{}", total, if total == 1 { "" } else { "s" });
|
||||
toast_overlay.add_toast(adw::Toast::new(&msg));
|
||||
}
|
||||
}
|
||||
_ => toast_overlay.add_toast(adw::Toast::new("Security scan failed")),
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
self.add_action(&scan_security_action);
|
||||
|
||||
// Toggle integration
|
||||
let toggle_integration_action = gio::SimpleAction::new("toggle-integration", param_type);
|
||||
{
|
||||
let window_weak = self.downgrade();
|
||||
toggle_integration_action.connect_activate(move |_, param| {
|
||||
let Some(window) = window_weak.upgrade() else { return };
|
||||
let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
|
||||
let db = window.database().clone();
|
||||
let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
|
||||
if let Ok(Some(record)) = db.get_appimage_by_id(record_id) {
|
||||
if record.integrated {
|
||||
integrator::remove_integration(&record).ok();
|
||||
db.set_integrated(record_id, false, None).ok();
|
||||
toast_overlay.add_toast(adw::Toast::new("Integration removed"));
|
||||
} else {
|
||||
match integrator::integrate(&record) {
|
||||
Ok(result) => {
|
||||
let desktop_path = result.desktop_file_path.to_string_lossy().to_string();
|
||||
db.set_integrated(record_id, true, Some(&desktop_path)).ok();
|
||||
toast_overlay.add_toast(adw::Toast::new("Integrated into desktop menu"));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Integration failed: {}", e);
|
||||
toast_overlay.add_toast(adw::Toast::new("Integration failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Refresh library view
|
||||
let lib_view = window.imp().library_view.get().unwrap();
|
||||
match db.get_all_appimages() {
|
||||
Ok(records) => lib_view.populate(records),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
self.add_action(&toggle_integration_action);
|
||||
|
||||
// Open containing folder
|
||||
let open_folder_action = gio::SimpleAction::new("open-folder", param_type);
|
||||
{
|
||||
let window_weak = self.downgrade();
|
||||
open_folder_action.connect_activate(move |_, param| {
|
||||
let Some(window) = window_weak.upgrade() else { return };
|
||||
let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
|
||||
let db = window.database().clone();
|
||||
if let Ok(Some(record)) = db.get_appimage_by_id(record_id) {
|
||||
let file = gio::File::for_path(&record.path);
|
||||
let file_launcher = gtk::FileLauncher::new(Some(&file));
|
||||
file_launcher.open_containing_folder(gtk::Window::NONE, None::<&gio::Cancellable>, |_| {});
|
||||
}
|
||||
});
|
||||
}
|
||||
self.add_action(&open_folder_action);
|
||||
|
||||
// Copy path to clipboard
|
||||
let copy_path_action = gio::SimpleAction::new("copy-path", param_type);
|
||||
{
|
||||
let window_weak = self.downgrade();
|
||||
copy_path_action.connect_activate(move |_, param| {
|
||||
let Some(window) = window_weak.upgrade() else { return };
|
||||
let Some(record_id) = param.and_then(|p| p.get::<i64>()) else { return };
|
||||
let db = window.database().clone();
|
||||
let toast_overlay = window.imp().toast_overlay.get().unwrap().clone();
|
||||
if let Ok(Some(record)) = db.get_appimage_by_id(record_id) {
|
||||
let display = gtk::prelude::WidgetExt::display(&window);
|
||||
let clipboard = display.clipboard();
|
||||
clipboard.set_text(&record.path);
|
||||
toast_overlay.add_toast(adw::Toast::new("Path copied to clipboard"));
|
||||
}
|
||||
});
|
||||
}
|
||||
self.add_action(©_path_action);
|
||||
|
||||
// Keyboard shortcuts
|
||||
if let Some(app) = self.application() {
|
||||
let gtk_app = app.downcast_ref::<gtk::Application>().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user