update: navigator and main updates

This commit is contained in:
JOhn 2026-07-01 11:01:26 -04:00
parent 80c1d2c4e6
commit 7a8c4ded60
11 changed files with 411 additions and 784 deletions

View file

@ -27,15 +27,7 @@ from gui.v2.infrastructure import orm
from gui.v2.infrastructure.navigator import Navigator from gui.v2.infrastructure.navigator import Navigator
from gui.v2.infrastructure.setup_observers import setup_observers from gui.v2.infrastructure.setup_observers import setup_observers
from gui.v2.infrastructure.connection_manager import ConnectionManager from gui.v2.infrastructure.connection_manager import ConnectionManager
from gui.v2.infrastructure.page_registry import (
PRELOAD_ORDER,
PRELOAD_SKIP,
ASYNC_PREPARE,
REGULAR_FLOW_ONLY,
PAGE_REGISTRY,
)
from gui.v2.workers.worker_thread import WorkerThread from gui.v2.workers.worker_thread import WorkerThread
from gui.v2.workers.page_data_worker import PageDataWorker
from gui.v2.ui.builders.bottom_section import create_bottom_section from gui.v2.ui.builders.bottom_section import create_bottom_section
from gui.v2.ui.builders.top_section import create_top_section from gui.v2.ui.builders.top_section import create_top_section
from gui.v2.ui.builders.system_tray import init_system_tray from gui.v2.ui.builders.system_tray import init_system_tray
@ -56,7 +48,6 @@ from gui.v2.actions.flags import (
has_shown_fast_mode_prompt, has_shown_fast_mode_prompt,
mark_fast_mode_prompt_shown, mark_fast_mode_prompt_shown,
set_fast_mode_enabled, set_fast_mode_enabled,
is_fast_registration_enabled,
check_first_launch, check_first_launch,
) )
from gui.v2.actions.profile_data import ( from gui.v2.actions.profile_data import (
@ -73,16 +64,17 @@ from gui.v2.actions.should_be_synchronized import should_be_synchronized
class CustomWindow(QMainWindow): class CustomWindow(QMainWindow):
def __init__(self, force_sync): def __init__(self, force_sync=False):
super().__init__() super().__init__()
self.force_sync = force_sync # passed in from loader
sys.excepthook = self._handle_exception sys.excepthook = self._handle_exception
self.setWindowFlags(Qt.WindowType.Window) self.setWindowFlags(Qt.WindowType.Window)
gui_dir = os.path.dirname(os.path.abspath(__file__)) gui_dir = os.path.dirname(os.path.abspath(__file__))
font_path = os.path.join(gui_dir, 'resources', 'fonts') font_path = os.path.join(gui_dir, 'resources', 'fonts')
# Get's SQLAlchemy going,
orm.start()
retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf') retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf')
font_id = QFontDatabase.addApplicationFont(retro_gaming_path) font_id = QFontDatabase.addApplicationFont(retro_gaming_path)
font_family = QFontDatabase.applicationFontFamilies(font_id)[0] font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
@ -121,6 +113,7 @@ class CustomWindow(QMainWindow):
self.css_path = os.getenv('CSS_PATH', os.path.join( self.css_path = os.getenv('CSS_PATH', os.path.join(
gui_dir, 'resources', 'styles')) gui_dir, 'resources', 'styles'))
self.force_sync = force_sync
self.is_downloading = False self.is_downloading = False
self.current_profile_id = None self.current_profile_id = None
self.connection_manager = ConnectionManager() self.connection_manager = ConnectionManager()
@ -174,21 +167,7 @@ class CustomWindow(QMainWindow):
self.animation_timer.timeout.connect(self.animate_toggle) self.animation_timer.timeout.connect(self.animate_toggle)
self.check_logging() self.check_logging()
self.navigator = Navigator(self.page_stack, self) self.navigator = Navigator(self)
self._preload_queue = []
self._preload_total = 0
self._preload_done = 0
self._preload_failed = 0
self._preload_skipped_runtime = 0
self._preload_dispatched = 0
self._preload_inflight = 0
self._preload_workers = {}
self._preload_summary_emitted = False
self._preload_t_start = 0.0
self._preload_timer = QTimer(self)
self._preload_timer.setSingleShot(True)
self._preload_timer.timeout.connect(self._preload_next)
self.page_stack.currentChanged.connect(self.page_changed) self.page_stack.currentChanged.connect(self.page_changed)
@ -207,138 +186,20 @@ class CustomWindow(QMainWindow):
f"connection={current_connection}, tor_mode={self.is_tor_mode})" f"connection={current_connection}, tor_mode={self.is_tor_mode})"
) )
if self.force_sync:
self.sync()
def init_ui(self): def init_ui(self):
if self.check_first_launch(): if self.check_first_launch():
self.navigator.navigate("welcome") self.navigator.navigate("welcome")
else: else:
self.navigator.navigate("menu") self.navigator.navigate("menu")
self._start_background_preload() self.navigator.start_preload()
def _start_background_preload(self):
import time
self._preload_queue = [n for n in PRELOAD_ORDER if n not in PRELOAD_SKIP]
fast_mode = is_fast_registration_enabled(self.gui_config_file)
skipped_regular = []
if fast_mode:
skipped_regular = [n for n in self._preload_queue if n in REGULAR_FLOW_ONLY]
self._preload_queue = [n for n in self._preload_queue if n not in REGULAR_FLOW_ONLY]
self._preload_total = len(self._preload_queue)
self._preload_done = 0
self._preload_failed = 0
self._preload_skipped_runtime = 0
self._preload_dispatched = 0
self._preload_inflight = 0
self._preload_summary_emitted = False
self._preload_t_start = time.perf_counter()
print(f"[preload] ============================================================", flush=True)
print(f"[preload] starting background preload: {self._preload_total} pages queued", flush=True)
print(f"[preload] order : {', '.join(self._preload_queue)}", flush=True)
print(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)", flush=True)
if fast_mode:
print(f"[preload] fastmode: ON -> skipping regular-flow pages: "
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True)
print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True)
print(f"[preload] ============================================================", flush=True)
self._preload_timer.start(700)
def _preload_next(self):
while self._preload_queue and self._preload_queue[0] in self.navigator._instances:
skipped = self._preload_queue.pop(0)
self._preload_skipped_runtime += 1
print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True)
if not self._preload_queue:
self._maybe_emit_preload_summary()
return
self._preload_dispatched += 1
idx = self._preload_dispatched
name = self._preload_queue.pop(0)
print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True)
if name in ASYNC_PREPARE:
self._dispatch_async_preload(name)
else:
ok = self.navigator.preload(name)
if ok:
self._preload_done += 1
else:
self._preload_failed += 1
if self._preload_queue:
self._preload_timer.start(250)
else:
self._maybe_emit_preload_summary()
def _dispatch_async_preload(self, name):
module_path, class_name = PAGE_REGISTRY[name]
print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True)
worker = PageDataWorker(name, module_path, class_name, self)
worker.data_ready.connect(self._on_preload_data_ready)
worker.failed.connect(self._on_preload_failed)
worker.finished.connect(lambda n=name: self._cleanup_preload_worker(n))
self._preload_workers[name] = worker
self._preload_inflight += 1
worker.start()
def _on_preload_data_ready(self, name, payload):
self._preload_inflight -= 1
if name in self.navigator._instances:
print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True)
else:
try:
page = self.navigator._instantiate(name, prepared=payload)
self.navigator.register_instance(name, page)
self._preload_done += 1
print(f"[preload] {name:30s} OK (async, cached={len(self.navigator._instances)}, "
f"stack_size={self.page_stack.count()})", flush=True)
except Exception as e:
self._preload_failed += 1
print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True)
self._maybe_emit_preload_summary()
def _on_preload_failed(self, name, error):
self._preload_inflight -= 1
print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True)
if name not in self.navigator._instances:
ok = self.navigator.preload(name)
if ok:
self._preload_done += 1
else:
self._preload_failed += 1
self._maybe_emit_preload_summary()
def _cleanup_preload_worker(self, name):
worker = self._preload_workers.pop(name, None)
if worker is not None:
worker.deleteLater()
def _maybe_emit_preload_summary(self):
if self._preload_summary_emitted:
return
if not self._preload_queue and self._preload_inflight == 0:
self._preload_summary_emitted = True
self._emit_preload_summary()
def _emit_preload_summary(self):
import time
total_s = time.perf_counter() - self._preload_t_start
print(f"[preload] ============================================================", flush=True)
print(f"[preload] complete: {self._preload_done} loaded, "
f"{self._preload_failed} failed, "
f"{self._preload_skipped_runtime} skipped (user beat us to it) "
f"in {total_s:.2f}s", flush=True)
print(f"[preload] cached pages now: {sorted(self.navigator._instances.keys())}", flush=True)
print(f"[preload] ============================================================", flush=True)
def perform_update_check(self): def perform_update_check(self):
from core.controllers.ClientController import ClientController from core.controllers.ClientController import ClientController
update_available = ClientController.can_be_updated() update_available = ClientController.can_be_updated()
if update_available: if update_available:
menu_page = self.navigator.get_cached("menu") self.navigator.update_if_cached(
if menu_page is not None: "menu", lambda p: p.on_update_check_finished())
menu_page.on_update_check_finished()
def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers): def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
if not status: if not status:
@ -363,24 +224,17 @@ class CustomWindow(QMainWindow):
for i, brw in enumerate(available_browsers) for i, brw in enumerate(available_browsers)
] ]
browser_page = self.navigator.get_cached("browser") self.navigator.update_if_cached(
if browser_page is not None: "browser", lambda p: p.create_interface_elements(available_browsers_list))
browser_page.create_interface_elements(available_browsers_list) self.navigator.update_if_cached(
"location", lambda p: p.create_interface_elements(available_locations_list))
self.navigator.update_if_cached(
"hidetor", lambda p: p.create_interface_elements(available_locations_list))
self.navigator.update_if_cached(
"protocol", lambda p: p.enable_protocol_buttons())
self.navigator.update_if_cached("menu", self._enable_menu_verification_icons)
location_page = self.navigator.get_cached("location") def _enable_menu_verification_icons(self, menu_page):
if location_page is not None:
location_page.create_interface_elements(available_locations_list)
hidetor_page = self.navigator.get_cached("hidetor")
if hidetor_page is not None:
hidetor_page.create_interface_elements(available_locations_list)
protocol_page = self.navigator.get_cached("protocol")
if protocol_page is not None:
protocol_page.enable_protocol_buttons()
menu_page = self.navigator.get_cached("menu")
if menu_page is not None:
if hasattr(menu_page, 'refresh_profiles_data'): if hasattr(menu_page, 'refresh_profiles_data'):
menu_page.refresh_profiles_data() menu_page.refresh_profiles_data()
if hasattr(menu_page, 'buttons'): if hasattr(menu_page, 'buttons'):
@ -803,9 +657,9 @@ class CustomWindow(QMainWindow):
if not self.has_shown_fast_mode_prompt(): if not self.has_shown_fast_mode_prompt():
self.navigator.navigate("fast_mode_prompt") self.navigator.navigate("fast_mode_prompt")
else: else:
menu_page = self.navigator.get_cached("menu") self.navigator.update_if_cached(
if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'): "menu",
menu_page.refresh_menu_buttons() lambda p: p.refresh_menu_buttons() if hasattr(p, 'refresh_menu_buttons') else None)
self.navigator.navigate("menu") self.navigator.navigate("menu")
def enable_marquee(self, text): def enable_marquee(self, text):

