Skip to content
API Documentation
BastionGuard™
MainWindow.vala
Go to the documentation of this file.
1/*
2 * MainWindow.vala
3 *
4 * Copyright 2012-2018 Tony George <teejeetech@gmail.com>
5 * Copyright 2025-2026 Calogero Scarnà — BastionGuard Backup
6 *
7 * GPL-2.0-or-later
8 *
9 * UI: sidebar + cards usando BastionGuard.css / BastionGuard-dark.css
10 */
11
12using Gtk;
13using Gee;
14using GLib;
15
16using TeeJee.Logging;
21using TeeJee.System;
22using TeeJee.Misc;
23
24
25class MainWindow : Gtk.Window {
26
27 // ── Layout ──────────────────────────────────────────────────────────
28 private Gtk.Box hbox_root;
29 private Gtk.Box vbox_sidebar;
30 private Gtk.Stack content_stack;
31 private Gtk.Widget? settings_page_widget = null;
32 private Gtk.Widget? wizard_page_widget = null;
33 private Gtk.Widget? about_page_widget = null;
34 private Gtk.Widget? backup_page_widget = null;
35 private Gtk.Widget? restore_page_widget = null;
36 private Gtk.Widget? delete_page_widget = null;
37 private Gtk.Widget? rsync_log_page_widget = null;
38
39 // ── Sidebar nav buttons ─────────────────────────────────────────────
40 private Gtk.Button btn_nav_snapshots;
41 private Gtk.Button btn_nav_settings;
42 private Gtk.Button btn_nav_wizard;
43 private Gtk.Button btn_nav_about;
44 private Gtk.Button? current_nav_btn = null;
45
46 // ── Theme ───────────────────────────────────────────────────────────
47 private Gtk.CssProvider? css_provider = null;
48 private bool dark_mode = false;
49 private Gtk.Button? btn_theme_light = null;
50 private Gtk.Button? btn_theme_dark = null;
51
52 private const string THEME_CONFIG_DIR = "/etc/bastionguard-backup";
53 private const string THEME_CONFIG_FILE = "/etc/bastionguard-backup/theme.json";
54 private const string LANG_CONFIG_FILE = "/etc/bastionguard-backup/language.json";
55
56 // ── Language ────────────────────────────────────────────────────────
57 private Gtk.ComboBoxText? lang_combo = null;
58 private string current_lang = "en_US";
59
60 // Supported languages: code must match the .po filename (e.g. ar_SA → ar_SA.po)
61 private static string[] LANG_CODES = {
62 "en_US", "ar_SA", "de_DE", "es_ES", "fr_FR",
63 "it_IT", "ja_JP", "nl_NL", "pl_PL", "pt_PT", "ru_RU"
64 };
65 private static string[] LANG_NAMES = {
66 "English", "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", "Deutsch", "Espa\u00f1ol", "Fran\u00e7ais",
67 "Italiano", "\u65e5\u672c\u8a9e", "Nederlands", "Polski", "Portugu\u00eas", "\u0420\u0443\u0441\u0441\u043a\u0438\u0439"
68 };
69
70 // ── Action buttons ──────────────────────────────────────────────────
71 private Gtk.Button btn_backup;
72 private Gtk.Button btn_restore;
73 private Gtk.Button btn_delete_snapshot;
74 private Gtk.Button btn_browse_snapshot;
75
76 // Tracks every theme-sensitive Gtk.Image: name + size for reload on theme switch
77 private Gee.ArrayList<Gtk.Image> themed_img_list = new Gee.ArrayList<Gtk.Image>();
78 private Gee.ArrayList<string> themed_img_name = new Gee.ArrayList<string>();
79 private Gee.ArrayList<int> themed_img_size = new Gee.ArrayList<int>();
80
81 // Embedded child windows that also need icon reload on theme change
84
85 // ── Snapshot list ───────────────────────────────────────────────────
87
88 // ── Status card labels ──────────────────────────────────────────────
89 private Gtk.Image img_shield;
90 private Gtk.Label lbl_status_title;
91 private Gtk.Label lbl_status_detail;
92 private Gtk.Label lbl_snap_count;
93 private Gtk.Label lbl_free_space;
94 private Gtk.Label lbl_device_name;
95
96 // ── Misc ────────────────────────────────────────────────────────────
97 private uint tmr_init;
98 private int def_width = 500;
99 private int def_height = -1;
100 private int compact_window_width = 980;
101 private int compact_window_height = 520;
102 private bool window_in_expanded_mode = false;
103
104 public MainWindow () {
105
106 log_debug("MainWindow: MainWindow()");
107
108 this.title = "BastionGuard™ Backup";
109 this.modal = true;
110 this.set_default_size(980, 520);
111 this.set_resizable(true);
112 this.close_request.connect(on_close_request);
113 this.set_icon_name("bastionguard-backup");
114 this.set_titlebar(HeaderFactory.create_main_headerbar(this, 0));
115
116 // Carica lingua da config (prima del CSS, prima di qualsiasi _())
119
120 // Carica tema da config
122 IconManager.use_dark = dark_mode; // set before first icon creation
124
125 this.set_titlebar(HeaderFactory.create_main_headerbar(this, 180));
126
127 hbox_root = new Gtk.Box(Orientation.HORIZONTAL, 0);
128 set_child(hbox_root);
129 hbox_root.hexpand = true;
130 hbox_root.vexpand = false;
131 hbox_root.halign = Gtk.Align.FILL;
132 hbox_root.valign = Gtk.Align.FILL;
133
134 init_sidebar();
135 init_content();
136 activate_nav(btn_nav_snapshots, "snapshots");
138
139 if (App.live_system()) {
140 btn_backup.sensitive = false;
141 btn_nav_settings.sensitive = false;
142 btn_nav_wizard.sensitive = false;
143 }
144
145 tmr_init = Timeout.add(100, init_delayed);
146 log_debug("MainWindow: MainWindow(): exit");
147 }
148
149 // ─────────────────────────────────────────────────────────────────────
150 // HEADERBAR personalizzata
151 // ─────────────────────────────────────────────────────────────────────
152 private void build_headerbar() {
153
154 var header = new Gtk.HeaderBar();
155 header.set_show_title_buttons(false);
156 header.set_decoration_layout("");
157 header.get_style_context().add_class("custom-headerbar");
158
159 var header_left = new Gtk.Box(Orientation.HORIZONTAL, 0);
160 header_left.set_size_request(180, -1);
161 header_left.get_style_context().add_class("header-left");
162 header_left.margin_start = 0;
163 header_left.margin_end = 0;
164 header_left.margin_top = 0;
165 header_left.margin_bottom = 0;
166 header.set_title_widget(new Gtk.Box(Orientation.HORIZONTAL, 0));
167 var header_root = (Gtk.Box) header.get_title_widget();
168 header_root.hexpand = true;
169 header_root.append(header_left);
170
171 var header_mid = new Gtk.Box(Orientation.HORIZONTAL, 0);
172 header_mid.hexpand = true;
173 header_mid.get_style_context().add_class("header-right");
174 header_root.append(header_mid);
175
176 var header_right = new Gtk.Box(Orientation.HORIZONTAL, 0);
177 header_right.get_style_context().add_class("header-right");
178 header_right.valign = Gtk.Align.CENTER;
179 header_right.margin_end = 6;
180
181 var btn_min = new Gtk.Button();
182 btn_min.set_child(themed_icon("window-minimize-symbolic", 16));
183 btn_min.get_style_context().add_class("header-button");
184 btn_min.clicked.connect(() => { this.minimize(); });
185 header_right.append(btn_min);
186
187 var btn_close = new Gtk.Button();
188 btn_close.set_child(themed_icon("window-close-symbolic", 16));
189 btn_close.get_style_context().add_class("header-button");
190 btn_close.clicked.connect(() => { this.close(); });
191 header_right.append(btn_close);
192
193 header_root.append(header_right);
194 this.set_titlebar(header);
195 }
196
197 // ─────────────────────────────────────────────────────────────────────
198 // Theme config
199 // ─────────────────────────────────────────────────────────────────────
200 private bool load_theme_preference() {
201 try {
202 if (!FileUtils.test(THEME_CONFIG_FILE, FileTest.EXISTS)) {
203 return false;
204 }
205
206 string content;
207 FileUtils.get_contents(THEME_CONFIG_FILE, out content);
208
209 var parser = new Json.Parser();
210 parser.load_from_data(content, -1);
211
212 Json.Node root = parser.get_root();
213 if (root == null || root.get_node_type() != Json.NodeType.OBJECT) {
214 log_error("Theme config non valido, uso light");
215 return false;
216 }
217
218 Json.Object obj = root.get_object();
219 if (!obj.has_member("theme")) {
220 log_debug("Chiave 'theme' mancante, uso light");
221 return false;
222 }
223
224 string theme = obj.get_string_member("theme");
225 if (theme == "dark") {
226 log_debug("Tema letto da config: dark");
227 return true;
228 }
229
230 log_debug("Tema letto da config: light");
231 return false;
232 }
233 catch (Error e) {
234 log_error("Errore lettura theme config: " + e.message);
235 return false;
236 }
237 }
238
239 private void save_theme_preference(bool use_dark) {
240 try {
241 // Crea la directory se manca
242 if (!FileUtils.test(THEME_CONFIG_DIR, FileTest.IS_DIR)) {
243 DirUtils.create_with_parents(THEME_CONFIG_DIR, 0755);
244 }
245
246 string theme = use_dark ? "dark" : "light";
247 string json = "{\n\t\"theme\": \"%s\"\n}\n".printf(theme);
248
249 FileUtils.set_contents(THEME_CONFIG_FILE, json);
250
251 log_debug("Theme salvato in " + THEME_CONFIG_FILE + ": " + theme);
252 }
253 catch (Error e) {
254 log_error("Errore salvataggio theme config: " + e.message);
255 }
256 }
257
258 // ─────────────────────────────────────────────────────────────────────
259 // Language config
260 // ─────────────────────────────────────────────────────────────────────
261
262 /**
263 * Reads the saved language code from language.json.
264 * Returns "en_US" if the file is absent or invalid (first run).
265 */
266 private string load_language_preference() {
267 try {
268 if (!FileUtils.test(LANG_CONFIG_FILE, FileTest.EXISTS)) {
269 log_debug("Language config non trovato, uso English (en_US)");
270 return "en_US";
271 }
272
273 string content;
274 FileUtils.get_contents(LANG_CONFIG_FILE, out content);
275
276 var parser = new Json.Parser();
277 parser.load_from_data(content, -1);
278
279 Json.Node root = parser.get_root();
280 if (root == null || root.get_node_type() != Json.NodeType.OBJECT) {
281 log_error("Language config non valido, uso en_US");
282 return "en_US";
283 }
284
285 Json.Object obj = root.get_object();
286 if (!obj.has_member("language")) {
287 log_debug("Chiave 'language' mancante, uso en_US");
288 return "en_US";
289 }
290
291 string lang = obj.get_string_member("language");
292
293 // Validate against supported codes
294 foreach (var code in LANG_CODES) {
295 if (lang == code) {
296 log_debug("Lingua letta da config: " + lang);
297 return lang;
298 }
299 }
300
301 log_debug("Lingua non supportata '%s', uso en_US".printf(lang));
302 return "en_US";
303 }
304 catch (Error e) {
305 log_error("Errore lettura language config: " + e.message);
306 return "en_US";
307 }
308 }
309
310 private void save_language_preference(string lang_code) {
311 try {
312 if (!FileUtils.test(THEME_CONFIG_DIR, FileTest.IS_DIR)) {
313 DirUtils.create_with_parents(THEME_CONFIG_DIR, 0755);
314 }
315
316 string json = "{\n\t\"language\": \"%s\"\n}\n".printf(lang_code);
317 FileUtils.set_contents(LANG_CONFIG_FILE, json);
318
319 log_debug("Lingua salvata in " + LANG_CONFIG_FILE + ": " + lang_code);
320 }
321 catch (Error e) {
322 log_error("Errore salvataggio language config: " + e.message);
323 }
324 }
325
326 /**
327 * Sets the process locale WITHOUT touching system settings.
328 * Must be called before any _() translation is evaluated.
329 * lang_code matches the .po filename: en_US, it_IT, de_DE, etc.
330 */
331 private void apply_language_env(string lang_code) {
332 string locale_val;
333 switch (lang_code) {
334 case "ar_SA": locale_val = "ar_SA.UTF-8"; break;
335 case "de_DE": locale_val = "de_DE.UTF-8"; break;
336 case "es_ES": locale_val = "es_ES.UTF-8"; break;
337 case "fr_FR": locale_val = "fr_FR.UTF-8"; break;
338 case "it_IT": locale_val = "it_IT.UTF-8"; break;
339 case "ja_JP": locale_val = "ja_JP.UTF-8"; break;
340 case "nl_NL": locale_val = "nl_NL.UTF-8"; break;
341 case "pl_PL": locale_val = "pl_PL.UTF-8"; break;
342 case "pt_PT": locale_val = "pt_PT.UTF-8"; break;
343 case "ru_RU": locale_val = "ru_RU.UTF-8"; break;
344 default: locale_val = "en_US.UTF-8"; break;
345 }
346
347 GLib.Environment.set_variable("LANGUAGE", lang_code, true);
348 GLib.Environment.set_variable("LC_MESSAGES", locale_val, true);
349 GLib.Environment.set_variable("LANG", locale_val, true);
350
351 GLib.Intl.setlocale(GLib.LocaleCategory.MESSAGES, locale_val);
352 GLib.Intl.bindtextdomain(GETTEXT_PACKAGE, LOCALE_DIR);
353 GLib.Intl.bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
354 GLib.Intl.textdomain(GETTEXT_PACKAGE);
355
356 if (lang_code == "ar_SA") {
357 Gtk.Widget.set_default_direction(Gtk.TextDirection.RTL);
358 } else {
359 Gtk.Widget.set_default_direction(Gtk.TextDirection.LTR);
360 }
361
362 log_debug("Locale applicato (processo): " + locale_val);
363 }
364
365 private void on_language_changed() {
366 if (lang_combo == null) return;
367
368 int idx = lang_combo.get_active();
369 if (idx < 0 || idx >= LANG_CODES.length) return;
370
371 string new_lang = LANG_CODES[idx];
372 if (new_lang == current_lang) return;
373
374 save_language_preference(new_lang);
375 current_lang = new_lang;
376
377 // gettext caches translations at first use — the only reliable way
378 // to switch language at runtime is to re-exec the current process.
379 var dlg = new Gtk.MessageDialog(
380 this,
381 Gtk.DialogFlags.MODAL,
382 Gtk.MessageType.QUESTION,
383 Gtk.ButtonsType.YES_NO,
384 _("Language saved. The application must restart to apply the new language.\n\nRestart now?")
385 );
386 var resp = gtk_dialog_run(dlg);
387 dlg.destroy();
388
389 if (resp == Gtk.ResponseType.YES) {
391 }
392 }
393
394 /**
395 * Spawns a fresh instance of the application and exits the current one.
396 * The new process reads language.json at startup and loads the correct locale
397 * before any _() call — this is the only reliable way to switch gettext locale.
398 */
399 private void restart_application() {
400 string exe_path = "";
401 try {
402 exe_path = GLib.FileUtils.read_link("/proc/self/exe");
403 } catch (Error e) {
404 log_error("restart: cannot read /proc/self/exe: " + e.message);
405 App.exit_app(0);
406 return;
407 }
408
409 string[] argv = { exe_path };
410
411 try {
412 GLib.Process.spawn_async(
413 null,
414 argv,
415 null,
416 GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
417 null,
418 null
419 );
420 } catch (Error e) {
421 log_error("restart: spawn failed: " + e.message);
422 App.exit_app(0);
423 return;
424 }
425
426 App.exit_app(0);
427 }
428
429 // ─────────────────────────────────────────────────────────────────────
430 // Carica CSS light/dark
431 // ─────────────────────────────────────────────────────────────────────
432 private void load_css(bool use_dark = false) {
433
434 string css_name = use_dark ? "BastionGuard-dark.css" : "BastionGuard.css";
435 string css_res = ResourceHelper.resource_prefix() + "/css/" + css_name;
436 string? path = ResourceHelper.resolve_css(css_name);
437
438 if (css_provider != null) {
439 Gtk.StyleContext.remove_provider_for_display(
440 Gdk.Display.get_default(),
442 );
443 css_provider = null;
444 }
445
446 css_provider = new Gtk.CssProvider();
447 try {
448 if (ResourceHelper.resource_exists("css/" + css_name)) {
449 css_provider.load_from_resource(css_res);
450 log_debug("CSS caricato da resource: " + css_res);
451 } else if (path != null && FileUtils.test(path, FileTest.EXISTS)) {
452 css_provider.load_from_path(path);
453 log_debug("CSS caricato da path: " + path);
454 } else {
455 log_debug(css_name + " non trovato, uso stile di default");
456 return;
457 }
458 Gtk.StyleContext.add_provider_for_display(
459 Gdk.Display.get_default(),
461 Gtk.STYLE_PROVIDER_PRIORITY_USER
462 );
463
464 var gtk_settings = Gtk.Settings.get_default();
465 if (gtk_settings != null) {
466 gtk_settings.gtk_application_prefer_dark_theme = use_dark;
467 }
468
469 dark_mode = use_dark;
470 // Tell IconManager which theme is active so future lookups use the right icons
471 IconManager.use_dark = use_dark;
472 // Reload already-rendered themed icons to the correct colour variant
475 return;
476 }
477 catch (Error e) {
478 log_error("CSS error: " + e.message);
479 }
480 }
481
482 private void apply_theme(bool use_dark) {
483 load_css(use_dark);
484 save_theme_preference(use_dark);
485 this.queue_draw();
486 }
487
488 private void update_theme_buttons() {
489 if (btn_theme_light == null || btn_theme_dark == null)
490 return;
491
492 var light_btn = btn_theme_light;
493 var dark_btn = btn_theme_dark;
494
495 if (dark_mode) {
496 dark_btn.get_style_context().add_class("active-sidebar-button");
497 light_btn.get_style_context().remove_class("active-sidebar-button");
498 }
499 else {
500 light_btn.get_style_context().add_class("active-sidebar-button");
501 dark_btn.get_style_context().remove_class("active-sidebar-button");
502 }
503 }
504
505 // ─────────────────────────────────────────────────────────────────────
506 // SIDEBAR
507 // ─────────────────────────────────────────────────────────────────────
508 private void init_sidebar() {
509
510 vbox_sidebar = new Gtk.Box(Orientation.VERTICAL, 0);
511 vbox_sidebar.set_size_request(180, -1);
512 vbox_sidebar.get_style_context().add_class("sidebar");
513 vbox_sidebar.hexpand = false;
514 vbox_sidebar.vexpand = true;
515 vbox_sidebar.halign = Gtk.Align.FILL;
516 vbox_sidebar.valign = Gtk.Align.FILL;
517 hbox_root.append(vbox_sidebar);
518
519 var header_box = new Gtk.Box(Orientation.HORIZONTAL, 8);
520 header_box.get_style_context().add_class("sidebar-header");
521 vbox_sidebar.append(header_box);
522
523 var logo = IconManager.lookup_image("bastionguard-backup", 32);
524 header_box.append(logo);
525
526 var title_lbl = new Gtk.Label("BastionGuard™\nBackup");
527 title_lbl.xalign = 0;
528 title_lbl.get_style_context().add_class("sidebar-title");
529 header_box.append(title_lbl);
530
531 vbox_sidebar.append(new Gtk.Box(Orientation.VERTICAL, 0));
532
533 btn_nav_snapshots = make_nav_btn("document-save-symbolic", _("Snapshot"), "snapshots");
534 btn_nav_settings = make_nav_btn("preferences-system-symbolic", _("Impostazioni"), "settings_page");
535 btn_nav_wizard = make_nav_btn("emblem-default-symbolic", _("Procedura guidata"), "wizard");
536
537 var spacer = new Gtk.Box(Orientation.VERTICAL, 0);
538 spacer.vexpand = true;
539 vbox_sidebar.append(spacer);
540
541 // Theme section
542 var theme_sep = new Gtk.Separator(Orientation.HORIZONTAL);
543 theme_sep.margin_top = 6;
544 theme_sep.margin_bottom = 6;
545 theme_sep.margin_start = 8;
546 theme_sep.margin_end = 8;
547 vbox_sidebar.append(theme_sep);
548
549 var theme_title = new Gtk.Label(_("Theme"));
550 theme_title.xalign = 0;
551 theme_title.margin_start = 18;
552 theme_title.margin_end = 18;
553 theme_title.margin_top = 4;
554 theme_title.margin_bottom = 6;
555 vbox_sidebar.append(theme_title);
556
557 var theme_box = new Gtk.Box(Orientation.HORIZONTAL, 6);
558 theme_box.margin_start = 10;
559 theme_box.margin_end = 10;
560 theme_box.margin_bottom = 8;
561 vbox_sidebar.append(theme_box);
562
563 btn_theme_light = make_theme_btn("weather-clear-symbolic", _("Chiaro"));
564 btn_theme_light.clicked.connect(() => { apply_theme(false); });
565 theme_box.append(btn_theme_light);
566
567 btn_theme_dark = make_theme_btn("weather-clear-night-symbolic", _("Scuro"));
568 btn_theme_dark.clicked.connect(() => { apply_theme(true); });
569 theme_box.append(btn_theme_dark);
570
571 // ── Language selector ──────────────────────────────────────────
572 var lang_sep = new Gtk.Separator(Orientation.HORIZONTAL);
573 lang_sep.margin_top = 6;
574 lang_sep.margin_bottom = 6;
575 lang_sep.margin_start = 8;
576 lang_sep.margin_end = 8;
577 vbox_sidebar.append(lang_sep);
578
579 var lang_title = new Gtk.Label(_("Language"));
580 lang_title.xalign = 0;
581 lang_title.margin_start = 18;
582 lang_title.margin_end = 18;
583 lang_title.margin_top = 2;
584 lang_title.margin_bottom = 4;
585 vbox_sidebar.append(lang_title);
586
587 lang_combo = new Gtk.ComboBoxText();
588 lang_combo.margin_start = 10;
589 lang_combo.margin_end = 10;
590 lang_combo.margin_bottom = 8;
591 lang_combo.get_style_context().add_class("sidebar-combo");
592
593 int active_idx = 0;
594 for (int i = 0; i < LANG_CODES.length; i++) {
595 lang_combo.append_text(LANG_NAMES[i]);
596 if (LANG_CODES[i] == current_lang) active_idx = i;
597 }
598 lang_combo.set_active(active_idx);
599 lang_combo.changed.connect(on_language_changed);
600 vbox_sidebar.append(lang_combo);
601 // ──────────────────────────────────────────────────────────────
602
603 var sep = new Gtk.Separator(Orientation.HORIZONTAL);
604 sep.margin_top = 6;
605 sep.margin_bottom = 6;
606 sep.margin_start = 8;
607 sep.margin_end = 8;
608 vbox_sidebar.append(sep);
609
610 btn_nav_about = make_nav_btn("help-about-symbolic", _("Informazioni"), "about");
611
613 }
614
615 private Gtk.Button make_nav_btn(string icon, string label, string page) {
616 var btn = new Gtk.Button();
617 btn.get_style_context().add_class("sidebar-button");
618 btn.hexpand = true;
619
620 var box = new Gtk.Box(Orientation.HORIZONTAL, 12);
621 box.margin_start = 6;
622
623 var img = themed_icon(icon, 18);
624 box.append(img);
625
626 var lbl = new Gtk.Label(label);
627 lbl.xalign = 0;
628 box.append(lbl);
629
630 btn.set_child(box);
631 vbox_sidebar.append(btn);
632
633 btn.clicked.connect(() => { activate_nav(btn, page); });
634 return btn;
635 }
636
637 private Gtk.Button make_theme_btn(string icon, string label) {
638 var btn = new Gtk.Button();
639 btn.get_style_context().add_class("sidebar-button");
640 btn.hexpand = true;
641
642 var box = new Gtk.Box(Orientation.HORIZONTAL, 8);
643 box.margin_start = 6;
644 box.margin_end = 6;
645
646 var img = themed_icon(icon, 18);
647 box.append(img);
648
649 var lbl = new Gtk.Label(label);
650 lbl.xalign = 0.5f;
651 box.append(lbl);
652
653 btn.set_child(box);
654 return btn;
655 }
656
657 private void activate_nav(Gtk.Button btn, string page) {
658 if (current_nav_btn != null)
659 current_nav_btn.get_style_context().remove_class("active-sidebar-button");
660 btn.get_style_context().add_class("active-sidebar-button");
661 current_nav_btn = btn;
662
663 switch (page) {
664 case "snapshots":
665 case "settings_page":
666 case "wizard":
667 case "about":
668 case "backup_flow":
669 case "restore_flow":
670 case "delete_flow":
671 case "rsync_log":
672 content_stack.set_visible_child_name(page);
674 break;
675 }
676 }
677
678
679 private void show_snapshots_page() {
680 activate_nav(btn_nav_snapshots, "snapshots");
681 }
682
683 private bool page_requires_expanded_window(string page) {
684 switch (page) {
685 case "settings_page":
686 case "wizard":
687 case "backup_flow":
688 case "restore_flow":
689 case "delete_flow":
690 case "rsync_log":
691 return true;
692 default:
693 return false;
694 }
695 }
696
699 return;
700
701 int current_width = this.get_width();
702 int current_height = this.get_height();
703
704 if (current_width > 0)
705 compact_window_width = current_width;
706 if (current_height > 0)
707 compact_window_height = current_height;
708 }
709
710 private void resize_main_window(int width, int height) {
711 this.set_default_size(width, height);
712 this.set_size_request(width, height);
713 Idle.add(() => {
714 this.set_size_request(-1, -1);
715 return false;
716 });
717 }
718
719 private void apply_window_mode_for_page(string page) {
723
724 switch (page) {
725 case "settings_page":
726 resize_main_window(1100, 760);
727 break;
728 case "wizard":
729 resize_main_window(980, 620);
730 break;
731 case "backup_flow":
732 resize_main_window(980, 680);
733 break;
734 case "restore_flow":
735 resize_main_window(1080, 720);
736 break;
737 case "delete_flow":
738 resize_main_window(500, 500);
739 break;
740 case "rsync_log":
741 resize_main_window(800, 600);
742 break;
743 }
744 }
745 else {
748 }
749 }
750
751 private void set_dynamic_page(string name, ref Gtk.Widget? slot, Gtk.Widget widget) {
752 if (slot != null) {
753 content_stack.remove(slot);
754 slot = null;
755 }
756 slot = widget;
757 content_stack.add_named(slot, name);
758 content_stack.set_visible_child_name(name);
760 }
761
762 private void open_backup_flow() {
763 var embedded = new BackupWindow(true);
764 embedded.request_close.connect(() => {
765 refresh_all();
766 ui_sensitive(true);
768 });
769 set_dynamic_page("backup_flow", ref backup_page_widget, embedded.detach_root_widget());
770 }
771
772 private void open_delete_flow() {
773 var embedded = new DeleteWindow(true);
774 embedded.request_close.connect(() => {
775 refresh_all();
776 ui_sensitive(true);
778 });
779 set_dynamic_page("delete_flow", ref delete_page_widget, embedded.detach_root_widget());
780 }
781
782 private void open_restore_flow() {
783 var embedded = new RestoreWindow(true);
784 embedded.request_close.connect(() => {
785 App.dry_run = false;
787 refresh_all();
789 });
790 set_dynamic_page("restore_flow", ref restore_page_widget, embedded.detach_root_widget());
791 }
792
793 private void open_rsync_log_page(string log_file) {
794 var embedded = new RsyncLogWindow(log_file, true);
795 embedded.request_close.connect(() => {
797 });
798 set_dynamic_page("rsync_log", ref rsync_log_page_widget, embedded.detach_root_widget());
799 }
800
801 private void init_settings_page() {
803 embedded_settings.request_close.connect(() => {
805 });
807 content_stack.add_named(settings_page_widget, "settings_page");
808 }
809
810 private void init_wizard_page() {
812 embedded_wizard.request_close.connect(() => {
814 });
816 content_stack.add_named(wizard_page_widget, "wizard");
817 }
818
819
820 private void init_about_page() {
821 var page = new Gtk.Box(Orientation.VERTICAL, 0);
822 page.hexpand = true;
823 page.vexpand = true;
824 page.halign = Gtk.Align.FILL;
825 page.valign = Gtk.Align.FILL;
826
827 var title = new Gtk.Label(_("Informazioni su BastionGuard Backup"));
828 title.xalign = 0;
829 title.get_style_context().add_class("page-title");
830 title.margin_start = 20;
831 title.margin_top = 20;
832 title.margin_bottom = 12;
833 page.append(title);
834
835 var card = make_card(true);
836 card.margin_start = 14;
837 card.margin_end = 14;
838 card.margin_bottom = 16;
839 page.append(card);
840
841 var body = new Gtk.Box(Orientation.VERTICAL, 10);
842 body.margin_start = 12;
843 body.margin_end = 12;
844 body.margin_top = 12;
845 body.margin_bottom = 12;
846 card.append(body);
847
848 body.append(IconManager.lookup_image("bastionguard-backup", 96));
849
850 var lbl_title = new Gtk.Label("BastionGuard™ Backup");
851 lbl_title.xalign = 0.5f;
852 lbl_title.wrap = true;
853 lbl_title.get_style_context().add_class("title");
854 body.append(lbl_title);
855
856 var lbl_version = new Gtk.Label(AppVersion);
857 lbl_version.xalign = 0.5f;
858 body.append(lbl_version);
859
860 var lbl_desc = new Gtk.Label("Fork di Timeshift con shell UI a pagina singola e sidebar persistente.");
861 lbl_desc.xalign = 0.5f;
862 lbl_desc.wrap = true;
863 lbl_desc.set_justify(Gtk.Justification.CENTER);
864 body.append(lbl_desc);
865
866 var link = new Gtk.LinkButton.with_label("https://bastionguard.eu", "bastionguard.eu");
867 link.set_halign(Gtk.Align.CENTER);
868 link.activate_link.connect(() => {
869 TeeJee.System.xdg_open("https://bastionguard.eu");
870 return true;
871 });
872 body.append(link);
873
874 var lbl_copy = new Gtk.Label("© 2025–2026 BastionGuard™. All Rights Reserved.");
875 lbl_copy.xalign = 0.5f;
876 lbl_copy.wrap = true;
877 lbl_copy.set_justify(Gtk.Justification.CENTER);
878 body.append(lbl_copy);
879
880 var lbl_tm = new Gtk.Label("BastionGuard was filed by Calogero Scarnà with the Italian Patent and Trademark Office (UIBM) on 16/12/2025 – Application No. 302025000196519.
881The BastionGuard™ name and branding are not licensed under the GPLv3.");
882 lbl_tm.xalign = 0.5f;
883 lbl_tm.wrap = true;
884 lbl_tm.set_justify(Gtk.Justification.CENTER);
885 body.append(lbl_tm);
886
887 var lbl_license = new Gtk.Label(_("BastionGuard Backup is a fork of Timeshift by Tony George.
888This program is free software licensed under GNU GPL v2 or later."));
889 lbl_license.xalign = 0.5f;
890 lbl_license.wrap = true;
891 lbl_license.set_justify(Gtk.Justification.CENTER);
892 body.append(lbl_license);
893
894 var filler = new Gtk.Box(Orientation.VERTICAL, 0);
895 filler.vexpand = true;
896 page.append(filler);
897
898 about_page_widget = page;
899 content_stack.add_named(about_page_widget, "about");
900 }
901
902
903 // ─────────────────────────────────────────────────────────────────────
904 // CONTENT
905 // ─────────────────────────────────────────────────────────────────────
906 private void init_content() {
907 var vbox_content = new Gtk.Box(Orientation.VERTICAL, 0);
908 vbox_content.hexpand = true;
909 vbox_content.vexpand = false;
910 vbox_content.halign = Gtk.Align.FILL;
911 vbox_content.valign = Gtk.Align.FILL;
912 hbox_root.append(vbox_content);
913
914 content_stack = new Gtk.Stack();
915 content_stack.hexpand = true;
916 content_stack.vexpand = false;
917 content_stack.set_hhomogeneous(false);
918 content_stack.set_vhomogeneous(false);
919 content_stack.halign = Gtk.Align.FILL;
920 content_stack.valign = Gtk.Align.FILL;
921 content_stack.transition_type = Gtk.StackTransitionType.CROSSFADE;
922 content_stack.transition_duration = 160;
923 vbox_content.append(content_stack);
924
929 }
930
931 private void init_snapshots_page() {
932
933 var vbox = new Gtk.Box(Orientation.VERTICAL, 0);
934 content_stack.add_named(vbox, "snapshots");
935 vbox.hexpand = true;
936 vbox.vexpand = false;
937 vbox.halign = Gtk.Align.FILL;
938 vbox.valign = Gtk.Align.FILL;
939
940 var page_title = new Gtk.Label(_("Snapshots"));
941 page_title.xalign = 0;
942 page_title.get_style_context().add_class("page-title");
943 page_title.margin_start = 14;
944 page_title.margin_top = 8;
945 page_title.margin_bottom = 6;
946 vbox.append(page_title);
947
948 var cards_box = new Gtk.Box(Orientation.HORIZONTAL, 0);
949 cards_box.margin_start = 14;
950 cards_box.margin_end = 14;
951 cards_box.margin_bottom = 6;
952 vbox.append(cards_box);
953
954 var card_status = make_card(true);
955 cards_box.append(card_status);
956
957 var status_inner = new Gtk.Box(Orientation.HORIZONTAL, 12);
958 card_status.append(status_inner);
959
960 img_shield = new Gtk.Image();
961 img_shield.set_from_pixbuf(IconManager.lookup(
963 status_inner.append(img_shield);
964
965 var status_text = new Gtk.Box(Orientation.VERTICAL, 2);
966 status_inner.append(status_text);
967
968 lbl_status_title = new Gtk.Label("—");
969 lbl_status_title.xalign = 0;
970 lbl_status_title.get_style_context().add_class("title");
971 lbl_status_title.wrap = true;
972 status_text.append(lbl_status_title);
973
974 lbl_status_detail = new Gtk.Label("");
975 lbl_status_detail.xalign = 0;
976 lbl_status_detail.wrap = true;
977 status_text.append(lbl_status_detail);
978
979 var card_snaps = make_card(false);
980 card_snaps.set_size_request(150, -1);
981 cards_box.append(card_snaps);
982
983 var snaps_hdr = new Gtk.Label(_("Snapshots"));
984 snaps_hdr.xalign = 0;
985 card_snaps.append(snaps_hdr);
986
987 lbl_snap_count = new Gtk.Label("0");
988 lbl_snap_count.xalign = 0;
989 lbl_snap_count.get_style_context().add_class("title");
990 card_snaps.append(lbl_snap_count);
991
992 var card_space = make_card(false);
993 card_space.set_size_request(170, -1);
994 cards_box.append(card_space);
995
996 var space_hdr = new Gtk.Label(_("Free space"));
997 space_hdr.xalign = 0;
998 card_space.append(space_hdr);
999
1000 lbl_free_space = new Gtk.Label("—");
1001 lbl_free_space.xalign = 0;
1002 lbl_free_space.get_style_context().add_class("title");
1003 card_space.append(lbl_free_space);
1004
1005 lbl_device_name = new Gtk.Label("");
1006 lbl_device_name.xalign = 0;
1007 card_space.append(lbl_device_name);
1008
1009 var action_bar = new Gtk.Box(Orientation.HORIZONTAL, 4);
1010 action_bar.margin_start = 14;
1011 action_bar.margin_end = 14;
1012 action_bar.margin_bottom = 6;
1013 vbox.append(action_bar);
1014
1015 btn_backup = make_action_btn("document-save-symbolic", _("Create"), false, false, "btn-success");
1016 action_bar.append(btn_backup);
1017 btn_backup.clicked.connect(create_snapshot);
1018
1019 btn_restore = make_action_btn("document-open-recent-symbolic", _("Restore"), false, false, "btn-primary");
1020 action_bar.append(btn_restore);
1021 btn_restore.clicked.connect(btn_restore_clicked);
1022
1023 btn_browse_snapshot = make_action_btn("folder-symbolic", _("Browse"), false, false, "btn-info");
1024 action_bar.append(btn_browse_snapshot);
1025 btn_browse_snapshot.clicked.connect(browse_selected);
1026
1027 btn_delete_snapshot = make_action_btn("edit-delete-symbolic", _("Delete"), false, false, "btn-danger");
1028 action_bar.append(btn_delete_snapshot);
1029 btn_delete_snapshot.clicked.connect(delete_selected);
1030
1031 var sep2 = new Gtk.Box(Orientation.HORIZONTAL, 0);
1032 sep2.hexpand = true;
1033 action_bar.append(sep2);
1034
1035 var btn_menu = make_action_btn("open-menu-symbolic", "", false, false);
1036 btn_menu.set_tooltip_text(_("More options"));
1037 action_bar.append(btn_menu);
1038 btn_menu.clicked.connect(() => { btn_about_clicked(); });
1039
1040 var list_card = new Gtk.Box(Orientation.VERTICAL, 0);
1041 list_card.get_style_context().add_class("card");
1042 list_card.margin_start = 14;
1043 list_card.margin_end = 14;
1044 list_card.margin_bottom = 6;
1045 list_card.hexpand = true;
1046 list_card.vexpand = false;
1047 list_card.halign = Gtk.Align.FILL;
1048 list_card.valign = Gtk.Align.FILL;
1049
1051 snapshot_list_box.vexpand = false;
1052 snapshot_list_box.hexpand = true;
1053 snapshot_list_box.halign = Gtk.Align.FILL;
1054 snapshot_list_box.valign = Gtk.Align.FILL;
1055 list_card.append(snapshot_list_box);
1056 vbox.append(list_card);
1057
1062 }
1063
1064 // ─────────────────────────────────────────────────────────────────────
1065 // Helper UI
1066 // ─────────────────────────────────────────────────────────────────────
1067 private Gtk.Box make_card(bool hexpand) {
1068 var card = new Gtk.Box(Orientation.VERTICAL, 4);
1069 card.get_style_context().add_class("card");
1070 card.hexpand = hexpand;
1071 card.margin_start = 4;
1072 card.margin_end = 4;
1073 card.margin_top = 4;
1074 card.margin_bottom = 4;
1075 return card;
1076 }
1077
1078 private Gtk.Button make_action_btn(string icon, string label, bool primary, bool danger, string css_class = "") {
1079 var btn = new Gtk.Button();
1080 btn.get_style_context().add_class("action-btn");
1081 if (css_class.length > 0) {
1082 btn.get_style_context().add_class(css_class);
1083 } else {
1084 if (primary) btn.get_style_context().add_class("btn-success");
1085 if (danger) btn.get_style_context().add_class("btn-danger");
1086 }
1087 btn.get_style_context().remove_class("button");
1088
1089 if (label.length == 0) {
1090 btn.set_child(themed_icon(icon, 16));
1091 } else {
1092 var box = new Gtk.Box(Orientation.HORIZONTAL, 6);
1093 box.append(themed_icon(icon, 16));
1094 box.append(new Gtk.Label(label));
1095 btn.set_child(box);
1096 }
1097 return btn;
1098 }
1099
1100 // ─────────────────────────────────────────────────────────────────────
1101 // Logica
1102 // ─────────────────────────────────────────────────────────────────────
1103 private bool init_delayed() {
1104 if (tmr_init > 0) { Source.remove(tmr_init); tmr_init = 0; }
1105 if ((App.repo == null) || !App.repo.available()) {
1106 if (App.backup_parent_uuid.length > 0)
1108 }
1109 refresh_all();
1111 return false;
1112 }
1113
1114 private bool refresh_all() {
1115 ui_sensitive(false);
1118 ui_sensitive(true);
1119 return false;
1120 }
1121
1122 private bool on_close_request() {
1123 this.close_request.disconnect(on_close_request);
1124 if (App.task.status == AppStatus.RUNNING) {
1125 log_error(_("Main window closed by user"));
1126 App.task.stop();
1127 }
1129 App.delete_list.clear();
1130 if (App.delete_file_task != null)
1132 }
1133 if (!App.live_system()) {
1134 if (!App.repo.available() || !App.repo.has_space()) {
1135 var dlg = new CustomMessageDialog(
1136 App.repo.status_message, _("Select another device?"),
1137 Gtk.MessageType.ERROR, this, Gtk.ButtonsType.YES_NO);
1138 var resp = gtk_dialog_run(dlg);
1139 dlg.destroy();
1140 if (resp == Gtk.ResponseType.YES) {
1141 this.close_request.connect(on_close_request);
1143 return true;
1144 }
1145 return false;
1146 }
1147 }
1148 App.exit_app(0);
1149 return false;
1150 }
1151
1152 public void create_snapshot() {
1153 if (check_if_deletion_running()) return;
1154 ui_sensitive(false);
1155 if (App.btrfs_mode && (App.check_btrfs_layout_system(this) == false)) {
1156 ui_sensitive(true); return;
1157 }
1158 if (!App.repo.available())
1159 gtk_messagebox(App.repo.status_message, App.repo.status_details, this, true);
1161 }
1162
1163 public void delete_selected() {
1164 if (!App.repo.available()) {
1165 gtk_messagebox(App.repo.status_message, _("Select another device"), this, false);
1166 return;
1167 }
1168 if (!App.repo.has_snapshots()) {
1169 gtk_messagebox(_("No snapshots on device"), _("Select another device"), this, false);
1170 return;
1171 }
1172 foreach (var bak in snapshot_list_box.selected_snapshots()) {
1173 if (bak.live) {
1174 gtk_messagebox(_("Cannot Delete Live Snapshot"),
1175 _("Snapshot '%s' is in use").printf(bak.date_formatted), this, false);
1176 return;
1177 }
1178 }
1179 var dlg = new Gtk.MessageDialog(this, Gtk.DialogFlags.MODAL,
1180 Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO,
1181 _("Are you sure you want to delete this snapshot?"));
1182 var resp = gtk_dialog_run(dlg); dlg.destroy();
1183 if (resp != Gtk.ResponseType.YES) return;
1185 foreach (var item in snapshot_list_box.selected_snapshots()) {
1186 bool dup = false;
1187 foreach (var bak in App.delete_list) {
1188 if (bak.name == item.name) { dup = true; break; }
1189 }
1190 if (!dup) App.delete_list.add(item);
1191 }
1192 }
1193 ui_sensitive(false);
1195 }
1196
1197 public void mark_selected() {
1198 var selected = snapshot_list_box.selected_snapshots();
1199 if (selected.size == 0) {
1200 gtk_messagebox(_("No Snapshots Selected"), _("Select the snapshots to mark"), this, false);
1201 return;
1202 }
1203 bool marked = false;
1204 foreach (var bak in selected) {
1205 bak.mark_for_deletion();
1206 marked = marked || bak.marked_for_deletion;
1207 }
1209 gtk_messagebox(
1210 marked ? _("Marked for deletion") : _("Unmarked"),
1211 marked ? _("Snapshots will be removed at next scheduled run")
1212 : _("Snapshots will not be removed"),
1213 this, false);
1215 }
1216
1217 public void browse_selected() {
1218 var selected = snapshot_list_box.selected_snapshots();
1219 if (selected.size == 0) {
1220 TeeJee.System.xdg_open(
1221 File.new_for_path(App.repo.snapshots_path).query_exists()
1223 return;
1224 }
1225 var bak = selected[0];
1226 TeeJee.System.xdg_open(App.btrfs_mode ? bak.path : bak.path + "/localhost");
1227 }
1228
1229 public void view_snapshot_log(bool view_restore_log) {
1230 var selected = snapshot_list_box.selected_snapshots();
1231 if (selected.size == 0) {
1232 gtk_messagebox(_("Select Snapshot"), _("Please select a snapshot"), this, false);
1233 return;
1234 }
1235 var bak = selected[0];
1236 string log_file = view_restore_log ? bak.rsync_restore_log_file : bak.rsync_log_file;
1237 if (file_exists(log_file) || file_exists(log_file + "-changes")) {
1238 open_rsync_log_page(log_file);
1239 }
1240 }
1241
1242 private void btn_restore_clicked() {
1243 if (check_if_deletion_running()) return;
1244 App.mirror_system = false;
1245 restore();
1246 }
1247
1250 ui_sensitive(true);
1251 gtk_messagebox(_("Snapshot deletion in progress..."), _("Please wait."), this, true);
1252 ui_sensitive(false);
1254 return true;
1255 }
1256 return false;
1257 }
1258
1259 private void restore() {
1260 if (!App.mirror_system) {
1261 var selected = snapshot_list_box.selected_snapshots();
1262 if (selected.size == 0) {
1263 gtk_messagebox(_("No snapshots selected"), _("Select the snapshot to restore"), this, false);
1264 return;
1265 }
1266 if (selected.size > 1) {
1267 gtk_messagebox(_("Multiple snapshots selected"), _("Select a single snapshot"), this, false);
1268 return;
1269 }
1270 Snapshot snap = selected[0];
1271 if ((snap != null) && snap.marked_for_deletion) {
1272 gtk_messagebox(_("Invalid snapshot"),
1273 _("Selected snapshot is marked for deletion"), this, false);
1274 return;
1275 }
1276 App.snapshot_to_restore = snap;
1277 } else {
1278 App.snapshot_to_restore = null;
1279 }
1282 }
1283
1284
1285 public void show_settings() {
1287 }
1288 private void btn_settings_clicked() {
1289 content_stack.set_visible_child_name("settings_page");
1290 }
1291
1292 private void btn_wizard_clicked() {
1293 content_stack.set_visible_child_name("wizard");
1294 }
1295
1296 private void settings_changed(bool btrfs_mode_prev) {
1297 if (btrfs_mode_prev != App.btrfs_mode) {
1298 if ((App.repo != null) && (App.repo.device != null) && (App.repo.device.uuid.length > 0))
1299 App.repo = new SnapshotRepo.from_uuid(App.repo.device.uuid, this, App.btrfs_mode);
1300 else if ((App.sys_root != null) && (App.sys_root.fstype == "btrfs"))
1301 App.repo = new SnapshotRepo.from_uuid(App.sys_root.uuid, this, App.btrfs_mode);
1302 else
1303 App.repo = new SnapshotRepo.from_null();
1304 }
1307 refresh_all();
1308 }
1309
1310 private void btn_about_clicked() {
1311 content_stack.set_visible_child_name("about");
1312 }
1313
1314 private void ui_sensitive(bool enable) {
1315 vbox_sidebar.sensitive = enable;
1316 snapshot_list_box.sensitive = enable;
1317 gtk_set_busy(!enable, this);
1318 }
1319
1320 private void update_statusbar() {
1322 string message = App.repo.status_message;
1323 string details = App.repo.status_details;
1324 int status_code = App.repo.status_code;
1325
1326 switch (status_code) {
1327 case SnapshotLocationStatus.HAS_SNAPSHOTS_HAS_SPACE:
1328 img_shield.set_from_pixbuf(IconManager.lookup(
1330 break;
1331 case SnapshotLocationStatus.NO_SNAPSHOTS_HAS_SPACE:
1332 case SnapshotLocationStatus.HAS_SNAPSHOTS_NO_SPACE:
1333 img_shield.set_from_pixbuf(IconManager.lookup(
1334 IconManager.SHIELD_MED, 48));
1335 break;
1336 default:
1337 img_shield.set_from_pixbuf(IconManager.lookup(
1338 IconManager.SHIELD_LOW, 48));
1339 break;
1340 }
1341
1342 lbl_status_title.set_text(message);
1343
1344 // Evita duplicazione con le card laterali
1345 switch (status_code) {
1346 case SnapshotLocationStatus.HAS_SNAPSHOTS_HAS_SPACE:
1347 lbl_status_detail.set_text(_("Repository available and ready"));
1348 break;
1349
1350 case SnapshotLocationStatus.NO_SNAPSHOTS_HAS_SPACE:
1351 lbl_status_detail.set_text(_("No snapshots yet"));
1352 break;
1353
1354 case SnapshotLocationStatus.HAS_SNAPSHOTS_NO_SPACE:
1355 lbl_status_detail.set_text(_("Low free space on backup device"));
1356 break;
1357
1358 default:
1359 lbl_status_detail.set_text(details);
1360 break;
1361 }
1362
1363 lbl_snap_count.set_text("%d".printf(App.repo.snapshots.size));
1364
1365 if ((App.repo != null) && (App.repo.device != null)) {
1368 } else {
1369 lbl_free_space.set_text("—");
1370 lbl_device_name.set_text("");
1371 }
1372 }
1373
1374
1375 // ─────────────────────────────────────────────────────────────────────
1376 // ICON THEME RELOAD
1377 // ─────────────────────────────────────────────────────────────────────
1378
1379 private Gtk.Image themed_icon(string icon_name, int size) {
1380 var img = IconManager.lookup_image(icon_name, size);
1381 themed_img_list.add(img);
1382 themed_img_name.add(icon_name);
1383 themed_img_size.add(size);
1384 return img;
1385 }
1386
1387 private void reload_themed_icons() {
1390 for (int i = 0; i < themed_img_list.size; i++) {
1391 var img = themed_img_list[i];
1392 var name = themed_img_name[i];
1393 var sz = themed_img_size[i];
1394 var pix = IconManager.lookup(name, sz);
1395 if (pix != null) {
1396 img.set_from_pixbuf(pix);
1397 } else {
1398 img.set_from_icon_name(name);
1399 }
1400 }
1401 }
1402}
1403
1404/* ──────────────────────────────────────────────────────────────────────
1405 * AboutWindow custom
1406 * ────────────────────────────────────────────────────────────────────── */
1407class AboutWindow : Gtk.Window {
1408
1409 private int def_width = 440;
1410 private int def_height = 430;
1411
1412 public AboutWindow(Gtk.Window parent) {
1413
1414 Object(
1415 title: _("About"),
1416 modal: true,
1417 transient_for: parent
1418 );
1419
1420 this.set_default_size(def_width, def_height);
1421 this.set_size_request(def_width, def_height);
1422 this.resizable = false;
1423 this.set_icon_name("bastionguard-backup");
1424 this.set_titlebar(HeaderFactory.create_main_headerbar(this, 0));
1425
1426 var root = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
1427 set_child(root);
1428
1429 var outer = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
1430 outer.margin_start = 18;
1431 outer.margin_end = 18;
1432 outer.margin_top = 18;
1433 outer.margin_bottom = 18;
1434 outer.get_style_context().add_class("card");
1435 root.append(outer);
1436
1437 var content = new Gtk.Box(Gtk.Orientation.VERTICAL, 10);
1438 content.margin_top = 12;
1439 content.margin_bottom = 12;
1440 content.margin_start = 18;
1441 content.margin_end = 18;
1442 outer.append(content);
1443
1444 Gtk.Widget logo_widget;
1445 try {
1446 var pix = IconManager.lookup("bastionguard-backup", 96);
1447 var img = new Gtk.Image.from_pixbuf(pix);
1448 logo_widget = img;
1449 }
1450 catch (Error e) {
1451 logo_widget = IconManager.lookup_image("help-about-symbolic", 96);
1452 }
1453 content.append(logo_widget);
1454
1455 var lbl_title = new Gtk.Label("BastionGuard™ Backup");
1456 lbl_title.get_style_context().add_class("title");
1457 lbl_title.set_justify(Gtk.Justification.CENTER);
1458 lbl_title.set_xalign(0.5f);
1459 content.append(lbl_title);
1460
1461 var lbl_version = new Gtk.Label(AppVersion);
1462 lbl_version.set_xalign(0.5f);
1463 content.append(lbl_version);
1464
1465 var lbl_desc = new Gtk.Label(_("System Restore Utility"));
1466 lbl_desc.set_xalign(0.5f);
1467 lbl_desc.set_justify(Gtk.Justification.CENTER);
1468 lbl_desc.wrap = true;
1469 content.append(lbl_desc);
1470
1471 var link = new Gtk.LinkButton.with_label("https://bastionguard.eu", "bastionguard.eu");
1472 link.set_halign(Gtk.Align.CENTER);
1473 link.activate_link.connect(() => {
1474 TeeJee.System.xdg_open("https://bastionguard.eu");
1475 return true;
1476 });
1477 content.append(link);
1478
1479 var lbl_copy = new Gtk.Label(
1480 "© 2025–2026 BastionGuard™. All Rights Reserved."
1481 );
1482 lbl_copy.set_xalign(0.5f);
1483 lbl_copy.set_justify(Gtk.Justification.CENTER);
1484 lbl_copy.wrap = true;
1485 content.append(lbl_copy);
1486
1487 var lbl_trademark = new Gtk.Label(
1488 "BastionGuard was filed by Calogero Scarnà with the Italian Patent and Trademark Office (UIBM) on 16/12/2025 – Application No. 302025000196519.\n"
1489 + "The BastionGuard™ name and branding are not licensed under the GPLv3."
1490 );
1491 lbl_trademark.set_xalign(0.5f);
1492 lbl_trademark.set_justify(Gtk.Justification.CENTER);
1493 lbl_trademark.wrap = true;
1494 content.append(lbl_trademark);
1495
1496 var lbl_license = new Gtk.Label(
1497 _("BastionGuard Backup is a fork of Timeshift by Tony George.\nThis program is free software licensed under GNU GPL v2 or later.")
1498 );
1499 lbl_license.set_xalign(0.5f);
1500 lbl_license.set_justify(Gtk.Justification.CENTER);
1501 lbl_license.wrap = true;
1502 content.append(lbl_license);
1503
1504 var actions = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
1505 actions.set_halign(Gtk.Align.CENTER);
1506 actions.margin_top = 8;
1507 content.append(actions);
1508
1509 var btn_close = new Gtk.Button.with_label(_("Close"));
1510 btn_close.set_size_request(120, -1);
1511 btn_close.get_style_context().add_class("suggested-action");
1512 btn_close.clicked.connect(() => { this.destroy(); });
1513 actions.append(btn_close);
1514 }
1515
1516}
Main App
AppStatus
nlohmann::json json
Definition Backend.cpp:2908
#define _(s)
SnapshotLocationStatus
AboutWindow(Gtk.Window parent)
void stop(AppStatus status_to_update=AppStatus.CANCELLED)
AppStatus status
BackupWindow(bool embedded=false)
CustomMessageDialog(string _msg_title, string _msg_body, Gtk.MessageType _msg_type, Window? parent, Gtk.ButtonsType _buttons_type)
DeleteWindow(bool embedded=false)
string uuid
Definition Device.vala:50
uint64 free_bytes
Definition Device.vala:120
string fstype
Definition Device.vala:65
string device
Definition Device.vala:44
static Gtk.HeaderBar create_main_headerbar(Gtk.Window win, int left_width=215)
static bool use_dark
static Gdk.? Pixbuf lookup(string icon_name, int icon_size, bool symbolic=false, bool use_hardcoded=false, int scale=1)
const string SHIELD_MED
const string SHIELD_LOW
const string SHIELD_HIGH
static Gtk.Image lookup_image(string icon_name, int icon_size, bool symbolic=false, bool use_hardcoded=false)
Gtk.Button btn_restore
bool load_theme_preference()
Gtk.Label lbl_device_name
Gtk.Label lbl_free_space
Gtk.Button btn_nav_wizard
void init_snapshots_page()
Gtk.? Button btn_theme_dark
Gee.ArrayList< Gtk.Image > themed_img_list
void btn_restore_clicked()
Gtk.? CssProvider css_provider
Gtk.Button btn_delete_snapshot
void update_theme_buttons()
Gtk.? Widget wizard_page_widget
int compact_window_width
void resize_main_window(int width, int height)
void create_snapshot()
void show_snapshots_page()
void open_restore_flow()
Gtk.Label lbl_status_detail
void activate_nav(Gtk.Button btn, string page)
const string THEME_CONFIG_FILE
bool on_close_request() override
bool refresh_all()
void init_content()
void browse_selected()
void minimize()
Gtk.? Button btn_theme_light
bool init_delayed()
Gtk.Label lbl_snap_count
void open_delete_flow()
Gtk.Box make_card(bool hexpand)
Gtk.Box vbox_sidebar
const string THEME_CONFIG_DIR
void view_snapshot_log(bool view_restore_log)
Gee.ArrayList< int > themed_img_size
void save_language_preference(string lang_code)
void update_statusbar()
void apply_language_env(string lang_code)
Gtk.Button make_nav_btn(string icon, string label, string page)
Gtk.Button make_action_btn(string icon, string label, bool primary, bool danger, string css_class="")
SettingsWindow? embedded_settings
static string[] LANG_NAMES
void restart_application()
static string[] LANG_CODES
Gtk.Stack content_stack
Gtk.Button btn_browse_snapshot
Gtk.Button btn_nav_about
void on_language_changed()
Gee.ArrayList< string > themed_img_name
Gtk.? Widget backup_page_widget
Gtk.Button btn_nav_settings
Gtk.Button make_theme_btn(string icon, string label)
Gtk.? Widget about_page_widget
Gtk.? Widget settings_page_widget
Gtk.Button btn_nav_snapshots
void mark_selected()
Gtk.Box hbox_root
void btn_settings_clicked()
void btn_about_clicked()
void settings_changed(bool btrfs_mode_prev)
Gtk.Label lbl_status_title
void build_headerbar()
void init_settings_page()
SetupWizardWindow? embedded_wizard
Gtk.Button btn_backup
void show_settings()
void ui_sensitive(bool enable)
void apply_theme(bool use_dark)
Gtk.? Widget delete_page_widget
int compact_window_height
void remember_compact_window_size()
Gtk.? ComboBoxText lang_combo
Gtk.Image themed_icon(string icon_name, int size)
void apply_window_mode_for_page(string page)
void reload_themed_icons()
void set_dynamic_page(string name, ref Gtk.Widget? slot, Gtk.Widget widget)
void btn_wizard_clicked()
Gtk.? Widget restore_page_widget
string load_language_preference()
void init_sidebar()
bool page_requires_expanded_window(string page)
void save_theme_preference(bool use_dark)
const string LANG_CONFIG_FILE
Gtk.? Widget rsync_log_page_widget
Gtk.Image img_shield
bool on_close_request()
void load_css(bool use_dark=false)
void init_wizard_page()
SnapshotListBox snapshot_list_box
void open_backup_flow()
string current_lang
void delete_selected()
Gtk.? Button current_nav_btn
void init_about_page()
void open_rsync_log_page(string log_file)
bool window_in_expanded_mode
bool check_if_deletion_running()
Device sys_root
Definition Main.vala:75
bool first_run
Definition Main.vala:47
SnapshotRepo repo
Definition Main.vala:70
RsyncTask task
Definition Main.vala:160
bool btrfs_mode
Definition Main.vala:52
bool check_btrfs_layout_system(Gtk.Window? win=null)
Definition Main.vala:480
void save_app_config()
Definition Main.vala:3338
Gee.ArrayList< Snapshot > delete_list
Definition Main.vala:133
bool live_system()
Definition Main.vala:1058
bool mirror_system
Definition Main.vala:85
string backup_parent_uuid
Definition Main.vala:50
void exit_app(int exit_code=0)
Definition Main.vala:4574
DeleteFileTask delete_file_task
Definition Main.vala:161
void init_mount_list()
Definition Main.vala:2075
bool thread_delete_running
Definition Main.vala:108
static bool resource_exists(string rel)
static ? string resolve_css(string name)
static string resource_prefix()
RestoreWindow(bool embedded=false)
RsyncLogWindow(string _rsync_log_file, bool embedded=false)
Gtk.Widget detach_root_widget()
signal void request_close()
SettingsWindow(ClamdConfig &config)
SetupWizardWindow(bool embedded=false)
Gtk.Widget detach_root_widget()
signal void request_close()
signal void browse_selected()
SnapshotListBox(Gtk.Window _parent_window)
Gee.ArrayList< Snapshot > selected_snapshots()
signal void view_snapshot_log(bool show_restore_log)
signal void mark_selected()
signal void delete_selected()
Gee.ArrayList< Snapshot?> snapshots
bool load_snapshots()
string status_details
bool has_space(uint64 needed=0)
string status_message
string snapshots_path
SnapshotRepo. from_null()
SnapshotRepo. from_uuid(string uuid, Gtk.Window? parent_win, bool btrfs_repo)
SnapshotLocationStatus status_code
bool marked_for_deletion
Definition Snapshot.vala:51
bool file_exists(string file_path)
string format_file_size(uint64 size, bool binary_units=false, string unit="", bool show_units=true, int decimals=1)
void gtk_set_busy(bool busy, Gtk.Window win)
int gtk_dialog_run(Gtk.Dialog dlg)
void log_debug(string message)
void log_error(string message, bool highlight=false, bool is_warning=false)