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:
lashman
2026-03-01 01:01:43 +02:00
parent 79519c500a
commit abb69dc753
9 changed files with 901 additions and 215 deletions

View File

@@ -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, &section2);
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)),
);
}
}
}
}
});
}
}