View file

@ -42,6 +42,13 @@ def is_fast_registration_enabled(gui_config_file):
return config.get("registrations", {}).get("fast_registration_enabled", False) return config.get("registrations", {}).get("fast_registration_enabled", False)
def is_auto_sync_enabled(gui_config_file):
config = load_gui_config(gui_config_file)
if not config:
return False
return config.get("registrations", {}).get("auto_sync_enabled", False)
def set_fast_mode_enabled(gui_config_file, enabled): def set_fast_mode_enabled(gui_config_file, enabled):
config = load_gui_config(gui_config_file) config = load_gui_config(gui_config_file)
if config is None: if config is None:

138
gui/v2/actions/settings_data.py Executable file
View file

@ -0,0 +1,138 @@
import json
import os
from core.Constants import Constants
from core.controllers.ConfigurationController import ConfigurationController
from core.controllers.PolicyController import PolicyController
from core.controllers.ProfileController import ProfileController
from gui.v2.actions.config import load_gui_config
from gui.v2.actions.profile_order import normalize_profile_order
from gui.v2.actions.ticket_failure import get_ticket_verification_failure
def read_systemwide_enabled():
try:
privilege_policy = PolicyController.get('privilege')
if privilege_policy is not None:
return PolicyController.is_instated(privilege_policy)
except Exception:
pass
return False
def read_bwrap_enabled():
try:
capability_policy = PolicyController.get('capability')
if capability_policy is not None:
return PolicyController.is_instated(capability_policy)
except Exception:
pass
return False
def prepare(gui_config_file):
profiles = ProfileController.get_all()
profile_order = normalize_profile_order(gui_config_file, profiles.keys())
try:
endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled()
except Exception:
endpoint_verification_enabled = False
return {
"profiles": profiles,
"profile_order": profile_order,
"current_connection": ConfigurationController.get_connection(),
"endpoint_verification_enabled": endpoint_verification_enabled,
"systemwide_enabled": read_systemwide_enabled(),
"bwrap_enabled": read_bwrap_enabled(),
"gui_config": load_gui_config(gui_config_file),
"ticket_failure": get_ticket_verification_failure(gui_config_file),
}
def empty_payload():
return {
"profiles": {},
"profile_order": [],
"current_connection": None,
"endpoint_verification_enabled": False,
"systemwide_enabled": False,
"bwrap_enabled": False,
"gui_config": None,
"ticket_failure": None,
}
def extract_endpoint_ip(profile):
try:
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile.id}'
wg_conf_path = f'{profile_path}/wg.conf.bak'
if not os.path.exists(wg_conf_path):
return None
with open(wg_conf_path, 'r') as f:
content = f.read()
for line in content.split('\n'):
if line.strip().startswith('Endpoint = '):
endpoint = line.strip().split(' = ')[1]
return endpoint.split(':')[0]
return None
except Exception:
return None
def truncate_key(text, max_length=50):
if not text or text == "N/A" or len(text) <= max_length:
return text
start_len = max_length // 2 - 2
end_len = max_length // 2 - 2
return text[:start_len] + "....." + text[-end_len:]
def build_verification_view(profile):
operator = None
if profile and getattr(profile, 'location', None) and profile.location.operator:
operator = profile.location.operator
if not operator:
return {
"operator_name": "N/A",
"nostr_public_key": "N/A",
"hydraveil_public_key": "N/A",
"nostr_attestation_event_reference": "N/A",
}
return {
"operator_name": operator.name or "N/A",
"nostr_public_key": operator.nostr_public_key or "N/A",
"hydraveil_public_key": operator.public_key or "N/A",
"nostr_attestation_event_reference": operator.nostr_attestation_event_reference or "N/A",
}
def build_subscription_view(profile):
subscription = profile.subscription
billing_code = str(subscription.billing_code)
if hasattr(subscription, 'expires_at') and subscription.expires_at:
expires_at = subscription.expires_at.strftime("%Y-%m-%d %H:%M:%S UTC")
else:
expires_at = "Not available"
return {"billing_code": billing_code, "expires_at": expires_at}
def format_ticket_failure_status(failure):
if not failure:
return "No saved verification failure. If ticket preparation fails validation, recovery data will appear here."
failed_validations = failure.get("failed_validations", [])
how_many_failed = failure.get("how_many_failed", len(failed_validations))
updated_at = failure.get("updated_at", "unknown time")
failed_text = ", ".join(str(item) for item in failed_validations)
return (
f"Saved verification failure: {how_many_failed} failed. "
f"Failed validation indices: {failed_text}. Saved: {updated_at}"
)
def format_ticket_recovery_result(label, result):
try:
payload = json.dumps(result, indent=2, default=str)
except TypeError:
payload = str(result)
return f"{label}:\n{payload}"

