Add Phase 5 enhancements: security, i18n, analysis, backup, notifications

- Database v8 migration: tags, pinned, avg_startup_ms columns
- Security scanning with CVE matching and batch scan
- Bundled library extraction and vulnerability reports
- Desktop notification system for security alerts
- Backup/restore system for AppImage configurations
- i18n framework with gettext support
- Runtime analysis and Wayland compatibility detection
- AppStream metadata and Flatpak-style build support
- File watcher module for live directory monitoring
- Preferences panel with GSettings integration
- CLI interface for headless operation
- Detail view: tabbed layout with ViewSwitcher in title bar,
  health score, sandbox controls, changelog links
- Library view: sort dropdown, context menu enhancements
- Dashboard: system status, disk usage, launch history
- Security report page with scan and export
- Packaging: meson build, PKGBUILD, metainfo
This commit is contained in:
lashman
2026-02-27 17:16:41 +02:00
parent a7ed3742fb
commit 423323d5a9
51 changed files with 10583 additions and 481 deletions

View File

@@ -2,10 +2,11 @@ use adw::prelude::*;
use gtk::gio;
use crate::config::APP_ID;
use crate::i18n::i18n;
pub fn show_preferences_dialog(parent: &impl IsA<gtk::Widget>) {
let dialog = adw::PreferencesDialog::new();
dialog.set_title("Preferences");
dialog.set_title(&i18n("Preferences"));
let settings = gio::Settings::new(APP_ID);
@@ -20,22 +21,22 @@ pub fn show_preferences_dialog(parent: &impl IsA<gtk::Widget>) {
fn build_general_page(settings: &gio::Settings, dialog: &adw::PreferencesDialog) -> adw::PreferencesPage {
let page = adw::PreferencesPage::builder()
.title("General")
.title(&i18n("General"))
.icon_name("emblem-system-symbolic")
.build();
// Appearance group
let appearance_group = adw::PreferencesGroup::builder()
.title("Appearance")
.description("Visual preferences for the application")
.title(&i18n("Appearance"))
.description(&i18n("Visual preferences for the application"))
.build();
let theme_row = adw::ComboRow::builder()
.title("Color Scheme")
.subtitle("Choose light, dark, or follow system preference")
.title(&i18n("Color Scheme"))
.subtitle(&i18n("Choose light, dark, or follow system preference"))
.build();
let model = gtk::StringList::new(&["Follow System", "Light", "Dark"]);
let model = gtk::StringList::new(&[&i18n("Follow System"), &i18n("Light"), &i18n("Dark")]);
theme_row.set_model(Some(&model));
let current = settings.string("color-scheme");
@@ -58,10 +59,10 @@ fn build_general_page(settings: &gio::Settings, dialog: &adw::PreferencesDialog)
appearance_group.add(&theme_row);
let view_row = adw::ComboRow::builder()
.title("Default View")
.subtitle("Library display mode")
.title(&i18n("Default View"))
.subtitle(&i18n("Library display mode"))
.build();
let view_model = gtk::StringList::new(&["Grid", "List"]);
let view_model = gtk::StringList::new(&[&i18n("Grid"), &i18n("List")]);
view_row.set_model(Some(&view_model));
let current_view = settings.string("view-mode");
view_row.set_selected(if current_view.as_str() == "list" { 1 } else { 0 });
@@ -73,12 +74,13 @@ fn build_general_page(settings: &gio::Settings, dialog: &adw::PreferencesDialog)
});
appearance_group.add(&view_row);
page.add(&appearance_group);
// Scan Locations group
let scan_group = adw::PreferencesGroup::builder()
.title("Scan Locations")
.description("Directories to scan for AppImage files")
.title(&i18n("Scan Locations"))
.description(&i18n("Directories to scan for AppImage files"))
.build();
let dirs = settings.strv("scan-directories");
@@ -86,7 +88,7 @@ fn build_general_page(settings: &gio::Settings, dialog: &adw::PreferencesDialog)
dir_list_box.add_css_class("boxed-list");
dir_list_box.set_selection_mode(gtk::SelectionMode::None);
dir_list_box.update_property(&[
gtk::accessible::Property::Label("Scan directories"),
gtk::accessible::Property::Label(&i18n("Scan directories")),
]);
for dir in &dirs {
@@ -97,11 +99,11 @@ fn build_general_page(settings: &gio::Settings, dialog: &adw::PreferencesDialog)
// Add location button
let add_button = gtk::Button::builder()
.label("Add Location")
.label(&i18n("Add Location"))
.build();
add_button.add_css_class("flat");
add_button.update_property(&[
gtk::accessible::Property::Label("Add scan directory"),
gtk::accessible::Property::Label(&i18n("Add scan directory")),
]);
let settings_add = settings.clone();
@@ -109,7 +111,7 @@ fn build_general_page(settings: &gio::Settings, dialog: &adw::PreferencesDialog)
let dialog_weak = dialog.downgrade();
add_button.connect_clicked(move |_| {
let file_dialog = gtk::FileDialog::builder()
.title("Choose a directory")
.title(i18n("Choose a directory"))
.modal(true)
.build();
@@ -158,19 +160,19 @@ fn build_general_page(settings: &gio::Settings, dialog: &adw::PreferencesDialog)
fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage {
let page = adw::PreferencesPage::builder()
.title("Behavior")
.title(&i18n("Behavior"))
.icon_name("preferences-other-symbolic")
.build();
// Automation group
let automation_group = adw::PreferencesGroup::builder()
.title("Automation")
.description("What Driftwood does automatically")
.title(&i18n("Automation"))
.description(&i18n("What Driftwood does automatically"))
.build();
let auto_scan_row = adw::SwitchRow::builder()
.title("Scan on startup")
.subtitle("Automatically scan for new AppImages when the app starts")
.title(&i18n("Scan on startup"))
.subtitle(&i18n("Automatically scan for new AppImages when the app starts"))
.active(settings.boolean("auto-scan-on-startup"))
.build();
let settings_scan = settings.clone();
@@ -180,8 +182,8 @@ fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage {
automation_group.add(&auto_scan_row);
let auto_update_row = adw::SwitchRow::builder()
.title("Check for updates")
.subtitle("Periodically check if newer versions of your AppImages are available")
.title(&i18n("Check for updates"))
.subtitle(&i18n("Periodically check if newer versions of your AppImages are available"))
.active(settings.boolean("auto-check-updates"))
.build();
let settings_upd = settings.clone();
@@ -191,8 +193,8 @@ fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage {
automation_group.add(&auto_update_row);
let auto_integrate_row = adw::SwitchRow::builder()
.title("Auto-integrate new AppImages")
.subtitle("Automatically add newly discovered AppImages to the desktop menu")
.title(&i18n("Auto-integrate new AppImages"))
.subtitle(&i18n("Automatically add newly discovered AppImages to the desktop menu"))
.active(settings.boolean("auto-integrate"))
.build();
let settings_int = settings.clone();
@@ -205,13 +207,13 @@ fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage {
// Backup group
let backup_group = adw::PreferencesGroup::builder()
.title("Backups")
.description("Config and data backup settings for updates")
.title(&i18n("Backups"))
.description(&i18n("Config and data backup settings for updates"))
.build();
let auto_backup_row = adw::SwitchRow::builder()
.title("Auto-backup before update")
.subtitle("Back up config and data files before updating an AppImage")
.title(&i18n("Auto-backup before update"))
.subtitle(&i18n("Back up config and data files before updating an AppImage"))
.active(settings.boolean("auto-backup-before-update"))
.build();
let settings_backup = settings.clone();
@@ -221,8 +223,8 @@ fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage {
backup_group.add(&auto_backup_row);
let retention_row = adw::SpinRow::builder()
.title("Backup retention")
.subtitle("Days to keep config backups before auto-cleanup")
.title(&i18n("Backup retention"))
.subtitle(&i18n("Days to keep config backups before auto-cleanup"))
.build();
let adjustment = gtk::Adjustment::new(
settings.int("backup-retention-days") as f64,
@@ -243,13 +245,13 @@ fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage {
// Safety group
let safety_group = adw::PreferencesGroup::builder()
.title("Safety")
.description("Confirmation and cleanup behavior")
.title(&i18n("Safety"))
.description(&i18n("Confirmation and cleanup behavior"))
.build();
let confirm_row = adw::SwitchRow::builder()
.title("Confirm before delete")
.subtitle("Show a confirmation dialog before deleting files or cleaning up")
.title(&i18n("Confirm before delete"))
.subtitle(&i18n("Show a confirmation dialog before deleting files or cleaning up"))
.active(settings.boolean("confirm-before-delete"))
.build();
let settings_confirm = settings.clone();
@@ -259,10 +261,10 @@ fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage {
safety_group.add(&confirm_row);
let cleanup_row = adw::ComboRow::builder()
.title("After updating an AppImage")
.subtitle("What to do with the old version after a successful update")
.title(&i18n("After updating an AppImage"))
.subtitle(&i18n("What to do with the old version after a successful update"))
.build();
let cleanup_model = gtk::StringList::new(&["Ask each time", "Remove old version", "Keep backup"]);
let cleanup_model = gtk::StringList::new(&[&i18n("Ask each time"), &i18n("Remove old version"), &i18n("Keep backup")]);
cleanup_row.set_model(Some(&cleanup_model));
let current_cleanup = settings.string("update-cleanup");
@@ -292,18 +294,18 @@ fn build_behavior_page(settings: &gio::Settings) -> adw::PreferencesPage {
fn build_security_page(settings: &gio::Settings) -> adw::PreferencesPage {
let page = adw::PreferencesPage::builder()
.title("Security")
.title(&i18n("Security"))
.icon_name("security-medium-symbolic")
.build();
let scan_group = adw::PreferencesGroup::builder()
.title("Vulnerability Scanning")
.description("Check bundled libraries for known CVEs via OSV.dev")
.title(&i18n("Vulnerability Scanning"))
.description(&i18n("Check bundled libraries for known CVEs via OSV.dev"))
.build();
let auto_security_row = adw::SwitchRow::builder()
.title("Auto-scan new AppImages")
.subtitle("Automatically run a security scan on newly discovered AppImages")
.title(&i18n("Auto-scan new AppImages"))
.subtitle(&i18n("Automatically run a security scan on newly discovered AppImages"))
.active(settings.boolean("auto-security-scan"))
.build();
let settings_sec = settings.clone();
@@ -313,8 +315,8 @@ fn build_security_page(settings: &gio::Settings) -> adw::PreferencesPage {
scan_group.add(&auto_security_row);
let info_row = adw::ActionRow::builder()
.title("Data source")
.subtitle("OSV.dev - Open Source Vulnerability database")
.title(&i18n("Data source"))
.subtitle(&i18n("OSV.dev - Open Source Vulnerability database"))
.build();
scan_group.add(&info_row);
@@ -322,13 +324,13 @@ fn build_security_page(settings: &gio::Settings) -> adw::PreferencesPage {
// Notification settings
let notify_group = adw::PreferencesGroup::builder()
.title("Notifications")
.description("Desktop notification settings for security alerts")
.title(&i18n("Notifications"))
.description(&i18n("Desktop notification settings for security alerts"))
.build();
let notify_row = adw::SwitchRow::builder()
.title("Security notifications")
.subtitle("Send desktop notifications when new CVEs are found")
.title(&i18n("Security notifications"))
.subtitle(&i18n("Send desktop notifications when new CVEs are found"))
.active(settings.boolean("security-notifications"))
.build();
let settings_notify = settings.clone();
@@ -338,10 +340,10 @@ fn build_security_page(settings: &gio::Settings) -> adw::PreferencesPage {
notify_group.add(&notify_row);
let threshold_row = adw::ComboRow::builder()
.title("Notification threshold")
.subtitle("Minimum severity to trigger a notification")
.title(&i18n("Notification threshold"))
.subtitle(&i18n("Minimum severity to trigger a notification"))
.build();
let threshold_model = gtk::StringList::new(&["Critical", "High", "Medium", "Low"]);
let threshold_model = gtk::StringList::new(&[&i18n("Critical"), &i18n("High"), &i18n("Medium"), &i18n("Low")]);
threshold_row.set_model(Some(&threshold_model));
let current_threshold = settings.string("security-notification-threshold");
@@ -369,19 +371,19 @@ fn build_security_page(settings: &gio::Settings) -> adw::PreferencesPage {
// About security scanning
let about_group = adw::PreferencesGroup::builder()
.title("How It Works")
.description("Understanding Driftwood's security scanning")
.title(&i18n("How It Works"))
.description(&i18n("Understanding Driftwood's security scanning"))
.build();
let about_row = adw::ActionRow::builder()
.title("Bundled library detection")
.subtitle("Driftwood extracts the list of shared libraries (.so files) bundled inside each AppImage and checks them against the OSV vulnerability database.")
.title(&i18n("Bundled library detection"))
.subtitle(&i18n("Driftwood extracts the list of shared libraries (.so files) bundled inside each AppImage and checks them against the OSV vulnerability database."))
.build();
about_group.add(&about_row);
let limits_row = adw::ActionRow::builder()
.title("Limitations")
.subtitle("Not all bundled libraries can be identified. Version detection uses heuristics and may not always be accurate. Results should be treated as advisory.")
.title(&i18n("Limitations"))
.subtitle(&i18n("Not all bundled libraries can be identified. Version detection uses heuristics and may not always be accurate. Results should be treated as advisory."))
.build();
about_group.add(&limits_row);
@@ -398,11 +400,11 @@ fn add_directory_row(list_box: &gtk::ListBox, dir: &str, settings: &gio::Setting
let remove_btn = gtk::Button::builder()
.icon_name("edit-delete-symbolic")
.valign(gtk::Align::Center)
.tooltip_text("Remove")
.tooltip_text(&i18n("Remove"))
.build();
remove_btn.add_css_class("flat");
remove_btn.update_property(&[
gtk::accessible::Property::Label(&format!("Remove directory {}", dir)),
gtk::accessible::Property::Label(&format!("{} {}", i18n("Remove directory"), dir)),
]);
let list_ref = list_box.clone();