Implement Driftwood AppImage manager - Phases 1 and 2

This commit is contained in:
2026-02-26 23:04:27 +02:00
parent d256c008f4
commit 667f0eb1c2
33 changed files with 10401 additions and 0 deletions

156
src/ui/duplicate_dialog.rs Normal file
View File

@@ -0,0 +1,156 @@
use adw::prelude::*;
use std::rc::Rc;
use crate::core::database::Database;
use crate::core::duplicates::{self, DuplicateGroup, MatchReason, MemberRecommendation};
use super::widgets;
/// Show a dialog listing duplicate/multi-version AppImages with resolution options.
pub fn show_duplicate_dialog(
parent: &impl IsA<gtk::Widget>,
db: &Rc<Database>,
toast_overlay: &adw::ToastOverlay,
) {
let groups = duplicates::detect_duplicates(db);
if groups.is_empty() {
let dialog = adw::AlertDialog::builder()
.heading("No Duplicates Found")
.body("No duplicate or multi-version AppImages were detected.")
.build();
dialog.add_response("ok", "OK");
dialog.set_default_response(Some("ok"));
dialog.present(Some(parent));
return;
}
let summary = duplicates::summarize_duplicates(&groups);
let dialog = adw::Dialog::builder()
.title("Duplicates & Old Versions")
.content_width(600)
.content_height(500)
.build();
let toolbar = adw::ToolbarView::new();
let header = adw::HeaderBar::new();
toolbar.add_top_bar(&header);
let scrolled = gtk::ScrolledWindow::builder()
.vexpand(true)
.build();
let content = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(16)
.margin_top(16)
.margin_bottom(16)
.margin_start(16)
.margin_end(16)
.build();
// Summary banner
let summary_text = format!(
"{} groups found ({} exact duplicates, {} with multiple versions). \
Potential savings: {}",
summary.total_groups,
summary.exact_duplicates,
summary.multi_version,
widgets::format_size(summary.total_potential_savings as i64),
);
let summary_label = gtk::Label::builder()
.label(&summary_text)
.wrap(true)
.halign(gtk::Align::Start)
.build();
summary_label.add_css_class("dim-label");
content.append(&summary_label);
// Build a list for each duplicate group
for group in &groups {
content.append(&build_group_widget(group));
}
scrolled.set_child(Some(&content));
toolbar.set_content(Some(&scrolled));
dialog.set_child(Some(&toolbar));
dialog.present(Some(parent));
}
fn build_group_widget(group: &DuplicateGroup) -> gtk::Box {
let container = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.build();
// Group header
let header_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
let name_label = gtk::Label::builder()
.label(&group.app_name)
.css_classes(["heading"])
.halign(gtk::Align::Start)
.hexpand(true)
.build();
header_box.append(&name_label);
let reason_badge = widgets::status_badge(
group.match_reason.label(),
match group.match_reason {
MatchReason::ExactDuplicate => "error",
MatchReason::MultiVersion => "warning",
MatchReason::SameVersionDifferentPath => "warning",
},
);
header_box.append(&reason_badge);
container.append(&header_box);
// Savings info
if group.potential_savings > 0 {
let savings_label = gtk::Label::builder()
.label(&format!(
"Potential savings: {}",
widgets::format_size(group.potential_savings as i64)
))
.halign(gtk::Align::Start)
.build();
savings_label.add_css_class("dim-label");
container.append(&savings_label);
}
// Members list
let list_box = gtk::ListBox::new();
list_box.add_css_class("boxed-list");
list_box.set_selection_mode(gtk::SelectionMode::None);
for member in &group.members {
let record = &member.record;
let version = record.app_version.as_deref().unwrap_or("unknown");
let size = widgets::format_size(record.size_bytes);
let row = adw::ActionRow::builder()
.title(&format!("{} ({})", version, size))
.subtitle(&record.path)
.build();
// Recommendation badge
let badge_class = match member.recommendation {
MemberRecommendation::KeepNewest | MemberRecommendation::KeepIntegrated => "success",
MemberRecommendation::RemoveOlder | MemberRecommendation::RemoveDuplicate => "error",
MemberRecommendation::UserChoice => "neutral",
};
let badge = widgets::status_badge(member.recommendation.label(), badge_class);
badge.set_valign(gtk::Align::Center);
row.add_suffix(&badge);
list_box.append(&row);
}
container.append(&list_box);
container
}