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:
157
src/ui/preferences.rs
Normal file
157
src/ui/preferences.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use adw::prelude::*;
|
||||
use gtk::gio;
|
||||
|
||||
use crate::config::APP_ID;
|
||||
|
||||
pub fn show_preferences_dialog(parent: &impl IsA<gtk::Widget>) {
|
||||
let dialog = adw::PreferencesDialog::new();
|
||||
dialog.set_title("Preferences");
|
||||
|
||||
let settings = gio::Settings::new(APP_ID);
|
||||
|
||||
// --- General page ---
|
||||
let general_page = adw::PreferencesPage::builder()
|
||||
.title("General")
|
||||
.icon_name("emblem-system-symbolic")
|
||||
.build();
|
||||
|
||||
// Appearance group
|
||||
let appearance_group = adw::PreferencesGroup::builder()
|
||||
.title("Appearance")
|
||||
.build();
|
||||
|
||||
let theme_row = adw::ComboRow::builder()
|
||||
.title("Color Scheme")
|
||||
.subtitle("Choose light, dark, or follow system preference")
|
||||
.build();
|
||||
|
||||
let model = gtk::StringList::new(&["Follow System", "Light", "Dark"]);
|
||||
theme_row.set_model(Some(&model));
|
||||
|
||||
let current = settings.string("color-scheme");
|
||||
theme_row.set_selected(match current.as_str() {
|
||||
"force-light" => 1,
|
||||
"force-dark" => 2,
|
||||
_ => 0,
|
||||
});
|
||||
|
||||
let settings_clone = settings.clone();
|
||||
theme_row.connect_selected_notify(move |row| {
|
||||
let value = match row.selected() {
|
||||
1 => "force-light",
|
||||
2 => "force-dark",
|
||||
_ => "default",
|
||||
};
|
||||
settings_clone.set_string("color-scheme", value).ok();
|
||||
});
|
||||
|
||||
appearance_group.add(&theme_row);
|
||||
general_page.add(&appearance_group);
|
||||
|
||||
// Scan Locations group
|
||||
let scan_group = adw::PreferencesGroup::builder()
|
||||
.title("Scan Locations")
|
||||
.description("Directories to scan for AppImage files")
|
||||
.build();
|
||||
|
||||
let dirs = settings.strv("scan-directories");
|
||||
let dir_list_box = gtk::ListBox::new();
|
||||
dir_list_box.add_css_class("boxed-list");
|
||||
dir_list_box.set_selection_mode(gtk::SelectionMode::None);
|
||||
|
||||
for dir in &dirs {
|
||||
add_directory_row(&dir_list_box, &dir, &settings);
|
||||
}
|
||||
|
||||
scan_group.add(&dir_list_box);
|
||||
|
||||
// Add location button
|
||||
let add_button = gtk::Button::builder()
|
||||
.label("Add Location")
|
||||
.build();
|
||||
add_button.add_css_class("flat");
|
||||
|
||||
let settings_add = settings.clone();
|
||||
let list_box_ref = dir_list_box.clone();
|
||||
let dialog_weak = dialog.downgrade();
|
||||
add_button.connect_clicked(move |_| {
|
||||
let file_dialog = gtk::FileDialog::builder()
|
||||
.title("Choose a directory")
|
||||
.modal(true)
|
||||
.build();
|
||||
|
||||
let settings_ref = settings_add.clone();
|
||||
let list_ref = list_box_ref.clone();
|
||||
let dlg = dialog_weak.upgrade();
|
||||
// Get the root window as the transient parent for the file dialog
|
||||
let parent_window: Option<gtk::Window> = dlg
|
||||
.as_ref()
|
||||
.and_then(|d| d.root())
|
||||
.and_then(|r| r.downcast::<gtk::Window>().ok());
|
||||
file_dialog.select_folder(
|
||||
parent_window.as_ref(),
|
||||
None::<&gio::Cancellable>,
|
||||
move |result| {
|
||||
if let Ok(file) = result {
|
||||
if let Some(path) = file.path() {
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
|
||||
let mut current_dirs: Vec<String> = settings_ref
|
||||
.strv("scan-directories")
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
if !current_dirs.contains(&path_str) {
|
||||
current_dirs.push(path_str.clone());
|
||||
let refs: Vec<&str> =
|
||||
current_dirs.iter().map(|s| s.as_str()).collect();
|
||||
settings_ref.set_strv("scan-directories", refs).ok();
|
||||
|
||||
add_directory_row(&list_ref, &path_str, &settings_ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
scan_group.add(&add_button);
|
||||
general_page.add(&scan_group);
|
||||
|
||||
dialog.add(&general_page);
|
||||
dialog.present(Some(parent));
|
||||
}
|
||||
|
||||
fn add_directory_row(list_box: >k::ListBox, dir: &str, settings: &gio::Settings) {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(dir)
|
||||
.build();
|
||||
|
||||
let remove_btn = gtk::Button::builder()
|
||||
.icon_name("edit-delete-symbolic")
|
||||
.valign(gtk::Align::Center)
|
||||
.tooltip_text("Remove")
|
||||
.build();
|
||||
remove_btn.add_css_class("flat");
|
||||
|
||||
let list_ref = list_box.clone();
|
||||
let settings_ref = settings.clone();
|
||||
let dir_str = dir.to_string();
|
||||
let row_ref = row.clone();
|
||||
remove_btn.connect_clicked(move |_| {
|
||||
let current_dirs: Vec<String> = settings_ref
|
||||
.strv("scan-directories")
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.filter(|s| s != &dir_str)
|
||||
.collect();
|
||||
let refs: Vec<&str> = current_dirs.iter().map(|s| s.as_str()).collect();
|
||||
settings_ref.set_strv("scan-directories", refs).ok();
|
||||
|
||||
list_ref.remove(&row_ref);
|
||||
});
|
||||
|
||||
row.add_suffix(&remove_btn);
|
||||
list_box.append(&row);
|
||||
}
|
||||
Reference in New Issue
Block a user