Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 168 additions & 6 deletions src/parch_driver_manager/ui/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ..backend import BackendRunner
from ..manager import DriverManager
from ..profiles import DriverProfiles, DriverProfile
from ..backup_manager import BackupManager

from .settings import _, is_rtl
from .widgets import LogBuffer
Expand Down Expand Up @@ -81,20 +82,17 @@ def _build_ui(self):
self.toast_overlay = Adw.ToastOverlay()
self.set_content(self.toast_overlay)

# Overlay Split View (standard GNOME collapsible sidebar)
self.split_view = Adw.OverlaySplitView()
self.split_view.set_min_sidebar_width(240)
self.split_view.set_max_sidebar_width(300)
self.split_view.set_sidebar_position(Gtk.PackType.START)
self.split_view.set_show_sidebar(True)
self.toast_overlay.set_child(self.split_view)

# Responsive Mobile Breakpoint
bp_mobile = Adw.Breakpoint.new(Adw.BreakpointCondition.parse("max-width: 720px"))
bp_mobile.add_setter(self.split_view, "collapsed", True)
self.add_breakpoint(bp_mobile)

# Sidebar Panel
sidebar_toolbar = Adw.ToolbarView()
self.split_view.set_sidebar(sidebar_toolbar)

Expand Down Expand Up @@ -132,7 +130,6 @@ def _build_ui(self):

self._populate_sidebar()

# Content Panel
content_toolbar = Adw.ToolbarView()
self.split_view.set_content(content_toolbar)

Expand Down Expand Up @@ -217,14 +214,14 @@ def _populate_sidebar(self):
(_("Audio"), "audio-card-symbolic", "PipeWire / ALSA", _("Audio Stack")),
(_("Bluetooth"), "preferences-system-bluetooth-symbolic", bt_sub, _("Bluetooth Services")),
(_("System Info"), "preferences-system-symbolic", self.system_info.get("os", "Parch GNU/Linux"), _("Hardware and OS Details")),
(_("Backup"), "drive-harddisk-symbolic", _("Driver backups"), _("Manage driver backups")),
(_("Logs"), "text-x-generic-symbolic", _("Operation logs"), _("Activity and Event Logs"))
]

for cat, icon, sub, subtitle_desc in categories:
row = Adw.ActionRow(title=cat, subtitle=sub)
row.set_icon_name(icon)

# Active indicator badge
if cat in (_("GPU"), _("Network"), _("Audio"), _("Bluetooth")):
active_icon = Gtk.Image.new_from_icon_name("emblem-ok-symbolic")
active_icon.add_css_class("success-icon")
Expand All @@ -245,6 +242,7 @@ def _rebuild_all_pages(self):
self._build_audio_page()
self._build_bluetooth_page()
self._build_info_page()
self._build_backup_page()
self._build_logs_page()

def _is_profile_installed(self, profile: DriverProfile) -> bool:
Expand Down Expand Up @@ -668,6 +666,170 @@ def _build_info_page(self):

self.stack.add_named(scrolled, _("System Info"))


def _build_backup_page(self):
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
scrolled.set_vexpand(True)

clamp = Adw.Clamp()
clamp.set_maximum_size(840)
clamp.set_margin_top(16)
clamp.set_margin_bottom(16)
clamp.set_margin_start(12)
clamp.set_margin_end(12)
scrolled.set_child(clamp)

main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20)
main_box.set_vexpand(False)
clamp.set_child(main_box)

actions_group = Adw.PreferencesGroup(
title=_("Backup Operations"),
description=_("Create and restore driver backups")
)
main_box.append(actions_group)

actions_grid = Gtk.Grid()
actions_grid.set_column_spacing(12)
actions_grid.set_row_spacing(12)
actions_grid.set_margin_top(12)
actions_grid.set_margin_bottom(12)
actions_grid.set_margin_start(12)
actions_grid.set_margin_end(12)
actions_group.add(actions_grid)

backup_btn = Gtk.Button(label=_("Create Backup"))
backup_btn.add_css_class("suggested-action")
backup_btn.connect("clicked", self._on_create_backup)
actions_grid.attach(backup_btn, 0, 0, 1, 1)

restore_btn = Gtk.Button(label=_("Restore Backup"))
restore_btn.connect("clicked", self._on_restore_backup)
actions_grid.attach(restore_btn, 1, 0, 1, 1)

delete_btn = Gtk.Button(label=_("Delete Backup"))
delete_btn.add_css_class("destructive-action")
delete_btn.connect("clicked", self._on_delete_backup)
actions_grid.attach(delete_btn, 2, 0, 1, 1)