View file

@ -2,15 +2,40 @@ import importlib
import time import time
import traceback import traceback
from gui.v2.infrastructure.page_registry import PAGE_REGISTRY, ASYNC_PREPARE from PyQt6.QtCore import QObject, QTimer
from gui.v2.infrastructure.page_registry import (
PAGE_REGISTRY,
ASYNC_PREPARE,
REGULAR_FLOW_ONLY,
PRELOAD_ORDER,
PRELOAD_SKIP,
)
from gui.v2.workers.page_data_worker import PageDataWorker
from gui.v2.actions.flags import is_fast_registration_enabled
class Navigator: class Navigator(QObject):
def __init__(self, page_stack, custom_window): def __init__(self, custom_window):
self.page_stack = page_stack super().__init__()
self.custom_window = custom_window self.custom_window = custom_window
self.page_stack = custom_window.page_stack
self._instances = {} self._instances = {}
self._preload_queue = []
self._preload_total = 0
self._preload_done = 0
self._preload_failed = 0
self._preload_skipped_runtime = 0
self._preload_dispatched = 0
self._preload_inflight = 0
self._preload_workers = {}
self._preload_summary_emitted = False
self._preload_t_start = 0.0
self._preload_timer = QTimer(self)
self._preload_timer.setSingleShot(True)
self._preload_timer.timeout.connect(self._preload_next)
def get_class(self, name): def get_class(self, name):
module_path, class_name = PAGE_REGISTRY[name] module_path, class_name = PAGE_REGISTRY[name]
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
@ -31,12 +56,28 @@ class Navigator:
if page is None: if page is None:
if name not in PAGE_REGISTRY: if name not in PAGE_REGISTRY:
self._warn_not_migrated(name) self._warn_not_migrated(name)
return return None
page = self._instantiate(name) page = self._instantiate(name)
self.register_instance(name, page) self.register_instance(name, page)
self.page_stack.setCurrentIndex(self.page_stack.indexOf(page)) self.page_stack.setCurrentIndex(self.page_stack.indexOf(page))
return page
def preload(self, name): def get_cached(self, name):
return self._instances.get(name)
def update_if_cached(self, name, fn):
page = self._instances.get(name)
if page is not None:
fn(page)
return page
def _warn_not_migrated(self, name):
try:
self.custom_window.update_status(f"Page '{name}' not migrated yet.")
except Exception:
pass
def _preload_one(self, name):
if name in self._instances: if name in self._instances:
print(f"[preload] {name:30s} SKIP (already cached)", flush=True) print(f"[preload] {name:30s} SKIP (already cached)", flush=True)
return False return False
@ -64,11 +105,114 @@ class Navigator:
f"{type(e).__name__}: {e}") f"{type(e).__name__}: {e}")
return False return False
def get_cached(self, name): def start_preload(self, fast_mode=None):
return self._instances.get(name) if fast_mode is None:
fast_mode = is_fast_registration_enabled(self.custom_window.gui_config_file)
self._preload_queue = [n for n in PRELOAD_ORDER if n not in PRELOAD_SKIP]
skipped_regular = []
if fast_mode:
skipped_regular = [n for n in self._preload_queue if n in REGULAR_FLOW_ONLY]
self._preload_queue = [n for n in self._preload_queue if n not in REGULAR_FLOW_ONLY]
self._preload_total = len(self._preload_queue)
self._preload_done = 0
self._preload_failed = 0
self._preload_skipped_runtime = 0
self._preload_dispatched = 0
self._preload_inflight = 0
self._preload_summary_emitted = False
self._preload_t_start = time.perf_counter()
print(f"[preload] ============================================================", flush=True)
print(f"[preload] starting background preload: {self._preload_total} pages queued", flush=True)
print(f"[preload] order : {', '.join(self._preload_queue)}", flush=True)
print(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)", flush=True)
if fast_mode:
print(f"[preload] fastmode: ON -> skipping regular-flow pages: "
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True)
print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True)
print(f"[preload] ============================================================", flush=True)
self._preload_timer.start(700)
def _warn_not_migrated(self, name): def _preload_next(self):
while self._preload_queue and self._preload_queue[0] in self._instances:
skipped = self._preload_queue.pop(0)
self._preload_skipped_runtime += 1
print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True)
if not self._preload_queue:
self._maybe_emit_preload_summary()
return
self._preload_dispatched += 1
idx = self._preload_dispatched
name = self._preload_queue.pop(0)
print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True)
if name in ASYNC_PREPARE:
self._dispatch_async_preload(name)
else:
ok = self._preload_one(name)
if ok:
self._preload_done += 1
else:
self._preload_failed += 1
if self._preload_queue:
self._preload_timer.start(250)
else:
self._maybe_emit_preload_summary()
def _dispatch_async_preload(self, name):
module_path, class_name = PAGE_REGISTRY[name]
print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True)
worker = PageDataWorker(name, module_path, class_name, self.custom_window)
worker.data_ready.connect(self._on_preload_data_ready)
worker.failed.connect(self._on_preload_failed)
worker.finished.connect(lambda n=name: self._cleanup_preload_worker(n))
self._preload_workers[name] = worker
self._preload_inflight += 1
worker.start()
def _on_preload_data_ready(self, name, payload):
self._preload_inflight -= 1
if name in self._instances:
print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True)
else:
try: try:
self.custom_window.update_status(f"Page '{name}' not migrated yet.") page = self._instantiate(name, prepared=payload)
except Exception: self.register_instance(name, page)
pass self._preload_done += 1
print(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, "
f"stack_size={self.page_stack.count()})", flush=True)
except Exception as e:
self._preload_failed += 1
print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True)
self._maybe_emit_preload_summary()
def _on_preload_failed(self, name, error):
self._preload_inflight -= 1
print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True)
if name not in self._instances:
ok = self._preload_one(name)
if ok:
self._preload_done += 1
else:
self._preload_failed += 1
self._maybe_emit_preload_summary()
def _cleanup_preload_worker(self, name):
worker = self._preload_workers.pop(name, None)
if worker is not None:
worker.deleteLater()
def _maybe_emit_preload_summary(self):
if self._preload_summary_emitted:
return
if not self._preload_queue and self._preload_inflight == 0:
self._preload_summary_emitted = True
self._emit_preload_summary()
def _emit_preload_summary(self):
total_s = time.perf_counter() - self._preload_t_start
print(f"[preload] ============================================================", flush=True)
print(f"[preload] complete: {self._preload_done} loaded, "
f"{self._preload_failed} failed, "
f"{self._preload_skipped_runtime} skipped (user beat us to it) "
f"in {total_s:.2f}s", flush=True)
print(f"[preload] cached pages now: {sorted(self._instances.keys())}", flush=True)
print(f"[preload] ============================================================", flush=True)

