diff --git a/gui/__pycache__/main_ui.cpython-312.pyc b/gui/__pycache__/main_ui.cpython-312.pyc index 273b5de..39261e1 100644 Binary files a/gui/__pycache__/main_ui.cpython-312.pyc and b/gui/__pycache__/main_ui.cpython-312.pyc differ diff --git a/gui/main_ui.py b/gui/main_ui.py index 9853dde..e4ba5b7 100755 --- a/gui/main_ui.py +++ b/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): diff --git a/gui/v2/actions/flags.py b/gui/v2/actions/flags.py index af16022..317728e 100755 --- a/gui/v2/actions/flags.py +++ b/gui/v2/actions/flags.py @@ -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: diff --git a/gui/v2/actions/settings_data.py b/gui/v2/actions/settings_data.py new file mode 100755 index 0000000..e190e2a --- /dev/null +++ b/gui/v2/actions/settings_data.py @@ -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}" diff --git a/gui/v2/infrastructure/navigator.py b/gui/v2/infrastructure/navigator.py index 4c54a8a..1499649 100755 --- a/gui/v2/infrastructure/navigator.py +++ b/gui/v2/infrastructure/navigator.py @@ -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) diff --git a/gui/v2/infrastructure/orm.py b/gui/v2/infrastructure/orm.py old mode 100644 new mode 100755 diff --git a/gui/v2/ui/pages/Page.py b/gui/v2/ui/pages/Page.py index d6b673c..daee6a0 100755 --- a/gui/v2/ui/pages/Page.py +++ b/gui/v2/ui/pages/Page.py @@ -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() diff --git a/gui/v2/ui/pages/settings_page.py b/gui/v2/ui/pages/settings_page.py index e5ab437..a4af5e8 100755 --- a/gui/v2/ui/pages/settings_page.py +++ b/gui/v2/ui/pages/settings_page.py @@ -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 diff --git a/gui/v2/ui/popups/Database_version.py b/gui/v2/ui/popups/Database_version.py deleted file mode 100644 index d42c2d6..0000000 --- a/gui/v2/ui/popups/Database_version.py +++ /dev/null @@ -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 diff --git a/gui/v2/ui/popups/pick_folder_to_move.py b/gui/v2/ui/popups/pick_folder_to_move.py deleted file mode 100644 index daff390..0000000 --- a/gui/v2/ui/popups/pick_folder_to_move.py +++ /dev/null @@ -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) diff --git a/gui/v2/ui/styles/styles.py b/gui/v2/ui/styles/styles.py index 3b8137d..ac75a4b 100755 --- a/gui/v2/ui/styles/styles.py +++ b/gui/v2/ui/styles/styles.py @@ -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 {{