list_group = Adw.PreferencesGroup(
title=_("Existing Backups"),
description=_("List of available driver backups")
)
main_box.append(list_group)

self.backup_listbox = Gtk.ListBox()
self.backup_listbox.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.backup_listbox.add_css_class("boxed-list")
list_group.add(self.backup_listbox)

self._refresh_backup_list()

self.stack.add_named(scrolled, _("Backup"))

def _refresh_backup_list(self):
child = self.backup_listbox.get_first_child()
while child is not None:
next_child = child.get_next_sibling()
self.backup_listbox.remove(child)
child = next_child

backups = BackupManager.list_backups()

if not backups:
row = Adw.ActionRow(
title=_("No backups found"),
subtitle=_("Create your first backup using the button above")
)
self.backup_listbox.append(row)
return

for backup in backups:
row = Adw.ActionRow(
title=backup["name"],
subtitle=f"{backup['driver_count']} drivers • {backup['size']} • {backup['date']}"
)
row.set_activatable(True)
self.backup_listbox.append(row)


def _on_create_backup(self, _button):
def task():
GLib.idle_add(self._start_operation)
self.log_buffer.append("➔ Creating backup...")
success, result = BackupManager.create_backup(self.backend)
if success:
GLib.idle_add(self._refresh_backup_list)
GLib.idle_add(self._show_toast, f"✅ Backup created: {os.path.basename(result)}")
self.log_buffer.append(f"✅ Backup created: {result}")
else:
GLib.idle_add(self._show_toast, f"❌ Backup failed: {result}")
self.log_buffer.append(f"❌ Backup failed: {result}")
GLib.idle_add(self._end_operation)

self._run_in_thread(task)

def _on_restore_backup(self, _button):
selected = self.backup_listbox.get_selected_row()
if not selected:
self._show_toast(_("Please select a backup first"))
return

backup_name = selected.get_title()
backup_path = os.path.join(BackupManager.BACKUP_DIR, backup_name)

self._show_confirm_dialog(
_("Restore Backup?"),
f"{_('This will install drivers from')} {backup_name}",
lambda: self._do_restore(backup_path)
)

def _do_restore(self, backup_path: str):
def task():
GLib.idle_add(self._start_operation)
self.log_buffer.append(f"➔ Restoring from: {backup_path}")
success, result = BackupManager.restore_backup(self.backend, backup_path)
if success:
GLib.idle_add(self._show_toast, f"✅ Restore completed: {result}")
self.log_buffer.append(f"✅ Restore completed: {result}")
else:
GLib.idle_add(self._show_toast, f"❌ Restore failed: {result}")
self.log_buffer.append(f"❌ Restore failed: {result}")
GLib.idle_add(self._end_operation)

self._run_in_thread(task)

def _on_delete_backup(self, _button):
selected = self.backup_listbox.get_selected_row()
if not selected:
self._show_toast(_("Please select a backup first"))
return

backup_name = selected.get_title()
backup_path = os.path.join(BackupManager.BACKUP_DIR, backup_name)

self._show_confirm_dialog(
_("Delete Backup?"),
f"{_('This will permanently delete')} {backup_name}",
lambda: self._do_delete(backup_path),
destructive=True
)

def _do_delete(self, backup_path: str):
def task():
GLib.idle_add(self._start_operation)
success, result = BackupManager.delete_backup(backup_path)
if success:
GLib.idle_add(self._refresh_backup_list)
self._show_toast("✅ Backup deleted")
else:
self._show_toast(f"❌ Delete failed: {result}")
GLib.idle_add(self._end_operation)

self._run_in_thread(task)


def _build_logs_page(self):
scrolled_outer = Gtk.ScrolledWindow()
scrolled_outer.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
Expand Down Expand Up @@ -728,6 +890,7 @@ def on_sidebar_selected(self, listbox, row):
_("Audio"): _("Audio Stack"),
_("Bluetooth"): _("Bluetooth Services"),
_("System Info"): _("Hardware and OS Details"),
_("Backup"): _("Manage driver backups"),
_("Logs"): _("Activity and Event Logs")
}
subtitle = subtitles.get(title, _("Parch Driver Manager"))
Expand All @@ -736,7 +899,6 @@ def on_sidebar_selected(self, listbox, row):
self.stack.set_visible_child_name(title)
self.current_category = title

# Close overlay sidebar automatically in mobile collapsed mode
if self.split_view.get_collapsed():
self.split_view.set_show_sidebar(False)

Expand Down