forked from Support/sp-hydra-veil-gui
update: navigator and main updates
This commit is contained in:
parent
80c1d2c4e6
commit
7a8c4ded60
11 changed files with 411 additions and 784 deletions
Binary file not shown.
210
gui/main_ui.py
210
gui/main_ui.py
|
|
@ -27,15 +27,7 @@ from gui.v2.infrastructure import orm
|
|||
from gui.v2.infrastructure.navigator import Navigator
|
||||
from gui.v2.infrastructure.setup_observers import setup_observers
|
||||
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.page_data_worker import PageDataWorker
|
||||
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.system_tray import init_system_tray
|
||||
|
|
@ -56,7 +48,6 @@ from gui.v2.actions.flags import (
|
|||
has_shown_fast_mode_prompt,
|
||||
mark_fast_mode_prompt_shown,
|
||||
set_fast_mode_enabled,
|
||||
is_fast_registration_enabled,
|
||||
check_first_launch,
|
||||
)
|
||||
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):
|
||||
def __init__(self, force_sync):
|
||||
def __init__(self, force_sync=False):
|
||||
super().__init__()
|
||||
self.force_sync = force_sync # passed in from loader
|
||||
|
||||
sys.excepthook = self._handle_exception
|
||||
self.setWindowFlags(Qt.WindowType.Window)
|
||||
|
||||
gui_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
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')
|
||||
font_id = QFontDatabase.addApplicationFont(retro_gaming_path)
|
||||
font_family = QFontDatabase.applicationFontFamilies(font_id)[0]
|
||||
|
|
@ -121,6 +113,7 @@ class CustomWindow(QMainWindow):
|
|||
self.css_path = os.getenv('CSS_PATH', os.path.join(
|
||||
gui_dir, 'resources', 'styles'))
|
||||
|
||||
self.force_sync = force_sync
|
||||
self.is_downloading = False
|
||||
self.current_profile_id = None
|
||||
self.connection_manager = ConnectionManager()
|
||||
|
|
@ -174,21 +167,7 @@ class CustomWindow(QMainWindow):
|
|||
self.animation_timer.timeout.connect(self.animate_toggle)
|
||||
self.check_logging()
|
||||
|
||||
self.navigator = Navigator(self.page_stack, 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.navigator = Navigator(self)
|
||||
|
||||
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})"
|
||||
)
|
||||
|
||||
if self.force_sync:
|
||||
self.sync()
|
||||
|
||||
|
||||
def init_ui(self):
|
||||
if self.check_first_launch():
|
||||
self.navigator.navigate("welcome")
|
||||
else:
|
||||
self.navigator.navigate("menu")
|
||||
self._start_background_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)
|
||||
self.navigator.start_preload()
|
||||
|
||||
def perform_update_check(self):
|
||||
from core.controllers.ClientController import ClientController
|
||||
update_available = ClientController.can_be_updated()
|
||||
|
||||
if update_available:
|
||||
menu_page = self.navigator.get_cached("menu")
|
||||
if menu_page is not None:
|
||||
menu_page.on_update_check_finished()
|
||||
self.navigator.update_if_cached(
|
||||
"menu", lambda p: p.on_update_check_finished())
|
||||
|
||||
def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
||||
if not status:
|
||||
|
|
@ -363,34 +224,27 @@ class CustomWindow(QMainWindow):
|
|||
for i, brw in enumerate(available_browsers)
|
||||
]
|
||||
|
||||
browser_page = self.navigator.get_cached("browser")
|
||||
if browser_page is not None:
|
||||
browser_page.create_interface_elements(available_browsers_list)
|
||||
self.navigator.update_if_cached(
|
||||
"browser", lambda p: p.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")
|
||||
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'):
|
||||
menu_page.refresh_profiles_data()
|
||||
if hasattr(menu_page, 'buttons'):
|
||||
for button in menu_page.buttons:
|
||||
parent = button.parent()
|
||||
if parent:
|
||||
verification_icons = parent.findChildren(QPushButton)
|
||||
for icon in verification_icons:
|
||||
if icon.geometry().width() == 20 and icon.geometry().height() == 20:
|
||||
icon.setEnabled(True)
|
||||
def _enable_menu_verification_icons(self, menu_page):
|
||||
if hasattr(menu_page, 'refresh_profiles_data'):
|
||||
menu_page.refresh_profiles_data()
|
||||
if hasattr(menu_page, 'buttons'):
|
||||
for button in menu_page.buttons:
|
||||
parent = button.parent()
|
||||
if parent:
|
||||
verification_icons = parent.findChildren(QPushButton)
|
||||
for icon in verification_icons:
|
||||
if icon.geometry().width() == 20 and icon.geometry().height() == 20:
|
||||
icon.setEnabled(True)
|
||||
|
||||
def sync(self):
|
||||
core_logger.info("User clicked sync button")
|
||||
|
|
@ -803,9 +657,9 @@ class CustomWindow(QMainWindow):
|
|||
if not self.has_shown_fast_mode_prompt():
|
||||
self.navigator.navigate("fast_mode_prompt")
|
||||
else:
|
||||
menu_page = self.navigator.get_cached("menu")
|
||||
if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'):
|
||||
menu_page.refresh_menu_buttons()
|
||||
self.navigator.update_if_cached(
|
||||
"menu",
|
||||
lambda p: p.refresh_menu_buttons() if hasattr(p, 'refresh_menu_buttons') else None)
|
||||
self.navigator.navigate("menu")
|
||||
|
||||
def enable_marquee(self, text):
|
||||
|
|
|
|||
|
|
@ -42,6 +42,13 @@ def is_fast_registration_enabled(gui_config_file):
|
|||
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):
|
||||
config = load_gui_config(gui_config_file)
|
||||
if config is None:
|
||||
|
|
|
|||
138
gui/v2/actions/settings_data.py
Executable file
138
gui/v2/actions/settings_data.py
Executable 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}"
|
||||
|
|
@ -2,15 +2,40 @@ import importlib
|
|||
import time
|
||||
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:
|
||||
def __init__(self, page_stack, custom_window):
|
||||
self.page_stack = page_stack
|
||||
class Navigator(QObject):
|
||||
def __init__(self, custom_window):
|
||||
super().__init__()
|
||||
self.custom_window = custom_window
|
||||
self.page_stack = custom_window.page_stack
|
||||
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):
|
||||
module_path, class_name = PAGE_REGISTRY[name]
|
||||
module = importlib.import_module(module_path)
|
||||
|
|
@ -31,12 +56,28 @@ class Navigator:
|
|||
if page is None:
|
||||
if name not in PAGE_REGISTRY:
|
||||
self._warn_not_migrated(name)
|
||||
return
|
||||
return None
|
||||
page = self._instantiate(name)
|
||||
self.register_instance(name, 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:
|
||||
print(f"[preload] {name:30s} SKIP (already cached)", flush=True)
|
||||
return False
|
||||
|
|
@ -64,11 +105,114 @@ class Navigator:
|
|||
f"{type(e).__name__}: {e}")
|
||||
return False
|
||||
|
||||
def get_cached(self, name):
|
||||
return self._instances.get(name)
|
||||
def start_preload(self, fast_mode=None):
|
||||
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):
|
||||
try:
|
||||
self.custom_window.update_status(f"Page '{name}' not migrated yet.")
|
||||
except Exception:
|
||||
pass
|
||||
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:
|
||||
page = self._instantiate(name, prepared=payload)
|
||||
self.register_instance(name, page)
|
||||
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
0
gui/v2/infrastructure/orm.py
Normal file → Executable file
|
|
@ -13,6 +13,7 @@ class Page(QWidget):
|
|||
self.btn_path = custom_window.btn_path
|
||||
self.name = name
|
||||
self.page_stack = page_stack
|
||||
self._prepared = None
|
||||
self.init_ui()
|
||||
self.selected_profiles = []
|
||||
self.selected_wireguard = []
|
||||
|
|
@ -23,6 +24,11 @@ class Page(QWidget):
|
|||
def prepare_data(custom_window):
|
||||
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):
|
||||
self.selected_profiles.clear()
|
||||
self.selected_wireguard.clear()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
from typing import Union
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame, QGridLayout,
|
||||
|
|
@ -34,6 +32,7 @@ from core.Errors import (
|
|||
)
|
||||
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.profile_order import normalize_profile_order
|
||||
from gui.v2.infrastructure.page_registry import PAGE_REGISTRY
|
||||
|
|
@ -44,6 +43,7 @@ from gui.v2.ui.styles.styles import (
|
|||
SCROLLBAR_CYAN_QSS,
|
||||
TERMINAL_LIST_QSS,
|
||||
checkbox_style,
|
||||
combobox_style,
|
||||
)
|
||||
from gui.v2.ui.widgets.clickable_label import ClickableValueLabel
|
||||
from gui.v2.ui.widgets.terminal_widget import TerminalWidget
|
||||
|
|
@ -59,7 +59,7 @@ class Settings(Page):
|
|||
|
||||
self.update_status = 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_workers = set()
|
||||
self.button_reverse.setVisible(True)
|
||||
|
|
@ -71,49 +71,7 @@ class Settings(Page):
|
|||
|
||||
@staticmethod
|
||||
def prepare_data(custom_window):
|
||||
profiles = ProfileController.get_all()
|
||||
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
|
||||
return settings_data.prepare(custom_window.gui_config_file)
|
||||
|
||||
def setup_ui(self):
|
||||
main_container = QWidget(self)
|
||||
|
|
@ -344,7 +302,7 @@ class Settings(Page):
|
|||
profile_selection_layout.addWidget(profile_label)
|
||||
|
||||
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.on_debug_profile_selected)
|
||||
profile_selection_layout.addWidget(self.debug_profile_selector)
|
||||
|
|
@ -690,7 +648,7 @@ class Settings(Page):
|
|||
self.cli_command.setText(
|
||||
f"'{app_path}' --cli profile enable -i {profile_id}")
|
||||
self.cli_copy_button.setEnabled(True)
|
||||
ip_address = self.extract_endpoint_ip(profile)
|
||||
ip_address = settings_data.extract_endpoint_ip(profile)
|
||||
if ip_address:
|
||||
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}")
|
||||
|
|
@ -722,25 +680,6 @@ class Settings(Page):
|
|||
self.wg_quick_up_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):
|
||||
profile_id = self.debug_profile_selector.currentData()
|
||||
if profile_id is None:
|
||||
|
|
@ -748,7 +687,7 @@ class Settings(Page):
|
|||
|
||||
profile = ProfileController.get(profile_id)
|
||||
if profile and isinstance(profile, SystemProfile):
|
||||
ip_address = self.extract_endpoint_ip(profile)
|
||||
ip_address = settings_data.extract_endpoint_ip(profile)
|
||||
if ip_address:
|
||||
ping_command = f"ping {ip_address}"
|
||||
clipboard = QApplication.clipboard()
|
||||
|
|
@ -763,7 +702,7 @@ class Settings(Page):
|
|||
|
||||
profile = ProfileController.get(profile_id)
|
||||
if profile and isinstance(profile, SystemProfile):
|
||||
ip_address = self.extract_endpoint_ip(profile)
|
||||
ip_address = settings_data.extract_endpoint_ip(profile)
|
||||
if ip_address:
|
||||
self.test_ping_button.setEnabled(False)
|
||||
self.ping_result_label.setText("Testing ping...")
|
||||
|
|
@ -853,75 +792,9 @@ class Settings(Page):
|
|||
self.content_layout.addWidget(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:
|
||||
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):
|
||||
self.custom_window.navigator.navigate("menu")
|
||||
|
||||
|
|
@ -956,7 +829,7 @@ class Settings(Page):
|
|||
profile_layout = QVBoxLayout(profile_group)
|
||||
|
||||
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)
|
||||
if profiles:
|
||||
for profile_id, profile in profiles.items():
|
||||
|
|
@ -1091,7 +964,7 @@ class Settings(Page):
|
|||
|
||||
self.verification_profile_selector = QComboBox()
|
||||
self.verification_profile_selector.setStyleSheet(
|
||||
self.get_combobox_style())
|
||||
combobox_style(self.font_style))
|
||||
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
||||
if profiles:
|
||||
for profile_id, profile in profiles.items():
|
||||
|
|
@ -1210,72 +1083,30 @@ class Settings(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):
|
||||
if index < 0:
|
||||
return
|
||||
|
||||
profile_id = self.verification_profile_selector.itemData(index)
|
||||
if profile_id is None:
|
||||
for key, widget in self.verification_info.items():
|
||||
widget.setText("N/A")
|
||||
self.verification_full_values[key] = "N/A"
|
||||
if key in self.verification_checkmarks:
|
||||
self.verification_checkmarks[key].hide()
|
||||
return
|
||||
profile = None
|
||||
if profile_id is not None:
|
||||
profile = ProfileController.get(profile_id)
|
||||
|
||||
profile = ProfileController.get(profile_id)
|
||||
if not profile:
|
||||
for key, widget in self.verification_info.items():
|
||||
widget.setText("N/A")
|
||||
self.verification_full_values[key] = "N/A"
|
||||
if key in self.verification_checkmarks:
|
||||
self.verification_checkmarks[key].hide()
|
||||
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:
|
||||
self.verification_info["operator_name"].setText("N/A")
|
||||
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"
|
||||
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():
|
||||
full_value = view.get(key, "N/A")
|
||||
self.verification_full_values[key] = full_value
|
||||
if key in truncated_keys and full_value != "N/A":
|
||||
widget.setText(settings_data.truncate_key(full_value))
|
||||
else:
|
||||
widget.setText(full_value)
|
||||
if key in self.verification_checkmarks:
|
||||
full_value = self.verification_full_values.get(key, "")
|
||||
if full_value and full_value != "N/A":
|
||||
self.verification_checkmarks[key].show()
|
||||
else:
|
||||
|
|
@ -1320,16 +1151,9 @@ class Settings(Page):
|
|||
|
||||
if profile and hasattr(profile, 'subscription') and profile.subscription:
|
||||
try:
|
||||
self.subscription_info["billing_code"].setText(
|
||||
str(profile.subscription.billing_code))
|
||||
|
||||
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")
|
||||
view = settings_data.build_subscription_view(profile)
|
||||
self.subscription_info["billing_code"].setText(view["billing_code"])
|
||||
self.subscription_info["expires_at"].setText(view["expires_at"])
|
||||
except Exception as e:
|
||||
print(f"Error updating subscription info: {e}")
|
||||
else:
|
||||
|
|
@ -1787,7 +1611,7 @@ class Settings(Page):
|
|||
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
|
||||
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(
|
||||
f"color: white; font-size: 12px; {self.font_style}")
|
||||
self.ticket_recovery_status_label.setWordWrap(True)
|
||||
|
|
@ -1841,23 +1665,11 @@ class Settings(Page):
|
|||
page_layout.addWidget(scroll_area)
|
||||
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):
|
||||
failure = self.update_status.get_ticket_verification_failure()
|
||||
has_failure = failure is not None
|
||||
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'):
|
||||
self.evaluate_public_key_button.setEnabled(has_failure)
|
||||
if hasattr(self, 'prepare_saved_blind_sigs_button'):
|
||||
|
|
@ -1873,13 +1685,6 @@ class Settings(Page):
|
|||
if hasattr(self, 'ticket_recovery_output'):
|
||||
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):
|
||||
failure = self.update_status.get_ticket_verification_failure()
|
||||
if failure is None:
|
||||
|
|
@ -1941,7 +1746,7 @@ class Settings(Page):
|
|||
self.ticket_recovery_worker.start()
|
||||
|
||||
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:
|
||||
self.update_status.clear_ticket_verification_failure()
|
||||
self.update_status.update_status("Tickets prepared from saved blind signatures.")
|
||||
|
|
@ -2187,7 +1992,7 @@ class Settings(Page):
|
|||
|
||||
layout.addStretch()
|
||||
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
|
||||
|
||||
def create_bwrap_page(self):
|
||||
|
|
@ -2240,12 +2045,12 @@ class Settings(Page):
|
|||
|
||||
layout.addStretch()
|
||||
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
|
||||
|
||||
def load_systemwide_settings(self, enabled=None):
|
||||
if enabled is None:
|
||||
enabled = self._read_systemwide_enabled()
|
||||
enabled = settings_data.read_systemwide_enabled()
|
||||
self.systemwide_toggle.setChecked(enabled)
|
||||
if enabled:
|
||||
self.systemwide_status_value.setText("Enabled")
|
||||
|
|
@ -2284,7 +2089,7 @@ class Settings(Page):
|
|||
|
||||
def load_bwrap_settings(self, enabled=None):
|
||||
if enabled is None:
|
||||
enabled = self._read_bwrap_enabled()
|
||||
enabled = settings_data.read_bwrap_enabled()
|
||||
self.bwrap_toggle.setChecked(enabled)
|
||||
if enabled:
|
||||
self.bwrap_status_value.setText("Enabled")
|
||||
|
|
@ -2382,26 +2187,3 @@ class Settings(Page):
|
|||
logging.error(f"Error saving registration settings: {str(e)}")
|
||||
self.update_status.update_status(
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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=""):
|
||||
return f"""
|
||||
QCheckBox {{
|
||||
|
|
|
|||
Loading…
Reference in a new issue