0
gui/v2/infrastructure/orm.py Normal file → Executable file
View file

View file

@ -13,6 +13,7 @@ class Page(QWidget):
self.btn_path = custom_window.btn_path self.btn_path = custom_window.btn_path
self.name = name self.name = name
self.page_stack = page_stack self.page_stack = page_stack
self._prepared = None
self.init_ui() self.init_ui()
self.selected_profiles = [] self.selected_profiles = []
self.selected_wireguard = [] self.selected_wireguard = []
@ -23,6 +24,11 @@ class Page(QWidget):
def prepare_data(custom_window): def prepare_data(custom_window):
return None return None
def _prepared_value(self, key, loader):
if self._prepared is not None and key in self._prepared:
return self._prepared[key]
return loader()
def add_selected_profile(self, profile): def add_selected_profile(self, profile):
self.selected_profiles.clear() self.selected_profiles.clear()
self.selected_wireguard.clear() self.selected_wireguard.clear()

View file

@ -1,11 +1,9 @@
import json
import logging import logging
import os import os
import subprocess import subprocess
import sys import sys
import threading import threading
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Union
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame, QGridLayout, QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame, QGridLayout,
@ -34,6 +32,7 @@ from core.Errors import (
) )
from core.errors.logger import logger as core_logger from core.errors.logger import logger as core_logger
from gui.v2.actions import settings_data
from gui.v2.actions.key_interpretation import interpret_key_results from gui.v2.actions.key_interpretation import interpret_key_results
from gui.v2.actions.profile_order import normalize_profile_order from gui.v2.actions.profile_order import normalize_profile_order
from gui.v2.infrastructure.page_registry import PAGE_REGISTRY from gui.v2.infrastructure.page_registry import PAGE_REGISTRY
@ -44,6 +43,7 @@ from gui.v2.ui.styles.styles import (
SCROLLBAR_CYAN_QSS, SCROLLBAR_CYAN_QSS,
TERMINAL_LIST_QSS, TERMINAL_LIST_QSS,
checkbox_style, checkbox_style,
combobox_style,
) )
from gui.v2.ui.widgets.clickable_label import ClickableValueLabel from gui.v2.ui.widgets.clickable_label import ClickableValueLabel
from gui.v2.ui.widgets.terminal_widget import TerminalWidget from gui.v2.ui.widgets.terminal_widget import TerminalWidget
@ -59,7 +59,7 @@ class Settings(Page):
self.update_status = main_window self.update_status = main_window
self.update_logging = main_window self.update_logging = main_window
self._prepared = prepared if prepared is not None else self.prepare_data(main_window) self._prepared = prepared if prepared is not None else settings_data.empty_payload()
self._settings_refresh_seq = 0 self._settings_refresh_seq = 0
self._settings_workers = set() self._settings_workers = set()
self.button_reverse.setVisible(True) self.button_reverse.setVisible(True)
@ -71,49 +71,7 @@ class Settings(Page):
@staticmethod @staticmethod
def prepare_data(custom_window): def prepare_data(custom_window):
profiles = ProfileController.get_all() return settings_data.prepare(custom_window.gui_config_file)
profile_order = normalize_profile_order(
getattr(custom_window, 'gui_config_file', None), profiles.keys())
current_connection = custom_window.get_current_connection()
try:
endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled()
except Exception:
endpoint_verification_enabled = False
return {
"profiles": profiles,
"profile_order": profile_order,
"current_connection": current_connection,
"endpoint_verification_enabled": endpoint_verification_enabled,
"systemwide_enabled": Settings._read_systemwide_enabled(),
"bwrap_enabled": Settings._read_bwrap_enabled(),
"gui_config": custom_window._load_gui_config(),
"ticket_failure": custom_window.get_ticket_verification_failure(),
}
def _prepared_value(self, key, loader):
if self._prepared is not None and key in self._prepared:
return self._prepared[key]
return loader()
@staticmethod
def _read_systemwide_enabled():
try:
privilege_policy = PolicyController.get('privilege')
if privilege_policy is not None:
return PolicyController.is_instated(privilege_policy)
except Exception:
pass
return False
@staticmethod
def _read_bwrap_enabled():
try:
capability_policy = PolicyController.get('capability')
if capability_policy is not None:
return PolicyController.is_instated(capability_policy)
except Exception:
pass
return False
def setup_ui(self): def setup_ui(self):
main_container = QWidget(self) main_container = QWidget(self)
@ -344,7 +302,7 @@ class Settings(Page):
profile_selection_layout.addWidget(profile_label) profile_selection_layout.addWidget(profile_label)
self.debug_profile_selector = QComboBox() self.debug_profile_selector = QComboBox()
self.debug_profile_selector.setStyleSheet(self.get_combobox_style()) self.debug_profile_selector.setStyleSheet(combobox_style(self.font_style))
self.debug_profile_selector.currentTextChanged.connect( self.debug_profile_selector.currentTextChanged.connect(
self.on_debug_profile_selected) self.on_debug_profile_selected)
profile_selection_layout.addWidget(self.debug_profile_selector) profile_selection_layout.addWidget(self.debug_profile_selector)
@ -690,7 +648,7 @@ class Settings(Page):
self.cli_command.setText( self.cli_command.setText(
f"'{app_path}' --cli profile enable -i {profile_id}") f"'{app_path}' --cli profile enable -i {profile_id}")
self.cli_copy_button.setEnabled(True) self.cli_copy_button.setEnabled(True)
ip_address = self.extract_endpoint_ip(profile) ip_address = settings_data.extract_endpoint_ip(profile)
if ip_address: if ip_address:
self.ping_instruction_label.setText( self.ping_instruction_label.setText(
f"Step 1, Can you Ping it? Copy-paste the command below into your terminal. This VPN Node's IP address is {ip_address}") f"Step 1, Can you Ping it? Copy-paste the command below into your terminal. This VPN Node's IP address is {ip_address}")
@ -722,25 +680,6 @@ class Settings(Page):
self.wg_quick_up_button.setEnabled(False) self.wg_quick_up_button.setEnabled(False)
self.wg_quick_down_button.setEnabled(False) self.wg_quick_down_button.setEnabled(False)
def extract_endpoint_ip(self, profile):
try:
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile.id}'
wg_conf_path = f'{profile_path}/wg.conf.bak'
if not os.path.exists(wg_conf_path):
return None
with open(wg_conf_path, 'r') as f:
content = f.read()
for line in content.split('\n'):
if line.strip().startswith('Endpoint = '):
endpoint = line.strip().split(' = ')[1]
ip_address = endpoint.split(':')[0]
return ip_address
return None
except Exception:
return None
def copy_ping_command(self): def copy_ping_command(self):
profile_id = self.debug_profile_selector.currentData() profile_id = self.debug_profile_selector.currentData()
if profile_id is None: if profile_id is None:
@ -748,7 +687,7 @@ class Settings(Page):
profile = ProfileController.get(profile_id) profile = ProfileController.get(profile_id)
if profile and isinstance(profile, SystemProfile): if profile and isinstance(profile, SystemProfile):
ip_address = self.extract_endpoint_ip(profile) ip_address = settings_data.extract_endpoint_ip(profile)
if ip_address: if ip_address:
ping_command = f"ping {ip_address}" ping_command = f"ping {ip_address}"
clipboard = QApplication.clipboard() clipboard = QApplication.clipboard()
@ -763,7 +702,7 @@ class Settings(Page):
profile = ProfileController.get(profile_id) profile = ProfileController.get(profile_id)
if profile and isinstance(profile, SystemProfile): if profile and isinstance(profile, SystemProfile):
ip_address = self.extract_endpoint_ip(profile) ip_address = settings_data.extract_endpoint_ip(profile)
if ip_address: if ip_address:
self.test_ping_button.setEnabled(False) self.test_ping_button.setEnabled(False)
self.ping_result_label.setText("Testing ping...") self.ping_result_label.setText("Testing ping...")
@ -853,75 +792,9 @@ class Settings(Page):
self.content_layout.addWidget(self.delete_page) self.content_layout.addWidget(self.delete_page)
self.content_layout.setCurrentWidget(self.delete_page) self.content_layout.setCurrentWidget(self.delete_page)
def get_combobox_style(self) -> str:
return f"""
QComboBox {{
color: black;
background: #f0f0f0;
padding: 5px 30px 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
min-width: 120px;
margin-bottom: 10px;
{self.font_style}
}}
QComboBox:disabled {{
color: #666;
background: #e0e0e0;
}}
QComboBox::drop-down {{
border: none;
width: 30px;
}}
QComboBox::down-arrow {{
image: url(assets/down_arrow.png);
width: 12px;
height: 12px;
}}
QComboBox QAbstractItemView {{
color: black;
background: white;
selection-background-color: #007bff;
selection-color: white;
border: 1px solid #ccc;
{self.font_style}
}}
"""
def get_checkbox_style(self) -> str: def get_checkbox_style(self) -> str:
return checkbox_style(self.font_style) return checkbox_style(self.font_style)
def populate_wireguard_profiles(self) -> None:
self.wireguard_profile_selector.clear()
profiles = ProfileController.get_all()
for profile_id, profile in profiles.items():
if profile.connection.code == 'wireguard':
self.wireguard_profile_selector.addItem(
f"Profile {profile_id}: {profile.name}", profile_id)
def convert_duration(self, value: Union[str, int], to_hours: bool = True) -> Union[str, int]:
if to_hours:
number, unit = value.split(' ')
number = int(number)
if unit in ['day', 'days']:
return number * 24
elif unit in ['week', 'weeks']:
return number * 7 * 24
else:
raise ValueError(f"Unsupported duration unit: {unit}")
else:
hours = int(value)
if hours % (7 * 24) == 0:
weeks = hours // (7 * 24)
return f"{weeks} {'week' if weeks == 1 else 'weeks'}"
elif hours % 24 == 0:
days = hours // 24
return f"{days} {'day' if days == 1 else 'days'}"
else:
raise ValueError(
f"Hours value {hours} cannot be converted to days or weeks cleanly")
def reverse(self): def reverse(self):
self.custom_window.navigator.navigate("menu") self.custom_window.navigator.navigate("menu")
@ -956,7 +829,7 @@ class Settings(Page):
profile_layout = QVBoxLayout(profile_group) profile_layout = QVBoxLayout(profile_group)
self.profile_selector = QComboBox() self.profile_selector = QComboBox()
self.profile_selector.setStyleSheet(self.get_combobox_style()) self.profile_selector.setStyleSheet(combobox_style(self.font_style))
profiles = self._prepared_value("profiles", ProfileController.get_all) profiles = self._prepared_value("profiles", ProfileController.get_all)
if profiles: if profiles:
for profile_id, profile in profiles.items(): for profile_id, profile in profiles.items():
@ -1091,7 +964,7 @@ class Settings(Page):
self.verification_profile_selector = QComboBox() self.verification_profile_selector = QComboBox()
self.verification_profile_selector.setStyleSheet( self.verification_profile_selector.setStyleSheet(
self.get_combobox_style()) combobox_style(self.font_style))
profiles = self._prepared_value("profiles", ProfileController.get_all) profiles = self._prepared_value("profiles", ProfileController.get_all)
if profiles: if profiles:
for profile_id, profile in profiles.items(): for profile_id, profile in profiles.items():
@ -1210,72 +1083,30 @@ class Settings(Page):
return page return page
def truncate_key(self, text, max_length=50):
if not text or text == "N/A" or len(text) <= max_length:
return text
start_len = max_length // 2 - 2
end_len = max_length // 2 - 2
return text[:start_len] + "....." + text[-end_len:]
def update_verification_info(self, index): def update_verification_info(self, index):
if index < 0: if index < 0:
return return
profile_id = self.verification_profile_selector.itemData(index) profile_id = self.verification_profile_selector.itemData(index)
if profile_id is None: profile = None
for key, widget in self.verification_info.items(): if profile_id is not None:
widget.setText("N/A")
self.verification_full_values[key] = "N/A"
if key in self.verification_checkmarks:
self.verification_checkmarks[key].hide()
return
profile = ProfileController.get(profile_id) profile = ProfileController.get(profile_id)
if not profile:
view = settings_data.build_verification_view(profile)
truncated_keys = (
"nostr_public_key",
"hydraveil_public_key",
"nostr_attestation_event_reference",
)
for key, widget in self.verification_info.items(): for key, widget in self.verification_info.items():
widget.setText("N/A") full_value = view.get(key, "N/A")
self.verification_full_values[key] = "N/A" self.verification_full_values[key] = full_value
if key in self.verification_checkmarks: if key in truncated_keys and full_value != "N/A":
self.verification_checkmarks[key].hide() widget.setText(settings_data.truncate_key(full_value))
return
operator = None
if profile.location and profile.location.operator:
operator = profile.location.operator
if operator:
operator_name = operator.name or "N/A"
self.verification_full_values["operator_name"] = operator_name
self.verification_info["operator_name"].setText(operator_name)
nostr_key = operator.nostr_public_key or "N/A"
self.verification_full_values["nostr_public_key"] = nostr_key
self.verification_info["nostr_public_key"].setText(
self.truncate_key(nostr_key) if nostr_key != "N/A" else nostr_key)
hydraveil_key = operator.public_key or "N/A"
self.verification_full_values["hydraveil_public_key"] = hydraveil_key
self.verification_info["hydraveil_public_key"].setText(
self.truncate_key(hydraveil_key) if hydraveil_key != "N/A" else hydraveil_key)
nostr_verification = operator.nostr_attestation_event_reference or "N/A"
self.verification_full_values["nostr_attestation_event_reference"] = nostr_verification
self.verification_info["nostr_attestation_event_reference"].setText(self.truncate_key(
nostr_verification) if nostr_verification != "N/A" else nostr_verification)
else: else:
self.verification_info["operator_name"].setText("N/A") widget.setText(full_value)
self.verification_full_values["operator_name"] = "N/A"
self.verification_info["nostr_public_key"].setText("N/A")
self.verification_full_values["nostr_public_key"] = "N/A"
self.verification_info["hydraveil_public_key"].setText("N/A")
self.verification_full_values["hydraveil_public_key"] = "N/A"
self.verification_info["nostr_attestation_event_reference"].setText(
"N/A")
self.verification_full_values["nostr_attestation_event_reference"] = "N/A"
for key, widget in self.verification_info.items():
if key in self.verification_checkmarks: if key in self.verification_checkmarks:
full_value = self.verification_full_values.get(key, "")
if full_value and full_value != "N/A": if full_value and full_value != "N/A":
self.verification_checkmarks[key].show() self.verification_checkmarks[key].show()
else: else:
@ -1320,16 +1151,9 @@ class Settings(Page):
if profile and hasattr(profile, 'subscription') and profile.subscription: if profile and hasattr(profile, 'subscription') and profile.subscription:
try: try:
self.subscription_info["billing_code"].setText( view = settings_data.build_subscription_view(profile)
str(profile.subscription.billing_code)) self.subscription_info["billing_code"].setText(view["billing_code"])
self.subscription_info["expires_at"].setText(view["expires_at"])
if hasattr(profile.subscription, 'expires_at') and profile.subscription.expires_at:
expires_at = profile.subscription.expires_at.strftime(
"%Y-%m-%d %H:%M:%S UTC")
self.subscription_info["expires_at"].setText(expires_at)
else:
self.subscription_info["expires_at"].setText(
"Not available")
except Exception as e: except Exception as e:
print(f"Error updating subscription info: {e}") print(f"Error updating subscription info: {e}")
else: else:
@ -1787,7 +1611,7 @@ class Settings(Page):
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
recovery_layout = QVBoxLayout(recovery_group) recovery_layout = QVBoxLayout(recovery_group)
self.ticket_recovery_status_label = QLabel(self._format_ticket_failure_status(saved_failure)) self.ticket_recovery_status_label = QLabel(settings_data.format_ticket_failure_status(saved_failure))
self.ticket_recovery_status_label.setStyleSheet( self.ticket_recovery_status_label.setStyleSheet(
f"color: white; font-size: 12px; {self.font_style}") f"color: white; font-size: 12px; {self.font_style}")
self.ticket_recovery_status_label.setWordWrap(True) self.ticket_recovery_status_label.setWordWrap(True)
@ -1841,23 +1665,11 @@ class Settings(Page):
page_layout.addWidget(scroll_area) page_layout.addWidget(scroll_area)
return page return page
def _format_ticket_failure_status(self, failure):
if not failure:
return "No saved verification failure. If ticket preparation fails validation, recovery data will appear here."
failed_validations = failure.get("failed_validations", [])
how_many_failed = failure.get("how_many_failed", len(failed_validations))
updated_at = failure.get("updated_at", "unknown time")
failed_text = ", ".join(str(item) for item in failed_validations)
return (
f"Saved verification failure: {how_many_failed} failed. "
f"Failed validation indices: {failed_text}. Saved: {updated_at}"
)
def _refresh_ticket_recovery_controls(self): def _refresh_ticket_recovery_controls(self):
failure = self.update_status.get_ticket_verification_failure() failure = self.update_status.get_ticket_verification_failure()
has_failure = failure is not None has_failure = failure is not None
if hasattr(self, 'ticket_recovery_status_label'): if hasattr(self, 'ticket_recovery_status_label'):
self.ticket_recovery_status_label.setText(self._format_ticket_failure_status(failure)) self.ticket_recovery_status_label.setText(settings_data.format_ticket_failure_status(failure))
if hasattr(self, 'evaluate_public_key_button'): if hasattr(self, 'evaluate_public_key_button'):
self.evaluate_public_key_button.setEnabled(has_failure) self.evaluate_public_key_button.setEnabled(has_failure)
if hasattr(self, 'prepare_saved_blind_sigs_button'): if hasattr(self, 'prepare_saved_blind_sigs_button'):
@ -1873,13 +1685,6 @@ class Settings(Page):
if hasattr(self, 'ticket_recovery_output'): if hasattr(self, 'ticket_recovery_output'):
self.ticket_recovery_output.setPlainText(text) self.ticket_recovery_output.setPlainText(text)
def _format_ticket_recovery_result(self, label, result):
try:
payload = json.dumps(result, indent=2, default=str)
except TypeError:
payload = str(result)
return f"{label}:\n{payload}"
def _get_ticket_failure_for_action(self): def _get_ticket_failure_for_action(self):
failure = self.update_status.get_ticket_verification_failure() failure = self.update_status.get_ticket_verification_failure()
if failure is None: if failure is None:
@ -1941,7 +1746,7 @@ class Settings(Page):
self.ticket_recovery_worker.start() self.ticket_recovery_worker.start()
def on_saved_blind_prep_done(self, result): def on_saved_blind_prep_done(self, result):
self._write_ticket_recovery_output(self._format_ticket_recovery_result("Results of preparation", result)) self._write_ticket_recovery_output(settings_data.format_ticket_recovery_result("Results of preparation", result))
if isinstance(result, dict) and result.get('valid') is True: if isinstance(result, dict) and result.get('valid') is True:
self.update_status.clear_ticket_verification_failure() self.update_status.clear_ticket_verification_failure()
self.update_status.update_status("Tickets prepared from saved blind signatures.") self.update_status.update_status("Tickets prepared from saved blind signatures.")
@ -2187,7 +1992,7 @@ class Settings(Page):
layout.addStretch() layout.addStretch()
self.load_systemwide_settings( self.load_systemwide_settings(
self._prepared_value("systemwide_enabled", self._read_systemwide_enabled)) self._prepared_value("systemwide_enabled", settings_data.read_systemwide_enabled))
return page return page
def create_bwrap_page(self): def create_bwrap_page(self):
@ -2240,12 +2045,12 @@ class Settings(Page):
layout.addStretch() layout.addStretch()
self.load_bwrap_settings( self.load_bwrap_settings(
self._prepared_value("bwrap_enabled", self._read_bwrap_enabled)) self._prepared_value("bwrap_enabled", settings_data.read_bwrap_enabled))
return page return page
def load_systemwide_settings(self, enabled=None): def load_systemwide_settings(self, enabled=None):
if enabled is None: if enabled is None:
enabled = self._read_systemwide_enabled() enabled = settings_data.read_systemwide_enabled()
self.systemwide_toggle.setChecked(enabled) self.systemwide_toggle.setChecked(enabled)
if enabled: if enabled:
self.systemwide_status_value.setText("Enabled") self.systemwide_status_value.setText("Enabled")
@ -2284,7 +2089,7 @@ class Settings(Page):
def load_bwrap_settings(self, enabled=None): def load_bwrap_settings(self, enabled=None):
if enabled is None: if enabled is None:
enabled = self._read_bwrap_enabled() enabled = settings_data.read_bwrap_enabled()
self.bwrap_toggle.setChecked(enabled) self.bwrap_toggle.setChecked(enabled)
if enabled: if enabled:
self.bwrap_status_value.setText("Enabled") self.bwrap_status_value.setText("Enabled")
@ -2382,26 +2187,3 @@ class Settings(Page):
logging.error(f"Error saving registration settings: {str(e)}") logging.error(f"Error saving registration settings: {str(e)}")
self.update_status.update_status( self.update_status.update_status(
"Error saving registration settings") "Error saving registration settings")
def is_auto_sync_enabled(self) -> bool:
try:
config = self.update_status._load_gui_config()
if config and "registrations" in config:
registrations = config["registrations"]
return registrations.get("auto_sync_enabled", False)
return False
except Exception as e:
logging.error(f"Error checking auto-sync setting: {str(e)}")
return False
def is_fast_registration_enabled(self) -> bool:
try:
config = self.update_status._load_gui_config()
if config and "registrations" in config:
registrations = config["registrations"]
return registrations.get("fast_registration_enabled", False)
return False
except Exception as e:
logging.error(
f"Error checking fast registration setting: {str(e)}")
return False

