Wire backup, notification, report export, and file watcher modules into UI

- Add export button to security report page (HTML/JSON/CSV via FileDialog)
- Send desktop notifications after security scans when enabled in settings
- Add backup section to storage tab with create/restore/delete and toast feedback
- Start file watcher on launch to auto-refresh library on AppImage changes
- Fix detail view tabs requesting tallest tab height (set vhomogeneous=false)
- Disable tab transitions to avoid visual glitch with variable-height tabs
This commit is contained in:
lashman
2026-02-27 21:03:19 +02:00
parent df3efa3b51
commit c9f032292a
3 changed files with 407 additions and 5 deletions

View File

@@ -11,9 +11,11 @@ use crate::core::database::Database;
use crate::core::discovery;
use crate::core::integrator;
use crate::core::launcher;
use crate::core::notification;
use crate::core::orphan;
use crate::core::security;
use crate::core::updater;
use crate::core::watcher;
use crate::i18n::{i18n, ni18n_f};
use crate::ui::cleanup_wizard;
use crate::ui::dashboard;
@@ -37,6 +39,7 @@ mod imp {
pub database: OnceCell<Rc<Database>>,
pub drop_overlay: OnceCell<gtk::Box>,
pub drop_revealer: OnceCell<gtk::Revealer>,
pub watcher_handle: std::cell::RefCell<Option<notify::RecommendedWatcher>>,
}
impl Default for DriftwoodWindow {
@@ -49,6 +52,7 @@ mod imp {
database: OnceCell::new(),
drop_overlay: OnceCell::new(),
drop_revealer: OnceCell::new(),
watcher_handle: std::cell::RefCell::new(None),
}
}
}
@@ -705,6 +709,19 @@ impl DriftwoodWindow {
let msg = format!("Found {} CVE{}", total, if total == 1 { "" } else { "s" });
toast_overlay.add_toast(adw::Toast::new(&msg));
}
// Send desktop notifications for new CVE findings if enabled
let settings = gio::Settings::new(APP_ID);
if settings.boolean("security-notifications") {
let threshold = settings.string("security-notification-threshold").to_string();
glib::spawn_future_local(async move {
let _ = gio::spawn_blocking(move || {
let bg_db = Database::open().expect("Failed to open database");
notification::check_and_notify(&bg_db, &threshold);
})
.await;
});
}
}
_ => toast_overlay.add_toast(adw::Toast::new("Security scan failed")),
}
@@ -816,6 +833,9 @@ impl DriftwoodWindow {
// Always scan on startup to discover new AppImages and complete pending analyses
self.trigger_scan();
// Start watching scan directories for new AppImage files
self.start_file_watcher();
// Check for orphaned desktop entries in the background
let toast_overlay = self.imp().toast_overlay.get().unwrap().clone();
glib::spawn_future_local(async move {
@@ -1019,6 +1039,44 @@ impl DriftwoodWindow {
});
}
fn start_file_watcher(&self) {
let settings = self.settings();
let dirs: Vec<std::path::PathBuf> = settings
.strv("scan-directories")
.iter()
.map(|s| discovery::expand_tilde(&s.to_string()))
.collect();
if dirs.is_empty() {
return;
}
// Use an atomic flag to communicate across the thread boundary.
// The watcher callback (on a background thread) sets the flag,
// and a glib timer on the main thread polls and dispatches.
let changed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let changed_watcher = changed.clone();
let handle = watcher::start_watcher(dirs, move |_event| {
changed_watcher.store(true, std::sync::atomic::Ordering::Relaxed);
});
if let Some(h) = handle {
self.imp().watcher_handle.replace(Some(h));
// Poll the flag every second from the main thread
let window_weak = self.downgrade();
glib::timeout_add_local(std::time::Duration::from_secs(1), move || {
if changed.swap(false, std::sync::atomic::Ordering::Relaxed) {
if let Some(window) = window_weak.upgrade() {
window.trigger_scan();
}
}
glib::ControlFlow::Continue
});
}
}
fn show_shortcuts_dialog(&self) {
let dialog = adw::Dialog::builder()
.title("Keyboard Shortcuts")