Add tags, export/import, and changelog features
- Tag editor in detail view with add/remove pill chips - Tag filter chips in library view for filtering by tag - Shared backup module for app list export/import (JSON v2) - CLI export/import refactored to use shared module - GUI export/import via file picker dialogs in hamburger menu - GitHub release history enrichment for catalog apps - Changelog preview in updates view with expandable rows - DB migration v19 for catalog release_history column
This commit is contained in:
105
src/window.rs
105
src/window.rs
@@ -7,6 +7,7 @@ use std::time::Instant;
|
||||
|
||||
use crate::config::APP_ID;
|
||||
use crate::core::analysis;
|
||||
use crate::core::backup;
|
||||
use crate::core::database::Database;
|
||||
use crate::core::discovery;
|
||||
use crate::core::footprint;
|
||||
@@ -170,6 +171,8 @@ impl DriftwoodWindow {
|
||||
section2.append(Some(&i18n("Find Duplicates")), Some("win.find-duplicates"));
|
||||
section2.append(Some(&i18n("Security Report")), Some("win.security-report"));
|
||||
section2.append(Some(&i18n("Disk Cleanup")), Some("win.cleanup"));
|
||||
section2.append(Some(&i18n("Export App List")), Some("win.export-app-list"));
|
||||
section2.append(Some(&i18n("Import App List")), Some("win.import-app-list"));
|
||||
menu.append_section(None, §ion2);
|
||||
|
||||
let section3 = gio::Menu::new();
|
||||
@@ -655,6 +658,20 @@ impl DriftwoodWindow {
|
||||
})
|
||||
.build();
|
||||
|
||||
// Export app list action
|
||||
let export_action = gio::ActionEntry::builder("export-app-list")
|
||||
.activate(|window: &Self, _, _| {
|
||||
window.show_export_dialog();
|
||||
})
|
||||
.build();
|
||||
|
||||
// Import app list action
|
||||
let import_action = gio::ActionEntry::builder("import-app-list")
|
||||
.activate(|window: &Self, _, _| {
|
||||
window.show_import_dialog();
|
||||
})
|
||||
.build();
|
||||
|
||||
self.add_action_entries([
|
||||
dashboard_action,
|
||||
preferences_action,
|
||||
@@ -668,6 +685,8 @@ impl DriftwoodWindow {
|
||||
catalog_action,
|
||||
shortcuts_action,
|
||||
show_drop_hint_action,
|
||||
export_action,
|
||||
import_action,
|
||||
]);
|
||||
|
||||
// Sort library action (parameterized with sort mode string)
|
||||
@@ -2003,4 +2022,90 @@ impl DriftwoodWindow {
|
||||
self.maximize();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_export_dialog(&self) {
|
||||
let db = self.database().clone();
|
||||
let toast_overlay = self.imp().toast_overlay.get().unwrap().clone();
|
||||
|
||||
let dialog = gtk::FileDialog::builder()
|
||||
.title(i18n("Export App List"))
|
||||
.initial_name("driftwood-apps.json")
|
||||
.build();
|
||||
|
||||
let json_filter = gtk::FileFilter::new();
|
||||
json_filter.set_name(Some("JSON files"));
|
||||
json_filter.add_pattern("*.json");
|
||||
let filters = gio::ListStore::new::<gtk::FileFilter>();
|
||||
filters.append(&json_filter);
|
||||
dialog.set_filters(Some(&filters));
|
||||
|
||||
let window = self.clone();
|
||||
dialog.save(Some(&window), gio::Cancellable::NONE, move |result| {
|
||||
if let Ok(file) = result {
|
||||
if let Some(path) = file.path() {
|
||||
match backup::export_app_list(&db, &path) {
|
||||
Ok(count) => {
|
||||
toast_overlay.add_toast(
|
||||
adw::Toast::new(&format!("Exported {} apps", count)),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
toast_overlay.add_toast(
|
||||
adw::Toast::new(&format!("Export failed: {}", e)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn show_import_dialog(&self) {
|
||||
let db = self.database().clone();
|
||||
let toast_overlay = self.imp().toast_overlay.get().unwrap().clone();
|
||||
|
||||
let dialog = gtk::FileDialog::builder()
|
||||
.title(i18n("Import App List"))
|
||||
.build();
|
||||
|
||||
let json_filter = gtk::FileFilter::new();
|
||||
json_filter.set_name(Some("JSON files"));
|
||||
json_filter.add_pattern("*.json");
|
||||
let filters = gio::ListStore::new::<gtk::FileFilter>();
|
||||
filters.append(&json_filter);
|
||||
dialog.set_filters(Some(&filters));
|
||||
|
||||
let window = self.clone();
|
||||
dialog.open(Some(&window), gio::Cancellable::NONE, move |result| {
|
||||
if let Ok(file) = result {
|
||||
if let Some(path) = file.path() {
|
||||
match backup::import_app_list(&db, &path) {
|
||||
Ok(result) => {
|
||||
let msg = if result.missing.is_empty() {
|
||||
format!("Imported metadata for {} apps", result.matched)
|
||||
} else {
|
||||
format!(
|
||||
"Imported {} apps, {} not found",
|
||||
result.matched,
|
||||
result.missing.len()
|
||||
)
|
||||
};
|
||||
toast_overlay.add_toast(adw::Toast::new(&msg));
|
||||
|
||||
// Show missing apps dialog if any
|
||||
if !result.missing.is_empty() {
|
||||
let missing_text = result.missing.join("\n");
|
||||
log::info!("Import - missing apps:\n{}", missing_text);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
toast_overlay.add_toast(
|
||||
adw::Toast::new(&format!("Import failed: {}", e)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user