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

This commit is contained in:
2026-02-27 17:16:41 +02:00
parent 870ef2a739
commit 40f7f12834
50 changed files with 10446 additions and 481 deletions

View File

@@ -5,6 +5,7 @@ use std::rc::Rc;
use crate::core::database::Database;
use crate::core::duplicates::{self, DuplicateGroup, MatchReason, MemberRecommendation};
use crate::core::integrator;
use crate::i18n::{i18n, i18n_f, ni18n_f};
use super::widgets;
/// Show a dialog listing duplicate/multi-version AppImages with resolution options.
@@ -17,10 +18,10 @@ pub fn show_duplicate_dialog(
if groups.is_empty() {
let dialog = adw::AlertDialog::builder()
.heading("No Duplicates Found")
.body("No duplicate or multi-version AppImages were detected.")
.heading(&i18n("No Duplicates Found"))
.body(&i18n("No duplicate or multi-version AppImages were detected."))
.build();
dialog.add_response("ok", "OK");
dialog.add_response("ok", &i18n("OK"));
dialog.set_default_response(Some("ok"));
dialog.present(Some(parent));
return;
@@ -29,7 +30,7 @@ pub fn show_duplicate_dialog(
let summary = duplicates::summarize_duplicates(&groups);
let dialog = adw::Dialog::builder()
.title("Duplicates & Old Versions")
.title(&i18n("Duplicates & Old Versions"))
.content_width(600)
.content_height(500)
.build();
@@ -39,12 +40,12 @@ pub fn show_duplicate_dialog(
// "Remove All Suggested" bulk action button
let bulk_btn = gtk::Button::builder()
.label("Remove All Suggested")
.tooltip_text("Delete all items recommended for removal")
.label(&i18n("Remove All Suggested"))
.tooltip_text(&i18n("Delete all items recommended for removal"))
.build();
bulk_btn.add_css_class("destructive-action");
bulk_btn.update_property(&[
gtk::accessible::Property::Label("Remove all suggested duplicates"),
gtk::accessible::Property::Label(&i18n("Remove all suggested duplicates")),
]);
header.pack_end(&bulk_btn);
@@ -64,13 +65,14 @@ pub fn show_duplicate_dialog(
.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_text = i18n_f(
"{groups} groups found ({exact} exact duplicates, {multi} with multiple versions). Potential savings: {savings}",
&[
("{groups}", &summary.total_groups.to_string()),
("{exact}", &summary.exact_duplicates.to_string()),
("{multi}", &summary.multi_version.to_string()),
("{savings}", &widgets::format_size(summary.total_potential_savings as i64)),
],
);
let summary_label = gtk::Label::builder()
.label(&summary_text)
@@ -101,13 +103,17 @@ pub fn show_duplicate_dialog(
return;
}
let plural = if count == 1 { "" } else { "s" };
let confirm = adw::AlertDialog::builder()
.heading("Confirm Removal")
.body(&format!("Remove {} suggested duplicate{}?", count, plural))
.heading(&i18n("Confirm Removal"))
.body(&ni18n_f(
"Remove {count} suggested duplicate?",
"Remove {count} suggested duplicates?",
count as u32,
&[("{count}", &count.to_string())],
))
.build();
confirm.add_response("cancel", "Cancel");
confirm.add_response("remove", "Remove");
confirm.add_response("cancel", &i18n("Cancel"));
confirm.add_response("remove", &i18n("Remove"));
confirm.set_response_appearance("remove", adw::ResponseAppearance::Destructive);
confirm.set_default_response(Some("cancel"));
confirm.set_close_response("cancel");
@@ -136,9 +142,14 @@ pub fn show_duplicate_dialog(
removed_count += 1;
}
if removed_count > 0 {
toast_confirm.add_toast(adw::Toast::new(&format!("Removed {} items", removed_count)));
toast_confirm.add_toast(adw::Toast::new(&ni18n_f(
"Removed {count} item",
"Removed {count} items",
removed_count as u32,
&[("{count}", &removed_count.to_string())],
)));
btn_ref.set_sensitive(false);
btn_ref.set_label("Done");
btn_ref.set_label(&i18n("Done"));
}
});
@@ -158,23 +169,27 @@ fn build_group_widget(
toast_overlay: &adw::ToastOverlay,
) -> (adw::PreferencesGroup, Vec<(i64, String, String, bool)>) {
let reason_text = match group.match_reason {
MatchReason::ExactDuplicate => "Exact duplicate",
MatchReason::MultiVersion => "Multiple versions",
MatchReason::SameVersionDifferentPath => "Same version, different path",
MatchReason::ExactDuplicate => i18n("Exact duplicate"),
MatchReason::MultiVersion => i18n("Multiple versions"),
MatchReason::SameVersionDifferentPath => i18n("Same version, different path"),
};
let description = if group.potential_savings > 0 {
format!(
"{} - Total: {} - Potential savings: {}",
reason_text,
widgets::format_size(group.total_size as i64),
widgets::format_size(group.potential_savings as i64),
i18n_f(
"{reason} - Total: {total} - Potential savings: {savings}",
&[
("{reason}", &reason_text),
("{total}", &widgets::format_size(group.total_size as i64)),
("{savings}", &widgets::format_size(group.potential_savings as i64)),
],
)
} else {
format!(
"{} - Total: {}",
reason_text,
widgets::format_size(group.total_size as i64),
i18n_f(
"{reason} - Total: {total}",
&[
("{reason}", &reason_text),
("{total}", &widgets::format_size(group.total_size as i64)),
],
)
};
@@ -187,11 +202,12 @@ fn build_group_widget(
for member in &group.members {
let record = &member.record;
let version = record.app_version.as_deref().unwrap_or("unknown");
let unknown = i18n("unknown");
let version = record.app_version.as_deref().unwrap_or(&unknown);
let size = widgets::format_size(record.size_bytes);
let title = if member.is_recommended {
format!("{} ({}) - Recommended", version, size)
i18n_f("{version} ({size}) - Recommended", &[("{version}", version), ("{size}", &size)])
} else {
format!("{} ({})", version, size)
};
@@ -225,7 +241,7 @@ fn build_group_widget(
let delete_btn = gtk::Button::builder()
.icon_name("user-trash-symbolic")
.tooltip_text("Delete this AppImage")
.tooltip_text(&i18n("Delete this AppImage"))
.css_classes(["flat", "circular"])
.valign(gtk::Align::Center)
.build();
@@ -235,7 +251,7 @@ fn build_group_widget(
let record_path = record.path.clone();
let record_name = record.app_name.clone().unwrap_or_else(|| record.filename.clone());
delete_btn.update_property(&[
gtk::accessible::Property::Label(&format!("Delete {}", record_name)),
gtk::accessible::Property::Label(&i18n_f("Delete {name}", &[("{name}", &record_name)])),
]);
let db_ref = db.clone();
let toast_ref = toast_overlay.clone();
@@ -253,7 +269,7 @@ fn build_group_widget(
db_ref.remove_appimage(record_id).ok();
// Update UI
btn.set_sensitive(false);
toast_ref.add_toast(adw::Toast::new(&format!("Removed {}", record_name)));
toast_ref.add_toast(adw::Toast::new(&i18n_f("Removed {name}", &[("{name}", &record_name)])));
});
row.add_suffix(&delete_btn);