View file

@ -1,290 +0,0 @@
from core.Constants import Constants
from PyQt6.QtWidgets import QApplication, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QScrollArea
from PyQt6.QtCore import Qt, QSize
from PyQt6.QtGui import QIcon, QFont, QColor, QPalette, QPixmap, QPainter
import sys
APP_VERSION = Constants.DB_VERSION_THIS_APP_WANTS
class DatabaseConflictDialog(QDialog):
WIPE = 1
MOVE_DB = 2
EXIT = 3
def __init__(self, check: dict, parent=None):
super().__init__(parent)
self.setWindowTitle("Database Recovery")
self.setModal(True)
self.user_choice = self.EXIT
self.setMinimumWidth(550)
self.setMinimumHeight(400)
self.setMaximumWidth(750)
self.setMaximumHeight(600)
# Modern dark theme
self.setStyleSheet("""
DatabaseConflictDialog {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #0f172a, stop:1 #1e293b);
}
""")
main_layout = QVBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# Header with gradient and icon
header = QFrame()
header.setStyleSheet("""
QFrame {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #3b82f6, stop:1 #1e40af);
border: none;
}
""")
header.setFixedHeight(100)
header_layout = QHBoxLayout()
header_layout.setContentsMargins(30, 20, 30, 20)
# Warning icon (using unicode)
icon_label = QLabel("⚠️")
icon_label.setFont(QFont("Segoe UI", 48))
header_layout.addWidget(icon_label, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
######## Title Section. ##############
# First which messages to display,
if check["status"] == "version_mismatch":
error_title = "Database Compatibility"
error_subtitle = "Your App version and Database don't match"
else:
error_title = "Database Error"
error_subtitle = "The Database can't properly start"
# then display them:
title_layout = QVBoxLayout()
title_layout.setSpacing(5)
title = QLabel(error_title)
title.setFont(QFont("Segoe UI", 18, QFont.Weight.Bold))
title.setStyleSheet("color: white;")
title_layout.addWidget(title)
subtitle = QLabel(error_subtitle)
subtitle.setFont(QFont("Segoe UI", 11))
subtitle.setStyleSheet("color: rgba(255, 255, 255, 0.8);")
title_layout.addWidget(subtitle)
header_layout.addLayout(title_layout, 1)
header.setLayout(header_layout)
main_layout.addWidget(header)
# Content area with scroll support
content = QFrame()
content.setStyleSheet("""
QFrame {
background: #1e293b;
border: none;
}
""")
content_layout = QVBoxLayout()
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(0)
# Scrollable area for dynamic content
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setStyleSheet("""
QScrollArea {
background: #1e293b;
border: none;
}
QScrollBar:vertical {
background: #1e293b;
width: 12px;
}
QScrollBar::handle:vertical {
background: #475569;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background: #64748b;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
border: none;
}
""")
# Inner scrollable widget
scroll_widget = QFrame()
scroll_widget.setStyleSheet("background: #1e293b; border: none;")
scroll_layout = QVBoxLayout()
scroll_layout.setContentsMargins(40, 30, 40, 30)
scroll_layout.setSpacing(15)
# Main message
message = QLabel(check["message"])
message.setFont(QFont("Segoe UI", 12))
message.setStyleSheet("color: #e2e8f0; line-height: 1.6;")
message.setWordWrap(True)
scroll_layout.addWidget(message)
# Version info
if check.get("db_version"):
info_frame = QFrame()
info_frame.setStyleSheet("""
QFrame {
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 6px;
}
""")
info_layout = QVBoxLayout()
info_layout.setContentsMargins(15, 12, 15, 12)
info_layout.setSpacing(5)
app_ver = QLabel(f"App is looking for database version: {APP_VERSION}")
app_ver.setFont(QFont("Segoe UI", 10))
app_ver.setStyleSheet("color: #94a3b8;")
info_layout.addWidget(app_ver)
db_ver = QLabel(f"Your Actual Database version: {check['db_version']}")
db_ver.setFont(QFont("Segoe UI", 10))
db_ver.setStyleSheet("color: #94a3b8;")
info_layout.addWidget(db_ver)
info_frame.setLayout(info_layout)
scroll_layout.addWidget(info_frame)
scroll_layout.addStretch()
scroll_widget.setLayout(scroll_layout)
scroll_area.setWidget(scroll_widget)
content_layout.addWidget(scroll_area, 1)
content.setLayout(content_layout)
main_layout.addWidget(content, 1)
# Button area
button_area = QFrame()
button_area.setStyleSheet("""
QFrame {
background: #0f172a;
border-top: 1px solid #334155;
}
""")
button_layout = QVBoxLayout()
button_layout.setContentsMargins(40, 20, 40, 20)
button_layout.setSpacing(12)
if check["status"] == "version_mismatch":
btn_backup = self._create_button(
"Create a Fresh Database. Then restart (recommended)",
"primary",
""
)
btn_backup.clicked.connect(self.on_wipe_create)
button_layout.addWidget(btn_backup)
btn_different = self._create_button(
"Move the Stale Database for Debug Purposes",
"secondary",
"📁"
)
btn_different.clicked.connect(self.on_move_db)
button_layout.addWidget(btn_different)
btn_exit = self._create_button(
"Exit App without action.",
"danger",
""
)
btn_exit.clicked.connect(self.on_exit)
button_layout.addWidget(btn_exit)
button_area.setLayout(button_layout)
main_layout.addWidget(button_area)
self.setLayout(main_layout)
def _create_button(self, text, style_type, icon):
"""Create a styled button"""
btn = QPushButton(f" {icon} {text}")
btn.setFont(QFont("Segoe UI", 11, QFont.Weight.Medium))
btn.setFixedHeight(45)
btn.setCursor(Qt.CursorShape.PointingHandCursor)
if style_type == "primary":
btn.setStyleSheet("""
QPushButton {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #3b82f6, stop:1 #2563eb);
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
}
QPushButton:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #2563eb, stop:1 #1d4ed8);
}
QPushButton:pressed {
background: #1d4ed8;
}
""")
elif style_type == "secondary":
btn.setStyleSheet("""
QPushButton {
background: #334155;
color: #e2e8f0;
border: 1px solid #475569;
border-radius: 6px;
font-weight: 600;
}
QPushButton:hover {
background: #475569;
border: 1px solid #64748b;
}
QPushButton:pressed {
background: #1e293b;
}
""")
elif style_type == "danger":
btn.setStyleSheet("""
QPushButton {
background: #64748b;
color: #e2e8f0;
border: 1px solid #475569;
border-radius: 6px;
font-weight: 600;
}
QPushButton:hover {
background: #ef4444;
border: 1px solid #dc2626;
}
QPushButton:pressed {
background: #dc2626;
}
""")
return btn
def on_wipe_create(self):
self.user_choice = self.WIPE
self.accept()
def on_move_db(self):
self.user_choice = self.MOVE_DB
self.accept()
def on_exit(self):
self.user_choice = self.EXIT
self.reject()
def show_recovery_dialog(check: dict):
"""Show recovery dialog as standalone app and return user choice"""
app = QApplication.instance() or QApplication(sys.argv)
dialog = DatabaseConflictDialog(check)
dialog.exec()
return dialog.user_choice

View file

@ -1,50 +0,0 @@
import shutil
import sys
from pathlib import Path
from PyQt6.QtWidgets import QApplication, QFileDialog
def move_database_file(original_filepath: str) -> bool:
"""
Prompts the user to select a destination for a .db file and moves it there.
Args:
original_filepath: The current path to the .db file
Returns:
True if the move was successful, False otherwise
"""
# Create a QApplication if one doesn't already exist
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# Validate that the original file exists
if not Path(original_filepath).exists():
print(f"Error: File not found at {original_filepath}")
return False
# Open the file save dialog
destination, _ = QFileDialog.getSaveFileName(
None,
"Select destination for database file",
str(Path(original_filepath).parent), # Start in the original file's directory
"Database Files (*.db);;All Files (*)"
)
# If the user cancelled the dialog
if not destination:
print("Operation cancelled by user")
return False
try:
# Move the file
shutil.move(original_filepath, destination)
print(f"File successfully moved to: {destination}")
return True
except Exception as e:
print(f"Error moving file: {e}")
return False
def launch_file_picker(original_path):
move_database_file(original_path)

View file

@ -116,6 +116,42 @@ POPUP_ACTION_BUTTON_RED_QSS = """
""" """
def combobox_style(font_style=""):
return f"""
QComboBox {{
color: black;
background: #f0f0f0;
padding: 5px 30px 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
min-width: 120px;
margin-bottom: 10px;
{font_style}
}}
QComboBox:disabled {{
color: #666;
background: #e0e0e0;
}}
QComboBox::drop-down {{
border: none;
width: 30px;
}}
QComboBox::down-arrow {{
image: url(assets/down_arrow.png);
width: 12px;
height: 12px;
}}
QComboBox QAbstractItemView {{
color: black;
background: white;
selection-background-color: #007bff;
selection-color: white;
border: 1px solid #ccc;
{font_style}
}}
"""
def checkbox_style(font_style=""): def checkbox_style(font_style=""):
return f""" return f"""
QCheckBox {{ QCheckBox {{