Compare commits
79 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3cfc06630 | ||
|
|
f13bc8d61e | ||
|
|
8039ab08bb | ||
|
|
0cc496a40b | ||
| 6e074482ab | |||
| cf8f7ce43f | |||
| ba816a3093 | |||
| f9f809a418 | |||
| ff090ea7bd | |||
|
|
40f0a467ab | ||
|
|
8a0a1fc74d | ||
|
|
bd6a9a4893 | ||
|
|
d8582c8d6d | ||
| 2258eda245 | |||
| 42560e9174 | |||
| dea5aada0c | |||
| 5cc93ed77b | |||
| 99eea377f2 | |||
| c3de90c313 | |||
| 44fd824292 | |||
| 86fc3ca56c | |||
| db1f903b0b | |||
| bd9b4ee106 | |||
| b023b9faf7 | |||
| 184728a948 | |||
|
|
3c67e72d2d | ||
| aecfdbad33 | |||
| 745c465cf2 | |||
| 40eba5b41b | |||
| 1d886728e0 | |||
|
|
b502f9130d | ||
| 23dbf97309 | |||
| 3a59cefb82 | |||
|
|
1a1344a486 | ||
| 1298c415d0 | |||
|
|
97f09ec42e | ||
|
|
2e026c5287 | ||
|
|
3483b51200 | ||
| b444ff9218 | |||
| 1633562325 | |||
| b8d8689885 | |||
| 75bd51b8f8 | |||
|
|
ab23fc958e | ||
| d52ca4842c | |||
|
|
cfd9a92a24 | ||
| c3cf7e241e | |||
| 9cacab4d71 | |||
| 95e0f676df | |||
| baf7f7cec1 | |||
| 0a6280276f | |||
| 25489bf203 | |||
| e5c83e72b1 | |||
| 59cac90c58 | |||
| b536204048 | |||
| f8a09858cc | |||
| 50e9469178 | |||
| 332a84f187 | |||
| 8d08d71a48 | |||
|
|
95f753f1fb | ||
| 7cb9f903db | |||
| 716b62a465 | |||
| 2f0ffe0351 | |||
|
|
8b17b74e56 | ||
| 7cedecd281 | |||
|
|
91e657a06d | ||
| 7910586e1a | |||
|
|
42e06b71c2 | ||
|
|
7a8c4ded60 | ||
| 80c1d2c4e6 | |||
| c508dddcac | |||
| 33301b0da8 | |||
| b13c355888 | |||
|
|
28a9787658 | ||
| 86348ce2e3 | |||
|
|
e98f2c99d2 | ||
|
|
ed79aae5a3 | ||
| dc1182234a | |||
| 1c79df8433 | |||
| b50d0768b2 |
17
.gitignore
vendored
|
|
@ -1,5 +1,18 @@
|
||||||
may23.py
|
.dev
|
||||||
old_main.py
|
|
||||||
.idea
|
.idea
|
||||||
.venv
|
.venv
|
||||||
dist
|
dist
|
||||||
|
gui/__pycache__/
|
||||||
|
gui/__pycache__
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
This is the second version with isolated UI elements from action functions.
|
||||||
|
|
||||||
# sp-hydra-veil-gui
|
# sp-hydra-veil-gui
|
||||||
|
|
||||||
The `sp-hydra-veil-gui` graphical user interface implements the `sp-hydra-veil-core` library.
|
The `sp-hydra-veil-gui` graphical user interface implements the `sp-hydra-veil-core` library.
|
||||||
|
|
|
||||||
12363
gui/___main__.py
288
gui/__main__.py
Executable file
|
|
@ -0,0 +1,288 @@
|
||||||
|
from core.errors.logger import logger
|
||||||
|
from core.models.manage.session_management import init_session, get_session, close_session, create_ALL_tables, create_ONLY_db_version_table, get_path, does_it_exist, does_db_version_table_exist
|
||||||
|
from core.models.manage.version_check import check_database_compatibility, insert_new_version, get_custom_message
|
||||||
|
from core.services.helpers.manage_assets import assets_folder_setup
|
||||||
|
from core.Constants import Constants
|
||||||
|
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
|
||||||
|
from core.models.manage.migrations import migrate_sql
|
||||||
|
from core.models.manage.clear_sql_model import clear_sql_model
|
||||||
|
from core.controllers.SyncController import new_sync
|
||||||
|
from core.controllers.ConfigurationController import ConfigurationController
|
||||||
|
from essentials.observers.ConnectionObserver import ConnectionObserver
|
||||||
|
from core.observers.ClientObserver import ClientObserver
|
||||||
|
# from gui.v2.actions.database_health import GuiStorageDatabaseError, validate_storage_database, has_required_sync_data
|
||||||
|
|
||||||
|
# generic
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# UTIL FUNCTIONS AND RECOVERY UI
|
||||||
|
# ============================================================================
|
||||||
|
def clear_sync_cache():
|
||||||
|
"""
|
||||||
|
Purpose:
|
||||||
|
Clear CachedSync
|
||||||
|
If that fails, prompt for a database wipe.
|
||||||
|
|
||||||
|
Why:
|
||||||
|
If they migrated, they have new schemas, but old data.
|
||||||
|
The problem with that, is the newest CachedSync entry will fool the sync data functions into thinking they don't need to sync new data,
|
||||||
|
Even though they do need to sync with the new schema.
|
||||||
|
"""
|
||||||
|
from core.models.orm_models.CachedSync import CachedSync
|
||||||
|
result = clear_sql_model(CachedSync)
|
||||||
|
if result:
|
||||||
|
logger.info(f"[DB MANAGEMENT] After Migration cleared the CachedSync table: {result}.")
|
||||||
|
else:
|
||||||
|
custom_error = "There were issues with clearing the Sync Cache in your database. Please delete the old version and fetch the new public data (what locations, browsers, ect) This will NOT affect your profiles or browser sessions."
|
||||||
|
recovery_dialog(custom_error, "CachedSync Issue")
|
||||||
|
|
||||||
|
def failed_migration(db_version_you_have):
|
||||||
|
close_session() # shut down db connection
|
||||||
|
custom_error = "There were issues with migrating your database. We transitioned to a new Database format for new features! Please delete the old version and fetch the new public data (what locations, browsers, ect) This will NOT affect your profiles or browser sessions."
|
||||||
|
recovery_dialog(custom_error, db_version_you_have)
|
||||||
|
|
||||||
|
def recovery_dialog(custom_error, db_version_you_have):
|
||||||
|
"""This ends the app by forcing them to delete the database, move it, or just quit."""
|
||||||
|
|
||||||
|
from gui.v2.ui.popups.Database_version import show_recovery_dialog
|
||||||
|
choice = show_recovery_dialog({"message": custom_error, "db_version": db_version_you_have, "status": "version_mismatch"})
|
||||||
|
logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}")
|
||||||
|
|
||||||
|
if choice == 1:
|
||||||
|
if os.path.exists(database_path):
|
||||||
|
os.remove(database_path)
|
||||||
|
logger.info(f"[DB MANAGEMENT] Deleted the DB file at {database_path}. Now putting lock on for forced sync..")
|
||||||
|
lock_file.touch()
|
||||||
|
logger.info(f"[DB MANAGEMENT] Closing the app gracefully, for them to reboot..")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
elif choice == 2:
|
||||||
|
logger.info(f"[DB MANAGEMENT] User opted to move the DB file.")
|
||||||
|
from gui.v2.ui.popups.pick_folder_to_move import launch_file_picker
|
||||||
|
launch_file_picker(database_path)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
def emergency_sync():
|
||||||
|
from gui.v2.ui.popups.generic_choice import show_generic_options
|
||||||
|
from gui.v2.ui.popups.terminal_threading import show_terminal
|
||||||
|
|
||||||
|
option = show_generic_options(
|
||||||
|
title="Sync the Database",
|
||||||
|
option_one="Clearweb (fastest)",
|
||||||
|
option_two="Tor",
|
||||||
|
option_three="No, Exit."
|
||||||
|
)
|
||||||
|
|
||||||
|
if option == 3:
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
client_observer = ClientObserver()
|
||||||
|
connection_observer = ConnectionObserver()
|
||||||
|
|
||||||
|
if option == 1:
|
||||||
|
ConfigurationController.set_connection("system")
|
||||||
|
elif option == 2:
|
||||||
|
ConfigurationController.set_connection("tor")
|
||||||
|
|
||||||
|
# setup the function to be able to have the UI background thread run it.
|
||||||
|
do_this_function = partial(new_sync, client_observer, connection_observer)
|
||||||
|
|
||||||
|
return show_terminal(
|
||||||
|
do_this_function=do_this_function,
|
||||||
|
connection_observer=connection_observer,
|
||||||
|
client_observer=client_observer
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# BEGIN PROGRAM
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ASSETS FOLDER
|
||||||
|
# ============================================================================
|
||||||
|
logger.info("Welcome, checking assets..")
|
||||||
|
if not assets_folder_setup():
|
||||||
|
from gui.v2.ui.popups.generic_error_popup import show_error
|
||||||
|
custom_error = "Unable to Setup your Assets Folder. Please create an assets directory at ~/.local/share/hydra-veil/assets"
|
||||||
|
show_error(custom_error, "Critical Error")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# INITIALIZE DATABASE
|
||||||
|
# ============================================================================
|
||||||
|
lock_file = Path(f"{Constants.HV_DATA_HOME}/deleted_db.lock")
|
||||||
|
system_path = get_path()
|
||||||
|
database_path = system_path / "storage.db"
|
||||||
|
main_db_exists = False
|
||||||
|
logger.info("[DB MANAGEMENT] Starting DB init..")
|
||||||
|
try:
|
||||||
|
main_db_exists = does_it_exist(database_path)
|
||||||
|
|
||||||
|
# Setup operations on the main DB which create it
|
||||||
|
init_session() # (engine, Session, _session all initialized from session_management)
|
||||||
|
session = get_session()
|
||||||
|
|
||||||
|
# does the version checker table exist?
|
||||||
|
version_table_exists = does_db_version_table_exist()
|
||||||
|
except Exception as error:
|
||||||
|
logger.error(f"[DB MANAGEMENT] Critical Error with database initialization: {error}")
|
||||||
|
close_session()
|
||||||
|
recovery_dialog("HydraVeil could not initialize the local storage database. Please move or reset storage.db, then restart.", "Startup Error")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LEGACY DATABASE CHECKS
|
||||||
|
# ============================================================================
|
||||||
|
migration_happened = False
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ZERO DATABASE = SYNC NOW
|
||||||
|
# ============================================================================
|
||||||
|
if not main_db_exists:
|
||||||
|
made_tables = create_ALL_tables()
|
||||||
|
if made_tables:
|
||||||
|
# force sync:
|
||||||
|
emergency_sync()
|
||||||
|
|
||||||
|
# we don't have to worry about migrations, they just got a new DB
|
||||||
|
create_ONLY_db_version_table()
|
||||||
|
|
||||||
|
from gui.main_ui import start_ui
|
||||||
|
try:
|
||||||
|
start_ui(force_sync=False)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Critical Error with loading UI: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
recovery_dialog(error_msg, "Sync Error")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LEGACY VERSION. MIGRATE
|
||||||
|
# ============================================================================
|
||||||
|
# are they upgrading from a legacy version? that would mean the table existed, but not the version table,
|
||||||
|
if not version_table_exists:
|
||||||
|
logger.info(f"[DB MANAGEMENT] We are dealing with a legacy database, we need to transition the user.")
|
||||||
|
|
||||||
|
migration = migrate_sql()
|
||||||
|
|
||||||
|
logger.info(f"[DB MANAGEMENT] Migration result is {migration.valid}.")
|
||||||
|
if migration.valid:
|
||||||
|
migration_happened = True
|
||||||
|
force_sync = True
|
||||||
|
clear_sync_cache()
|
||||||
|
else:
|
||||||
|
# THEN DISPLAY CHOICES AND RECOVERY UI
|
||||||
|
db_version_you_have = "Old System"
|
||||||
|
failed_migration(db_version_you_have)
|
||||||
|
|
||||||
|
# If they're still here, then if it's NOT a legacy version,
|
||||||
|
|
||||||
|
# and the new version table doesn't exist, then create it:
|
||||||
|
if not version_table_exists:
|
||||||
|
create_ONLY_db_version_table()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# COMPARE DB VERISONS
|
||||||
|
# ============================================================================
|
||||||
|
compatability_dict = check_database_compatibility(session)
|
||||||
|
reason = compatability_dict.get("reason", "error")
|
||||||
|
is_compatable = compatability_dict.get("result", False)
|
||||||
|
logger.info(f"[DB MANAGEMENT] The result of the check is {is_compatable} and the reason is {reason}")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# IF the versions do NOT match
|
||||||
|
# ============================================================================
|
||||||
|
migrated = False
|
||||||
|
if not is_compatable:
|
||||||
|
|
||||||
|
# MIGRATE?
|
||||||
|
if reason == "upgrade":
|
||||||
|
migration = migrate_sql()
|
||||||
|
if migration.valid:
|
||||||
|
migrated = True
|
||||||
|
|
||||||
|
# MIGRATE FAILED OR NOT POSSIBLE
|
||||||
|
if not migrated:
|
||||||
|
logger.error(f"[DB MANAGEMENT] The App is expecting a different DB version than the real database. The reason is {reason}")
|
||||||
|
db_version_you_have = compatability_dict.get("old_db_version", "Error getting the Version")
|
||||||
|
|
||||||
|
# WHAT IS THE REASON?
|
||||||
|
custom_error = get_custom_message(reason, compatability_dict)
|
||||||
|
|
||||||
|
# shut down db connection
|
||||||
|
close_session()
|
||||||
|
|
||||||
|
# THEN DISPLAY CHOICES AND RECOVERY UI
|
||||||
|
recovery_dialog(custom_error, db_version_you_have)
|
||||||
|
# goodbye. exits.
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# THE VERSIONS MATCH IF THEY ARE STILL HERE
|
||||||
|
# ============================================================================
|
||||||
|
try:
|
||||||
|
made_tables = create_ALL_tables()
|
||||||
|
# SCREEN FAILED TABLES
|
||||||
|
if not made_tables:
|
||||||
|
custom_error = "Critical Failure with starting the models of the database."
|
||||||
|
logger.error(f"[DB MANAGEMENT] {custom_error}")
|
||||||
|
close_session()
|
||||||
|
from gui.v2.ui.popups.Database_version import show_recovery_dialog
|
||||||
|
choice = show_recovery_dialog({"message": custom_error, "db_version": 0, "status": "cant_make"})
|
||||||
|
logger.info(f"[DB MANAGEMENT] From the database error options, the user picked {choice}")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
except:
|
||||||
|
logger.info(f"[DB MANAGEMENT] create ALL tables failed. Running migrations...")
|
||||||
|
migration = migrate_sql()
|
||||||
|
logger.info(f"[DB MANAGEMENT] Results of migration is {migration.valid}")
|
||||||
|
|
||||||
|
if not migration.valid:
|
||||||
|
db_version_you_have = 0
|
||||||
|
failed_migration(db_version_you_have)
|
||||||
|
|
||||||
|
# ASSUME TABLES CREATED
|
||||||
|
logger.info("[DB MANAGEMENT] Tables successfully made or initialized if pre-existing")
|
||||||
|
|
||||||
|
# UPDATE DATA
|
||||||
|
did_it_insert = insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS)
|
||||||
|
if not did_it_insert:
|
||||||
|
logger.error(f"[DB MANAGEMENT] Critical Failure with updating/inserting the new DB version into the database.")
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# sync_data_missing = not has_required_sync_data(database_path)
|
||||||
|
# except GuiStorageDatabaseError as error:
|
||||||
|
# logger.error(f"[DB MANAGEMENT] Critical storage database error during synced data check: {error}")
|
||||||
|
# close_session()
|
||||||
|
# recovery_dialog(str(error), "Unreadable")
|
||||||
|
# sync_data_missing = True
|
||||||
|
|
||||||
|
# Load GUI either way:
|
||||||
|
from gui.main_ui import start_ui
|
||||||
|
|
||||||
|
logger.info(f"[DB MANAGEMENT] Launcher is now passing it off to launch the main GUI window WITHOUT force sync..")
|
||||||
|
if migration_happened:
|
||||||
|
force_sync = True
|
||||||
|
clear_sync_cache()
|
||||||
|
else:
|
||||||
|
force_sync = False
|
||||||
|
|
||||||
|
# if we deleted their DB, we want to force sync,
|
||||||
|
if lock_file.exists():
|
||||||
|
force_sync = True
|
||||||
|
lock_file.unlink() # unlock for next time
|
||||||
|
|
||||||
|
# Start GUI either way:
|
||||||
|
logger.info(f"[DB MANAGEMENT] Starting GUI..")
|
||||||
|
start_ui(force_sync)
|
||||||
24
gui/assets/yaml_mappings/application_versions.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
fields:
|
||||||
|
- name: id
|
||||||
|
path: ['id']
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- name: application_code
|
||||||
|
path: ['application', 'code']
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- name: version_number
|
||||||
|
path: ['version_number']
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- name: format_revision
|
||||||
|
path: ['format_revision']
|
||||||
|
|
||||||
|
- name: download_path
|
||||||
|
path: ['download_path']
|
||||||
|
|
||||||
|
- name: released_at
|
||||||
|
path: ['released_at']
|
||||||
|
|
||||||
|
- name: file_hash
|
||||||
|
path: ['file_hash']
|
||||||
54
gui/assets/yaml_mappings/locations.yaml
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
fields:
|
||||||
|
- name: country_code
|
||||||
|
path: ['country', 'code']
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- name: code # this is city code
|
||||||
|
path: ['code']
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- name: id
|
||||||
|
path: ['id']
|
||||||
|
|
||||||
|
- name: country_name
|
||||||
|
path: ['country', 'name']
|
||||||
|
|
||||||
|
- name: name # this is CITY name
|
||||||
|
path: ['name']
|
||||||
|
|
||||||
|
- name: time_zone
|
||||||
|
path: ['time_zone', 'code']
|
||||||
|
|
||||||
|
- name: operator_id
|
||||||
|
path: ['operator_id']
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- name: provider_name
|
||||||
|
path: ['provider', 'name']
|
||||||
|
|
||||||
|
- name: is_proxy_capable
|
||||||
|
path: ['is_proxy_capable']
|
||||||
|
|
||||||
|
- name: is_wireguard_capable
|
||||||
|
path: ['is_wireguard_capable']
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- name: is_hysteria2_capable
|
||||||
|
path: ['is_hysteria2_capable']
|
||||||
|
|
||||||
|
- name: is_vless_capable
|
||||||
|
path: ['is_vless_capable']
|
||||||
|
|
||||||
|
|
||||||
|
# original version:
|
||||||
|
# locations.append((
|
||||||
|
# location['country']['code'],
|
||||||
|
# location['code'],
|
||||||
|
# location['id'],
|
||||||
|
# location['country']['name'],
|
||||||
|
# location['name'],
|
||||||
|
# location['time_zone']['code'],
|
||||||
|
# location['operator_id'],
|
||||||
|
# location['provider']['name'],
|
||||||
|
# location['is_proxy_capable'],
|
||||||
|
# location['is_wireguard_capable']))
|
||||||
8
gui/assets/yaml_mappings/mapping_one.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fields:
|
||||||
|
- name: group_a_code
|
||||||
|
path: ['group_a', 'code']
|
||||||
|
- name: code
|
||||||
|
path: ['code']
|
||||||
|
- name: id
|
||||||
|
path: ['id']
|
||||||
|
required: true
|
||||||
19
gui/assets/yaml_mappings/operators.yaml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
fields:
|
||||||
|
- name: id
|
||||||
|
path: ['id']
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- name: name
|
||||||
|
path: ['name']
|
||||||
|
|
||||||
|
- name: public_key # this is ed25519
|
||||||
|
path: ['public_key']
|
||||||
|
|
||||||
|
- name: nostr_public_key
|
||||||
|
path: ['nostr_public_key']
|
||||||
|
|
||||||
|
- name: nostr_profile_reference
|
||||||
|
path: ['nostr_profile_reference']
|
||||||
|
|
||||||
|
- name: nostr_attestation_event_reference
|
||||||
|
path: ['nostr_attestation', 'event_reference']
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
_WORKSPACE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if _WORKSPACE_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, _WORKSPACE_ROOT)
|
||||||
|
|
||||||
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton,
|
QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton,
|
||||||
QDialog, QVBoxLayout, QHBoxLayout
|
QDialog, QVBoxLayout, QHBoxLayout, QMessageBox
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase
|
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase
|
||||||
from PyQt6 import QtGui
|
from PyQt6 import QtGui
|
||||||
|
|
@ -13,14 +18,20 @@ from PyQt6.QtCore import Qt, QTimer, QEvent
|
||||||
|
|
||||||
from core.Constants import Constants
|
from core.Constants import Constants
|
||||||
from core.controllers.ConfigurationController import ConfigurationController
|
from core.controllers.ConfigurationController import ConfigurationController
|
||||||
|
from core.controllers.ProfileController import ProfileController
|
||||||
from core.Errors import UnknownConnectionTypeError
|
from core.Errors import UnknownConnectionTypeError
|
||||||
from core.errors.logger import logger as core_logger
|
from core.errors.logger import logger as core_logger
|
||||||
|
|
||||||
core_logger.propagate = False
|
core_logger.propagate = False
|
||||||
|
|
||||||
|
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
|
||||||
|
mainthread = ThreadSafetyTool()
|
||||||
|
|
||||||
|
from gui.v2.infrastructure import orm
|
||||||
from gui.v2.infrastructure.navigator import Navigator
|
from gui.v2.infrastructure.navigator import Navigator
|
||||||
from gui.v2.infrastructure.setup_observers import setup_observers
|
from gui.v2.infrastructure.setup_observers import setup_observers
|
||||||
from gui.v2.infrastructure.connection_manager import ConnectionManager
|
from gui.v2.infrastructure.connection_manager import ConnectionManager
|
||||||
|
from gui.v2.infrastructure.screen_size import calculate_max_screensize
|
||||||
from gui.v2.workers.worker_thread import WorkerThread
|
from gui.v2.workers.worker_thread import WorkerThread
|
||||||
from gui.v2.ui.builders.bottom_section import create_bottom_section
|
from gui.v2.ui.builders.bottom_section import create_bottom_section
|
||||||
from gui.v2.ui.builders.top_section import create_top_section
|
from gui.v2.ui.builders.top_section import create_top_section
|
||||||
|
|
@ -49,6 +60,8 @@ from gui.v2.actions.profile_data import (
|
||||||
write_profile_data,
|
write_profile_data,
|
||||||
clear_profile_data,
|
clear_profile_data,
|
||||||
)
|
)
|
||||||
|
from gui.v2.actions.database_health import GuiStorageDatabaseError
|
||||||
|
from gui.v2.actions.profile_status import is_profile_enabled_for_gui
|
||||||
from gui.v2.actions.ticket_failure import (
|
from gui.v2.actions.ticket_failure import (
|
||||||
save_ticket_verification_failure,
|
save_ticket_verification_failure,
|
||||||
get_ticket_verification_failure,
|
get_ticket_verification_failure,
|
||||||
|
|
@ -58,14 +71,13 @@ from gui.v2.actions.should_be_synchronized import should_be_synchronized
|
||||||
|
|
||||||
|
|
||||||
class CustomWindow(QMainWindow):
|
class CustomWindow(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self, force_sync=False):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
sys.excepthook = self._handle_exception
|
sys.excepthook = self._handle_exception
|
||||||
self.setWindowFlags(Qt.WindowType.Window)
|
self.setWindowFlags(Qt.WindowType.Window)
|
||||||
|
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
gui_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
parent_dir = os.path.dirname(current_dir)
|
font_path = os.path.join(gui_dir, 'resources', 'fonts')
|
||||||
font_path = os.path.join(parent_dir, 'resources', 'fonts')
|
|
||||||
|
|
||||||
retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf')
|
retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf')
|
||||||
font_id = QFontDatabase.addApplicationFont(retro_gaming_path)
|
font_id = QFontDatabase.addApplicationFont(retro_gaming_path)
|
||||||
|
|
@ -101,10 +113,11 @@ class CustomWindow(QMainWindow):
|
||||||
self.gui_cache_home, self.gui_config_home, self.gui_config_file)
|
self.gui_cache_home, self.gui_config_home, self.gui_config_file)
|
||||||
|
|
||||||
self.btn_path = os.getenv('BTN_PATH', os.path.join(
|
self.btn_path = os.getenv('BTN_PATH', os.path.join(
|
||||||
parent_dir, 'resources', 'images'))
|
gui_dir, 'resources', 'images'))
|
||||||
self.css_path = os.getenv('CSS_PATH', os.path.join(
|
self.css_path = os.getenv('CSS_PATH', os.path.join(
|
||||||
parent_dir, 'resources', 'styles'))
|
gui_dir, 'resources', 'styles'))
|
||||||
|
|
||||||
|
self.force_sync = force_sync
|
||||||
self.is_downloading = False
|
self.is_downloading = False
|
||||||
self.current_profile_id = None
|
self.current_profile_id = None
|
||||||
self.connection_manager = ConnectionManager()
|
self.connection_manager = ConnectionManager()
|
||||||
|
|
@ -116,6 +129,9 @@ class CustomWindow(QMainWindow):
|
||||||
self.animation_step = 0
|
self.animation_step = 0
|
||||||
self.page_history = []
|
self.page_history = []
|
||||||
self.log_path = None
|
self.log_path = None
|
||||||
|
self._close_confirmation_pending = False
|
||||||
|
self._close_disconnect_in_progress = False
|
||||||
|
self._closing_after_disconnect = False
|
||||||
|
|
||||||
self.setFixedSize(800, 570)
|
self.setFixedSize(800, 570)
|
||||||
|
|
||||||
|
|
@ -158,7 +174,8 @@ class CustomWindow(QMainWindow):
|
||||||
self.animation_timer.timeout.connect(self.animate_toggle)
|
self.animation_timer.timeout.connect(self.animate_toggle)
|
||||||
self.check_logging()
|
self.check_logging()
|
||||||
|
|
||||||
self.navigator = Navigator(self.page_stack, self)
|
self.navigator = Navigator(self)
|
||||||
|
|
||||||
self.page_stack.currentChanged.connect(self.page_changed)
|
self.page_stack.currentChanged.connect(self.page_changed)
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
|
|
@ -181,15 +198,51 @@ class CustomWindow(QMainWindow):
|
||||||
self.navigator.navigate("welcome")
|
self.navigator.navigate("welcome")
|
||||||
else:
|
else:
|
||||||
self.navigator.navigate("menu")
|
self.navigator.navigate("menu")
|
||||||
|
self.navigator.start_preload()
|
||||||
|
if self.force_sync:
|
||||||
|
QTimer.singleShot(0, self._prompt_force_sync)
|
||||||
|
|
||||||
|
|
||||||
|
def _prompt_force_sync(self):
|
||||||
|
"""
|
||||||
|
Popup to encourage remote database sync
|
||||||
|
|
||||||
|
Context:
|
||||||
|
Database loader in __main__ passes in the 'forced_sync' variable,
|
||||||
|
If true, this popup encourages sync to avoid NoneType errors.
|
||||||
|
"""
|
||||||
|
connection = ConfigurationController.get_connection()
|
||||||
|
box = QMessageBox(self)
|
||||||
|
box.setWindowTitle("Data Update Needed")
|
||||||
|
box.setText(
|
||||||
|
"Please download the new data to avoid errors. You have profiles that need the data to function.\n\n"
|
||||||
|
f"Connection type: {connection}")
|
||||||
|
|
||||||
|
# Apply styling
|
||||||
|
# import it inside the function to reduce load on non-display flows.
|
||||||
|
from gui.v2.ui.styles.css.main_ui_css import forced_sync_popup_style
|
||||||
|
stylesheet = forced_sync_popup_style
|
||||||
|
box.setStyleSheet(stylesheet)
|
||||||
|
|
||||||
|
# Add styled buttons
|
||||||
|
sync_button = box.addButton("Ok Fetch", QMessageBox.ButtonRole.AcceptRole)
|
||||||
|
sync_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
cancel_button = box.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
|
||||||
|
cancel_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
box.setMinimumWidth(450) # button size
|
||||||
|
box.exec()
|
||||||
|
|
||||||
|
if box.clickedButton() == sync_button:
|
||||||
|
self.sync()
|
||||||
|
|
||||||
|
|
||||||
def perform_update_check(self):
|
def perform_update_check(self):
|
||||||
from core.controllers.ClientController import ClientController
|
from core.controllers.ClientController import ClientController
|
||||||
update_available = ClientController.can_be_updated()
|
update_available = ClientController.can_be_updated()
|
||||||
|
|
||||||
if update_available:
|
if update_available:
|
||||||
menu_page = self.navigator.get_cached("menu")
|
self.navigator.update_if_cached(
|
||||||
if menu_page is not None:
|
"menu", lambda p: p.on_update_check_finished())
|
||||||
menu_page.on_update_check_finished()
|
|
||||||
|
|
||||||
def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
||||||
if not status:
|
if not status:
|
||||||
|
|
@ -214,34 +267,27 @@ class CustomWindow(QMainWindow):
|
||||||
for i, brw in enumerate(available_browsers)
|
for i, brw in enumerate(available_browsers)
|
||||||
]
|
]
|
||||||
|
|
||||||
browser_page = self.navigator.get_cached("browser")
|
self.navigator.update_if_cached(
|
||||||
if browser_page is not None:
|
"browser", lambda p: p.create_interface_elements(available_browsers_list))
|
||||||
browser_page.create_interface_elements(available_browsers_list)
|
self.navigator.update_if_cached(
|
||||||
|
"location", lambda p: p.create_interface_elements(available_locations_list))
|
||||||
|
self.navigator.update_if_cached(
|
||||||
|
"hidetor", lambda p: p.create_interface_elements(available_locations_list))
|
||||||
|
self.navigator.update_if_cached(
|
||||||
|
"protocol", lambda p: p.enable_protocol_buttons())
|
||||||
|
self.navigator.update_if_cached("menu", self._enable_menu_verification_icons)
|
||||||
|
|
||||||
location_page = self.navigator.get_cached("location")
|
def _enable_menu_verification_icons(self, menu_page):
|
||||||
if location_page is not None:
|
if hasattr(menu_page, 'refresh_profiles_data'):
|
||||||
location_page.create_interface_elements(available_locations_list)
|
menu_page.refresh_profiles_data()
|
||||||
|
if hasattr(menu_page, 'buttons'):
|
||||||
hidetor_page = self.navigator.get_cached("hidetor")
|
for button in menu_page.buttons:
|
||||||
if hidetor_page is not None:
|
parent = button.parent()
|
||||||
hidetor_page.create_interface_elements(available_locations_list)
|
if parent:
|
||||||
|
verification_icons = parent.findChildren(QPushButton)
|
||||||
protocol_page = self.navigator.get_cached("protocol")
|
for icon in verification_icons:
|
||||||
if protocol_page is not None:
|
if icon.geometry().width() == 20 and icon.geometry().height() == 20:
|
||||||
protocol_page.enable_protocol_buttons()
|
icon.setEnabled(True)
|
||||||
|
|
||||||
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 sync(self):
|
def sync(self):
|
||||||
core_logger.info("User clicked sync button")
|
core_logger.info("User clicked sync button")
|
||||||
|
|
@ -260,6 +306,18 @@ class CustomWindow(QMainWindow):
|
||||||
if issubclass(identifier, UnknownConnectionTypeError):
|
if issubclass(identifier, UnknownConnectionTypeError):
|
||||||
self.setup_popup()
|
self.setup_popup()
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
|
elif issubclass(identifier, GuiStorageDatabaseError):
|
||||||
|
core_logger.error(
|
||||||
|
f"Storage database error:\n"
|
||||||
|
f"Type: {identifier.__name__}\n"
|
||||||
|
f"Value: {str(message)}\n"
|
||||||
|
f"Traceback:\n{''.join(traceback.format_tb(trace))}"
|
||||||
|
)
|
||||||
|
QMessageBox.critical(
|
||||||
|
self,
|
||||||
|
"Database Error",
|
||||||
|
"The local storage database could not be read. Restart HydraVeil and use the database recovery options.",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
config = self._load_gui_config()
|
config = self._load_gui_config()
|
||||||
if config and config["logging"]["gui_logging_enabled"] == True:
|
if config and config["logging"]["gui_logging_enabled"] == True:
|
||||||
|
|
@ -584,14 +642,95 @@ class CustomWindow(QMainWindow):
|
||||||
|
|
||||||
def closeEvent(self, event=None):
|
def closeEvent(self, event=None):
|
||||||
core_logger.info("HydraVeil application closing")
|
core_logger.info("HydraVeil application closing")
|
||||||
connected_profiles = self.connection_manager.get_connected_profiles()
|
|
||||||
|
if self._closing_after_disconnect:
|
||||||
|
orm.stop()
|
||||||
|
if event is not None:
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._close_confirmation_pending or self._close_disconnect_in_progress:
|
||||||
|
if event is not None:
|
||||||
|
event.ignore()
|
||||||
|
return
|
||||||
|
|
||||||
|
connected_profiles = self._get_enabled_profile_ids()
|
||||||
if connected_profiles:
|
if connected_profiles:
|
||||||
self.update_status('Profiles are still connected (disconnect flow lands in Chunk 9).')
|
|
||||||
if event is not None:
|
if event is not None:
|
||||||
event.accept()
|
event.ignore()
|
||||||
else:
|
self._show_close_disconnect_confirmation(connected_profiles)
|
||||||
if event is not None:
|
return
|
||||||
event.accept()
|
|
||||||
|
orm.stop()
|
||||||
|
if event is not None:
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def _get_enabled_profile_ids(self):
|
||||||
|
try:
|
||||||
|
profiles = ProfileController.get_all()
|
||||||
|
except Exception:
|
||||||
|
return self.connection_manager.get_connected_profiles()
|
||||||
|
|
||||||
|
connected_profiles = []
|
||||||
|
self.connection_manager._connected_profiles.clear()
|
||||||
|
for profile_id, profile in profiles.items():
|
||||||
|
try:
|
||||||
|
if is_profile_enabled_for_gui(profile):
|
||||||
|
connected_profiles.append(profile_id)
|
||||||
|
self.connection_manager.add_connected_profile(profile_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return connected_profiles
|
||||||
|
|
||||||
|
def _show_close_disconnect_confirmation(self, connected_profiles):
|
||||||
|
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
|
||||||
|
|
||||||
|
self._close_confirmation_pending = True
|
||||||
|
message = self._create_close_disconnect_message(connected_profiles)
|
||||||
|
self.popup = ConfirmationPopup(
|
||||||
|
self, message=message, action_button_text="Exit", cancel_button_text="Cancel")
|
||||||
|
self.popup.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||||
|
self.popup.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
||||||
|
self.popup.finished.connect(
|
||||||
|
lambda result: self._handle_close_disconnect_result(result, connected_profiles))
|
||||||
|
self.popup.destroyed.connect(
|
||||||
|
lambda *_: self._handle_close_disconnect_popup_closed())
|
||||||
|
self.popup.show()
|
||||||
|
|
||||||
|
def _create_close_disconnect_message(self, connected_profiles):
|
||||||
|
profile_numbers = ', '.join(str(profile_id)
|
||||||
|
for profile_id in connected_profiles)
|
||||||
|
is_are = "is" if len(connected_profiles) == 1 else "are"
|
||||||
|
return f'Profile{"" if len(connected_profiles) == 1 else "s"} {profile_numbers} {is_are} still connected.\nAll connected profiles will be disconnected on exit.\nDo you want to proceed?'
|
||||||
|
|
||||||
|
def _handle_close_disconnect_result(self, result, connected_profiles):
|
||||||
|
self._close_confirmation_pending = False
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._close_disconnect_in_progress = True
|
||||||
|
self.update_status('Disabling profiles...')
|
||||||
|
self.worker_thread = WorkerThread(
|
||||||
|
'DISABLE_ALL_PROFILES', profile_data=connected_profiles)
|
||||||
|
self.worker_thread.text_output.connect(self.update_status)
|
||||||
|
self.worker_thread.finished.connect(self._finish_close_disconnect)
|
||||||
|
self.worker_thread.start()
|
||||||
|
|
||||||
|
def _handle_close_disconnect_popup_closed(self):
|
||||||
|
if self._close_confirmation_pending:
|
||||||
|
self._close_confirmation_pending = False
|
||||||
|
self.popup = None
|
||||||
|
|
||||||
|
def _finish_close_disconnect(self, _ok=True):
|
||||||
|
self._close_disconnect_in_progress = False
|
||||||
|
connected_profiles = self._get_enabled_profile_ids()
|
||||||
|
if connected_profiles:
|
||||||
|
self.update_status('Could not disconnect all profiles. Exit canceled.')
|
||||||
|
return
|
||||||
|
|
||||||
|
self.update_status('All profiles disabled. Bye!')
|
||||||
|
self._closing_after_disconnect = True
|
||||||
|
QTimer.singleShot(0, self.close)
|
||||||
|
|
||||||
def update_image(self, appearance_value="original"):
|
def update_image(self, appearance_value="original"):
|
||||||
image_path = os.path.join(self.btn_path, f"{appearance_value}.png")
|
image_path = os.path.join(self.btn_path, f"{appearance_value}.png")
|
||||||
|
|
@ -612,7 +751,21 @@ class CustomWindow(QMainWindow):
|
||||||
self.status_label.setStyleSheet(
|
self.status_label.setStyleSheet(
|
||||||
f"color: rgb(0, 255, 255); font-size: {font_size}px;")
|
f"color: rgb(0, 255, 255); font-size: {font_size}px;")
|
||||||
|
|
||||||
|
@mainthread # wrapper of ThreadSafetyTool
|
||||||
def update_status(self, text, clear=False):
|
def update_status(self, text, clear=False):
|
||||||
|
"""
|
||||||
|
Purpose:
|
||||||
|
Update the bottom left status bar
|
||||||
|
|
||||||
|
Features:
|
||||||
|
Forced on the main Thread by ThreadSafetyTool
|
||||||
|
|
||||||
|
Depends on:
|
||||||
|
v2.infrastructure.ThreadSafetyTool
|
||||||
|
|
||||||
|
Called by:
|
||||||
|
v2.infrastructure.setup_observers (outside this class)
|
||||||
|
"""
|
||||||
if text is None:
|
if text is None:
|
||||||
self._set_status_font_size(16)
|
self._set_status_font_size(16)
|
||||||
self.status_label.setText('Status:')
|
self.status_label.setText('Status:')
|
||||||
|
|
@ -622,11 +775,13 @@ class CustomWindow(QMainWindow):
|
||||||
if clear:
|
if clear:
|
||||||
self._set_status_font_size(16)
|
self._set_status_font_size(16)
|
||||||
self.status_label.setText('')
|
self.status_label.setText('')
|
||||||
|
self.disable_marquee()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
text = str(text)
|
||||||
full_text = 'Status: ' + text
|
full_text = 'Status: ' + text
|
||||||
metrics = self.status_label.fontMetrics()
|
metrics = self.status_label.fontMetrics()
|
||||||
available_width = self.status_label.width() - 10
|
available_width = getattr(self, 'text_width', self.status_label.width() - 10)
|
||||||
|
|
||||||
if metrics.horizontalAdvance(full_text) > available_width:
|
if metrics.horizontalAdvance(full_text) > available_width:
|
||||||
self.enable_marquee(text)
|
self.enable_marquee(text)
|
||||||
|
|
@ -650,16 +805,18 @@ class CustomWindow(QMainWindow):
|
||||||
if not self.has_shown_fast_mode_prompt():
|
if not self.has_shown_fast_mode_prompt():
|
||||||
self.navigator.navigate("fast_mode_prompt")
|
self.navigator.navigate("fast_mode_prompt")
|
||||||
else:
|
else:
|
||||||
menu_page = self.navigator.get_cached("menu")
|
self.navigator.update_if_cached(
|
||||||
if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'):
|
"menu",
|
||||||
menu_page.refresh_menu_buttons()
|
lambda p: p.refresh_menu_buttons() if hasattr(p, 'refresh_menu_buttons') else None)
|
||||||
self.navigator.navigate("menu")
|
self.navigator.navigate("menu")
|
||||||
|
|
||||||
def enable_marquee(self, text):
|
def enable_marquee(self, text):
|
||||||
self.marquee_text = text + " "
|
self.marquee_text = str(text)
|
||||||
|
self.marquee_gap = " "
|
||||||
self.marquee_position = 0
|
self.marquee_position = 0
|
||||||
self.marquee_enabled = True
|
self.marquee_enabled = True
|
||||||
self.marquee_timer.start(500)
|
self.update_marquee()
|
||||||
|
self.marquee_timer.start(180)
|
||||||
|
|
||||||
def disable_marquee(self):
|
def disable_marquee(self):
|
||||||
self.marquee_enabled = False
|
self.marquee_enabled = False
|
||||||
|
|
@ -670,31 +827,33 @@ class CustomWindow(QMainWindow):
|
||||||
return
|
return
|
||||||
|
|
||||||
metrics = self.status_label.fontMetrics()
|
metrics = self.status_label.fontMetrics()
|
||||||
text_width = metrics.horizontalAdvance(self.marquee_text)
|
prefix = 'Status: '
|
||||||
|
content_width = max(
|
||||||
|
80,
|
||||||
|
getattr(self, 'text_width', self.status_label.width() - 10)
|
||||||
|
- metrics.horizontalAdvance(prefix)
|
||||||
|
)
|
||||||
|
|
||||||
self.marquee_position += self.scroll_speed
|
marquee_unit = self.marquee_text + getattr(self, 'marquee_gap', " ")
|
||||||
if self.marquee_position >= text_width:
|
if not marquee_unit.strip():
|
||||||
|
self.disable_marquee()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.marquee_position >= len(marquee_unit):
|
||||||
self.marquee_position = 0
|
self.marquee_position = 0
|
||||||
|
|
||||||
looped_text = self.marquee_text * 2
|
looped_text = marquee_unit + marquee_unit
|
||||||
|
visible_text = ''
|
||||||
|
for char in looped_text[self.marquee_position:]:
|
||||||
|
next_text = visible_text + char
|
||||||
|
if metrics.horizontalAdvance(next_text) > content_width:
|
||||||
|
break
|
||||||
|
visible_text = next_text
|
||||||
|
|
||||||
chars_that_fit = metrics.horizontalAdvance(
|
display_text = prefix + visible_text
|
||||||
looped_text[:self.text_width])
|
|
||||||
|
|
||||||
visible_text = looped_text[self.marquee_position:
|
|
||||||
self.marquee_position + chars_that_fit]
|
|
||||||
|
|
||||||
while metrics.horizontalAdvance(visible_text) < self.text_width:
|
|
||||||
visible_text += self.marquee_text
|
|
||||||
|
|
||||||
while metrics.horizontalAdvance(visible_text) > self.text_width:
|
|
||||||
visible_text = visible_text[:-1]
|
|
||||||
|
|
||||||
display_text = 'Status: ' + ' ' * \
|
|
||||||
(self.text_start_x - metrics.horizontalAdvance('Status: '))
|
|
||||||
display_text += visible_text
|
|
||||||
|
|
||||||
self.status_label.setText(display_text)
|
self.status_label.setText(display_text)
|
||||||
|
self.marquee_position += max(1, int(self.scroll_speed / 7))
|
||||||
|
|
||||||
def set_scroll_speed(self, speed):
|
def set_scroll_speed(self, speed):
|
||||||
self.scroll_speed = speed
|
self.scroll_speed = speed
|
||||||
|
|
@ -753,7 +912,8 @@ class CustomWindow(QMainWindow):
|
||||||
self.page_history.append(current_page)
|
self.page_history.append(current_page)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def start_ui(force_sync):
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
window = CustomWindow()
|
calculate_max_screensize(app)
|
||||||
|
window = CustomWindow(force_sync=force_sync)
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
def interpret_key_results(result: dict) -> str:
|
|
||||||
if not isinstance(result, dict):
|
|
||||||
return "There was an error with the format of the reply from the operation."
|
|
||||||
|
|
||||||
# unpack results:
|
|
||||||
valid = result.get('valid', False)
|
|
||||||
comparison = result.get('comparison', 'error')
|
|
||||||
error_msg = result.get('message', None)
|
|
||||||
|
|
||||||
# errors:
|
|
||||||
if error_msg:
|
|
||||||
if error_msg == "api_connection_issue":
|
|
||||||
return "There was a connection error with connecting to the API."
|
|
||||||
elif error_msg == "invalid_key":
|
|
||||||
return "Your original public key is in an invalid format to begin with."
|
|
||||||
else:
|
|
||||||
return "Unknown Error."
|
|
||||||
|
|
||||||
# comparison:
|
|
||||||
if comparison == "same":
|
|
||||||
first_sentence = "The key you had locally matched what the server had also."
|
|
||||||
elif comparison == "different":
|
|
||||||
first_sentence = "Your local key was different than the server's key."
|
|
||||||
else:
|
|
||||||
first_sentence = "There was an error with the comparison."
|
|
||||||
|
|
||||||
# validity:
|
|
||||||
if valid:
|
|
||||||
second_sentence = "The signature now matches with the new key."
|
|
||||||
else:
|
|
||||||
second_sentence = "But the signature still does not match, even with the new key. If you prepare tickets anyway, they might not verify when used."
|
|
||||||
|
|
||||||
# final return
|
|
||||||
final_msg = first_sentence + " " + second_sentence
|
|
||||||
return final_msg
|
|
||||||
BIN
gui/resources/images/hysteria2.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
gui/resources/images/hysteria2_mini.png
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
gui/resources/images/hystria2_button.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
gui/resources/images/networking_shield.png
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 96 KiB |
BIN
gui/resources/images/vless.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
gui/resources/images/vless_button.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
gui/resources/images/vless_mini.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
gui/resources/images/wireguard_button.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 118 KiB |
BIN
gui/resources/images/wireguard_mini.png
Executable file → Normal file
|
Before Width: | Height: | Size: 884 B After Width: | Height: | Size: 4.5 KiB |
BIN
gui/resources/images/wireguard_mini_original.png
Executable file
|
After Width: | Height: | Size: 884 B |
BIN
gui/resources/images/wireguard_mini_transparent.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 117 KiB |
89
gui/v2/actions/database_health.py
Executable file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class GuiStorageDatabaseError(Exception):
|
||||||
|
def __init__(self, message, detail=None):
|
||||||
|
self.detail = detail
|
||||||
|
super().__init__(f"{message} Details: {detail}" if detail else message)
|
||||||
|
|
||||||
|
|
||||||
|
def _connect_readonly(database_path):
|
||||||
|
path = Path(database_path).resolve()
|
||||||
|
return sqlite3.connect(f"{path.as_uri()}?mode=ro", uri=True)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_storage_database(database_path):
|
||||||
|
path = Path(database_path)
|
||||||
|
if not path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
connection = None
|
||||||
|
try:
|
||||||
|
connection = _connect_readonly(path)
|
||||||
|
connection.execute("PRAGMA schema_version").fetchone()
|
||||||
|
result = connection.execute("PRAGMA quick_check").fetchone()
|
||||||
|
if result and result[0] != "ok":
|
||||||
|
raise GuiStorageDatabaseError(
|
||||||
|
"The local storage database failed SQLite integrity checks.",
|
||||||
|
str(result[0]),
|
||||||
|
)
|
||||||
|
table_rows = connection.execute("SELECT name FROM sqlite_master WHERE type = 'table'").fetchall()
|
||||||
|
table_names = {row[0] for row in table_rows}
|
||||||
|
known_tables = {
|
||||||
|
"applications",
|
||||||
|
"application_versions",
|
||||||
|
"cached_sync",
|
||||||
|
"client_versions",
|
||||||
|
"database_version",
|
||||||
|
"encryptedproxies",
|
||||||
|
"locations",
|
||||||
|
"operators",
|
||||||
|
"subscription_plans",
|
||||||
|
}
|
||||||
|
if table_names and table_names.isdisjoint(known_tables):
|
||||||
|
raise GuiStorageDatabaseError(
|
||||||
|
"The local storage database does not look like a HydraVeil storage database.",
|
||||||
|
", ".join(sorted(table_names)),
|
||||||
|
)
|
||||||
|
except GuiStorageDatabaseError:
|
||||||
|
raise
|
||||||
|
except sqlite3.Error as error:
|
||||||
|
raise GuiStorageDatabaseError(
|
||||||
|
"The local storage database could not be read. It may be malformed, stale, or incompatible.",
|
||||||
|
str(error),
|
||||||
|
) from error
|
||||||
|
finally:
|
||||||
|
if connection is not None:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _table_has_rows(database_path, table_name):
|
||||||
|
path = Path(database_path)
|
||||||
|
if not path.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
connection = None
|
||||||
|
try:
|
||||||
|
connection = _connect_readonly(path)
|
||||||
|
return connection.execute(f'SELECT 1 FROM "{table_name}" LIMIT 1').fetchone() is not None
|
||||||
|
except sqlite3.OperationalError as error:
|
||||||
|
if "no such table" in str(error).lower():
|
||||||
|
return False
|
||||||
|
raise GuiStorageDatabaseError(
|
||||||
|
"The local storage database could not be checked for synced data.",
|
||||||
|
str(error),
|
||||||
|
) from error
|
||||||
|
except sqlite3.Error as error:
|
||||||
|
raise GuiStorageDatabaseError(
|
||||||
|
"The local storage database could not be checked for synced data.",
|
||||||
|
str(error),
|
||||||
|
) from error
|
||||||
|
finally:
|
||||||
|
if connection is not None:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def has_required_sync_data(database_path):
|
||||||
|
required_tables = ("applications", "application_versions", "locations")
|
||||||
|
return all(_table_has_rows(database_path, table_name) for table_name in required_tables)
|
||||||
65
gui/v2/actions/disable_profiles.py
Executable file
|
|
@ -0,0 +1,65 @@
|
||||||
|
from core.models.session.SessionProfile import SessionProfile
|
||||||
|
from core.models.system.SystemProfile import SystemProfile
|
||||||
|
from core.controllers.ProfileController import ProfileController
|
||||||
|
from core.models.BaseProfile import BaseProfile
|
||||||
|
|
||||||
|
# not currently used, but can be added:
|
||||||
|
# application_version_observer,
|
||||||
|
# client_observer,
|
||||||
|
# invoice_observer,
|
||||||
|
|
||||||
|
from gui.v2.infrastructure.setup_observers import (
|
||||||
|
connection_observer,
|
||||||
|
profile_observer,
|
||||||
|
ticket_observer,
|
||||||
|
)
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
def filter_profiles_by_type(profile_data: list) -> tuple:
|
||||||
|
"""
|
||||||
|
Purpose:
|
||||||
|
Isolate system and session profiles into separate lists
|
||||||
|
Why:
|
||||||
|
Session profiles must be disabled first before System profiles.
|
||||||
|
Called by:
|
||||||
|
worker_thread's disable_all_profiles
|
||||||
|
"""
|
||||||
|
system_profiles = []
|
||||||
|
session_profiles = []
|
||||||
|
for profile_id in profile_data:
|
||||||
|
profile = ProfileController.get(int(profile_id))
|
||||||
|
|
||||||
|
if isinstance(profile, SessionProfile):
|
||||||
|
session_profiles.append(profile)
|
||||||
|
|
||||||
|
elif isinstance(profile, SystemProfile):
|
||||||
|
system_profiles.append(profile)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Skipping/discarding unknown profile {profile_id} type {type(profile)} full info: {profile}")
|
||||||
|
|
||||||
|
return session_profiles, system_profiles
|
||||||
|
|
||||||
|
|
||||||
|
def disable_profile_via_controller(profile: BaseProfile):
|
||||||
|
"""
|
||||||
|
Purpose:
|
||||||
|
Disable a profile via the controller
|
||||||
|
Why:
|
||||||
|
Inspect signature deals with changing observers
|
||||||
|
Called by:
|
||||||
|
worker_thread's disable_all_profiles
|
||||||
|
"""
|
||||||
|
kwargs = {
|
||||||
|
'profile_observer': profile_observer,
|
||||||
|
'ticket_observer': ticket_observer,
|
||||||
|
'connection_observer': connection_observer,
|
||||||
|
}
|
||||||
|
supported = inspect.signature(ProfileController.disable).parameters
|
||||||
|
ProfileController.disable(
|
||||||
|
profile,
|
||||||
|
**{key: value for key, value in kwargs.items() if key in supported}
|
||||||
|
)
|
||||||
|
|
||||||
|
# We return nothing, because core signals via observers for success. And raises errors for failure.
|
||||||
|
|
@ -35,6 +35,20 @@ def mark_fast_mode_prompt_shown(gui_config_file):
|
||||||
save_gui_config(gui_config_file, config)
|
save_gui_config(gui_config_file, config)
|
||||||
|
|
||||||
|
|
||||||
|
def is_fast_registration_enabled(gui_config_file):
|
||||||
|
config = load_gui_config(gui_config_file)
|
||||||
|
if not config:
|
||||||
|
return False
|
||||||
|
return config.get("registrations", {}).get("fast_registration_enabled", False)
|
||||||
|
|
||||||
|
|
||||||
|
def is_auto_sync_enabled(gui_config_file):
|
||||||
|
config = load_gui_config(gui_config_file)
|
||||||
|
if not config:
|
||||||
|
return False
|
||||||
|
return config.get("registrations", {}).get("auto_sync_enabled", False)
|
||||||
|
|
||||||
|
|
||||||
def set_fast_mode_enabled(gui_config_file, enabled):
|
def set_fast_mode_enabled(gui_config_file, enabled):
|
||||||
config = load_gui_config(gui_config_file)
|
config = load_gui_config(gui_config_file)
|
||||||
if config is None:
|
if config is None:
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
def location_candidates(profile, preferred=None):
|
|
||||||
candidates = []
|
|
||||||
seen = set()
|
|
||||||
|
|
||||||
def add(val):
|
|
||||||
if val is None:
|
|
||||||
return
|
|
||||||
s = str(val).strip().lower().replace(' ', '_')
|
|
||||||
if s and s not in seen:
|
|
||||||
seen.add(s)
|
|
||||||
candidates.append(s)
|
|
||||||
|
|
||||||
if preferred:
|
|
||||||
add(preferred)
|
|
||||||
|
|
||||||
sources = []
|
|
||||||
try:
|
|
||||||
if profile and profile.connection and profile.connection.location:
|
|
||||||
sources.append(profile.connection.location)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
if profile and profile.location:
|
|
||||||
sources.append(profile.location)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for source in sources:
|
|
||||||
add(getattr(source, 'id', None))
|
|
||||||
add(getattr(source, 'country_name', None))
|
|
||||||
add(getattr(source, 'name', None))
|
|
||||||
add(getattr(source, 'code', None))
|
|
||||||
add(getattr(source, 'country_code', None))
|
|
||||||
|
|
||||||
return candidates
|
|
||||||
74
gui/v2/actions/operation_result_dispatch.py
Executable file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import inspect
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.controllers.ConfigurationController import ConfigurationController
|
||||||
|
from core.models.Configuration import ConnectionChoice
|
||||||
|
from core.models.Result import ResultError
|
||||||
|
from core.services.prepare_tickets import ticket_tracker
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ENUM_MAP = {
|
||||||
|
ResultError.SUBSCRIPTION: (
|
||||||
|
ticket_tracker.wipe_one_ticket_sub,
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
ResultError.CONNECTION: (
|
||||||
|
ConfigurationController.set_connection_enum,
|
||||||
|
{'connection_enum': ConnectionChoice.SYSTEM},
|
||||||
|
),
|
||||||
|
ResultError.INVALID_API_REPLY: None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def has_function(enum_map: dict[Enum, Any], enum_key: Enum) -> bool:
|
||||||
|
action = enum_map.get(enum_key)
|
||||||
|
if action is None:
|
||||||
|
return False
|
||||||
|
function, _ = _normalize_action(action)
|
||||||
|
return callable(function)
|
||||||
|
|
||||||
|
|
||||||
|
def error_dispatch(enum_map: dict[Enum, Any], enum_key: Enum, **kwargs: Any) -> Any:
|
||||||
|
if enum_key not in enum_map:
|
||||||
|
return None
|
||||||
|
action = enum_map[enum_key]
|
||||||
|
if action is None:
|
||||||
|
return None
|
||||||
|
function, default_kwargs = _normalize_action(action)
|
||||||
|
call_kwargs = dict(default_kwargs)
|
||||||
|
call_kwargs.update(kwargs)
|
||||||
|
return function(**_filter_kwargs(function, call_kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
def has_result_action(result, enum_map: dict[Enum, Any] | None = None) -> bool:
|
||||||
|
return has_function(enum_map or DEFAULT_ENUM_MAP, result.error_type)
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch_result_action(result, enum_map: dict[Enum, Any] | None = None, **kwargs: Any) -> Any:
|
||||||
|
return error_dispatch(enum_map or DEFAULT_ENUM_MAP, result.error_type, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_action(action: Any) -> tuple[Any, dict[str, Any]]:
|
||||||
|
if isinstance(action, tuple):
|
||||||
|
function = action[0]
|
||||||
|
if len(action) > 1 and isinstance(action[1], dict):
|
||||||
|
return function, action[1]
|
||||||
|
return function, {}
|
||||||
|
return action, {}
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_kwargs(function: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
signature = inspect.signature(function)
|
||||||
|
parameters = signature.parameters
|
||||||
|
if any(param.kind == inspect.Parameter.VAR_KEYWORD for param in parameters.values()):
|
||||||
|
return kwargs
|
||||||
|
accepted = {
|
||||||
|
name
|
||||||
|
for name, param in parameters.items()
|
||||||
|
if param.kind in (
|
||||||
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
||||||
|
inspect.Parameter.KEYWORD_ONLY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return {key: value for key, value in kwargs.items() if key in accepted}
|
||||||
18
gui/v2/actions/operation_results.py
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
|
||||||
|
|
||||||
|
def result_from_exception(exception: Exception, error_type: ResultError = ResultError.UNKNOWN) -> Result:
|
||||||
|
message = str(exception) or type(exception).__name__
|
||||||
|
return Result(valid=False, error_type=error_type, message=message)
|
||||||
|
|
||||||
|
|
||||||
|
def result_from_payload(payload) -> Result:
|
||||||
|
if isinstance(payload, Result):
|
||||||
|
return payload
|
||||||
|
if isinstance(payload, Exception):
|
||||||
|
return result_from_exception(payload)
|
||||||
|
return Result(
|
||||||
|
valid=False,
|
||||||
|
error_type=ResultError.UNKNOWN,
|
||||||
|
message=str(payload) or 'The operation could not be completed.',
|
||||||
|
)
|
||||||
27
gui/v2/actions/profile_status.py
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from core.controllers.ProfileController import ProfileController
|
||||||
|
from core.controllers.SystemStateController import SystemStateController
|
||||||
|
from core.models.system.SystemProfile import SystemProfile
|
||||||
|
|
||||||
|
|
||||||
|
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
|
||||||
|
|
||||||
|
|
||||||
|
def _connection_code(profile):
|
||||||
|
connection = getattr(profile, "connection", None)
|
||||||
|
return getattr(connection, "code", None)
|
||||||
|
|
||||||
|
|
||||||
|
def _system_state_profile_id():
|
||||||
|
state = SystemStateController.get()
|
||||||
|
if state is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(state.profile_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_profile_enabled_for_gui(profile):
|
||||||
|
if isinstance(profile, SystemProfile) and _connection_code(profile) in SINGBOX_PROTOCOLS:
|
||||||
|
return _system_state_profile_id() == int(profile.id)
|
||||||
|
return ProfileController.is_enabled(profile)
|
||||||
171
gui/v2/actions/settings_data.py
Executable file
|
|
@ -0,0 +1,171 @@
|
||||||
|
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_firewall_setting(gui_config_file=None):
|
||||||
|
try:
|
||||||
|
configuration = ConfigurationController.get()
|
||||||
|
if configuration is not None:
|
||||||
|
return bool(configuration.firewall)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def read_managed_dns_setting(gui_config_file=None):
|
||||||
|
try:
|
||||||
|
configuration = ConfigurationController.get()
|
||||||
|
if configuration is not None:
|
||||||
|
return bool(configuration.dns)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
try:
|
||||||
|
profiles = ProfileController.get_all()
|
||||||
|
except Exception:
|
||||||
|
profiles = {}
|
||||||
|
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
|
||||||
|
try:
|
||||||
|
current_connection = ConfigurationController.get_connection()
|
||||||
|
except Exception:
|
||||||
|
current_connection = None
|
||||||
|
return {
|
||||||
|
"profiles": profiles,
|
||||||
|
"profile_order": profile_order,
|
||||||
|
"current_connection": current_connection,
|
||||||
|
"endpoint_verification_enabled": endpoint_verification_enabled,
|
||||||
|
"systemwide_enabled": read_systemwide_enabled(),
|
||||||
|
"bwrap_enabled": read_bwrap_enabled(),
|
||||||
|
"firewall_setting": read_firewall_setting(gui_config_file),
|
||||||
|
"managed_dns_setting": read_managed_dns_setting(gui_config_file),
|
||||||
|
"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,
|
||||||
|
"firewall_setting": False,
|
||||||
|
"managed_dns_setting": 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
|
||||||
|
location = getattr(profile, 'location', None) if profile else None
|
||||||
|
if location and not isinstance(location, dict) and getattr(location, 'operator', None):
|
||||||
|
operator = 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}"
|
||||||
20
gui/v2/actions/singbox_prereqs.py
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from core.services.helpers.install_dependencies import SUDO_SINGBOX_LOCATION
|
||||||
|
from core.services.helpers.setup_sudo_scripts import (
|
||||||
|
is_singbox_wrapper_ready,
|
||||||
|
test_if_in_sudo_folder,
|
||||||
|
)
|
||||||
|
from core.utils.basic_operations.does_file_exist import does_file_exist
|
||||||
|
|
||||||
|
|
||||||
|
def singbox_prereqs_installed():
|
||||||
|
try:
|
||||||
|
if not is_singbox_wrapper_ready():
|
||||||
|
return False
|
||||||
|
|
||||||
|
sudo_scripts = test_if_in_sudo_folder()
|
||||||
|
if not getattr(sudo_scripts, "valid", False):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return does_file_exist(SUDO_SINGBOX_LOCATION)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
21
gui/v2/infrastructure/ThreadSafetyTool.py
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QLabel
|
||||||
|
from PyQt6.QtCore import QObject, pyqtSignal, Qt
|
||||||
|
|
||||||
|
class ThreadSafetyTool(QObject):
|
||||||
|
"""Marshal function calls to the main thread."""
|
||||||
|
_signal = pyqtSignal(object, tuple, dict)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._signal.connect(self._execute, Qt.ConnectionType.QueuedConnection)
|
||||||
|
|
||||||
|
def _execute(self, func, args, kwargs):
|
||||||
|
func(*args, **kwargs)
|
||||||
|
|
||||||
|
def __call__(self, func):
|
||||||
|
"""Decorator that forces a function to run on the main thread."""
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
self._signal.emit(func, args, kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
@ -19,7 +19,10 @@ class ConnectionManager:
|
||||||
self.profile_button_objects[profile_id] = profile_button_objects
|
self.profile_button_objects[profile_id] = profile_button_objects
|
||||||
|
|
||||||
def get_available_resolutions(self, profile_id):
|
def get_available_resolutions(self, profile_id):
|
||||||
profile = ProfileController.get(profile_id)
|
try:
|
||||||
|
profile = ProfileController.get(profile_id)
|
||||||
|
except Exception:
|
||||||
|
profile = None
|
||||||
if profile and hasattr(profile, 'resolution') and profile.resolution:
|
if profile and hasattr(profile, 'resolution') and profile.resolution:
|
||||||
self.available_resolutions.append(profile.resolution)
|
self.available_resolutions.append(profile.resolution)
|
||||||
return self.available_resolutions
|
return self.available_resolutions
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,224 @@
|
||||||
import importlib
|
import importlib
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
from gui.v2.infrastructure.page_registry import PAGE_REGISTRY
|
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
|
||||||
|
from core.errors.logger import logger
|
||||||
|
|
||||||
|
|
||||||
class Navigator:
|
class Navigator(QObject):
|
||||||
def __init__(self, page_stack, custom_window):
|
def __init__(self, custom_window):
|
||||||
self.page_stack = page_stack
|
super().__init__()
|
||||||
self.custom_window = custom_window
|
self.custom_window = custom_window
|
||||||
|
self.page_stack = custom_window.page_stack
|
||||||
self._instances = {}
|
self._instances = {}
|
||||||
|
|
||||||
|
self._preload_queue = []
|
||||||
|
self._preload_total = 0
|
||||||
|
self._preload_done = 0
|
||||||
|
self._preload_failed = 0
|
||||||
|
self._preload_skipped_runtime = 0
|
||||||
|
self._preload_dispatched = 0
|
||||||
|
self._preload_inflight = 0
|
||||||
|
self._preload_workers = {}
|
||||||
|
self._preload_summary_emitted = False
|
||||||
|
self._preload_t_start = 0.0
|
||||||
|
self._preload_timer = QTimer(self)
|
||||||
|
self._preload_timer.setSingleShot(True)
|
||||||
|
self._preload_timer.timeout.connect(self._preload_next)
|
||||||
|
|
||||||
|
def get_class(self, name):
|
||||||
|
module_path, class_name = PAGE_REGISTRY[name]
|
||||||
|
module = importlib.import_module(module_path)
|
||||||
|
return getattr(module, class_name)
|
||||||
|
|
||||||
|
def _instantiate(self, name, prepared=None):
|
||||||
|
cls = self.get_class(name)
|
||||||
|
if name in ASYNC_PREPARE:
|
||||||
|
return cls(self.page_stack, self.custom_window, prepared=prepared)
|
||||||
|
return cls(self.page_stack, self.custom_window)
|
||||||
|
|
||||||
|
def register_instance(self, name, page):
|
||||||
|
self.page_stack.addWidget(page)
|
||||||
|
self._instances[name] = page
|
||||||
|
|
||||||
def navigate(self, name):
|
def navigate(self, name):
|
||||||
page = self._instances.get(name)
|
page = self._instances.get(name)
|
||||||
if page is None:
|
if page is None:
|
||||||
if name not in PAGE_REGISTRY:
|
if name not in PAGE_REGISTRY:
|
||||||
self._warn_not_migrated(name)
|
self._warn_not_migrated(name)
|
||||||
return
|
return None
|
||||||
module_path, class_name = PAGE_REGISTRY[name]
|
page = self._instantiate(name)
|
||||||
module = importlib.import_module(module_path)
|
self.register_instance(name, page)
|
||||||
cls = getattr(module, class_name)
|
|
||||||
page = cls(self.page_stack, self.custom_window)
|
|
||||||
self.page_stack.addWidget(page)
|
|
||||||
self._instances[name] = page
|
|
||||||
self.page_stack.setCurrentIndex(self.page_stack.indexOf(page))
|
self.page_stack.setCurrentIndex(self.page_stack.indexOf(page))
|
||||||
|
return page
|
||||||
|
|
||||||
def get_cached(self, name):
|
def get_cached(self, name):
|
||||||
return self._instances.get(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):
|
def _warn_not_migrated(self, name):
|
||||||
try:
|
try:
|
||||||
self.custom_window.update_status(f"Page '{name}' not migrated yet.")
|
self.custom_window.update_status(f"Page '{name}' not migrated yet.")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _preload_one(self, name):
|
||||||
|
if name in self._instances:
|
||||||
|
logger.debug(f"[preload] {name:30s} SKIP (already cached)")
|
||||||
|
return False
|
||||||
|
if name not in PAGE_REGISTRY:
|
||||||
|
logger.debug(f"[preload] {name:30s} SKIP (not in registry)")
|
||||||
|
return False
|
||||||
|
module_path, class_name = PAGE_REGISTRY[name]
|
||||||
|
logger.debug(f"[preload] {name:30s} LOAD {module_path}.{class_name}")
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
try:
|
||||||
|
page = self._instantiate(name)
|
||||||
|
self.register_instance(name, page)
|
||||||
|
dt_ms = (time.perf_counter() - t0) * 1000.0
|
||||||
|
logger.debug(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, "
|
||||||
|
f"cached={len(self._instances)}, stack_size={self.page_stack.count()})")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
dt_ms = (time.perf_counter() - t0) * 1000.0
|
||||||
|
logger.debug(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) "
|
||||||
|
f"{type(e).__name__}: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
from core.errors.logger import logger as core_logger
|
||||||
|
core_logger.warning(f"Background preload failed for '{name}': "
|
||||||
|
f"{type(e).__name__}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
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()
|
||||||
|
logger.debug(f"[preload] ============================================================")
|
||||||
|
logger.debug(f"[preload] starting background preload: {self._preload_total} pages queued")
|
||||||
|
logger.debug(f"[preload] order : {', '.join(self._preload_queue)}")
|
||||||
|
logger.debug(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)")
|
||||||
|
if fast_mode:
|
||||||
|
logger.debug(f"[preload] fastmode: ON -> skipping regular-flow pages: "
|
||||||
|
f"{', '.join(skipped_regular) if skipped_regular else '(none)'}")
|
||||||
|
logger.debug(f"[preload] gap : 250 ms between loads, 700 ms initial delay")
|
||||||
|
logger.debug(f"[preload] ============================================================")
|
||||||
|
self._preload_timer.start(700)
|
||||||
|
|
||||||
|
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
|
||||||
|
logger.debug(f"[preload] {skipped:30s} SKIP (user navigated here first)")
|
||||||
|
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)
|
||||||
|
logger.debug(f"[preload] ---- {idx}/{self._preload_total} ----")
|
||||||
|
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 _prepare_page_data(self, name):
|
||||||
|
module_path, class_name = PAGE_REGISTRY[name]
|
||||||
|
module = importlib.import_module(module_path)
|
||||||
|
cls = getattr(module, class_name)
|
||||||
|
return cls.prepare_data(self.custom_window)
|
||||||
|
|
||||||
|
def _dispatch_async_preload(self, name):
|
||||||
|
module_path, class_name = PAGE_REGISTRY[name]
|
||||||
|
logger.debug(f"[preload] {name:30s} ASYNC {module_path}.{class_name}")
|
||||||
|
worker = PageDataWorker(name, self._prepare_page_data, name)
|
||||||
|
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:
|
||||||
|
logger.debug(f"[preload] {name:30s} SKIP (already cached, async result dropped)")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
page = self._instantiate(name, prepared=payload)
|
||||||
|
self.register_instance(name, page)
|
||||||
|
self._preload_done += 1
|
||||||
|
logger.debug(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, "
|
||||||
|
f"stack_size={self.page_stack.count()})")
|
||||||
|
except Exception as e:
|
||||||
|
self._preload_failed += 1
|
||||||
|
logger.debug(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}")
|
||||||
|
self._maybe_emit_preload_summary()
|
||||||
|
|
||||||
|
def _on_preload_failed(self, name, error):
|
||||||
|
self._preload_inflight -= 1
|
||||||
|
logger.debug(f"[preload] {name:30s} FAIL (async prepare) {error}")
|
||||||
|
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
|
||||||
|
logger.debug(f"[preload] ============================================================")
|
||||||
|
logger.debug(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")
|
||||||
|
logger.debug(f"[preload] cached pages now: {sorted(self._instances.keys())}")
|
||||||
|
logger.debug(f"[preload] ============================================================")
|
||||||
|
|
|
||||||
8
gui/v2/infrastructure/orm.py
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
from core.models.manage.session_management import init_session, close_session
|
||||||
|
|
||||||
|
def start():
|
||||||
|
init_session()
|
||||||
|
|
||||||
|
def stop():
|
||||||
|
close_session()
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
PAGE_REGISTRY = {
|
PAGE_REGISTRY = {
|
||||||
"blank": ("gui.v2.ui.pages._blank_page", "BlankPage"),
|
"blank": ("gui.v2.ui.pages._blank_page", "BlankPage"),
|
||||||
"welcome": ("gui.v2.ui.pages.welcome_page", "WelcomePage"),
|
"welcome": ("gui.v2.ui.pages.welcome_page", "WelcomePage"),
|
||||||
|
"networking_setup": ("gui.v2.ui.pages.networking_setup_page", "NetworkingSetupPage"),
|
||||||
"menu": ("gui.v2.ui.pages.menu_page", "MenuPage"),
|
"menu": ("gui.v2.ui.pages.menu_page", "MenuPage"),
|
||||||
"protocol": ("gui.v2.ui.pages.protocol_page", "ProtocolPage"),
|
"protocol": ("gui.v2.ui.pages.protocol_page", "ProtocolPage"),
|
||||||
"hidetor": ("gui.v2.ui.pages.hidetor_page", "HidetorPage"),
|
"hidetor": ("gui.v2.ui.pages.hidetor_page", "HidetorPage"),
|
||||||
|
|
@ -30,3 +31,57 @@ PAGE_REGISTRY = {
|
||||||
"fast_registration": ("gui.v2.ui.pages.fast_registration_page", "FastRegistrationPage"),
|
"fast_registration": ("gui.v2.ui.pages.fast_registration_page", "FastRegistrationPage"),
|
||||||
"fast_mode_prompt": ("gui.v2.ui.pages.fast_mode_prompt_page", "FastModePromptPage"),
|
"fast_mode_prompt": ("gui.v2.ui.pages.fast_mode_prompt_page", "FastModePromptPage"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PRELOAD_SKIP = {
|
||||||
|
"blank",
|
||||||
|
"welcome",
|
||||||
|
"payment_confirmed",
|
||||||
|
"ticket_prep",
|
||||||
|
}
|
||||||
|
|
||||||
|
ASYNC_PREPARE = {
|
||||||
|
"settings",
|
||||||
|
"fast_registration",
|
||||||
|
"editor",
|
||||||
|
}
|
||||||
|
|
||||||
|
REGULAR_FLOW_ONLY = {
|
||||||
|
"protocol",
|
||||||
|
"location",
|
||||||
|
"hidetor",
|
||||||
|
"residential",
|
||||||
|
"tor",
|
||||||
|
"connection",
|
||||||
|
"screen",
|
||||||
|
"browser",
|
||||||
|
"wireguard",
|
||||||
|
"sync_screen",
|
||||||
|
}
|
||||||
|
|
||||||
|
PRELOAD_ORDER = [
|
||||||
|
"settings",
|
||||||
|
"editor",
|
||||||
|
"ticket_or_billing_choice",
|
||||||
|
"payment_details",
|
||||||
|
"plan_picker",
|
||||||
|
"ticket_crypto_picker",
|
||||||
|
"duration_selection",
|
||||||
|
"currency_selection",
|
||||||
|
"policy_suggestion",
|
||||||
|
"systemwide_prompt",
|
||||||
|
"fast_mode_prompt",
|
||||||
|
"fast_registration",
|
||||||
|
"id",
|
||||||
|
"protocol",
|
||||||
|
"location",
|
||||||
|
"hidetor",
|
||||||
|
"residential",
|
||||||
|
"tor",
|
||||||
|
"connection",
|
||||||
|
"screen",
|
||||||
|
"browser",
|
||||||
|
"resume",
|
||||||
|
"wireguard",
|
||||||
|
"install_system_package",
|
||||||
|
"sync_screen",
|
||||||
|
]
|
||||||
|
|
|
||||||
31
gui/v2/infrastructure/screen_size.py
Executable file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from core.errors.logger import logger
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from PyQt6.QtGui import QGuiApplication
|
||||||
|
import sys
|
||||||
|
|
||||||
|
max_screen_size = None
|
||||||
|
DEFAULT_VALUE = "800x800"
|
||||||
|
|
||||||
|
def calculate_max_screensize(app):
|
||||||
|
global max_screen_size
|
||||||
|
|
||||||
|
try:
|
||||||
|
screen = app.primaryScreen()
|
||||||
|
available = screen.availableGeometry()
|
||||||
|
dpi_ratio = screen.devicePixelRatio()
|
||||||
|
|
||||||
|
actual_width = int(available.width() * dpi_ratio)
|
||||||
|
actual_height = int(available.height() * dpi_ratio)
|
||||||
|
max_screen_size = f"{actual_width}x{actual_height}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Critical Error with calculating the screen size: {str(e)}")
|
||||||
|
max_screen_size = DEFAULT_VALUE
|
||||||
|
|
||||||
|
|
||||||
|
def get_max_screensize():
|
||||||
|
if max_screen_size is not None:
|
||||||
|
return max_screen_size
|
||||||
|
else:
|
||||||
|
return DEFAULT_VALUE
|
||||||
|
|
@ -4,8 +4,8 @@ from core.observers.ConnectionObserver import ConnectionObserver
|
||||||
from core.observers.InvoiceObserver import InvoiceObserver
|
from core.observers.InvoiceObserver import InvoiceObserver
|
||||||
from core.observers.ProfileObserver import ProfileObserver
|
from core.observers.ProfileObserver import ProfileObserver
|
||||||
from core.observers.TicketObserver import TicketObserver
|
from core.observers.TicketObserver import TicketObserver
|
||||||
from core.controllers.ApplicationController import ApplicationController
|
# from core.controllers.ApplicationController import ApplicationController
|
||||||
|
from essentials.models.Event import Event
|
||||||
|
|
||||||
application_version_observer = ApplicationVersionObserver()
|
application_version_observer = ApplicationVersionObserver()
|
||||||
client_observer = ClientObserver()
|
client_observer = ClientObserver()
|
||||||
|
|
@ -15,43 +15,82 @@ profile_observer = ProfileObserver()
|
||||||
ticket_observer = TicketObserver()
|
ticket_observer = TicketObserver()
|
||||||
|
|
||||||
|
|
||||||
|
def _format_connecting_status(event):
|
||||||
|
subject = getattr(event, 'subject', None)
|
||||||
|
if isinstance(subject, dict):
|
||||||
|
attempt_count = subject.get("attempt_count")
|
||||||
|
maximum_attempts = subject.get("maximum_number_of_attempts")
|
||||||
|
if attempt_count is not None and maximum_attempts is not None:
|
||||||
|
return f'[{attempt_count}/{maximum_attempts}] Performing connection attempt...'
|
||||||
|
if subject:
|
||||||
|
return str(subject)
|
||||||
|
return 'Connecting..'
|
||||||
|
|
||||||
|
|
||||||
def setup_observers(update_status):
|
def setup_observers(update_status):
|
||||||
profile_observer.subscribe(
|
profile_observer.subscribe(
|
||||||
'created', lambda event: update_status('Profile Created'))
|
'created', lambda event: update_status('Profile Created'))
|
||||||
profile_observer.subscribe(
|
profile_observer.subscribe(
|
||||||
'destroyed', lambda event: update_status('Profile destroyed'))
|
'destroyed', lambda event: update_status('Profile destroyed'))
|
||||||
|
|
||||||
|
# client_observer.subscribe(
|
||||||
|
# 'synchronizing', lambda event: update_status('Sync in progress...'))
|
||||||
|
|
||||||
client_observer.subscribe(
|
client_observer.subscribe(
|
||||||
'synchronizing', lambda event: update_status('Sync in progress...'))
|
'synchronizing', lambda event: update_status(f'{event.subject if event.subject else "Sync in progress..."}'))
|
||||||
|
|
||||||
client_observer.subscribe(
|
client_observer.subscribe(
|
||||||
'synchronized', lambda event: update_status('Sync complete'))
|
'synchronized', lambda event: update_status('Sync complete'))
|
||||||
|
|
||||||
client_observer.subscribe(
|
client_observer.subscribe(
|
||||||
'updating', lambda event: update_status('Updating client...'))
|
'updating', lambda event: update_status('Updating client...'))
|
||||||
client_observer.subscribe('update_progressing', lambda event: update_status(
|
client_observer.subscribe('update_progressing', lambda event: update_status(
|
||||||
f'Current progress: {event.meta.get('progress'):.2f}%'))
|
f"Current progress: {event.meta.get('progress'):.2f}%"))
|
||||||
client_observer.subscribe('updated', lambda event: update_status(
|
client_observer.subscribe('updated', lambda event: update_status(
|
||||||
'Restart client to apply update.'))
|
'Restart client to apply update.'))
|
||||||
|
|
||||||
|
client_observer.subscribe(
|
||||||
|
'custom_message', lambda event: update_status(f'{event.subject if event.subject else "Error, check logs"}'))
|
||||||
|
|
||||||
application_version_observer.subscribe('downloading', lambda event: update_status(
|
application_version_observer.subscribe('downloading', lambda event: update_status(
|
||||||
f'Downloading {ApplicationController.get(event.subject.application_code).name}'))
|
f'{event.subject if event.subject else "Downloading.."}'))
|
||||||
|
|
||||||
application_version_observer.subscribe('download_progressing', lambda event: update_status(
|
application_version_observer.subscribe('download_progressing', lambda event: update_status(
|
||||||
f'Downloading {ApplicationController.get(event.subject.application_code).name} {event.meta.get('progress'):.2f}%'))
|
f'{event.subject if event.subject else "Downloading.."}'))
|
||||||
|
|
||||||
application_version_observer.subscribe('downloaded', lambda event: update_status(
|
application_version_observer.subscribe('downloaded', lambda event: update_status(
|
||||||
f'Downloaded {ApplicationController.get(event.subject.application_code).name}'))
|
f'{event.subject if event.subject else "Downloaded"}'))
|
||||||
|
|
||||||
|
# application_version_observer.subscribe('downloading', lambda event: update_status(
|
||||||
|
# f'Downloading {ApplicationController.get(event.subject.application_code).name}'))
|
||||||
|
# application_version_observer.subscribe('download_progressing', lambda event: update_status(
|
||||||
|
# f'Downloading {ApplicationController.get(event.subject.application_code).name} {event.meta.get('progress'):.2f}%'))
|
||||||
|
|
||||||
|
# application_version_observer.subscribe('downloaded', lambda event: update_status(
|
||||||
|
# f'Downloaded {ApplicationController.get(event.subject.application_code).name}'))
|
||||||
|
|
||||||
connection_observer.subscribe('connecting', lambda event: update_status(
|
connection_observer.subscribe('connecting', lambda event: update_status(
|
||||||
f'[{event.subject.get("attempt_count")}/{event.subject.get("maximum_number_of_attempts")}] Performing connection attempt...'))
|
_format_connecting_status(event)))
|
||||||
|
|
||||||
connection_observer.subscribe('tor_bootstrapping', lambda event: update_status(
|
connection_observer.subscribe('tor_bootstrapping', lambda event: update_status(
|
||||||
'Establishing Tor connection...'))
|
'Establishing Tor connection...'))
|
||||||
|
|
||||||
connection_observer.subscribe('tor_bootstrap_progressing', lambda event: update_status(
|
connection_observer.subscribe(
|
||||||
f'Bootstrapping Tor {event.meta.get('progress'):.2f}%'))
|
'tor_bootstrap_progressing', lambda event: update_status(f'{event.subject if event.subject else "Tor Bootstrapping.."}'))
|
||||||
|
|
||||||
|
connection_observer.subscribe(
|
||||||
|
'message', lambda event: update_status(f'{event.subject if event.subject else "Connecting.."}'))
|
||||||
|
|
||||||
|
# original replaced version:
|
||||||
|
# connection_observer.subscribe('tor_bootstrap_progressing', lambda event: update_status(
|
||||||
|
# f'Bootstrapping Tor {event.meta.get('progress'):.2f}%'))
|
||||||
|
|
||||||
connection_observer.subscribe(
|
connection_observer.subscribe(
|
||||||
'tor_bootstrapped', lambda event: update_status('Tor connection established.'))
|
'tor_bootstrapped', lambda event: update_status('Tor connection established.'))
|
||||||
|
|
||||||
|
client_observer.subscribe(
|
||||||
|
'custom_message', lambda event: update_status(f'{event.subject if event.subject else ""}'))
|
||||||
|
|
||||||
ticket_observer.subscribe('connecting', lambda event: update_status('Connecting to ticket server...'))
|
ticket_observer.subscribe('connecting', lambda event: update_status('Connecting to ticket server...'))
|
||||||
ticket_observer.subscribe('sync_done', lambda event: update_status('Ticket prices synced.'))
|
ticket_observer.subscribe('sync_done', lambda event: update_status('Ticket prices synced.'))
|
||||||
ticket_observer.subscribe('waiting', lambda event: update_status('Waiting for payment...'))
|
ticket_observer.subscribe('waiting', lambda event: update_status('Waiting for payment...'))
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,5 @@ def create_top_section(parent, css_path):
|
||||||
'marquee_timer': marquee_timer,
|
'marquee_timer': marquee_timer,
|
||||||
'text_start_x': 15,
|
'text_start_x': 15,
|
||||||
'text_end_x': 420,
|
'text_end_x': 420,
|
||||||
'text_width': 405,
|
'text_width': 500,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,22 @@ class Page(QWidget):
|
||||||
self.btn_path = custom_window.btn_path
|
self.btn_path = custom_window.btn_path
|
||||||
self.name = name
|
self.name = name
|
||||||
self.page_stack = page_stack
|
self.page_stack = page_stack
|
||||||
|
self._prepared = None
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
self.selected_profiles = []
|
self.selected_profiles = []
|
||||||
self.selected_wireguard = []
|
self.selected_wireguard = []
|
||||||
self.selected_residential = []
|
self.selected_residential = []
|
||||||
self.buttons = []
|
self.buttons = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
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):
|
def add_selected_profile(self, profile):
|
||||||
self.selected_profiles.clear()
|
self.selected_profiles.clear()
|
||||||
self.selected_wireguard.clear()
|
self.selected_wireguard.clear()
|
||||||
|
|
@ -84,3 +94,10 @@ class Page(QWidget):
|
||||||
boton.setChecked(False)
|
boton.setChecked(False)
|
||||||
|
|
||||||
self.button_next.setVisible(False)
|
self.button_next.setVisible(False)
|
||||||
|
|
||||||
|
def replace_click_handler(self, button, handler):
|
||||||
|
try:
|
||||||
|
button.clicked.disconnect()
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
button.clicked.connect(handler)
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ class BrowserPage(Page):
|
||||||
self.title.setText("Pick a Browser")
|
self.title.setText("Pick a Browser")
|
||||||
self.button_back.setVisible(True)
|
self.button_back.setVisible(True)
|
||||||
|
|
||||||
cm = main_window.connection_manager
|
self.connection_manager = main_window.connection_manager
|
||||||
if cm.is_synced():
|
if self.connection_manager.is_synced():
|
||||||
from gui.v2.actions.sync import generate_grid_positions
|
from gui.v2.actions.sync import generate_grid_positions
|
||||||
browsers = cm.get_browser_list()
|
browsers = self.connection_manager.get_browser_list()
|
||||||
positions = generate_grid_positions(len(browsers))
|
positions = generate_grid_positions(len(browsers))
|
||||||
available = [(QPushButton, brw, positions[i]) for i, brw in enumerate(browsers)]
|
available = [(QPushButton, brw, positions[i]) for i, brw in enumerate(browsers)]
|
||||||
self.create_interface_elements(available)
|
self.create_interface_elements(available)
|
||||||
|
|
@ -239,3 +239,21 @@ class BrowserPage(Page):
|
||||||
|
|
||||||
def gestionar_next(self):
|
def gestionar_next(self):
|
||||||
self.custom_window.navigator.navigate("screen")
|
self.custom_window.navigator.navigate("screen")
|
||||||
|
|
||||||
|
def gestionar_back(self):
|
||||||
|
profile_data = self.update_status.read_data()
|
||||||
|
protocol = profile_data.get("protocol", "")
|
||||||
|
|
||||||
|
if not self.connection_manager.is_synced() or not self.connection_manager.get_browser_list():
|
||||||
|
self.custom_window.navigator.navigate("menu")
|
||||||
|
return
|
||||||
|
|
||||||
|
if protocol == "wireguard":
|
||||||
|
self.custom_window.navigator.navigate("location")
|
||||||
|
return
|
||||||
|
|
||||||
|
if protocol in ("hidetor", "residential"):
|
||||||
|
self.custom_window.navigator.navigate("hidetor")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.custom_window.navigator.navigate("menu")
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,24 @@ from PyQt6 import QtCore, QtGui
|
||||||
|
|
||||||
from core.controllers.ProfileController import ProfileController
|
from core.controllers.ProfileController import ProfileController
|
||||||
from core.controllers.LocationController import LocationController
|
from core.controllers.LocationController import LocationController
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
from core.controllers.profile_state.update_profile import update_profile
|
||||||
|
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
from gui.v2.ui.pages.browser_page import BrowserPage
|
from gui.v2.ui.pages.browser_page import BrowserPage
|
||||||
from gui.v2.ui.pages.location_page import LocationPage
|
from gui.v2.ui.pages.location_page import LocationPage
|
||||||
from gui.v2.ui.pages.screen_page import ScreenPage
|
from gui.v2.ui.pages.screen_page import ScreenPage
|
||||||
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
|
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
|
||||||
|
from gui.v2.workers.page_data_worker import PageDataWorker
|
||||||
|
|
||||||
|
|
||||||
class EditorPage(Page):
|
class EditorPage(Page):
|
||||||
def __init__(self, page_stack, main_window):
|
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
|
||||||
|
PROTOCOL_BUTTON_ASSETS = {
|
||||||
|
"hysteria2": "hystria2",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, page_stack, main_window, prepared=None):
|
||||||
super().__init__("Editor", page_stack, main_window)
|
super().__init__("Editor", page_stack, main_window)
|
||||||
self.page_stack = page_stack
|
self.page_stack = page_stack
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
|
|
@ -80,6 +88,13 @@ class EditorPage(Page):
|
||||||
self.brow_disp.hide()
|
self.brow_disp.hide()
|
||||||
self.brow_disp.lower()
|
self.brow_disp.lower()
|
||||||
|
|
||||||
|
self._prefetched_profiles = prepared
|
||||||
|
self._editor_workers = set()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def prepare_data(custom_window):
|
||||||
|
return ProfileController.get_all()
|
||||||
|
|
||||||
def _selected_profiles_str(self):
|
def _selected_profiles_str(self):
|
||||||
menu_page = self.find_menu_page()
|
menu_page = self.find_menu_page()
|
||||||
if menu_page is not None and menu_page.selected_profiles:
|
if menu_page is not None and menu_page.selected_profiles:
|
||||||
|
|
@ -117,8 +132,43 @@ class EditorPage(Page):
|
||||||
def showEvent(self, event):
|
def showEvent(self, event):
|
||||||
super().showEvent(event)
|
super().showEvent(event)
|
||||||
self.res_hint_shown = False
|
self.res_hint_shown = False
|
||||||
|
if self._selected_profile_in_cache():
|
||||||
|
self.extraccion()
|
||||||
|
return
|
||||||
|
worker = PageDataWorker("editor", EditorPage.prepare_data, self.custom_window)
|
||||||
|
worker.data_ready.connect(
|
||||||
|
lambda name, profiles: self._on_editor_data_ready(profiles))
|
||||||
|
worker.failed.connect(
|
||||||
|
lambda name, error: self._on_editor_data_failed(error))
|
||||||
|
worker.finished.connect(
|
||||||
|
lambda w=worker: self._cleanup_editor_worker(w))
|
||||||
|
self._editor_workers.add(worker)
|
||||||
|
worker.start()
|
||||||
|
|
||||||
|
def _on_editor_data_ready(self, profiles):
|
||||||
|
self._prefetched_profiles = profiles
|
||||||
self.extraccion()
|
self.extraccion()
|
||||||
|
|
||||||
|
def _on_editor_data_failed(self, error):
|
||||||
|
self.update_status.update_status(f"Editor data load failed: {error}")
|
||||||
|
self.extraccion()
|
||||||
|
|
||||||
|
def _cleanup_editor_worker(self, worker):
|
||||||
|
self._editor_workers.discard(worker)
|
||||||
|
worker.deleteLater()
|
||||||
|
|
||||||
|
def _selected_profile_in_cache(self):
|
||||||
|
if not self._prefetched_profiles:
|
||||||
|
return False
|
||||||
|
selected = self._selected_profiles_str()
|
||||||
|
if not selected:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
profile_id = int(selected.split('_')[1])
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
return False
|
||||||
|
return profile_id in self._prefetched_profiles
|
||||||
|
|
||||||
def extraccion(self):
|
def extraccion(self):
|
||||||
self.data_profile = {}
|
self.data_profile = {}
|
||||||
for label in self.labels:
|
for label in self.labels:
|
||||||
|
|
@ -131,7 +181,10 @@ class EditorPage(Page):
|
||||||
selected_profiles_str = self._selected_profiles_str()
|
selected_profiles_str = self._selected_profiles_str()
|
||||||
menu_page = self.find_menu_page()
|
menu_page = self.find_menu_page()
|
||||||
if menu_page:
|
if menu_page:
|
||||||
new_profiles = ProfileController.get_all()
|
if self._prefetched_profiles is not None:
|
||||||
|
new_profiles = self._prefetched_profiles
|
||||||
|
else:
|
||||||
|
new_profiles = ProfileController.get_all()
|
||||||
self.profiles_data = menu_page.match_core_profiles(
|
self.profiles_data = menu_page.match_core_profiles(
|
||||||
profiles_dict=new_profiles)
|
profiles_dict=new_profiles)
|
||||||
self.data_profile = self.profiles_data[selected_profiles_str].copy(
|
self.data_profile = self.profiles_data[selected_profiles_str].copy(
|
||||||
|
|
@ -160,6 +213,13 @@ class EditorPage(Page):
|
||||||
"dimentions": self.connection_manager.get_available_resolutions(data_profile.get('id', ''))
|
"dimentions": self.connection_manager.get_available_resolutions(data_profile.get('id', ''))
|
||||||
}, selected_profile_str)
|
}, selected_profile_str)
|
||||||
|
|
||||||
|
elif protocol in self.SINGBOX_PROTOCOLS:
|
||||||
|
self.process_and_show_labels(data_profile, {
|
||||||
|
"protocol": [protocol],
|
||||||
|
"connection": ['system-wide'],
|
||||||
|
"location": self.connection_manager.get_location_list(),
|
||||||
|
}, selected_profile_str)
|
||||||
|
|
||||||
elif protocol == "residential" or protocol == "hidetor":
|
elif protocol == "residential" or protocol == "hidetor":
|
||||||
self.process_and_show_labels(data_profile, {
|
self.process_and_show_labels(data_profile, {
|
||||||
"protocol": ['residential', 'wireguard', 'hidetor'],
|
"protocol": ['residential', 'wireguard', 'hidetor'],
|
||||||
|
|
@ -215,6 +275,20 @@ class EditorPage(Page):
|
||||||
else:
|
else:
|
||||||
self.brow_disp.hide()
|
self.brow_disp.hide()
|
||||||
|
|
||||||
|
if protocol in self.SINGBOX_PROTOCOLS:
|
||||||
|
if protocol == "vless":
|
||||||
|
self.display.setGeometry(0, 90, 540, 405)
|
||||||
|
else:
|
||||||
|
self.display.setGeometry(0, 60, 540, 405)
|
||||||
|
self.display.show()
|
||||||
|
self.display.setPixmap(self._encrypted_proxy_display_pixmap(
|
||||||
|
protocol, location).scaled(
|
||||||
|
self.display.size(),
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation))
|
||||||
|
self.garaje.hide()
|
||||||
|
self.brow_disp.hide()
|
||||||
|
|
||||||
if protocol == "residential":
|
if protocol == "residential":
|
||||||
self.display.setGeometry(0, 60, 540, 405)
|
self.display.setGeometry(0, 60, 540, 405)
|
||||||
self.brow_disp.show()
|
self.brow_disp.show()
|
||||||
|
|
@ -253,7 +327,7 @@ class EditorPage(Page):
|
||||||
l_name = f"{location_info.country_name}, {location_info.name}" if hasattr(
|
l_name = f"{location_info.country_name}, {location_info.name}" if hasattr(
|
||||||
location_info, 'country_name') else ""
|
location_info, 'country_name') else ""
|
||||||
|
|
||||||
if operator_name != 'Simplified Privacy' and operator_name != "" and protocol != 'hidetor':
|
if operator_name != 'Simplified Privacy' and operator_name != "" and protocol not in ('hidetor', *self.SINGBOX_PROTOCOLS):
|
||||||
text_color = "white"
|
text_color = "white"
|
||||||
if profile_obj and profile_obj.is_session_profile():
|
if profile_obj and profile_obj.is_session_profile():
|
||||||
text_color = "black"
|
text_color = "black"
|
||||||
|
|
@ -311,14 +385,25 @@ class EditorPage(Page):
|
||||||
if browser_version == '':
|
if browser_version == '':
|
||||||
browser_version = data_profile.get('browser_version', '')
|
browser_version = data_profile.get('browser_version', '')
|
||||||
browser_value = f"{browser_type} {browser_version}"
|
browser_value = f"{browser_type} {browser_version}"
|
||||||
if connection == 'system-wide' or not browser_value.strip():
|
fallback_path = os.path.join(
|
||||||
|
self.btn_path, "default_browser_button.png")
|
||||||
|
normalized_browser = browser_value.strip().lower()
|
||||||
|
unknown_browser = (
|
||||||
|
not normalized_browser
|
||||||
|
or normalized_browser == "unknown"
|
||||||
|
or normalized_browser.startswith("unknown browser")
|
||||||
|
)
|
||||||
|
if connection == 'system-wide':
|
||||||
base_image = QPixmap()
|
base_image = QPixmap()
|
||||||
|
elif unknown_browser:
|
||||||
|
base_image = QPixmap(fallback_path)
|
||||||
|
if base_image.isNull():
|
||||||
|
base_image = BrowserPage.create_browser_button_image(
|
||||||
|
"Browser", fallback_path, True)
|
||||||
else:
|
else:
|
||||||
base_image = BrowserPage.create_browser_button_image(
|
base_image = BrowserPage.create_browser_button_image(
|
||||||
browser_value, self.btn_path)
|
browser_value, self.btn_path)
|
||||||
if base_image.isNull():
|
if base_image.isNull():
|
||||||
fallback_path = os.path.join(
|
|
||||||
self.btn_path, "default_browser_button.png")
|
|
||||||
base_image = BrowserPage.create_browser_button_image(
|
base_image = BrowserPage.create_browser_button_image(
|
||||||
browser_value, fallback_path, True)
|
browser_value, fallback_path, True)
|
||||||
elif key == 'location':
|
elif key == 'location':
|
||||||
|
|
@ -354,9 +439,12 @@ class EditorPage(Page):
|
||||||
base_image = ScreenPage.create_resolution_button_image(
|
base_image = ScreenPage.create_resolution_button_image(
|
||||||
self, current_value)
|
self, current_value)
|
||||||
else:
|
else:
|
||||||
image_path = os.path.join(
|
|
||||||
self.btn_path, f"{data_profile.get(key, '')}_button.png")
|
|
||||||
current_value = data_profile.get(key, '')
|
current_value = data_profile.get(key, '')
|
||||||
|
if key == 'protocol':
|
||||||
|
image_path = self._protocol_button_asset(current_value)
|
||||||
|
else:
|
||||||
|
image_path = os.path.join(
|
||||||
|
self.btn_path, f"{current_value}_button.png")
|
||||||
base_image = QPixmap(image_path)
|
base_image = QPixmap(image_path)
|
||||||
|
|
||||||
if key == 'dimentions':
|
if key == 'dimentions':
|
||||||
|
|
@ -520,7 +608,7 @@ class EditorPage(Page):
|
||||||
|
|
||||||
prev_button.setVisible(True)
|
prev_button.setVisible(True)
|
||||||
next_button.setVisible(True)
|
next_button.setVisible(True)
|
||||||
if key == 'protocol' or (protocol == 'wireguard' and key == 'connection'):
|
if key == 'protocol' or (connection == 'system-wide' and key == 'connection'):
|
||||||
prev_button.setDisabled(True)
|
prev_button.setDisabled(True)
|
||||||
next_button.setDisabled(True)
|
next_button.setDisabled(True)
|
||||||
|
|
||||||
|
|
@ -528,6 +616,44 @@ class EditorPage(Page):
|
||||||
prev_button.setVisible(False)
|
prev_button.setVisible(False)
|
||||||
next_button.setVisible(False)
|
next_button.setVisible(False)
|
||||||
|
|
||||||
|
def _protocol_button_asset(self, protocol):
|
||||||
|
asset_name = self.PROTOCOL_BUTTON_ASSETS.get(protocol, protocol)
|
||||||
|
return os.path.join(self.btn_path, f"{asset_name}_button.png")
|
||||||
|
|
||||||
|
def _encrypted_proxy_display_pixmap(self, protocol, location):
|
||||||
|
pixmap = QPixmap(os.path.join(self.btn_path, f"{protocol}.png"))
|
||||||
|
if pixmap.isNull():
|
||||||
|
pixmap = QPixmap(os.path.join(self.btn_path, "system_wide_global.png"))
|
||||||
|
if pixmap.isNull():
|
||||||
|
return pixmap
|
||||||
|
|
||||||
|
location_pixmap = QPixmap(os.path.join(
|
||||||
|
self.btn_path, f"icon_mini_{location}.png"))
|
||||||
|
if location_pixmap.isNull():
|
||||||
|
location_pixmap = QPixmap(os.path.join(
|
||||||
|
self.btn_path, "default_location_mini.png"))
|
||||||
|
if location_pixmap.isNull():
|
||||||
|
return pixmap
|
||||||
|
|
||||||
|
icon_size = max(42, min(72, int(min(pixmap.width(), pixmap.height()) * 0.22)))
|
||||||
|
location_pixmap = location_pixmap.scaled(
|
||||||
|
icon_size,
|
||||||
|
icon_size,
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation)
|
||||||
|
x = int(pixmap.width() * 0.58)
|
||||||
|
if protocol == 'hysteria2':
|
||||||
|
y = int(pixmap.height() * 0.35)
|
||||||
|
else:
|
||||||
|
y = int(pixmap.height() * 0.30)
|
||||||
|
x = min(max(0, x), max(0, pixmap.width() - location_pixmap.width()))
|
||||||
|
y = min(max(0, y), max(0, pixmap.height() - location_pixmap.height()))
|
||||||
|
|
||||||
|
painter = QPainter(pixmap)
|
||||||
|
painter.drawPixmap(x, y, location_pixmap)
|
||||||
|
painter.end()
|
||||||
|
return pixmap
|
||||||
|
|
||||||
def on_sync_complete_for_edit_profile(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
def on_sync_complete_for_edit_profile(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):
|
||||||
if status:
|
if status:
|
||||||
self.update_status.update_status('Sync complete.')
|
self.update_status.update_status('Sync complete.')
|
||||||
|
|
@ -545,8 +671,14 @@ class EditorPage(Page):
|
||||||
self.on_sync_complete_for_edit_profile)
|
self.on_sync_complete_for_edit_profile)
|
||||||
return
|
return
|
||||||
|
|
||||||
previous_index = (index - 1) % len(parameters[key])
|
values = parameters.get(key, [])
|
||||||
previous_value = parameters[key][previous_index]
|
if not values:
|
||||||
|
self.update_status.update_status(
|
||||||
|
f"No {key} data available. Sync the database and try again.")
|
||||||
|
return
|
||||||
|
|
||||||
|
previous_index = (index - 1) % len(values)
|
||||||
|
previous_value = values[previous_index]
|
||||||
self.update_temp_value(key, previous_value)
|
self.update_temp_value(key, previous_value)
|
||||||
|
|
||||||
def show_next_value(self, key: str, index: int, parameters: dict) -> None:
|
def show_next_value(self, key: str, index: int, parameters: dict) -> None:
|
||||||
|
|
@ -558,8 +690,14 @@ class EditorPage(Page):
|
||||||
self.on_sync_complete_for_edit_profile)
|
self.on_sync_complete_for_edit_profile)
|
||||||
return
|
return
|
||||||
|
|
||||||
next_index = (index + 1) % len(parameters[key])
|
values = parameters.get(key, [])
|
||||||
next_value = parameters[key][next_index]
|
if not values:
|
||||||
|
self.update_status.update_status(
|
||||||
|
f"No {key} data available. Sync the database and try again.")
|
||||||
|
return
|
||||||
|
|
||||||
|
next_index = (index + 1) % len(values)
|
||||||
|
next_value = values[next_index]
|
||||||
|
|
||||||
self.update_temp_value(key, next_value)
|
self.update_temp_value(key, next_value)
|
||||||
|
|
||||||
|
|
@ -746,51 +884,27 @@ class EditorPage(Page):
|
||||||
|
|
||||||
self.temp_changes.pop(selected_profiles_str, None)
|
self.temp_changes.pop(selected_profiles_str, None)
|
||||||
self.original_values.pop(selected_profiles_str, None)
|
self.original_values.pop(selected_profiles_str, None)
|
||||||
|
self._prefetched_profiles = None
|
||||||
|
|
||||||
def update_core_profiles(self, key, new_value):
|
def update_core_profiles(self, key, new_value):
|
||||||
profile = ProfileController.get(
|
profile_id = int(self.update_status.current_profile_id)
|
||||||
int(self.update_status.current_profile_id))
|
|
||||||
self.update_res = False
|
|
||||||
if key == 'dimentions':
|
|
||||||
profile.resolution = new_value
|
|
||||||
self.update_res = True
|
|
||||||
|
|
||||||
elif key == 'name':
|
# Sends to core directly,
|
||||||
profile.name = new_value
|
result = update_profile(profile_id, key, new_value)
|
||||||
|
|
||||||
elif key == 'connection':
|
if result.valid:
|
||||||
if new_value == 'tor':
|
self.update_status.update_status(
|
||||||
profile.connection.code = new_value
|
f'Updated profile {profile_id}')
|
||||||
profile.connection.masked = True
|
|
||||||
elif new_value == 'just proxy':
|
|
||||||
profile.connection.code = 'system'
|
|
||||||
profile.connection.masked = True
|
|
||||||
else:
|
|
||||||
self.update_status.update_status(
|
|
||||||
'System wide profiles not supported atm')
|
|
||||||
|
|
||||||
elif key == 'browser':
|
if key == "resolution":
|
||||||
browser_type, browser_version = new_value.split(':', 1)
|
self.update_res = True
|
||||||
profile.application_version.application_code = browser_type
|
|
||||||
profile.application_version.version_number = browser_version
|
|
||||||
|
|
||||||
elif key == 'protocol':
|
if result.data == "edit_to_session":
|
||||||
if self.data_profile.get('connection') == 'system-wide':
|
|
||||||
self.edit_to_session()
|
self.edit_to_session()
|
||||||
else:
|
|
||||||
profile.connection.code = 'wireguard' if new_value == 'wireguard' else 'tor'
|
|
||||||
profile.connection.masked = False if new_value == 'wireguard' else True
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
location = self.connection_manager.get_location_info(new_value)
|
self.update_status.update_status(result.message)
|
||||||
|
|
||||||
if location:
|
|
||||||
profile.location.code = location.code
|
|
||||||
profile.location.country_code = location.country_code
|
|
||||||
profile.location.time_zone = location.time_zone
|
|
||||||
profile.subscription = None
|
|
||||||
|
|
||||||
ProfileController.update(profile)
|
|
||||||
|
|
||||||
def edit_to_session(self):
|
def edit_to_session(self):
|
||||||
id = int(self.update_status.current_profile_id)
|
id = int(self.update_status.current_profile_id)
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,23 @@ from PyQt6 import QtCore
|
||||||
from core.controllers.ProfileController import ProfileController
|
from core.controllers.ProfileController import ProfileController
|
||||||
|
|
||||||
from gui.v2.actions.profile_order import append_profile_to_visual_order
|
from gui.v2.actions.profile_order import append_profile_to_visual_order
|
||||||
|
from gui.v2.actions.singbox_prereqs import singbox_prereqs_installed
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
from gui.v2.ui.pages.browser_page import BrowserPage
|
from gui.v2.ui.pages.browser_page import BrowserPage
|
||||||
from gui.v2.ui.pages.location_page import LocationPage
|
from gui.v2.ui.pages.location_page import LocationPage
|
||||||
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
|
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
|
||||||
|
from gui.v2.workers.page_data_worker import PageDataWorker
|
||||||
from gui.v2.workers.worker_thread import WorkerThread
|
from gui.v2.workers.worker_thread import WorkerThread
|
||||||
|
|
||||||
|
|
||||||
class FastRegistrationPage(Page):
|
class FastRegistrationPage(Page):
|
||||||
def __init__(self, page_stack, main_window):
|
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
|
||||||
|
PROTOCOLS = ("wireguard", "hysteria2", "vless", "hidetor")
|
||||||
|
PROTOCOL_BUTTON_ASSETS = {
|
||||||
|
"hysteria2": "hystria2",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, page_stack, main_window, prepared=None):
|
||||||
super().__init__("FastRegistration", page_stack, main_window)
|
super().__init__("FastRegistration", page_stack, main_window)
|
||||||
self.page_stack = page_stack
|
self.page_stack = page_stack
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
|
|
@ -73,15 +81,23 @@ class FastRegistrationPage(Page):
|
||||||
'resolution': '1024x760'
|
'resolution': '1024x760'
|
||||||
}
|
}
|
||||||
self.res_index = 1
|
self.res_index = 1
|
||||||
|
self._prefetched_profiles = prepared
|
||||||
|
self._fast_reg_workers = set()
|
||||||
|
|
||||||
self.initialize_default_selections()
|
self.initialize_default_selections()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def prepare_data(custom_window):
|
||||||
|
return ProfileController.get_all()
|
||||||
|
|
||||||
def initialize_default_selections(self):
|
def initialize_default_selections(self):
|
||||||
if not self.selected_values['location']:
|
if not self.selected_values['location']:
|
||||||
locations = self.connection_manager.get_location_list()
|
locations = self._available_locations_for_protocol()
|
||||||
if locations:
|
if locations:
|
||||||
random_index = random.randint(0, len(locations) - 1)
|
random_index = random.randint(0, len(locations) - 1)
|
||||||
self.selected_values['location'] = locations[random_index]
|
self.selected_values['location'] = locations[random_index]
|
||||||
|
else:
|
||||||
|
self._select_valid_location_for_protocol()
|
||||||
|
|
||||||
if not self.selected_values['browser']:
|
if not self.selected_values['browser']:
|
||||||
browsers = self.connection_manager.get_browser_list()
|
browsers = self.connection_manager.get_browser_list()
|
||||||
|
|
@ -107,6 +123,24 @@ class FastRegistrationPage(Page):
|
||||||
super().showEvent(event)
|
super().showEvent(event)
|
||||||
self.initialize_default_selections()
|
self.initialize_default_selections()
|
||||||
self.create_interface_elements()
|
self.create_interface_elements()
|
||||||
|
self._start_profiles_prefetch()
|
||||||
|
|
||||||
|
def _start_profiles_prefetch(self):
|
||||||
|
worker = PageDataWorker(
|
||||||
|
"fast_registration", FastRegistrationPage.prepare_data, self.custom_window)
|
||||||
|
worker.data_ready.connect(
|
||||||
|
lambda name, profiles: self._on_profiles_prefetched(profiles))
|
||||||
|
worker.finished.connect(
|
||||||
|
lambda w=worker: self._cleanup_fast_reg_worker(w))
|
||||||
|
self._fast_reg_workers.add(worker)
|
||||||
|
worker.start()
|
||||||
|
|
||||||
|
def _on_profiles_prefetched(self, profiles):
|
||||||
|
self._prefetched_profiles = profiles
|
||||||
|
|
||||||
|
def _cleanup_fast_reg_worker(self, worker):
|
||||||
|
self._fast_reg_workers.discard(worker)
|
||||||
|
worker.deleteLater()
|
||||||
|
|
||||||
def create_interface_elements(self):
|
def create_interface_elements(self):
|
||||||
for label in self.labels:
|
for label in self.labels:
|
||||||
|
|
@ -142,8 +176,8 @@ class FastRegistrationPage(Page):
|
||||||
label = QLabel("Protocol", self)
|
label = QLabel("Protocol", self)
|
||||||
label.setGeometry(300, 150, 185, 75)
|
label.setGeometry(300, 150, 185, 75)
|
||||||
|
|
||||||
protocol_image = QPixmap(os.path.join(
|
protocol_image = QPixmap(self._protocol_button_asset(
|
||||||
self.btn_path, f"{self.selected_values['protocol']}_button.png"))
|
self.selected_values['protocol']))
|
||||||
label.setPixmap(protocol_image)
|
label.setPixmap(protocol_image)
|
||||||
label.setScaledContents(True)
|
label.setScaledContents(True)
|
||||||
label.show()
|
label.show()
|
||||||
|
|
@ -171,6 +205,10 @@ class FastRegistrationPage(Page):
|
||||||
next_button.setIconSize(next_button.size())
|
next_button.setIconSize(next_button.size())
|
||||||
self.buttons.append(next_button)
|
self.buttons.append(next_button)
|
||||||
|
|
||||||
|
def _protocol_button_asset(self, protocol):
|
||||||
|
asset_name = self.PROTOCOL_BUTTON_ASSETS.get(protocol, protocol)
|
||||||
|
return os.path.join(self.btn_path, f"{asset_name}_button.png")
|
||||||
|
|
||||||
def create_connection_section(self):
|
def create_connection_section(self):
|
||||||
label = QLabel("Connection", self)
|
label = QLabel("Connection", self)
|
||||||
label.setGeometry(150, 250, 185, 75)
|
label.setGeometry(150, 250, 185, 75)
|
||||||
|
|
@ -202,6 +240,8 @@ class FastRegistrationPage(Page):
|
||||||
rotated_icon = icon.transformed(transform)
|
rotated_icon = icon.transformed(transform)
|
||||||
prev_button.setIcon(QIcon(rotated_icon))
|
prev_button.setIcon(QIcon(rotated_icon))
|
||||||
prev_button.setIconSize(prev_button.size())
|
prev_button.setIconSize(prev_button.size())
|
||||||
|
if self.selected_values['protocol'] in self.SINGBOX_PROTOCOLS:
|
||||||
|
prev_button.setDisabled(True)
|
||||||
self.buttons.append(prev_button)
|
self.buttons.append(prev_button)
|
||||||
|
|
||||||
next_button = QPushButton(self)
|
next_button = QPushButton(self)
|
||||||
|
|
@ -211,6 +251,8 @@ class FastRegistrationPage(Page):
|
||||||
next_button.setIcon(
|
next_button.setIcon(
|
||||||
QIcon(os.path.join(self.btn_path, "UP_button.png")))
|
QIcon(os.path.join(self.btn_path, "UP_button.png")))
|
||||||
next_button.setIconSize(next_button.size())
|
next_button.setIconSize(next_button.size())
|
||||||
|
if self.selected_values['protocol'] in self.SINGBOX_PROTOCOLS:
|
||||||
|
next_button.setDisabled(True)
|
||||||
self.buttons.append(next_button)
|
self.buttons.append(next_button)
|
||||||
|
|
||||||
def create_location_section(self):
|
def create_location_section(self):
|
||||||
|
|
@ -277,7 +319,7 @@ class FastRegistrationPage(Page):
|
||||||
|
|
||||||
locations = self.connection_manager.get_location_info(
|
locations = self.connection_manager.get_location_info(
|
||||||
self.selected_values['location'])
|
self.selected_values['location'])
|
||||||
if self.selected_values['protocol'] == 'hidetor' and locations and not (hasattr(locations, 'is_proxy_capable') and locations.is_proxy_capable):
|
if locations and not self._location_supports_selected_protocol(locations):
|
||||||
label.hide()
|
label.hide()
|
||||||
else:
|
else:
|
||||||
label.show()
|
label.show()
|
||||||
|
|
@ -504,46 +546,34 @@ class FastRegistrationPage(Page):
|
||||||
|
|
||||||
def update_ui_state_for_connection(self):
|
def update_ui_state_for_connection(self):
|
||||||
is_system_wide = self.selected_values['connection'] == 'system-wide'
|
is_system_wide = self.selected_values['connection'] == 'system-wide'
|
||||||
|
is_singbox = self.selected_values['protocol'] in self.SINGBOX_PROTOCOLS
|
||||||
|
|
||||||
for button in self.buttons:
|
for button in self.buttons:
|
||||||
if hasattr(button, 'geometry'):
|
if hasattr(button, 'geometry'):
|
||||||
button_geometry = button.geometry()
|
button_geometry = button.geometry()
|
||||||
|
if is_singbox and button_geometry.y() == 250 and button_geometry.x() in [115, 340]:
|
||||||
|
button.setEnabled(False)
|
||||||
if button_geometry.y() == 350:
|
if button_geometry.y() == 350:
|
||||||
if button_geometry.x() in [115, 340, 400, 625]:
|
if button_geometry.x() in [115, 340, 400, 625]:
|
||||||
button.setEnabled(not is_system_wide)
|
button.setEnabled(not is_system_wide)
|
||||||
|
|
||||||
def show_previous_value(self, key):
|
def show_previous_value(self, key):
|
||||||
if key == 'protocol':
|
if key == 'protocol':
|
||||||
protocols = ['wireguard', 'hidetor']
|
protocols = list(self.PROTOCOLS)
|
||||||
current_index = protocols.index(self.selected_values[key])
|
current_index = protocols.index(self.selected_values[key])
|
||||||
previous_index = (current_index - 1) % len(protocols)
|
previous_index = (current_index - 1) % len(protocols)
|
||||||
self.selected_values[key] = protocols[previous_index]
|
self.selected_values[key] = protocols[previous_index]
|
||||||
if self.selected_values[key] == 'wireguard':
|
self._apply_protocol_defaults()
|
||||||
self.selected_values['connection'] = 'browser-only'
|
|
||||||
else:
|
|
||||||
self.selected_values['connection'] = 'tor'
|
|
||||||
loc_info = self.connection_manager.get_location_info(
|
|
||||||
self.selected_values['location'])
|
|
||||||
if not (loc_info and hasattr(loc_info, 'is_proxy_capable') and loc_info.is_proxy_capable):
|
|
||||||
locations = self.connection_manager.get_location_list()
|
|
||||||
proxy_locations = [loc for loc in locations if (l := self.connection_manager.get_location_info(
|
|
||||||
loc)) and hasattr(l, 'is_proxy_capable') and l.is_proxy_capable]
|
|
||||||
if proxy_locations:
|
|
||||||
self.selected_values['location'] = proxy_locations[0]
|
|
||||||
elif key == 'connection':
|
elif key == 'connection':
|
||||||
if self.selected_values['protocol'] == 'wireguard':
|
connections = self._connections_for_protocol()
|
||||||
connections = ['browser-only', 'system-wide']
|
if self.selected_values[key] not in connections:
|
||||||
else:
|
self.selected_values[key] = connections[0]
|
||||||
connections = ['tor', 'just proxy']
|
|
||||||
current_index = connections.index(self.selected_values[key])
|
current_index = connections.index(self.selected_values[key])
|
||||||
previous_index = (current_index - 1) % len(connections)
|
previous_index = (current_index - 1) % len(connections)
|
||||||
self.selected_values[key] = connections[previous_index]
|
self.selected_values[key] = connections[previous_index]
|
||||||
self.update_ui_state_for_connection()
|
self.update_ui_state_for_connection()
|
||||||
elif key == 'location':
|
elif key == 'location':
|
||||||
locations = self.connection_manager.get_location_list()
|
locations = self._available_locations_for_protocol()
|
||||||
if self.selected_values['protocol'] == 'hidetor':
|
|
||||||
locations = [loc for loc in locations if (l := self.connection_manager.get_location_info(
|
|
||||||
loc)) and hasattr(l, 'is_proxy_capable') and l.is_proxy_capable]
|
|
||||||
|
|
||||||
if locations and self.selected_values[key] in locations:
|
if locations and self.selected_values[key] in locations:
|
||||||
current_index = locations.index(self.selected_values[key])
|
current_index = locations.index(self.selected_values[key])
|
||||||
|
|
@ -592,36 +622,21 @@ class FastRegistrationPage(Page):
|
||||||
|
|
||||||
def show_next_value(self, key):
|
def show_next_value(self, key):
|
||||||
if key == 'protocol':
|
if key == 'protocol':
|
||||||
protocols = ['wireguard', 'hidetor']
|
protocols = list(self.PROTOCOLS)
|
||||||
current_index = protocols.index(self.selected_values[key])
|
current_index = protocols.index(self.selected_values[key])
|
||||||
next_index = (current_index + 1) % len(protocols)
|
next_index = (current_index + 1) % len(protocols)
|
||||||
self.selected_values[key] = protocols[next_index]
|
self.selected_values[key] = protocols[next_index]
|
||||||
if self.selected_values[key] == 'wireguard':
|
self._apply_protocol_defaults()
|
||||||
self.selected_values['connection'] = 'browser-only'
|
|
||||||
else:
|
|
||||||
self.selected_values['connection'] = 'tor'
|
|
||||||
loc_info = self.connection_manager.get_location_info(
|
|
||||||
self.selected_values['location'])
|
|
||||||
if not (loc_info and hasattr(loc_info, 'is_proxy_capable') and loc_info.is_proxy_capable):
|
|
||||||
locations = self.connection_manager.get_location_list()
|
|
||||||
proxy_locations = [loc for loc in locations if (l := self.connection_manager.get_location_info(
|
|
||||||
loc)) and hasattr(l, 'is_proxy_capable') and l.is_proxy_capable]
|
|
||||||
if proxy_locations:
|
|
||||||
self.selected_values['location'] = proxy_locations[0]
|
|
||||||
elif key == 'connection':
|
elif key == 'connection':
|
||||||
if self.selected_values['protocol'] == 'wireguard':
|
connections = self._connections_for_protocol()
|
||||||
connections = ['browser-only', 'system-wide']
|
if self.selected_values[key] not in connections:
|
||||||
else:
|
self.selected_values[key] = connections[0]
|
||||||
connections = ['tor', 'just proxy']
|
|
||||||
current_index = connections.index(self.selected_values[key])
|
current_index = connections.index(self.selected_values[key])
|
||||||
next_index = (current_index + 1) % len(connections)
|
next_index = (current_index + 1) % len(connections)
|
||||||
self.selected_values[key] = connections[next_index]
|
self.selected_values[key] = connections[next_index]
|
||||||
self.update_ui_state_for_connection()
|
self.update_ui_state_for_connection()
|
||||||
elif key == 'location':
|
elif key == 'location':
|
||||||
locations = self.connection_manager.get_location_list()
|
locations = self._available_locations_for_protocol()
|
||||||
if self.selected_values['protocol'] == 'hidetor':
|
|
||||||
locations = [loc for loc in locations if (l := self.connection_manager.get_location_info(
|
|
||||||
loc)) and hasattr(l, 'is_proxy_capable') and l.is_proxy_capable]
|
|
||||||
|
|
||||||
if locations and self.selected_values[key] in locations:
|
if locations and self.selected_values[key] in locations:
|
||||||
current_index = locations.index(self.selected_values[key])
|
current_index = locations.index(self.selected_values[key])
|
||||||
|
|
@ -668,6 +683,57 @@ class FastRegistrationPage(Page):
|
||||||
|
|
||||||
self.create_interface_elements()
|
self.create_interface_elements()
|
||||||
|
|
||||||
|
def _connections_for_protocol(self):
|
||||||
|
protocol = self.selected_values['protocol']
|
||||||
|
if protocol == 'wireguard':
|
||||||
|
return ['browser-only', 'system-wide']
|
||||||
|
if protocol in self.SINGBOX_PROTOCOLS:
|
||||||
|
return ['system-wide']
|
||||||
|
return ['tor', 'just proxy']
|
||||||
|
|
||||||
|
def _apply_protocol_defaults(self):
|
||||||
|
protocol = self.selected_values['protocol']
|
||||||
|
if protocol == 'wireguard':
|
||||||
|
self.selected_values['connection'] = 'browser-only'
|
||||||
|
elif protocol in self.SINGBOX_PROTOCOLS:
|
||||||
|
self.selected_values['connection'] = 'system-wide'
|
||||||
|
else:
|
||||||
|
self.selected_values['connection'] = 'tor'
|
||||||
|
self._select_valid_location_for_protocol()
|
||||||
|
|
||||||
|
def _is_enabled_capability(self, value):
|
||||||
|
return value in (True, 1, "1", "true", "True")
|
||||||
|
|
||||||
|
def _location_supports_selected_protocol(self, location_info):
|
||||||
|
protocol = self.selected_values['protocol']
|
||||||
|
if protocol == 'hidetor':
|
||||||
|
return bool(getattr(location_info, 'is_proxy_capable', False))
|
||||||
|
capability_by_protocol = {
|
||||||
|
"wireguard": "is_wireguard_capable",
|
||||||
|
"hysteria2": "is_hysteria2_capable",
|
||||||
|
"vless": "is_vless_capable",
|
||||||
|
}
|
||||||
|
capability_name = capability_by_protocol.get(protocol)
|
||||||
|
if capability_name is None:
|
||||||
|
return True
|
||||||
|
return self._is_enabled_capability(getattr(location_info, capability_name, False))
|
||||||
|
|
||||||
|
def _available_locations_for_protocol(self):
|
||||||
|
locations = self.connection_manager.get_location_list()
|
||||||
|
return [
|
||||||
|
loc for loc in locations
|
||||||
|
if (info := self.connection_manager.get_location_info(loc))
|
||||||
|
and self._location_supports_selected_protocol(info)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _select_valid_location_for_protocol(self):
|
||||||
|
locations = self._available_locations_for_protocol()
|
||||||
|
if not locations:
|
||||||
|
self.selected_values['location'] = ''
|
||||||
|
return
|
||||||
|
if self.selected_values['location'] not in locations:
|
||||||
|
self.selected_values['location'] = locations[0]
|
||||||
|
|
||||||
def go_back(self):
|
def go_back(self):
|
||||||
menu_page = self.custom_window.navigator.get_cached("menu")
|
menu_page = self.custom_window.navigator.get_cached("menu")
|
||||||
if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'):
|
if menu_page is not None and hasattr(menu_page, 'refresh_menu_buttons'):
|
||||||
|
|
@ -709,6 +775,9 @@ class FastRegistrationPage(Page):
|
||||||
|
|
||||||
if self.selected_values['protocol'] == 'wireguard':
|
if self.selected_values['protocol'] == 'wireguard':
|
||||||
self.create_wireguard_profile(profile_data)
|
self.create_wireguard_profile(profile_data)
|
||||||
|
elif self.selected_values['protocol'] in self.SINGBOX_PROTOCOLS:
|
||||||
|
self.create_singbox_profile(profile_data)
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
self.create_tor_profile(profile_data)
|
self.create_tor_profile(profile_data)
|
||||||
|
|
||||||
|
|
@ -728,7 +797,7 @@ class FastRegistrationPage(Page):
|
||||||
self.update_status.update_status('Invalid location selected')
|
self.update_status.update_status('Invalid location selected')
|
||||||
return
|
return
|
||||||
|
|
||||||
profiles = ProfileController.get_all()
|
profiles = self._prefetched_profiles if self._prefetched_profiles is not None else ProfileController.get_all()
|
||||||
profile_id = self.get_next_available_profile_id(profiles)
|
profile_id = self.get_next_available_profile_id(profiles)
|
||||||
profile_data_for_resume = {
|
profile_data_for_resume = {
|
||||||
'id': profile_id,
|
'id': profile_id,
|
||||||
|
|
@ -753,6 +822,58 @@ class FastRegistrationPage(Page):
|
||||||
profile_id,
|
profile_id,
|
||||||
profiles.keys())
|
profiles.keys())
|
||||||
|
|
||||||
|
def create_singbox_profile(self, profile_data):
|
||||||
|
location_info = self.connection_manager.get_location_info(
|
||||||
|
profile_data['location'])
|
||||||
|
if not location_info:
|
||||||
|
self.update_status.update_status('Invalid location selected')
|
||||||
|
return
|
||||||
|
|
||||||
|
profiles = self._prefetched_profiles if self._prefetched_profiles is not None else ProfileController.get_all()
|
||||||
|
profile_id = self.get_next_available_profile_id(profiles)
|
||||||
|
existing_profile_ids = tuple(profiles.keys())
|
||||||
|
|
||||||
|
if not singbox_prereqs_installed():
|
||||||
|
self.show_singbox_prereq_setup(profile_data, profile_id, existing_profile_ids)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.finish_singbox_profile_creation(profile_data, profile_id, existing_profile_ids)
|
||||||
|
|
||||||
|
def show_singbox_prereq_setup(self, profile_data, profile_id, existing_profile_ids):
|
||||||
|
setup_page = self.custom_window.navigator.navigate("networking_setup")
|
||||||
|
if setup_page is None:
|
||||||
|
self.update_status.update_status("Singbox prerequisite page is unavailable.")
|
||||||
|
return
|
||||||
|
setup_page.configure_for_singbox_profile(
|
||||||
|
lambda: self.finish_singbox_profile_creation(profile_data, profile_id, existing_profile_ids))
|
||||||
|
|
||||||
|
def finish_singbox_profile_creation(self, profile_data, profile_id, existing_profile_ids):
|
||||||
|
location_info = self.connection_manager.get_location_info(
|
||||||
|
profile_data['location'])
|
||||||
|
if not location_info:
|
||||||
|
self.update_status.update_status('Invalid location selected')
|
||||||
|
return
|
||||||
|
|
||||||
|
profile_data_for_resume = {
|
||||||
|
'id': profile_id,
|
||||||
|
'name': profile_data['name'],
|
||||||
|
'country_code': location_info.country_code,
|
||||||
|
'code': location_info.code,
|
||||||
|
'application': '',
|
||||||
|
'connection_type': profile_data['protocol'],
|
||||||
|
'resolution': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
self._spawn_create_profile_worker(
|
||||||
|
'CREATE_SYSTEM_PROFILE', profile_data_for_resume, 'system')
|
||||||
|
|
||||||
|
if ProfileController.get(profile_id) is not None:
|
||||||
|
append_profile_to_visual_order(
|
||||||
|
getattr(self.update_status, 'gui_config_file', None),
|
||||||
|
profile_id,
|
||||||
|
existing_profile_ids)
|
||||||
|
self.go_back()
|
||||||
|
|
||||||
def create_tor_profile(self, profile_data):
|
def create_tor_profile(self, profile_data):
|
||||||
location_info = self.connection_manager.get_location_info(
|
location_info = self.connection_manager.get_location_info(
|
||||||
profile_data['location'])
|
profile_data['location'])
|
||||||
|
|
@ -762,7 +883,7 @@ class FastRegistrationPage(Page):
|
||||||
|
|
||||||
connection_type = 'tor' if profile_data['connection'] == 'tor' else 'system'
|
connection_type = 'tor' if profile_data['connection'] == 'tor' else 'system'
|
||||||
|
|
||||||
profiles = ProfileController.get_all()
|
profiles = self._prefetched_profiles if self._prefetched_profiles is not None else ProfileController.get_all()
|
||||||
profile_id = self.get_next_available_profile_id(profiles)
|
profile_id = self.get_next_available_profile_id(profiles)
|
||||||
profile_data_for_resume = {
|
profile_data_for_resume = {
|
||||||
'id': profile_id,
|
'id': profile_id,
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ class HidetorPage(Page):
|
||||||
self.selected_location_icon = None
|
self.selected_location_icon = None
|
||||||
self.connection_manager = main_window.connection_manager
|
self.connection_manager = main_window.connection_manager
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
self.button_next.clicked.connect(self.go_selected)
|
self.replace_click_handler(self.button_next, self.go_selected)
|
||||||
self.button_reverse.setVisible(True)
|
self.button_reverse.setVisible(True)
|
||||||
self.button_reverse.clicked.connect(self.reverse)
|
self.replace_click_handler(self.button_reverse, self.reverse)
|
||||||
self.display.setGeometry(QtCore.QRect(5, 10, 390, 520))
|
self.display.setGeometry(QtCore.QRect(5, 10, 390, 520))
|
||||||
self.title.setGeometry(395, 40, 380, 40)
|
self.title.setGeometry(395, 40, 380, 40)
|
||||||
self.title.setText("Pick a location")
|
self.title.setText("Pick a location")
|
||||||
|
|
@ -91,7 +91,12 @@ class HidetorPage(Page):
|
||||||
self.update_swarp_json()
|
self.update_swarp_json()
|
||||||
|
|
||||||
def reverse(self):
|
def reverse(self):
|
||||||
self.custom_window.navigator.navigate("protocol")
|
profile_data = self.update_status.read_data()
|
||||||
|
self.limpiar()
|
||||||
|
if profile_data.get("protocol") == "residential":
|
||||||
|
self.custom_window.navigator.navigate("residential")
|
||||||
|
else:
|
||||||
|
self.custom_window.navigator.navigate("protocol")
|
||||||
|
|
||||||
def go_selected(self):
|
def go_selected(self):
|
||||||
self.custom_window.navigator.navigate("browser")
|
self.custom_window.navigator.navigate("browser")
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,17 @@ import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QButtonGroup, QFrame, QLabel, QPushButton, QTextEdit,
|
QButtonGroup, QFrame, QLabel, QMessageBox, QPushButton, QTextEdit,
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import QIcon, QPixmap
|
from PyQt6.QtGui import QIcon, QPixmap
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6 import QtCore
|
from PyQt6 import QtCore
|
||||||
|
|
||||||
|
from core.services.payment_phase.ticket_config_tools import do_we_have_billing_id
|
||||||
|
from core.services.prepare_tickets.ticket_tracker import delete_ticket_data
|
||||||
|
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
|
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
|
||||||
|
|
||||||
|
|
||||||
HOW_MANY_PROFILES_DEFAULT = 6
|
HOW_MANY_PROFILES_DEFAULT = 6
|
||||||
|
|
@ -150,11 +154,53 @@ class IdPage(Page):
|
||||||
self.custom_window.navigator.navigate("duration_selection")
|
self.custom_window.navigator.navigate("duration_selection")
|
||||||
|
|
||||||
def go_multiple_profiles(self):
|
def go_multiple_profiles(self):
|
||||||
|
billing_id = do_we_have_billing_id()
|
||||||
|
if billing_id:
|
||||||
|
self._prompt_existing_tickets(billing_id)
|
||||||
|
else:
|
||||||
|
self._continue_multiple_flow()
|
||||||
|
|
||||||
|
def _continue_multiple_flow(self):
|
||||||
self.custom_window.navigator.navigate("plan_picker")
|
self.custom_window.navigator.navigate("plan_picker")
|
||||||
plan_page = self.custom_window.navigator.get_cached("plan_picker")
|
plan_page = self.custom_window.navigator.get_cached("plan_picker")
|
||||||
if plan_page is not None:
|
if plan_page is not None:
|
||||||
plan_page.start_sync()
|
plan_page.start_sync()
|
||||||
|
|
||||||
|
def _prompt_existing_tickets(self, billing_id):
|
||||||
|
box = QMessageBox(self)
|
||||||
|
box.setWindowTitle("Existing ticket purchase found")
|
||||||
|
box.setText(
|
||||||
|
"You already have a pending ticket purchase. Continue to check if it's paid, "
|
||||||
|
"or wipe it and start over?")
|
||||||
|
style_message_box(box)
|
||||||
|
check_button = box.addButton(
|
||||||
|
"Check payment", QMessageBox.ButtonRole.AcceptRole)
|
||||||
|
mark_confirm_button(check_button)
|
||||||
|
wipe_button = box.addButton(
|
||||||
|
"Wipe and start over", QMessageBox.ButtonRole.DestructiveRole)
|
||||||
|
box.exec()
|
||||||
|
clicked = box.clickedButton()
|
||||||
|
if clicked == check_button:
|
||||||
|
self._resume_existing_ticket(billing_id)
|
||||||
|
elif clicked == wipe_button:
|
||||||
|
deleted = delete_ticket_data()
|
||||||
|
if deleted:
|
||||||
|
self._continue_multiple_flow()
|
||||||
|
else:
|
||||||
|
failed_delete = QMessageBox(self)
|
||||||
|
failed_delete.setWindowTitle("Delete Failed")
|
||||||
|
failed_delete.setText(
|
||||||
|
"Delete ticket folders failed. Please speak with customer support, and check your file permissions/storage")
|
||||||
|
style_message_box(failed_delete)
|
||||||
|
failed_delete.exec()
|
||||||
|
|
||||||
|
def _resume_existing_ticket(self, billing_id):
|
||||||
|
self.update_status.update_status("Checking existing ticket payment...")
|
||||||
|
self.custom_window.navigator.navigate("payment_details")
|
||||||
|
payment_page = self.custom_window.navigator.get_cached("payment_details")
|
||||||
|
if payment_page is not None:
|
||||||
|
payment_page.resume_ticket_billing(billing_id)
|
||||||
|
|
||||||
def toggle_button_state(self):
|
def toggle_button_state(self):
|
||||||
text = self.text_edit.toPlainText()
|
text = self.text_edit.toPlainText()
|
||||||
if text.strip():
|
if text.strip():
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ class LocationPage(Page):
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
self.button_reverse.setVisible(True)
|
self.button_reverse.setVisible(True)
|
||||||
self.connection_manager = main_window.connection_manager
|
self.connection_manager = main_window.connection_manager
|
||||||
self.button_reverse.clicked.connect(self.reverse)
|
self.replace_click_handler(self.button_reverse, self.reverse)
|
||||||
|
self.replace_click_handler(self.button_next, self.go_selected)
|
||||||
self.display.setGeometry(QtCore.QRect(5, 10, 390, 520))
|
self.display.setGeometry(QtCore.QRect(5, 10, 390, 520))
|
||||||
self.title.setGeometry(395, 40, 380, 40)
|
self.title.setGeometry(395, 40, 380, 40)
|
||||||
self.title.setText("Pick a location")
|
self.title.setText("Pick a location")
|
||||||
|
|
@ -66,8 +67,10 @@ class LocationPage(Page):
|
||||||
button.setChecked(False)
|
button.setChecked(False)
|
||||||
if hasattr(self, 'verification_button'):
|
if hasattr(self, 'verification_button'):
|
||||||
self.verification_button.setEnabled(False)
|
self.verification_button.setEnabled(False)
|
||||||
|
self.refresh_protocol_location_visibility()
|
||||||
|
|
||||||
def create_interface_elements(self, available_locations):
|
def create_interface_elements(self, available_locations):
|
||||||
|
|
||||||
self.buttonGroup = QButtonGroup(self)
|
self.buttonGroup = QButtonGroup(self)
|
||||||
self.buttons = []
|
self.buttons = []
|
||||||
|
|
||||||
|
|
@ -78,14 +81,21 @@ class LocationPage(Page):
|
||||||
boton.setCheckable(True)
|
boton.setCheckable(True)
|
||||||
|
|
||||||
locations = self.connection_manager.get_location_info(icon_name)
|
locations = self.connection_manager.get_location_info(icon_name)
|
||||||
if locations and not (hasattr(locations, 'is_wireguard_capable') and locations.is_wireguard_capable):
|
if locations and not self._location_supports_selected_protocol(locations):
|
||||||
boton.setVisible(False)
|
boton.setVisible(False)
|
||||||
icon_path = os.path.join(self.btn_path, f"button_{icon_name}.png")
|
icon_path = os.path.join(self.btn_path, f"button_{icon_name}.png")
|
||||||
boton.setIcon(QIcon(icon_path))
|
boton.setIcon(QIcon(icon_path))
|
||||||
fallback_path = os.path.join(
|
fallback_path = os.path.join(
|
||||||
self.btn_path, "default_location_button.png")
|
self.btn_path, "default_location_button.png")
|
||||||
provider = locations.operator.name if locations and hasattr(
|
|
||||||
locations, 'operator') else None
|
if locations:
|
||||||
|
try:
|
||||||
|
provider = locations.operator.name if locations and hasattr(
|
||||||
|
locations, 'operator') else None
|
||||||
|
except:
|
||||||
|
print("Assign none")
|
||||||
|
provider = None
|
||||||
|
|
||||||
if boton.icon().isNull():
|
if boton.icon().isNull():
|
||||||
if locations and hasattr(locations, 'country_name'):
|
if locations and hasattr(locations, 'country_name'):
|
||||||
location_name = locations.country_name
|
location_name = locations.country_name
|
||||||
|
|
@ -113,6 +123,30 @@ class LocationPage(Page):
|
||||||
boton.clicked.connect(
|
boton.clicked.connect(
|
||||||
lambda checked, loc=icon_name: self.show_location(loc))
|
lambda checked, loc=icon_name: self.show_location(loc))
|
||||||
|
|
||||||
|
def refresh_protocol_location_visibility(self):
|
||||||
|
for button in self.buttons:
|
||||||
|
location_key = getattr(button, 'location_icon_name', None)
|
||||||
|
locations = self.connection_manager.get_location_info(location_key)
|
||||||
|
button.setVisible(not locations or self._location_supports_selected_protocol(locations))
|
||||||
|
|
||||||
|
def _current_protocol(self):
|
||||||
|
profile_data = self.update_status.read_data()
|
||||||
|
return profile_data.get("protocol", "wireguard")
|
||||||
|
|
||||||
|
def _is_enabled_capability(self, value):
|
||||||
|
return value in (True, 1, "1", "true", "True")
|
||||||
|
|
||||||
|
def _location_supports_selected_protocol(self, locations):
|
||||||
|
capability_by_protocol = {
|
||||||
|
"wireguard": "is_wireguard_capable",
|
||||||
|
"hysteria2": "is_hysteria2_capable",
|
||||||
|
"vless": "is_vless_capable",
|
||||||
|
}
|
||||||
|
capability_name = capability_by_protocol.get(self._current_protocol())
|
||||||
|
if capability_name is None:
|
||||||
|
return True
|
||||||
|
return self._is_enabled_capability(getattr(locations, capability_name, False))
|
||||||
|
|
||||||
def update_swarp_json(self, get_connection=False):
|
def update_swarp_json(self, get_connection=False):
|
||||||
profile_data = self.update_status.read_data()
|
profile_data = self.update_status.read_data()
|
||||||
self.connection_type = profile_data.get("connection", "")
|
self.connection_type = profile_data.get("connection", "")
|
||||||
|
|
@ -133,7 +167,6 @@ class LocationPage(Page):
|
||||||
target_size, Qt.AspectRatioMode.KeepAspectRatio))
|
target_size, Qt.AspectRatioMode.KeepAspectRatio))
|
||||||
self.selected_location_icon = location
|
self.selected_location_icon = location
|
||||||
self.button_next.setVisible(True)
|
self.button_next.setVisible(True)
|
||||||
self.button_next.clicked.connect(self.go_selected)
|
|
||||||
self.update_swarp_json()
|
self.update_swarp_json()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -146,10 +179,19 @@ class LocationPage(Page):
|
||||||
self.verification_button.setEnabled(True)
|
self.verification_button.setEnabled(True)
|
||||||
|
|
||||||
def reverse(self):
|
def reverse(self):
|
||||||
self.custom_window.navigator.navigate("protocol")
|
profile_data = self.update_status.read_data()
|
||||||
|
self.limpiar()
|
||||||
|
if hasattr(self, 'initial_display'):
|
||||||
|
self.initial_display.show()
|
||||||
|
if hasattr(self, 'verification_button'):
|
||||||
|
self.verification_button.setEnabled(False)
|
||||||
|
if profile_data.get("protocol") == "wireguard":
|
||||||
|
self.custom_window.navigator.navigate("wireguard")
|
||||||
|
else:
|
||||||
|
self.custom_window.navigator.navigate("protocol")
|
||||||
|
|
||||||
def go_selected(self):
|
def go_selected(self):
|
||||||
if self.connection_type == "system-wide":
|
if self.update_swarp_json(get_connection=True) == "system-wide":
|
||||||
self.custom_window.navigator.navigate("resume")
|
self.custom_window.navigator.navigate("resume")
|
||||||
else:
|
else:
|
||||||
self.custom_window.navigator.navigate("browser")
|
self.custom_window.navigator.navigate("browser")
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from PyQt6.QtWidgets import (
|
||||||
|
|
||||||
from core.controllers.ConfigurationController import ConfigurationController
|
from core.controllers.ConfigurationController import ConfigurationController
|
||||||
from core.controllers.ProfileController import ProfileController
|
from core.controllers.ProfileController import ProfileController
|
||||||
|
from core.errors.logger import logger
|
||||||
from core.controllers.tickets.UseTicketController import (
|
from core.controllers.tickets.UseTicketController import (
|
||||||
do_we_use_a_random_ticket,
|
do_we_use_a_random_ticket,
|
||||||
get_unused_tickets,
|
get_unused_tickets,
|
||||||
|
|
@ -20,12 +21,17 @@ from core.models.session.SessionProfile import SessionProfile
|
||||||
from core.models.system.SystemProfile import SystemProfile
|
from core.models.system.SystemProfile import SystemProfile
|
||||||
|
|
||||||
from gui.v2.infrastructure.setup_observers import ticket_observer
|
from gui.v2.infrastructure.setup_observers import ticket_observer
|
||||||
|
from gui.v2.actions.database_health import GuiStorageDatabaseError
|
||||||
|
from gui.v2.actions.operation_result_dispatch import has_result_action
|
||||||
|
from gui.v2.actions.operation_results import result_from_payload
|
||||||
from gui.v2.actions.profile_order import normalize_profile_order
|
from gui.v2.actions.profile_order import normalize_profile_order
|
||||||
|
from gui.v2.actions.profile_status import is_profile_enabled_for_gui
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
|
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
|
||||||
from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS
|
from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS
|
||||||
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
|
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
|
||||||
from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup
|
from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup
|
||||||
|
from gui.v2.ui.popups.operation_result_popup import OperationResultPopup
|
||||||
from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup
|
from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup
|
||||||
from gui.v2.workers.worker import Worker
|
from gui.v2.workers.worker import Worker
|
||||||
from gui.v2.workers.worker_thread import WorkerThread
|
from gui.v2.workers.worker_thread import WorkerThread
|
||||||
|
|
@ -205,6 +211,88 @@ class MenuPage(Page):
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _safe_profiles(self):
|
||||||
|
try:
|
||||||
|
return ProfileController.get_all()
|
||||||
|
except GuiStorageDatabaseError as error:
|
||||||
|
logger.error(f"[MENU] Storage database error while loading profiles: {error}")
|
||||||
|
self.update_status.update_status("Local storage database could not be read. Restart and recover storage.db.")
|
||||||
|
except Exception as error:
|
||||||
|
logger.error(f"[MENU] Error while loading profiles: {error}")
|
||||||
|
self.update_status.update_status("Could not load profiles. Sync or restart and try again.")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _safe_profile(self, profile_id):
|
||||||
|
try:
|
||||||
|
return ProfileController.get(int(profile_id))
|
||||||
|
except GuiStorageDatabaseError as error:
|
||||||
|
logger.error(f"[MENU] Storage database error while loading profile {profile_id}: {error}")
|
||||||
|
self.update_status.update_status("Local storage database could not be read. Restart and recover storage.db.")
|
||||||
|
except Exception as error:
|
||||||
|
logger.error(f"[MENU] Error while loading profile {profile_id}: {error}")
|
||||||
|
self.update_status.update_status("Could not load profile data. Sync or restart and try again.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _connection_code(self, profile):
|
||||||
|
connection = getattr(profile, 'connection', None)
|
||||||
|
return getattr(connection, 'code', None) or 'unknown'
|
||||||
|
|
||||||
|
def _location_key(self, profile):
|
||||||
|
location = getattr(profile, 'location', None)
|
||||||
|
if isinstance(location, dict):
|
||||||
|
country_code = location.get("country_code") or "na"
|
||||||
|
code = location.get("code") or "na"
|
||||||
|
return f'{country_code}_{code}'
|
||||||
|
|
||||||
|
country_code = getattr(location, 'country_code', None)
|
||||||
|
code = getattr(location, 'code', None)
|
||||||
|
if country_code and code:
|
||||||
|
return f'{country_code}_{code}'
|
||||||
|
|
||||||
|
return 'na_na'
|
||||||
|
|
||||||
|
def _browser_info(self, profile):
|
||||||
|
if not isinstance(profile, SessionProfile):
|
||||||
|
return "unknown browser", "", False
|
||||||
|
|
||||||
|
application_version = getattr(profile, 'application_version', None)
|
||||||
|
if isinstance(application_version, dict):
|
||||||
|
browser = application_version.get('application_code') or "unknown browser"
|
||||||
|
version = application_version.get('version_number') or ""
|
||||||
|
return browser, version, False
|
||||||
|
|
||||||
|
browser = getattr(application_version, 'application_code', None) or "unknown browser"
|
||||||
|
version = getattr(application_version, 'version_number', None) or ""
|
||||||
|
supported = bool(getattr(application_version, 'supported', False))
|
||||||
|
return browser, version, supported
|
||||||
|
|
||||||
|
def _profile_incomplete_reason(self, profile):
|
||||||
|
if profile is None:
|
||||||
|
return "Profile data could not be loaded. Sync or restart and try again."
|
||||||
|
|
||||||
|
connection = getattr(profile, 'connection', None)
|
||||||
|
if not getattr(connection, 'code', None):
|
||||||
|
return "Profile connection data is incomplete. Sync the database and try again."
|
||||||
|
|
||||||
|
location = getattr(profile, 'location', None)
|
||||||
|
if location is None or isinstance(location, dict):
|
||||||
|
return "Profile location data is incomplete. Sync the database and try again."
|
||||||
|
|
||||||
|
if isinstance(profile, SessionProfile):
|
||||||
|
application_version = getattr(profile, 'application_version', None)
|
||||||
|
if isinstance(application_version, dict) or application_version is None:
|
||||||
|
return "Profile browser data is incomplete. Sync the database and try again."
|
||||||
|
if not getattr(application_version, 'application_code', None) or not getattr(application_version, 'version_number', None):
|
||||||
|
return "Profile browser data is incomplete. Sync the database and try again."
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _location_operator(self, profile):
|
||||||
|
location = getattr(profile, 'location', None)
|
||||||
|
if isinstance(location, dict) or location is None:
|
||||||
|
return None
|
||||||
|
return getattr(location, 'operator', None)
|
||||||
|
|
||||||
def match_core_profiles(self, profiles_dict):
|
def match_core_profiles(self, profiles_dict):
|
||||||
new_dict = {}
|
new_dict = {}
|
||||||
|
|
||||||
|
|
@ -213,47 +301,43 @@ class MenuPage(Page):
|
||||||
profiles_dict.keys())
|
profiles_dict.keys())
|
||||||
|
|
||||||
for idx in profile_ids:
|
for idx in profile_ids:
|
||||||
profile = profiles_dict[idx]
|
profile = profiles_dict.get(idx)
|
||||||
|
if profile is None:
|
||||||
|
continue
|
||||||
|
|
||||||
new_profile = {}
|
new_profile = {}
|
||||||
|
protocol = self._connection_code(profile)
|
||||||
protocol = profile.connection.code
|
location = self._location_key(profile)
|
||||||
|
|
||||||
location = f'{profile.location.country_code}_{profile.location.code}'
|
|
||||||
|
|
||||||
new_profile['location'] = location
|
new_profile['location'] = location
|
||||||
|
|
||||||
if protocol == 'wireguard':
|
if protocol in ('wireguard', 'hysteria2', 'vless'):
|
||||||
new_profile['protocol'] = 'wireguard'
|
new_profile['protocol'] = protocol
|
||||||
else:
|
else:
|
||||||
new_profile['protocol'] = 'hidetor'
|
new_profile['protocol'] = 'hidetor'
|
||||||
|
|
||||||
connection_type = 'browser-only' if isinstance(
|
connection_type = 'browser-only' if isinstance(
|
||||||
profile, SessionProfile) else 'system-wide'
|
profile, SessionProfile) else 'system-wide'
|
||||||
|
|
||||||
if protocol == 'wireguard':
|
if protocol in ('wireguard', 'hysteria2', 'vless'):
|
||||||
new_profile['connection'] = connection_type
|
new_profile['connection'] = connection_type
|
||||||
elif protocol == 'tor':
|
elif protocol == 'tor':
|
||||||
new_profile['connection'] = protocol
|
new_profile['connection'] = protocol
|
||||||
else:
|
else:
|
||||||
new_profile['connection'] = 'just proxy'
|
new_profile['connection'] = 'just proxy'
|
||||||
|
|
||||||
if isinstance(profile, SessionProfile):
|
browser, browser_version, browser_supported = self._browser_info(profile)
|
||||||
browser = profile.application_version.application_code
|
new_profile['browser'] = browser
|
||||||
else:
|
new_profile['browser_version'] = browser_version
|
||||||
browser = 'unknown'
|
new_profile['browser_supported'] = browser_supported
|
||||||
if browser != 'unknown':
|
|
||||||
new_profile['browser'] = browser
|
|
||||||
new_profile['browser_version'] = profile.application_version.version_number
|
|
||||||
new_profile['browser_supported'] = profile.application_version.supported
|
|
||||||
else:
|
|
||||||
new_profile['browser'] = 'unknown browser'
|
|
||||||
new_profile['browser_supported'] = False
|
|
||||||
|
|
||||||
resolution = profile.resolution if hasattr(
|
resolution = profile.resolution if hasattr(
|
||||||
profile, 'resolution') else 'None'
|
profile, 'resolution') else 'None'
|
||||||
new_profile['dimentions'] = resolution
|
new_profile['dimentions'] = resolution
|
||||||
|
|
||||||
new_profile['name'] = profile.name
|
new_profile['name'] = getattr(profile, 'name', None) or f'Profile {idx}'
|
||||||
|
new_profile['incomplete_reason'] = self._profile_incomplete_reason(profile)
|
||||||
|
|
||||||
|
|
||||||
new_dict[f'Profile_{idx}'] = new_profile
|
new_dict[f'Profile_{idx}'] = new_profile
|
||||||
|
|
||||||
|
|
@ -266,7 +350,7 @@ class MenuPage(Page):
|
||||||
if hasattr(self, 'verification_button'):
|
if hasattr(self, 'verification_button'):
|
||||||
self.verification_button.setEnabled(False)
|
self.verification_button.setEnabled(False)
|
||||||
self.profiles_data = self.match_core_profiles(
|
self.profiles_data = self.match_core_profiles(
|
||||||
ProfileController.get_all())
|
self._safe_profiles())
|
||||||
|
|
||||||
self.number_of_profiles = len(self.profiles_data)
|
self.number_of_profiles = len(self.profiles_data)
|
||||||
self.profile_info = dict(self.profiles_data)
|
self.profile_info = dict(self.profiles_data)
|
||||||
|
|
@ -281,7 +365,7 @@ class MenuPage(Page):
|
||||||
|
|
||||||
def refresh_profiles_data(self):
|
def refresh_profiles_data(self):
|
||||||
self.profiles_data = self.match_core_profiles(
|
self.profiles_data = self.match_core_profiles(
|
||||||
ProfileController.get_all())
|
self._safe_profiles())
|
||||||
self.number_of_profiles = len(self.profiles_data)
|
self.number_of_profiles = len(self.profiles_data)
|
||||||
self.profile_info = dict(self.profiles_data)
|
self.profile_info = dict(self.profiles_data)
|
||||||
for profile_name, profile_value in self.profiles_data.items():
|
for profile_name, profile_value in self.profiles_data.items():
|
||||||
|
|
@ -289,12 +373,16 @@ class MenuPage(Page):
|
||||||
self.update_scroll_widget_size()
|
self.update_scroll_widget_size()
|
||||||
|
|
||||||
def refresh_menu_buttons(self):
|
def refresh_menu_buttons(self):
|
||||||
profiles = ProfileController.get_all()
|
profiles = self._safe_profiles()
|
||||||
self.button_states.clear()
|
self.button_states.clear()
|
||||||
self.connection_manager._connected_profiles.clear()
|
self.connection_manager._connected_profiles.clear()
|
||||||
self.IsSystem = 0
|
self.IsSystem = 0
|
||||||
for profile_id, profile in profiles.items():
|
for profile_id, profile in profiles.items():
|
||||||
if ProfileController.is_enabled(profile):
|
try:
|
||||||
|
is_enabled = is_profile_enabled_for_gui(profile)
|
||||||
|
except Exception:
|
||||||
|
is_enabled = False
|
||||||
|
if is_enabled:
|
||||||
self.connection_manager.add_connected_profile(profile_id)
|
self.connection_manager.add_connected_profile(profile_id)
|
||||||
if isinstance(profile, SessionProfile):
|
if isinstance(profile, SessionProfile):
|
||||||
if profile.connection and profile.connection.code == 'tor':
|
if profile.connection and profile.connection.code == 'tor':
|
||||||
|
|
@ -409,7 +497,7 @@ class MenuPage(Page):
|
||||||
def create_profile_name_label(self, parent_label, name):
|
def create_profile_name_label(self, parent_label, name):
|
||||||
child_label = QLabel(parent_label)
|
child_label = QLabel(parent_label)
|
||||||
child_label.setGeometry(0, 65, 175, 30)
|
child_label.setGeometry(0, 65, 175, 30)
|
||||||
child_label.setText(name)
|
child_label.setText(str(name or "Unnamed Profile"))
|
||||||
child_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
child_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
child_label.setAttribute(
|
child_label.setAttribute(
|
||||||
Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||||
|
|
@ -491,7 +579,10 @@ class MenuPage(Page):
|
||||||
child_label.show()
|
child_label.show()
|
||||||
|
|
||||||
def get_icon_path(self, label_name, value, connection_type):
|
def get_icon_path(self, label_name, value, connection_type):
|
||||||
|
protocol = value.get('protocol', '')
|
||||||
if label_name == 'protocol':
|
if label_name == 'protocol':
|
||||||
|
if protocol in ('hysteria2', 'vless'):
|
||||||
|
return os.path.join(self.btn_path, f"{protocol}_mini.png")
|
||||||
if connection_type == 'tor':
|
if connection_type == 'tor':
|
||||||
return os.path.join(self.btn_path, "toricon_mini.png")
|
return os.path.join(self.btn_path, "toricon_mini.png")
|
||||||
elif connection_type == 'just proxy':
|
elif connection_type == 'just proxy':
|
||||||
|
|
@ -500,6 +591,8 @@ class MenuPage(Page):
|
||||||
return os.path.join(self.btn_path, "wireguard_mini.png")
|
return os.path.join(self.btn_path, "wireguard_mini.png")
|
||||||
elif label_name == 'browser':
|
elif label_name == 'browser':
|
||||||
if connection_type == 'system-wide':
|
if connection_type == 'system-wide':
|
||||||
|
if protocol in ('hysteria2', 'vless'):
|
||||||
|
return os.path.join(self.btn_path, "wireguard_system_wide.png")
|
||||||
return os.path.join(self.btn_path, "wireguard_system_wide.png")
|
return os.path.join(self.btn_path, "wireguard_system_wide.png")
|
||||||
else:
|
else:
|
||||||
return os.path.join(self.btn_path, f"{value[label_name]} latest_mini.png")
|
return os.path.join(self.btn_path, f"{value[label_name]} latest_mini.png")
|
||||||
|
|
@ -522,9 +615,7 @@ class MenuPage(Page):
|
||||||
verification_layout.setContentsMargins(20, 20, 20, 20)
|
verification_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
verification_layout.setSpacing(15)
|
verification_layout.setSpacing(15)
|
||||||
|
|
||||||
operator = None
|
operator = self._location_operator(profile_obj)
|
||||||
if profile_obj.location and profile_obj.location.operator:
|
|
||||||
operator = profile_obj.location.operator
|
|
||||||
|
|
||||||
info_items = [
|
info_items = [
|
||||||
("Operator Name", "operator_name"),
|
("Operator Name", "operator_name"),
|
||||||
|
|
@ -681,14 +772,18 @@ class MenuPage(Page):
|
||||||
connection = profile.get("connection", "")
|
connection = profile.get("connection", "")
|
||||||
country_garaje = profile.get("country_garaje", "")
|
country_garaje = profile.get("country_garaje", "")
|
||||||
|
|
||||||
profile_obj = ProfileController.get(self.reverse_id)
|
profile_obj = self._safe_profile(self.reverse_id)
|
||||||
is_profile_enabled = self.connection_manager.is_profile_connected(
|
is_profile_enabled = self.connection_manager.is_profile_connected(
|
||||||
self.reverse_id)
|
self.reverse_id)
|
||||||
show_verification_widget = False
|
show_verification_widget = False
|
||||||
label_principal = None
|
label_principal = None
|
||||||
label_tor = None
|
label_tor = None
|
||||||
text_color = "white"
|
text_color = "white"
|
||||||
operator_name = profile_obj.location.operator.name if profile_obj.location and profile_obj.location.operator else ""
|
profile_location = getattr(profile_obj, 'location', None)
|
||||||
|
if isinstance(profile_location, dict):
|
||||||
|
profile_location = None
|
||||||
|
profile_operator = self._location_operator(profile_obj)
|
||||||
|
operator_name = getattr(profile_operator, 'name', '') if profile_operator else ""
|
||||||
|
|
||||||
if protocol.lower() == "wireguard" and is_profile_enabled and profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard':
|
if protocol.lower() == "wireguard" and is_profile_enabled and profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard':
|
||||||
verification_widget = self.create_verification_widget(
|
verification_widget = self.create_verification_widget(
|
||||||
|
|
@ -698,7 +793,7 @@ class MenuPage(Page):
|
||||||
label_principal = None
|
label_principal = None
|
||||||
|
|
||||||
elif operator_name != 'Simplified Privacy' and protocol.lower() == "wireguard":
|
elif operator_name != 'Simplified Privacy' and protocol.lower() == "wireguard":
|
||||||
if profile_obj.is_session_profile():
|
if isinstance(profile_obj, SessionProfile):
|
||||||
text_color = "black"
|
text_color = "black"
|
||||||
label_background = QLabel(self)
|
label_background = QLabel(self)
|
||||||
label_background.setGeometry(0, 60, 410, 354)
|
label_background.setGeometry(0, 60, 410, 354)
|
||||||
|
|
@ -718,9 +813,9 @@ class MenuPage(Page):
|
||||||
l_img.setScaledContents(True)
|
l_img.setScaledContents(True)
|
||||||
l_img.show()
|
l_img.show()
|
||||||
self.additional_labels.append(l_img)
|
self.additional_labels.append(l_img)
|
||||||
l_name = f"{profile_obj.location.country_name}, {profile_obj.location.name}" if profile_obj.location else ""
|
l_name = f"{profile_location.country_name}, {profile_location.name}" if profile_location else ""
|
||||||
o_name = profile_obj.location.operator.name if profile_obj.location and profile_obj.location.operator else ""
|
o_name = getattr(profile_operator, 'name', '') if profile_operator else ""
|
||||||
n_key = profile_obj.location.operator.nostr_public_key if profile_obj.location and profile_obj.location.operator else ""
|
n_key = getattr(profile_operator, 'nostr_public_key', '') if profile_operator else ""
|
||||||
|
|
||||||
info_txt = QTextEdit(self)
|
info_txt = QTextEdit(self)
|
||||||
info_txt.setGeometry(130, 110, 260, 250)
|
info_txt.setGeometry(130, 110, 260, 250)
|
||||||
|
|
@ -755,17 +850,21 @@ class MenuPage(Page):
|
||||||
nostr_txt.show()
|
nostr_txt.show()
|
||||||
self.additional_labels.append(nostr_txt)
|
self.additional_labels.append(nostr_txt)
|
||||||
|
|
||||||
elif protocol.lower() in ["wireguard", "open", "residential", "hidetor"]:
|
elif protocol.lower() in ["wireguard", "open", "residential", "hidetor", "hysteria2", "vless"]:
|
||||||
label_principal = QLabel(self)
|
label_principal = QLabel(self)
|
||||||
label_principal.setGeometry(0, 90, 400, 300)
|
label_principal.setGeometry(0, 90, 400, 300)
|
||||||
pixmap = QPixmap(os.path.join(
|
if protocol.lower() in ["hysteria2", "vless"]:
|
||||||
self.btn_path, f"{protocol}_{location}.png"))
|
pixmap = self.build_encrypted_proxy_detail_pixmap(
|
||||||
|
protocol.lower(), location)
|
||||||
|
else:
|
||||||
|
pixmap = QPixmap(os.path.join(
|
||||||
|
self.btn_path, f"{protocol}_{location}.png"))
|
||||||
label_principal.setPixmap(pixmap)
|
label_principal.setPixmap(pixmap)
|
||||||
label_principal.setScaledContents(True)
|
label_principal.setScaledContents(True)
|
||||||
label_principal.show()
|
label_principal.show()
|
||||||
self.additional_labels.append(label_principal)
|
self.additional_labels.append(label_principal)
|
||||||
|
|
||||||
if protocol.lower() in ["wireguard", "open", "residential", "hidetor"]:
|
if protocol.lower() in ["wireguard", "open", "residential", "hidetor", "hysteria2", "vless"]:
|
||||||
|
|
||||||
if protocol.lower() == "wireguard" and ConfigurationController.get_endpoint_verification_enabled():
|
if protocol.lower() == "wireguard" and ConfigurationController.get_endpoint_verification_enabled():
|
||||||
if is_profile_enabled and profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard':
|
if is_profile_enabled and profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard':
|
||||||
|
|
@ -868,12 +967,45 @@ class MenuPage(Page):
|
||||||
|
|
||||||
self.current_profile_location = location
|
self.current_profile_location = location
|
||||||
|
|
||||||
|
def build_encrypted_proxy_detail_pixmap(self, protocol, location):
|
||||||
|
pixmap = QPixmap(os.path.join(self.btn_path, f"{protocol}.png"))
|
||||||
|
if pixmap.isNull():
|
||||||
|
return pixmap
|
||||||
|
|
||||||
|
location_pixmap = QPixmap(os.path.join(
|
||||||
|
self.btn_path, f"icon_mini_{location}.png"))
|
||||||
|
if location_pixmap.isNull():
|
||||||
|
location_pixmap = QPixmap(os.path.join(
|
||||||
|
self.btn_path, "default_location_mini.png"))
|
||||||
|
if location_pixmap.isNull():
|
||||||
|
return pixmap
|
||||||
|
|
||||||
|
icon_size = max(42, min(72, int(min(pixmap.width(), pixmap.height()) * 0.22)))
|
||||||
|
location_pixmap = location_pixmap.scaled(
|
||||||
|
icon_size,
|
||||||
|
icon_size,
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation)
|
||||||
|
x = int(pixmap.width() * 0.58)
|
||||||
|
if protocol == 'hysteria2':
|
||||||
|
y = int(pixmap.height() * 0.35)
|
||||||
|
else:
|
||||||
|
y = int(pixmap.height() * 0.30)
|
||||||
|
x = min(max(0, x), max(0, pixmap.width() - location_pixmap.width()))
|
||||||
|
y = min(max(0, y), max(0, pixmap.height() - location_pixmap.height()))
|
||||||
|
|
||||||
|
painter = QPainter(pixmap)
|
||||||
|
painter.drawPixmap(x, y, location_pixmap)
|
||||||
|
painter.end()
|
||||||
|
return pixmap
|
||||||
|
|
||||||
def show_profile_verification(self, profile_id):
|
def show_profile_verification(self, profile_id):
|
||||||
profile = ProfileController.get(profile_id)
|
profile = self._safe_profile(profile_id)
|
||||||
if not profile or not profile.location:
|
profile_location = getattr(profile, 'location', None)
|
||||||
|
if not profile or not profile_location or isinstance(profile_location, dict):
|
||||||
return
|
return
|
||||||
|
|
||||||
location_key = f'{profile.location.country_code}_{profile.location.code}'
|
location_key = f'{profile_location.country_code}_{profile_location.code}'
|
||||||
location_info = self.connection_manager.get_location_info(location_key)
|
location_info = self.connection_manager.get_location_info(location_key)
|
||||||
if not location_info:
|
if not location_info:
|
||||||
return
|
return
|
||||||
|
|
@ -904,9 +1036,22 @@ class MenuPage(Page):
|
||||||
self.boton_just.setEnabled(False)
|
self.boton_just.setEnabled(False)
|
||||||
self.boton_just_session.setEnabled(False)
|
self.boton_just_session.setEnabled(False)
|
||||||
|
|
||||||
profile = ProfileController.get(int(self.reverse_id))
|
profile = self._safe_profile(int(self.reverse_id))
|
||||||
is_tor = self.update_status.get_current_connection() == 'tor'
|
incomplete_reason = self._profile_incomplete_reason(profile)
|
||||||
if profile.connection.code == 'tor' and not profile.application_version.installed and not is_tor:
|
if incomplete_reason:
|
||||||
|
self.update_status.update_status(incomplete_reason)
|
||||||
|
self.boton_just.setEnabled(True)
|
||||||
|
self.boton_just_session.setEnabled(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_tor = self.update_status.get_current_connection() == 'tor'
|
||||||
|
except Exception:
|
||||||
|
is_tor = False
|
||||||
|
|
||||||
|
connection = getattr(profile, 'connection', None)
|
||||||
|
application_version = getattr(profile, 'application_version', None)
|
||||||
|
if getattr(connection, 'code', None) == 'tor' and not getattr(application_version, 'installed', False) and not is_tor:
|
||||||
message = f'You are using a Tor profile, but the associated browser is not installed. If you want the browser to be downloaded with Tor, you must switch to a Tor connection. Otherwise, proceed with a clearweb connection.'
|
message = f'You are using a Tor profile, but the associated browser is not installed. If you want the browser to be downloaded with Tor, you must switch to a Tor connection. Otherwise, proceed with a clearweb connection.'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
@ -958,7 +1103,12 @@ class MenuPage(Page):
|
||||||
profile_data = {
|
profile_data = {
|
||||||
'id': int(self.reverse_id)
|
'id': int(self.reverse_id)
|
||||||
}
|
}
|
||||||
profile = ProfileController.get(int(self.reverse_id))
|
profile = self._safe_profile(int(self.reverse_id))
|
||||||
|
if profile is None:
|
||||||
|
self.update_status.update_status("Could not load profile data. Sync or restart and try again.")
|
||||||
|
self.disconnect_button.setEnabled(True)
|
||||||
|
self.disconnect_system_wide_button.setEnabled(True)
|
||||||
|
return
|
||||||
is_session_profile = isinstance(profile, SessionProfile)
|
is_session_profile = isinstance(profile, SessionProfile)
|
||||||
if not is_session_profile:
|
if not is_session_profile:
|
||||||
connected_profiles = self.connection_manager.get_connected_profiles()
|
connected_profiles = self.connection_manager.get_connected_profiles()
|
||||||
|
|
@ -971,13 +1121,18 @@ class MenuPage(Page):
|
||||||
return
|
return
|
||||||
self.worker_thread = WorkerThread(
|
self.worker_thread = WorkerThread(
|
||||||
'DISABLE_PROFILE', profile_data=profile_data)
|
'DISABLE_PROFILE', profile_data=profile_data)
|
||||||
|
self.worker_thread.text_output.connect(
|
||||||
|
lambda text: self.update_status.update_status(str(text)))
|
||||||
self.worker_thread.finished.connect(self.on_disconnect_done)
|
self.worker_thread.finished.connect(self.on_disconnect_done)
|
||||||
self.worker_thread.start()
|
self.worker_thread.start()
|
||||||
|
|
||||||
def on_disconnect_done(self):
|
def on_disconnect_done(self):
|
||||||
self.disconnect_button.setEnabled(True)
|
self.disconnect_button.setEnabled(True)
|
||||||
self.disconnect_system_wide_button.setEnabled(True)
|
self.disconnect_system_wide_button.setEnabled(True)
|
||||||
pass
|
selected_profile_id = self.reverse_id
|
||||||
|
self.refresh_menu_buttons()
|
||||||
|
if selected_profile_id is not None and self._safe_profile(selected_profile_id):
|
||||||
|
self.print_profile_details(f"Profile_{selected_profile_id}")
|
||||||
|
|
||||||
def _config_flag(self, section, key, default=False):
|
def _config_flag(self, section, key, default=False):
|
||||||
try:
|
try:
|
||||||
|
|
@ -1013,7 +1168,7 @@ class MenuPage(Page):
|
||||||
'Sync failed. Please try again later.')
|
'Sync failed. Please try again later.')
|
||||||
|
|
||||||
def change_connect_button(self):
|
def change_connect_button(self):
|
||||||
profile = ProfileController.get(int(self.reverse_id))
|
profile = self._safe_profile(int(self.reverse_id))
|
||||||
is_connected = self.connection_manager.is_profile_connected(
|
is_connected = self.connection_manager.is_profile_connected(
|
||||||
int(self.reverse_id))
|
int(self.reverse_id))
|
||||||
is_session_profile = isinstance(profile, SessionProfile)
|
is_session_profile = isinstance(profile, SessionProfile)
|
||||||
|
|
@ -1030,6 +1185,10 @@ class MenuPage(Page):
|
||||||
is_connected and not is_session_profile)
|
is_connected and not is_session_profile)
|
||||||
|
|
||||||
def update_status_message(self, profile, is_connected):
|
def update_status_message(self, profile, is_connected):
|
||||||
|
if profile is None:
|
||||||
|
self.update_status.update_status("Profile data could not be loaded. Sync or restart and try again.")
|
||||||
|
return
|
||||||
|
|
||||||
if is_connected:
|
if is_connected:
|
||||||
message = Worker.generate_profile_message(profile, is_enabled=True)
|
message = Worker.generate_profile_message(profile, is_enabled=True)
|
||||||
self.update_status.enable_marquee(message)
|
self.update_status.enable_marquee(message)
|
||||||
|
|
@ -1102,10 +1261,43 @@ class MenuPage(Page):
|
||||||
self.worker.update_signal.connect(self.update_gui_main_thread)
|
self.worker.update_signal.connect(self.update_gui_main_thread)
|
||||||
self.worker.change_page.connect(self.change_app_page)
|
self.worker.change_page.connect(self.change_app_page)
|
||||||
self.worker.ticket_data_loss.connect(self.show_ticket_data_loss_popup)
|
self.worker.ticket_data_loss.connect(self.show_ticket_data_loss_popup)
|
||||||
|
self.worker.operation_failed.connect(self.handle_operation_failure)
|
||||||
|
|
||||||
thread = threading.Thread(target=self.worker.run)
|
thread = threading.Thread(target=self.worker.run)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
def handle_operation_failure(self, payload):
|
||||||
|
result = result_from_payload(payload)
|
||||||
|
if result.valid:
|
||||||
|
if result.message:
|
||||||
|
self.update_status.update_status(result.message)
|
||||||
|
return
|
||||||
|
|
||||||
|
message = result.message or result.user_message()
|
||||||
|
self.update_status.update_status(message)
|
||||||
|
self.boton_just.setEnabled(True)
|
||||||
|
self.boton_just_session.setEnabled(True)
|
||||||
|
self.disconnect_button.setEnabled(True)
|
||||||
|
self.disconnect_system_wide_button.setEnabled(True)
|
||||||
|
|
||||||
|
is_actionable = has_result_action(result)
|
||||||
|
|
||||||
|
self.popup = OperationResultPopup(
|
||||||
|
self,
|
||||||
|
message=message,
|
||||||
|
action_button_text="Yes" if is_actionable else "OK",
|
||||||
|
cancel_button_text="No" if is_actionable else None,
|
||||||
|
action_result=is_actionable,
|
||||||
|
)
|
||||||
|
self.popup.action_selected.connect(
|
||||||
|
lambda accepted: self.handle_operation_popup_action(accepted))
|
||||||
|
self.popup.show()
|
||||||
|
|
||||||
|
def handle_operation_popup_action(self, accepted):
|
||||||
|
if self.worker is None:
|
||||||
|
return
|
||||||
|
self.worker.handle_operation_popup_choice(accepted)
|
||||||
|
|
||||||
def show_ticket_data_loss_popup(self, ticket_number, billing_code):
|
def show_ticket_data_loss_popup(self, ticket_number, billing_code):
|
||||||
self.custom_window.navigator.navigate("menu")
|
self.custom_window.navigator.navigate("menu")
|
||||||
self.update_status.update_status('Critical: invalid billing code from ticket.')
|
self.update_status.update_status('Critical: invalid billing code from ticket.')
|
||||||
|
|
@ -1181,7 +1373,7 @@ class MenuPage(Page):
|
||||||
self.popup.show()
|
self.popup.show()
|
||||||
return
|
return
|
||||||
|
|
||||||
if profile_id < 0:
|
if profile_id is not None and profile_id < 0:
|
||||||
self.DisplayInstallScreen(text)
|
self.DisplayInstallScreen(text)
|
||||||
return
|
return
|
||||||
if is_enabled:
|
if is_enabled:
|
||||||
|
|
@ -1197,9 +1389,15 @@ class MenuPage(Page):
|
||||||
def change_app_page(self, text, Is_changed):
|
def change_app_page(self, text, Is_changed):
|
||||||
self.update_status.update_status(str(text))
|
self.update_status.update_status(str(text))
|
||||||
if Is_changed:
|
if Is_changed:
|
||||||
current_connection = self.update_status.get_current_connection()
|
try:
|
||||||
profile = ProfileController.get(int(self.reverse_id))
|
current_connection = self.update_status.get_current_connection()
|
||||||
if current_connection != 'tor' and profile.connection.code == 'tor':
|
except Exception:
|
||||||
|
current_connection = None
|
||||||
|
profile = self._safe_profile(int(self.reverse_id))
|
||||||
|
incomplete_reason = self._profile_incomplete_reason(profile)
|
||||||
|
if incomplete_reason:
|
||||||
|
self.update_status.update_status(incomplete_reason)
|
||||||
|
elif current_connection != 'tor' and getattr(getattr(profile, 'connection', None), 'code', None) == 'tor':
|
||||||
message = f'You are using a Tor profile, but the profile subscription is missing or expired. If you want the billing to be done thourgh Tor, you must switch to a Tor connection. Otherwise, proceed with a clearweb connection.'
|
message = f'You are using a Tor profile, but the profile subscription is missing or expired. If you want the billing to be done thourgh Tor, you must switch to a Tor connection. Otherwise, proceed with a clearweb connection.'
|
||||||
self.popup = self._make_confirmation_popup(message, button_text='Proceed')
|
self.popup = self._make_confirmation_popup(message, button_text='Proceed')
|
||||||
self.popup.finished.connect(
|
self.popup.finished.connect(
|
||||||
|
|
@ -1244,7 +1442,7 @@ class MenuPage(Page):
|
||||||
self.boton_edit.setEnabled(False)
|
self.boton_edit.setEnabled(False)
|
||||||
|
|
||||||
if profile_id == self.reverse_id:
|
if profile_id == self.reverse_id:
|
||||||
profile_obj = ProfileController.get(profile_id)
|
profile_obj = self._safe_profile(profile_id)
|
||||||
if profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard' and ConfigurationController.get_endpoint_verification_enabled():
|
if profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard' and ConfigurationController.get_endpoint_verification_enabled():
|
||||||
self.print_profile_details(f"Profile_{profile_id}")
|
self.print_profile_details(f"Profile_{profile_id}")
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
307
gui/v2/ui/pages/networking_setup_page.py
Executable file
|
|
@ -0,0 +1,307 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QLabel, QPushButton, QWidget
|
||||||
|
from PyQt6.QtGui import QPixmap
|
||||||
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
|
|
||||||
|
from core.services.helpers.setup_sudo_scripts import auto_install_sudo_script, test_if_in_sudo_folder, is_singbox_wrapper_ready
|
||||||
|
from core.services.helpers.manage_assets import sudo_assets_folder_setup
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from gui.v2.actions.singbox_prereqs import singbox_prereqs_installed
|
||||||
|
from gui.v2.ui.pages.Page import Page
|
||||||
|
from gui.v2.workers.worker_thread import WorkerThread
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkingSetupPage(Page):
|
||||||
|
def __init__(self, page_stack, main_window=None, parent=None):
|
||||||
|
super().__init__("NetworkingSetup", page_stack, main_window, parent)
|
||||||
|
self.btn_path = main_window.btn_path
|
||||||
|
self.update_status = main_window
|
||||||
|
self.manual_scripts_ready = False
|
||||||
|
self.singbox_completion = None
|
||||||
|
self.singbox_setup_started = False
|
||||||
|
self.singbox_worker = None
|
||||||
|
self.latest_singbox_message = ""
|
||||||
|
self.setStyleSheet("font-family: Arial;")
|
||||||
|
self.button_next.clicked.disconnect(self.gestionar_next)
|
||||||
|
self.button_next.setVisible(True)
|
||||||
|
self.button_next.clicked.connect(self.go_to_install)
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
super().showEvent(event)
|
||||||
|
self.button_next.setVisible(True)
|
||||||
|
self.button_next.raise_()
|
||||||
|
if self._is_singbox_mode():
|
||||||
|
self.button_next.setVisible(False)
|
||||||
|
QTimer.singleShot(0, self.prepare_singbox_prereqs)
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
header_icon = self._icon_label("networking_shield.png", 48, self)
|
||||||
|
header_icon.setGeometry(74, 66, 48, 48)
|
||||||
|
|
||||||
|
self.title_label = QLabel("Firewall & Managed-DNS Setup", self)
|
||||||
|
self.title_label.setGeometry(138, 68, 590, 44)
|
||||||
|
self.title_label.setStyleSheet("font-family: Arial; font-size: 26px; font-weight: bold; color: cyan;")
|
||||||
|
|
||||||
|
self.subtitle_label = QLabel(
|
||||||
|
"Optional privileged networking helpers",
|
||||||
|
self)
|
||||||
|
self.subtitle_label.setGeometry(140, 108, 560, 24)
|
||||||
|
self.subtitle_label.setStyleSheet("font-family: Arial; font-size: 14px; color: #d8ffff;")
|
||||||
|
|
||||||
|
self.description_label = QLabel(
|
||||||
|
"These firewall and managed-DNS helpers are separate bash scripts. "
|
||||||
|
"They stay outside the AppImage Python runtime, and if installed they are placed "
|
||||||
|
"in sudo-protected folders owned by root.",
|
||||||
|
self)
|
||||||
|
self.description_label.setGeometry(80, 152, 640, 72)
|
||||||
|
self.description_label.setWordWrap(True)
|
||||||
|
self.description_label.setStyleSheet("font-family: Arial; font-size: 15px; color: cyan;")
|
||||||
|
|
||||||
|
manual_panel = self._option_panel(74, 248, "Option 1", "Manual review")
|
||||||
|
manual_text = QLabel(
|
||||||
|
"Copy the scripts to Downloads, review them, then run sudo bash setup.sh yourself.",
|
||||||
|
manual_panel)
|
||||||
|
manual_text.setGeometry(24, 76, 252, 48)
|
||||||
|
manual_text.setWordWrap(True)
|
||||||
|
manual_text.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;")
|
||||||
|
|
||||||
|
self.manual_button = QPushButton("Get scripts", manual_panel)
|
||||||
|
self.manual_button.setGeometry(24, 132, 252, 38)
|
||||||
|
self.manual_button.setStyleSheet(self._button_style("#007AFF", "#0056CC"))
|
||||||
|
self.manual_button.clicked.connect(self.handle_manual_setup)
|
||||||
|
|
||||||
|
auto_panel = self._option_panel(426, 248, "Option 2", "Automated installer")
|
||||||
|
auto_text = QLabel(
|
||||||
|
"Let HydraVeil run the installer, then test that the scripts reached the sudo folder.",
|
||||||
|
auto_panel)
|
||||||
|
auto_text.setGeometry(24, 76, 265, 48)
|
||||||
|
auto_text.setWordWrap(True)
|
||||||
|
auto_text.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;")
|
||||||
|
|
||||||
|
self.auto_button = QPushButton("Automated installer", auto_panel)
|
||||||
|
self.auto_button.setGeometry(24, 132, 252, 38)
|
||||||
|
self.auto_button.setStyleSheet(self._button_style("#16a085", "#117a65"))
|
||||||
|
self.auto_button.clicked.connect(self.install_sudo_scripts_automatically)
|
||||||
|
|
||||||
|
self.status_label = QLabel("Choose manual or automated setup, or press Next to skip and continue.", self)
|
||||||
|
self.status_label.setGeometry(82, 462, 636, 38)
|
||||||
|
self.status_label.setWordWrap(True)
|
||||||
|
self.status_label.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;")
|
||||||
|
|
||||||
|
|
||||||
|
def _option_panel(self, x, y, label, title):
|
||||||
|
panel = QWidget(self)
|
||||||
|
panel.setObjectName("networkingOptionPanel")
|
||||||
|
panel.setGeometry(x, y, 300, 186)
|
||||||
|
panel.setStyleSheet("""
|
||||||
|
QWidget#networkingOptionPanel {
|
||||||
|
background-color: #17323d;
|
||||||
|
border: 1px solid #2c7a8d;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
option_label = QLabel(label, panel)
|
||||||
|
option_label.setGeometry(24, 20, 120, 20)
|
||||||
|
option_label.setStyleSheet("font-family: Arial; font-size: 11px; font-weight: bold; color: #8de9ff; border: none;")
|
||||||
|
|
||||||
|
title_label = QLabel(title, panel)
|
||||||
|
title_label.setGeometry(24, 42, 240, 28)
|
||||||
|
title_label.setStyleSheet("font-family: Arial; font-size: 18px; font-weight: bold; color: cyan; border: none;")
|
||||||
|
|
||||||
|
return panel
|
||||||
|
|
||||||
|
def _icon_label(self, icon_name, size, parent):
|
||||||
|
label = QLabel(parent)
|
||||||
|
icon_path = os.path.join(self.btn_path, icon_name)
|
||||||
|
label.setPixmap(QPixmap(icon_path).scaled(
|
||||||
|
size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
||||||
|
label.setFixedSize(size, size)
|
||||||
|
label.setStyleSheet("border: none;")
|
||||||
|
return label
|
||||||
|
|
||||||
|
def _button_style(self, background, hover):
|
||||||
|
return f"""
|
||||||
|
QPushButton {{
|
||||||
|
background: {background};
|
||||||
|
color: #f4ffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: Arial;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}}
|
||||||
|
QPushButton:hover {{
|
||||||
|
background: {hover};
|
||||||
|
}}
|
||||||
|
QPushButton:disabled {{
|
||||||
|
background: #3f4954;
|
||||||
|
color: #94a3ad;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _set_status(self, message, color="#d8ffff"):
|
||||||
|
self.status_label.setText(message)
|
||||||
|
self.status_label.setStyleSheet(f"font-family: Arial; font-size: 13px; color: {color};")
|
||||||
|
self.update_status.update_status(message)
|
||||||
|
|
||||||
|
def _result_is_valid(self, result: Result):
|
||||||
|
if hasattr(result, "valid"):
|
||||||
|
return result.valid
|
||||||
|
if result is None:
|
||||||
|
return True
|
||||||
|
return bool(result)
|
||||||
|
|
||||||
|
def _result_message(self, result: Result, fallback):
|
||||||
|
return getattr(result, "message", fallback)
|
||||||
|
|
||||||
|
def _is_singbox_mode(self):
|
||||||
|
return self.singbox_completion is not None
|
||||||
|
|
||||||
|
def configure_for_singbox_profile(self, completion_callback):
|
||||||
|
self.singbox_completion = completion_callback
|
||||||
|
self.singbox_setup_started = False
|
||||||
|
self.singbox_worker = None
|
||||||
|
self.latest_singbox_message = ""
|
||||||
|
self.manual_scripts_ready = False
|
||||||
|
self.title_label.setText("Singbox Prerequisites")
|
||||||
|
self.subtitle_label.setText("Required for Hysteria2 and VLESS systemwide profiles")
|
||||||
|
self.description_label.setText(
|
||||||
|
"Hysteria2 and VLESS need the firewall, managed-DNS, Singbox wrapper, "
|
||||||
|
"and Singbox binary before the profile can be created.")
|
||||||
|
self.manual_button.setText("Get scripts")
|
||||||
|
self.manual_button.setDisabled(False)
|
||||||
|
self.auto_button.setDisabled(False)
|
||||||
|
self.button_next.setVisible(False)
|
||||||
|
self._set_status("Checking Singbox prerequisites...")
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _sudo_scripts_ready_for_singbox(self):
|
||||||
|
if not is_singbox_wrapper_ready():
|
||||||
|
return False
|
||||||
|
confirmed = test_if_in_sudo_folder()
|
||||||
|
return confirmed.valid
|
||||||
|
|
||||||
|
def prepare_singbox_prereqs(self):
|
||||||
|
if not self._is_singbox_mode():
|
||||||
|
return
|
||||||
|
if singbox_prereqs_installed():
|
||||||
|
self._set_status("Singbox prerequisites are already installed. Creating profile...", "#2ecc71")
|
||||||
|
QTimer.singleShot(200, self.finish_singbox_prereq_flow)
|
||||||
|
return
|
||||||
|
if not self._sudo_scripts_ready_for_singbox():
|
||||||
|
self.manual_button.setDisabled(False)
|
||||||
|
self.auto_button.setDisabled(False)
|
||||||
|
self.button_next.setVisible(False)
|
||||||
|
self._set_status("Firewall, managed-DNS, and the Singbox wrapper are required. Choose manual or automated setup.", "#d8ffff")
|
||||||
|
return
|
||||||
|
self.start_singbox_binary_setup()
|
||||||
|
|
||||||
|
def start_singbox_binary_setup(self):
|
||||||
|
if self.singbox_setup_started:
|
||||||
|
return
|
||||||
|
self.singbox_setup_started = True
|
||||||
|
self.latest_singbox_message = ""
|
||||||
|
self.manual_button.setDisabled(True)
|
||||||
|
self.auto_button.setDisabled(True)
|
||||||
|
self.button_next.setVisible(False)
|
||||||
|
self._set_status("Sudo wrapper scripts are ready. Setting up Singbox binary...")
|
||||||
|
self.singbox_worker = WorkerThread('SETUP_SINGBOX_BINARY')
|
||||||
|
self.singbox_worker.text_output.connect(self._set_singbox_status)
|
||||||
|
self.singbox_worker.finished.connect(self.on_singbox_setup_finished)
|
||||||
|
self.singbox_worker.start()
|
||||||
|
|
||||||
|
def _set_singbox_status(self, message):
|
||||||
|
self.latest_singbox_message = message
|
||||||
|
self._set_status(message)
|
||||||
|
|
||||||
|
def on_singbox_setup_finished(self, success):
|
||||||
|
if success:
|
||||||
|
self._set_status("Singbox setup complete. Creating profile...", "#2ecc71")
|
||||||
|
QTimer.singleShot(600, self.finish_singbox_prereq_flow)
|
||||||
|
return
|
||||||
|
self.singbox_setup_started = False
|
||||||
|
self.manual_button.setDisabled(False)
|
||||||
|
self.auto_button.setDisabled(False)
|
||||||
|
self.button_next.setVisible(True)
|
||||||
|
self._set_status(self.latest_singbox_message or "Singbox setup failed.", "#ff6b6b")
|
||||||
|
|
||||||
|
def finish_singbox_prereq_flow(self):
|
||||||
|
completion_callback = self.singbox_completion
|
||||||
|
self.singbox_completion = None
|
||||||
|
self.singbox_setup_started = False
|
||||||
|
self.latest_singbox_message = ""
|
||||||
|
self.title_label.setText("Firewall & Managed-DNS Setup")
|
||||||
|
self.subtitle_label.setText("Optional privileged networking helpers")
|
||||||
|
self.description_label.setText(
|
||||||
|
"These firewall and managed-DNS helpers are separate bash scripts. "
|
||||||
|
"They stay outside the AppImage Python runtime, and if installed they are placed "
|
||||||
|
"in sudo-protected folders owned by root.")
|
||||||
|
self.manual_button.setText("Get scripts")
|
||||||
|
self.manual_button.setDisabled(False)
|
||||||
|
self.auto_button.setDisabled(False)
|
||||||
|
if completion_callback is not None:
|
||||||
|
completion_callback()
|
||||||
|
|
||||||
|
def handle_manual_setup(self):
|
||||||
|
if self.manual_scripts_ready:
|
||||||
|
self.confirm_sudo_scripts()
|
||||||
|
return
|
||||||
|
results = sudo_assets_folder_setup()
|
||||||
|
if not self._result_is_valid(results):
|
||||||
|
self._set_status(
|
||||||
|
f"Could not copy scripts: {self._result_message(results, 'Unknown error')}", "#ff6b6b")
|
||||||
|
return
|
||||||
|
self.manual_scripts_ready = True
|
||||||
|
self.manual_button.setText("Confirm manual install")
|
||||||
|
self._set_status(
|
||||||
|
"Copied to Downloads/hydraveil_sudo_scripts. Review them, run 'sudo bash setup.sh', then click Confirm manual install.", "#d8ffff")
|
||||||
|
|
||||||
|
def confirm_sudo_scripts(self):
|
||||||
|
confirmed = test_if_in_sudo_folder()
|
||||||
|
if confirmed.valid:
|
||||||
|
if self._is_singbox_mode():
|
||||||
|
self._set_status("Confirmed it worked. Continuing to Singbox setup.", "#2ecc71")
|
||||||
|
QTimer.singleShot(600, self.start_singbox_binary_setup)
|
||||||
|
return
|
||||||
|
self._set_status("Confirmed it worked. Continuing to prerequisite installation.", "#2ecc71")
|
||||||
|
QTimer.singleShot(600, self.go_to_install)
|
||||||
|
else:
|
||||||
|
self._set_status(
|
||||||
|
f"I'm sorry it did not work, {confirmed.message}", "#ff6b6b")
|
||||||
|
|
||||||
|
def install_sudo_scripts_automatically(self):
|
||||||
|
results = auto_install_sudo_script()
|
||||||
|
if results.valid:
|
||||||
|
confirmed = test_if_in_sudo_folder()
|
||||||
|
if confirmed.valid:
|
||||||
|
if self._is_singbox_mode():
|
||||||
|
self._set_status("The setup script ran successfully. Continuing to Singbox setup.", "#2ecc71")
|
||||||
|
QTimer.singleShot(600, self.start_singbox_binary_setup)
|
||||||
|
return
|
||||||
|
self._set_status("The setup script ran successfully. Continuing to prerequisite installation.", "#2ecc71")
|
||||||
|
QTimer.singleShot(600, self.go_to_install)
|
||||||
|
else:
|
||||||
|
self._set_status(
|
||||||
|
f"I'm sorry it did not work, {confirmed.message}", "#ff6b6b")
|
||||||
|
else:
|
||||||
|
self._set_status(
|
||||||
|
f"Setup script did NOT work because {results.message}", "#ff6b6b")
|
||||||
|
|
||||||
|
def go_to_install(self):
|
||||||
|
if self._is_singbox_mode():
|
||||||
|
self.prepare_singbox_prereqs()
|
||||||
|
return
|
||||||
|
self.custom_window.navigator.navigate("install_system_package")
|
||||||
|
install_page = self.custom_window.navigator.get_cached("install_system_package")
|
||||||
|
if install_page is not None:
|
||||||
|
install_page.configure(package_name='all', distro='debian')
|
||||||
|
|
||||||
|
def back_to_welcome(self):
|
||||||
|
self.custom_window.navigator.navigate("welcome")
|
||||||
|
|
@ -343,6 +343,16 @@ class PaymentDetailsPage(Page):
|
||||||
self.ticket_poll_timer.start(3000)
|
self.ticket_poll_timer.start(3000)
|
||||||
self.update_status.update_status('Awaiting ticket payment...')
|
self.update_status.update_status('Awaiting ticket payment...')
|
||||||
|
|
||||||
|
def resume_ticket_billing(self, temp_billing_code):
|
||||||
|
self.ticketing = True
|
||||||
|
self.temp_billing_code = temp_billing_code
|
||||||
|
self.selected_currency = None
|
||||||
|
self.text_fields[0].setText(str(temp_billing_code or ''))
|
||||||
|
if self.ticket_poll_timer.isActive():
|
||||||
|
self.ticket_poll_timer.stop()
|
||||||
|
self.ticket_poll_timer.start(3000)
|
||||||
|
self.update_status.update_status('Checking existing ticket payment...')
|
||||||
|
|
||||||
def _populate_ticket_fields(self, invoice):
|
def _populate_ticket_fields(self, invoice):
|
||||||
self.text_fields[0].setText(str(getattr(invoice, 'temp_billing_code', '') or ''))
|
self.text_fields[0].setText(str(getattr(invoice, 'temp_billing_code', '') or ''))
|
||||||
self.text_fields[1].setText(self.selected_duration)
|
self.text_fields[1].setText(self.selected_duration)
|
||||||
|
|
@ -405,7 +415,7 @@ class PaymentDetailsPage(Page):
|
||||||
self.update_status.update_status('Awaiting ticket payment...')
|
self.update_status.update_status('Awaiting ticket payment...')
|
||||||
|
|
||||||
def _on_ticket_check_failed(self, msg):
|
def _on_ticket_check_failed(self, msg):
|
||||||
self.update_status.update_status(f'Payment check failed: {msg}')
|
self.update_status.update_status('Not Paid')
|
||||||
|
|
||||||
def show_qr_code(self):
|
def show_qr_code(self):
|
||||||
full_amount = self.text_fields[2].text()
|
full_amount = self.text_fields[2].text()
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ from gui.v2.ui.pages.Page import Page
|
||||||
|
|
||||||
|
|
||||||
class ProtocolPage(Page):
|
class ProtocolPage(Page):
|
||||||
|
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
|
||||||
|
PROTOCOL_BUTTON_ASSETS = {
|
||||||
|
"hysteria2": "hystria2",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, page_stack, main_window=None, parent=None):
|
def __init__(self, page_stack, main_window=None, parent=None):
|
||||||
super().__init__("Protocol", page_stack, main_window, parent)
|
super().__init__("Protocol", page_stack, main_window, parent)
|
||||||
self.main_window = main_window
|
self.main_window = main_window
|
||||||
|
|
@ -18,7 +23,8 @@ class ProtocolPage(Page):
|
||||||
self.connection_manager = main_window.connection_manager
|
self.connection_manager = main_window.connection_manager
|
||||||
self.button_back.setVisible(True)
|
self.button_back.setVisible(True)
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
self.button_go.clicked.connect(self.go_selected)
|
self.replace_click_handler(self.button_back, self.reverse)
|
||||||
|
self.replace_click_handler(self.button_go, self.go_selected)
|
||||||
self.coming_soon_label = QLabel("Coming soon", self)
|
self.coming_soon_label = QLabel("Coming soon", self)
|
||||||
self.coming_soon_label.setGeometry(210, 50, 200, 40)
|
self.coming_soon_label.setGeometry(210, 50, 200, 40)
|
||||||
self.coming_soon_label.setStyleSheet("font-size: 22px;")
|
self.coming_soon_label.setStyleSheet("font-size: 22px;")
|
||||||
|
|
@ -36,9 +42,11 @@ class ProtocolPage(Page):
|
||||||
self.buttons = []
|
self.buttons = []
|
||||||
self.selected_page_name = None
|
self.selected_page_name = None
|
||||||
for j, (object_type, icon_name, page_name, geometry) in enumerate([
|
for j, (object_type, icon_name, page_name, geometry) in enumerate([
|
||||||
(QPushButton, "wireguard", "wireguard", (585, 90, 185, 75)),
|
(QPushButton, "wireguard", "wireguard", (585, 80, 185, 75)),
|
||||||
(QPushButton, "residential", "residential", (585, 90+30+75, 185, 75)),
|
(QPushButton, "hysteria2", "location", (585, 160, 185, 75)),
|
||||||
(QPushButton, "hidetor", "hidetor", (585, 90+30+75+30+75, 185, 75))
|
(QPushButton, "vless", "location", (585, 240, 185, 75)),
|
||||||
|
(QPushButton, "residential", "residential", (585, 320, 185, 75)),
|
||||||
|
(QPushButton, "hidetor", "hidetor", (585, 400, 185, 75))
|
||||||
]):
|
]):
|
||||||
boton = object_type(self)
|
boton = object_type(self)
|
||||||
boton.setGeometry(*geometry)
|
boton.setGeometry(*geometry)
|
||||||
|
|
@ -46,28 +54,40 @@ class ProtocolPage(Page):
|
||||||
boton.setCheckable(True)
|
boton.setCheckable(True)
|
||||||
boton.setDisabled(True)
|
boton.setDisabled(True)
|
||||||
boton.setIcon(
|
boton.setIcon(
|
||||||
QIcon(os.path.join(self.btn_path, f"{icon_name}_button.png")))
|
QIcon(self._button_asset(icon_name)))
|
||||||
self.buttons.append(boton)
|
self.buttons.append(boton)
|
||||||
self.buttonGroup.addButton(boton, j)
|
self.buttonGroup.addButton(boton, j)
|
||||||
boton.clicked.connect(
|
boton.clicked.connect(
|
||||||
lambda _, name=page_name, protocol=icon_name: self.show_protocol(name, protocol))
|
lambda _, name=page_name, protocol=icon_name: self.show_protocol(name, protocol))
|
||||||
|
|
||||||
|
def _button_asset(self, protocol):
|
||||||
|
asset_name = self.PROTOCOL_BUTTON_ASSETS.get(protocol, protocol)
|
||||||
|
return os.path.join(self.btn_path, f"{asset_name}_button.png")
|
||||||
|
|
||||||
|
def _display_asset(self, protocol):
|
||||||
|
full_size_path = os.path.join(self.btn_path, f"{protocol}.png")
|
||||||
|
if os.path.exists(full_size_path):
|
||||||
|
return full_size_path
|
||||||
|
return self._button_asset(protocol)
|
||||||
|
|
||||||
def enable_protocol_buttons(self):
|
def enable_protocol_buttons(self):
|
||||||
for button in self.buttons:
|
for button in self.buttons:
|
||||||
button.setDisabled(False)
|
button.setDisabled(False)
|
||||||
|
|
||||||
def update_swarp_json(self):
|
def update_swarp_json(self):
|
||||||
self.update_status.write_data(
|
data = {"protocol": self.selected_protocol_icon}
|
||||||
{"protocol": self.selected_protocol_icon})
|
if self.selected_protocol_icon in self.SINGBOX_PROTOCOLS:
|
||||||
|
data["connection"] = "system-wide"
|
||||||
|
self.update_status.write_data(data)
|
||||||
|
|
||||||
def show_protocol(self, page_name, protocol):
|
def show_protocol(self, page_name, protocol):
|
||||||
self.update_status.clear_data()
|
self.update_status.clear_data()
|
||||||
self.display.setPixmap(QPixmap(os.path.join(self.btn_path, f"{protocol}.png")).scaled(
|
self.display.setPixmap(QPixmap(self._display_asset(protocol)).scaled(
|
||||||
self.display.size(), Qt.AspectRatioMode.KeepAspectRatio))
|
self.display.size(), Qt.AspectRatioMode.KeepAspectRatio))
|
||||||
self.selected_protocol_icon = protocol
|
self.selected_protocol_icon = protocol
|
||||||
self.selected_page_name = page_name
|
self.selected_page_name = page_name
|
||||||
|
|
||||||
if protocol in ["wireguard", "hidetor"]:
|
if protocol in ["wireguard", "hidetor", *self.SINGBOX_PROTOCOLS]:
|
||||||
self.button_go.setVisible(True)
|
self.button_go.setVisible(True)
|
||||||
self.coming_soon_label.setVisible(False)
|
self.coming_soon_label.setVisible(False)
|
||||||
else:
|
else:
|
||||||
|
|
@ -85,3 +105,10 @@ class ProtocolPage(Page):
|
||||||
|
|
||||||
def find_menu_page(self):
|
def find_menu_page(self):
|
||||||
return self.custom_window.navigator.get_cached("menu")
|
return self.custom_window.navigator.get_cached("menu")
|
||||||
|
|
||||||
|
def reverse(self):
|
||||||
|
self.display.clear()
|
||||||
|
for boton in self.buttons:
|
||||||
|
boton.setChecked(False)
|
||||||
|
self.button_go.setVisible(False)
|
||||||
|
self.custom_window.navigator.navigate("menu")
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ class ResidentialPage(Page):
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
self.connection_choice = None
|
self.connection_choice = None
|
||||||
self.button_reverse.setVisible(True)
|
self.button_reverse.setVisible(True)
|
||||||
self.button_reverse.clicked.connect(self.reverse)
|
self.replace_click_handler(self.button_reverse, self.reverse)
|
||||||
self.button_go.clicked.connect(self.go_selected)
|
self.replace_click_handler(self.button_go, self.go_selected)
|
||||||
|
|
||||||
self.display_1 = QLabel(self)
|
self.display_1 = QLabel(self)
|
||||||
self.display_1.setGeometry(QtCore.QRect(
|
self.display_1.setGeometry(QtCore.QRect(
|
||||||
|
|
@ -98,4 +98,5 @@ class ResidentialPage(Page):
|
||||||
self.update_status.write_data(inserted_data)
|
self.update_status.write_data(inserted_data)
|
||||||
|
|
||||||
def reverse(self):
|
def reverse(self):
|
||||||
|
self.button_go.setVisible(False)
|
||||||
self.custom_window.navigator.navigate("protocol")
|
self.custom_window.navigator.navigate("protocol")
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,18 @@ from PyQt6 import QtCore, QtGui
|
||||||
from core.controllers.ProfileController import ProfileController
|
from core.controllers.ProfileController import ProfileController
|
||||||
|
|
||||||
from gui.v2.actions.profile_order import append_profile_to_visual_order
|
from gui.v2.actions.profile_order import append_profile_to_visual_order
|
||||||
|
from gui.v2.actions.singbox_prereqs import singbox_prereqs_installed
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
from gui.v2.ui.pages.location_page import LocationPage
|
from gui.v2.ui.pages.location_page import LocationPage
|
||||||
from gui.v2.ui.pages.screen_page import ScreenPage
|
from gui.v2.ui.pages.screen_page import ScreenPage
|
||||||
|
|
||||||
|
|
||||||
class ResumePage(Page):
|
class ResumePage(Page):
|
||||||
|
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
|
||||||
|
PROTOCOL_BUTTON_ASSETS = {
|
||||||
|
"hysteria2": "hystria2",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, page_stack, main_window=None, parent=None):
|
def __init__(self, page_stack, main_window=None, parent=None):
|
||||||
super().__init__("Resume", page_stack, main_window, parent)
|
super().__init__("Resume", page_stack, main_window, parent)
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
|
|
@ -23,13 +29,13 @@ class ResumePage(Page):
|
||||||
self.btn_path = main_window.btn_path
|
self.btn_path = main_window.btn_path
|
||||||
self.labels_creados = []
|
self.labels_creados = []
|
||||||
self.additional_labels = []
|
self.additional_labels = []
|
||||||
self.button_go.clicked.connect(self.copy_profile)
|
self.replace_click_handler(self.button_go, self.copy_profile)
|
||||||
self.button_back.setVisible(True)
|
self.button_back.setVisible(True)
|
||||||
self.title.setGeometry(585, 40, 185, 40)
|
self.title.setGeometry(585, 40, 185, 40)
|
||||||
self.title.setText("Profile Summary")
|
self.title.setText("Profile Summary")
|
||||||
self.display.setGeometry(QtCore.QRect(5, 50, 580, 435))
|
self.display.setGeometry(QtCore.QRect(5, 50, 580, 435))
|
||||||
self.buttonGroup = QButtonGroup(self)
|
self.buttonGroup = QButtonGroup(self)
|
||||||
self.button_back.clicked.connect(self.reverse)
|
self.replace_click_handler(self.button_back, self.reverse)
|
||||||
self.create_arrow()
|
self.create_arrow()
|
||||||
self.create_interface_elements()
|
self.create_interface_elements()
|
||||||
|
|
||||||
|
|
@ -200,8 +206,11 @@ class ResumePage(Page):
|
||||||
parent_label.show()
|
parent_label.show()
|
||||||
self.labels_creados.append(parent_label)
|
self.labels_creados.append(parent_label)
|
||||||
else:
|
else:
|
||||||
icon_path = os.path.join(
|
if item == 'protocol':
|
||||||
self.btn_path, f"{text}_button.png")
|
icon_path = self._protocol_button_asset(text)
|
||||||
|
else:
|
||||||
|
icon_path = os.path.join(
|
||||||
|
self.btn_path, f"{text}_button.png")
|
||||||
geometry = (585, initial_y + i * label_height, 185, 75)
|
geometry = (585, initial_y + i * label_height, 185, 75)
|
||||||
parent_label = QLabel(self)
|
parent_label = QLabel(self)
|
||||||
parent_label.setGeometry(*geometry)
|
parent_label.setGeometry(*geometry)
|
||||||
|
|
@ -278,8 +287,8 @@ class ResumePage(Page):
|
||||||
|
|
||||||
elif connection_exists:
|
elif connection_exists:
|
||||||
if profile_1.get("connection", "") == "system-wide":
|
if profile_1.get("connection", "") == "system-wide":
|
||||||
image_path = os.path.join(
|
image_path = self._system_profile_image(
|
||||||
self.btn_path, f"wireguard_{profile_1.get('location', '')}.png")
|
profile_1.get('protocol', 'wireguard'), profile_1.get('location', ''))
|
||||||
main_label = QLabel(self)
|
main_label = QLabel(self)
|
||||||
main_label.setGeometry(10, 130, 500, 375)
|
main_label.setGeometry(10, 130, 500, 375)
|
||||||
main_label.setPixmap(QPixmap(image_path))
|
main_label.setPixmap(QPixmap(image_path))
|
||||||
|
|
@ -336,6 +345,21 @@ class ResumePage(Page):
|
||||||
if hasattr(self, 'arrow_label'):
|
if hasattr(self, 'arrow_label'):
|
||||||
self.arrow_label.raise_()
|
self.arrow_label.raise_()
|
||||||
|
|
||||||
|
def _protocol_button_asset(self, protocol):
|
||||||
|
asset_name = self.PROTOCOL_BUTTON_ASSETS.get(protocol, protocol)
|
||||||
|
return os.path.join(self.btn_path, f"{asset_name}_button.png")
|
||||||
|
|
||||||
|
def _system_profile_image(self, protocol, location):
|
||||||
|
candidates = [
|
||||||
|
os.path.join(self.btn_path, f"{protocol}_{location}.png"),
|
||||||
|
os.path.join(self.btn_path, f"icon_{location}.png"),
|
||||||
|
os.path.join(self.btn_path, "system_wide_global.png"),
|
||||||
|
]
|
||||||
|
for candidate in candidates:
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
return candidates[-1]
|
||||||
|
|
||||||
def toggle_button_visibility(self):
|
def toggle_button_visibility(self):
|
||||||
self.button_go.setVisible(bool(self.line_edit.text()))
|
self.button_go.setVisible(bool(self.line_edit.text()))
|
||||||
|
|
||||||
|
|
@ -344,10 +368,6 @@ class ResumePage(Page):
|
||||||
|
|
||||||
def copy_profile(self):
|
def copy_profile(self):
|
||||||
profile_name = self.line_edit.text()
|
profile_name = self.line_edit.text()
|
||||||
menu_page = self.find_menu_page()
|
|
||||||
if menu_page:
|
|
||||||
number_of_profiles = menu_page.number_of_profiles
|
|
||||||
|
|
||||||
profile_data = self.update_status.read_data()
|
profile_data = self.update_status.read_data()
|
||||||
|
|
||||||
required_fields = [profile_data.get("protocol"), profile_name]
|
required_fields = [profile_data.get("protocol"), profile_name]
|
||||||
|
|
@ -361,13 +381,29 @@ class ResumePage(Page):
|
||||||
profiles = ProfileController.get_all()
|
profiles = ProfileController.get_all()
|
||||||
profile_id = self.get_next_available_id(profiles)
|
profile_id = self.get_next_available_id(profiles)
|
||||||
new_profile = profile_data
|
new_profile = profile_data
|
||||||
|
existing_profile_ids = tuple(profiles.keys())
|
||||||
|
|
||||||
|
if new_profile.get('protocol') in self.SINGBOX_PROTOCOLS and not singbox_prereqs_installed():
|
||||||
|
self.show_singbox_prereq_setup(new_profile, profile_id, existing_profile_ids)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.finish_profile_creation(new_profile, profile_id, existing_profile_ids)
|
||||||
|
|
||||||
|
def show_singbox_prereq_setup(self, profile, profile_id, existing_profile_ids):
|
||||||
|
setup_page = self.custom_window.navigator.navigate("networking_setup")
|
||||||
|
if setup_page is None:
|
||||||
|
self.update_status.update_status("Singbox prerequisite page is unavailable.")
|
||||||
|
return
|
||||||
|
setup_page.configure_for_singbox_profile(
|
||||||
|
lambda: self.finish_profile_creation(profile, profile_id, existing_profile_ids))
|
||||||
|
|
||||||
|
def finish_profile_creation(self, new_profile, profile_id, existing_profile_ids):
|
||||||
self.create_core_profiles(new_profile, profile_id)
|
self.create_core_profiles(new_profile, profile_id)
|
||||||
if ProfileController.get(profile_id) is not None:
|
if ProfileController.get(profile_id) is not None:
|
||||||
append_profile_to_visual_order(
|
append_profile_to_visual_order(
|
||||||
getattr(self.update_status, 'gui_config_file', None),
|
getattr(self.update_status, 'gui_config_file', None),
|
||||||
profile_id,
|
profile_id,
|
||||||
profiles.keys())
|
existing_profile_ids)
|
||||||
|
|
||||||
main = self.update_status
|
main = self.update_status
|
||||||
if hasattr(main, 'navigate_after_profile_created'):
|
if hasattr(main, 'navigate_after_profile_created'):
|
||||||
|
|
@ -376,7 +412,6 @@ class ResumePage(Page):
|
||||||
self.custom_window.navigator.navigate("menu")
|
self.custom_window.navigator.navigate("menu")
|
||||||
|
|
||||||
self.update_status.clear_data()
|
self.update_status.clear_data()
|
||||||
|
|
||||||
self.line_edit.clear()
|
self.line_edit.clear()
|
||||||
self.display.clear()
|
self.display.clear()
|
||||||
self.button_go.setVisible(False)
|
self.button_go.setVisible(False)
|
||||||
|
|
@ -408,8 +443,8 @@ class ResumePage(Page):
|
||||||
parts = profile.get('location').split('_')
|
parts = profile.get('location').split('_')
|
||||||
country_code = parts[0]
|
country_code = parts[0]
|
||||||
location_code = parts[1]
|
location_code = parts[1]
|
||||||
if profile.get('protocol') == 'wireguard':
|
if profile.get('protocol') in ('wireguard', 'hysteria2', 'vless'):
|
||||||
connection_type = 'wireguard'
|
connection_type = profile.get('protocol')
|
||||||
elif profile.get('protocol') == 'hidetor' or profile.get('protocol') == 'residential':
|
elif profile.get('protocol') == 'hidetor' or profile.get('protocol') == 'residential':
|
||||||
if profile.get('connection') == 'tor':
|
if profile.get('connection') == 'tor':
|
||||||
connection_type = 'tor'
|
connection_type = 'tor'
|
||||||
|
|
|
||||||
|
|
@ -345,3 +345,6 @@ class ScreenPage(Page):
|
||||||
|
|
||||||
def gestionar_next(self):
|
def gestionar_next(self):
|
||||||
self.custom_window.navigator.navigate("resume")
|
self.custom_window.navigator.navigate("resume")
|
||||||
|
|
||||||
|
def gestionar_back(self):
|
||||||
|
self.custom_window.navigator.navigate("browser")
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame, QGridLayout,
|
QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame, QGridLayout,
|
||||||
|
|
@ -19,6 +17,7 @@ from core.Constants import Constants
|
||||||
from core.controllers.ConfigurationController import ConfigurationController
|
from core.controllers.ConfigurationController import ConfigurationController
|
||||||
from core.controllers.PolicyController import PolicyController
|
from core.controllers.PolicyController import PolicyController
|
||||||
from core.controllers.ProfileController import ProfileController
|
from core.controllers.ProfileController import ProfileController
|
||||||
|
from core.controllers.SystemStateController import SystemStateController
|
||||||
from core.controllers.tickets.UseTicketController import (
|
from core.controllers.tickets.UseTicketController import (
|
||||||
do_we_use_a_random_ticket,
|
do_we_use_a_random_ticket,
|
||||||
get_unused_tickets,
|
get_unused_tickets,
|
||||||
|
|
@ -33,7 +32,9 @@ from core.Errors import (
|
||||||
PolicyRevocationError,
|
PolicyRevocationError,
|
||||||
)
|
)
|
||||||
from core.errors.logger import logger as core_logger
|
from core.errors.logger import logger as core_logger
|
||||||
|
from core.services.helpers.setup_sudo_scripts import test_if_in_sudo_folder
|
||||||
|
|
||||||
|
from gui.v2.actions import settings_data
|
||||||
from gui.v2.actions.key_interpretation import interpret_key_results
|
from gui.v2.actions.key_interpretation import interpret_key_results
|
||||||
from gui.v2.actions.profile_order import normalize_profile_order
|
from gui.v2.actions.profile_order import normalize_profile_order
|
||||||
from gui.v2.infrastructure.setup_observers import ticket_observer
|
from gui.v2.infrastructure.setup_observers import ticket_observer
|
||||||
|
|
@ -43,20 +44,25 @@ from gui.v2.ui.styles.styles import (
|
||||||
SCROLLBAR_CYAN_QSS,
|
SCROLLBAR_CYAN_QSS,
|
||||||
TERMINAL_LIST_QSS,
|
TERMINAL_LIST_QSS,
|
||||||
checkbox_style,
|
checkbox_style,
|
||||||
|
combobox_style,
|
||||||
)
|
)
|
||||||
from gui.v2.ui.widgets.clickable_label import ClickableValueLabel
|
from gui.v2.ui.widgets.clickable_label import ClickableValueLabel
|
||||||
from gui.v2.ui.widgets.terminal_widget import TerminalWidget
|
from gui.v2.ui.widgets.terminal_widget import TerminalWidget
|
||||||
|
from gui.v2.workers.page_data_worker import PageDataWorker
|
||||||
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
||||||
from gui.v2.workers.worker_thread import WorkerThread
|
from gui.v2.workers.worker_thread import WorkerThread
|
||||||
|
|
||||||
|
|
||||||
class Settings(Page):
|
class Settings(Page):
|
||||||
def __init__(self, page_stack, main_window, parent=None):
|
def __init__(self, page_stack, main_window, parent=None, prepared=None):
|
||||||
super().__init__("Settings", page_stack, main_window, parent)
|
super().__init__("Settings", page_stack, main_window, parent)
|
||||||
self.font_style = f"font-family: '{self.custom_window.open_sans_family}';"
|
self.font_style = f"font-family: '{self.custom_window.open_sans_family}';"
|
||||||
|
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
self.update_logging = main_window
|
self.update_logging = main_window
|
||||||
|
self._prepared = prepared if prepared is not None else settings_data.empty_payload()
|
||||||
|
self._settings_refresh_seq = 0
|
||||||
|
self._settings_workers = set()
|
||||||
self.button_reverse.setVisible(True)
|
self.button_reverse.setVisible(True)
|
||||||
self.button_reverse.setEnabled(True)
|
self.button_reverse.setEnabled(True)
|
||||||
self.button_reverse.clicked.connect(self.reverse)
|
self.button_reverse.clicked.connect(self.reverse)
|
||||||
|
|
@ -64,6 +70,10 @@ class Settings(Page):
|
||||||
self.title.setText("Settings")
|
self.title.setText("Settings")
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def prepare_data(custom_window):
|
||||||
|
return settings_data.prepare(custom_window.gui_config_file)
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
main_container = QWidget(self)
|
main_container = QWidget(self)
|
||||||
main_container.setGeometry(0, 0, 800, 520)
|
main_container.setGeometry(0, 0, 800, 520)
|
||||||
|
|
@ -95,6 +105,7 @@ class Settings(Page):
|
||||||
("Verification", self.show_verification_page),
|
("Verification", self.show_verification_page),
|
||||||
("Legacy-Version", self.show_systemwide_page),
|
("Legacy-Version", self.show_systemwide_page),
|
||||||
("Bwrap Permission", self.show_bwrap_page),
|
("Bwrap Permission", self.show_bwrap_page),
|
||||||
|
("Connections", self.show_connection_page),
|
||||||
("Delete Profile", self.show_delete_page),
|
("Delete Profile", self.show_delete_page),
|
||||||
("Error Logs", self.show_logs_page),
|
("Error Logs", self.show_logs_page),
|
||||||
("Debug Help", self.show_debug_page)
|
("Debug Help", self.show_debug_page)
|
||||||
|
|
@ -198,11 +209,13 @@ class Settings(Page):
|
||||||
return page
|
return page
|
||||||
|
|
||||||
def create_delete_profile_buttons(self):
|
def create_delete_profile_buttons(self):
|
||||||
profiles = ProfileController.get_all()
|
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
||||||
|
|
||||||
profile_ids = normalize_profile_order(
|
profile_ids = self._prepared_value(
|
||||||
getattr(self.update_status, 'gui_config_file', None),
|
"profile_order",
|
||||||
profiles.keys())
|
lambda: normalize_profile_order(
|
||||||
|
getattr(self.update_status, 'gui_config_file', None),
|
||||||
|
profiles.keys()))
|
||||||
|
|
||||||
for index, profile_id in enumerate(profile_ids):
|
for index, profile_id in enumerate(profile_ids):
|
||||||
profile = profiles[profile_id]
|
profile = profiles[profile_id]
|
||||||
|
|
@ -291,7 +304,7 @@ class Settings(Page):
|
||||||
profile_selection_layout.addWidget(profile_label)
|
profile_selection_layout.addWidget(profile_label)
|
||||||
|
|
||||||
self.debug_profile_selector = QComboBox()
|
self.debug_profile_selector = QComboBox()
|
||||||
self.debug_profile_selector.setStyleSheet(self.get_combobox_style())
|
self.debug_profile_selector.setStyleSheet(combobox_style(self.font_style))
|
||||||
self.debug_profile_selector.currentTextChanged.connect(
|
self.debug_profile_selector.currentTextChanged.connect(
|
||||||
self.on_debug_profile_selected)
|
self.on_debug_profile_selected)
|
||||||
profile_selection_layout.addWidget(self.debug_profile_selector)
|
profile_selection_layout.addWidget(self.debug_profile_selector)
|
||||||
|
|
@ -600,7 +613,7 @@ class Settings(Page):
|
||||||
|
|
||||||
def update_debug_profile_list(self):
|
def update_debug_profile_list(self):
|
||||||
self.debug_profile_selector.clear()
|
self.debug_profile_selector.clear()
|
||||||
profiles = ProfileController.get_all()
|
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
||||||
system_profiles = {pid: profile for pid, profile in profiles.items(
|
system_profiles = {pid: profile for pid, profile in profiles.items(
|
||||||
) if isinstance(profile, SystemProfile)}
|
) if isinstance(profile, SystemProfile)}
|
||||||
|
|
||||||
|
|
@ -637,7 +650,7 @@ class Settings(Page):
|
||||||
self.cli_command.setText(
|
self.cli_command.setText(
|
||||||
f"'{app_path}' --cli profile enable -i {profile_id}")
|
f"'{app_path}' --cli profile enable -i {profile_id}")
|
||||||
self.cli_copy_button.setEnabled(True)
|
self.cli_copy_button.setEnabled(True)
|
||||||
ip_address = self.extract_endpoint_ip(profile)
|
ip_address = settings_data.extract_endpoint_ip(profile)
|
||||||
if ip_address:
|
if ip_address:
|
||||||
self.ping_instruction_label.setText(
|
self.ping_instruction_label.setText(
|
||||||
f"Step 1, Can you Ping it? Copy-paste the command below into your terminal. This VPN Node's IP address is {ip_address}")
|
f"Step 1, Can you Ping it? Copy-paste the command below into your terminal. This VPN Node's IP address is {ip_address}")
|
||||||
|
|
@ -669,25 +682,6 @@ class Settings(Page):
|
||||||
self.wg_quick_up_button.setEnabled(False)
|
self.wg_quick_up_button.setEnabled(False)
|
||||||
self.wg_quick_down_button.setEnabled(False)
|
self.wg_quick_down_button.setEnabled(False)
|
||||||
|
|
||||||
def extract_endpoint_ip(self, profile):
|
|
||||||
try:
|
|
||||||
profile_path = Constants.HV_PROFILE_CONFIG_HOME + f'/{profile.id}'
|
|
||||||
wg_conf_path = f'{profile_path}/wg.conf.bak'
|
|
||||||
if not os.path.exists(wg_conf_path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
with open(wg_conf_path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
for line in content.split('\n'):
|
|
||||||
if line.strip().startswith('Endpoint = '):
|
|
||||||
endpoint = line.strip().split(' = ')[1]
|
|
||||||
ip_address = endpoint.split(':')[0]
|
|
||||||
return ip_address
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def copy_ping_command(self):
|
def copy_ping_command(self):
|
||||||
profile_id = self.debug_profile_selector.currentData()
|
profile_id = self.debug_profile_selector.currentData()
|
||||||
if profile_id is None:
|
if profile_id is None:
|
||||||
|
|
@ -695,7 +689,7 @@ class Settings(Page):
|
||||||
|
|
||||||
profile = ProfileController.get(profile_id)
|
profile = ProfileController.get(profile_id)
|
||||||
if profile and isinstance(profile, SystemProfile):
|
if profile and isinstance(profile, SystemProfile):
|
||||||
ip_address = self.extract_endpoint_ip(profile)
|
ip_address = settings_data.extract_endpoint_ip(profile)
|
||||||
if ip_address:
|
if ip_address:
|
||||||
ping_command = f"ping {ip_address}"
|
ping_command = f"ping {ip_address}"
|
||||||
clipboard = QApplication.clipboard()
|
clipboard = QApplication.clipboard()
|
||||||
|
|
@ -710,7 +704,7 @@ class Settings(Page):
|
||||||
|
|
||||||
profile = ProfileController.get(profile_id)
|
profile = ProfileController.get(profile_id)
|
||||||
if profile and isinstance(profile, SystemProfile):
|
if profile and isinstance(profile, SystemProfile):
|
||||||
ip_address = self.extract_endpoint_ip(profile)
|
ip_address = settings_data.extract_endpoint_ip(profile)
|
||||||
if ip_address:
|
if ip_address:
|
||||||
self.test_ping_button.setEnabled(False)
|
self.test_ping_button.setEnabled(False)
|
||||||
self.ping_result_label.setText("Testing ping...")
|
self.ping_result_label.setText("Testing ping...")
|
||||||
|
|
@ -800,75 +794,9 @@ class Settings(Page):
|
||||||
self.content_layout.addWidget(self.delete_page)
|
self.content_layout.addWidget(self.delete_page)
|
||||||
self.content_layout.setCurrentWidget(self.delete_page)
|
self.content_layout.setCurrentWidget(self.delete_page)
|
||||||
|
|
||||||
def get_combobox_style(self) -> str:
|
|
||||||
return f"""
|
|
||||||
QComboBox {{
|
|
||||||
color: black;
|
|
||||||
background: #f0f0f0;
|
|
||||||
padding: 5px 30px 5px 10px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-width: 120px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
{self.font_style}
|
|
||||||
}}
|
|
||||||
QComboBox:disabled {{
|
|
||||||
color: #666;
|
|
||||||
background: #e0e0e0;
|
|
||||||
}}
|
|
||||||
QComboBox::drop-down {{
|
|
||||||
border: none;
|
|
||||||
width: 30px;
|
|
||||||
}}
|
|
||||||
QComboBox::down-arrow {{
|
|
||||||
image: url(assets/down_arrow.png);
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
}}
|
|
||||||
QComboBox QAbstractItemView {{
|
|
||||||
color: black;
|
|
||||||
background: white;
|
|
||||||
selection-background-color: #007bff;
|
|
||||||
selection-color: white;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
{self.font_style}
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_checkbox_style(self) -> str:
|
def get_checkbox_style(self) -> str:
|
||||||
return checkbox_style(self.font_style)
|
return checkbox_style(self.font_style)
|
||||||
|
|
||||||
def populate_wireguard_profiles(self) -> None:
|
|
||||||
self.wireguard_profile_selector.clear()
|
|
||||||
profiles = ProfileController.get_all()
|
|
||||||
for profile_id, profile in profiles.items():
|
|
||||||
if profile.connection.code == 'wireguard':
|
|
||||||
|
|
||||||
self.wireguard_profile_selector.addItem(
|
|
||||||
f"Profile {profile_id}: {profile.name}", profile_id)
|
|
||||||
|
|
||||||
def convert_duration(self, value: Union[str, int], to_hours: bool = True) -> Union[str, int]:
|
|
||||||
if to_hours:
|
|
||||||
number, unit = value.split(' ')
|
|
||||||
number = int(number)
|
|
||||||
if unit in ['day', 'days']:
|
|
||||||
return number * 24
|
|
||||||
elif unit in ['week', 'weeks']:
|
|
||||||
return number * 7 * 24
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported duration unit: {unit}")
|
|
||||||
else:
|
|
||||||
hours = int(value)
|
|
||||||
if hours % (7 * 24) == 0:
|
|
||||||
weeks = hours // (7 * 24)
|
|
||||||
return f"{weeks} {'week' if weeks == 1 else 'weeks'}"
|
|
||||||
elif hours % 24 == 0:
|
|
||||||
days = hours // 24
|
|
||||||
return f"{days} {'day' if days == 1 else 'days'}"
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Hours value {hours} cannot be converted to days or weeks cleanly")
|
|
||||||
|
|
||||||
def reverse(self):
|
def reverse(self):
|
||||||
self.custom_window.navigator.navigate("menu")
|
self.custom_window.navigator.navigate("menu")
|
||||||
|
|
||||||
|
|
@ -903,8 +831,8 @@ class Settings(Page):
|
||||||
profile_layout = QVBoxLayout(profile_group)
|
profile_layout = QVBoxLayout(profile_group)
|
||||||
|
|
||||||
self.profile_selector = QComboBox()
|
self.profile_selector = QComboBox()
|
||||||
self.profile_selector.setStyleSheet(self.get_combobox_style())
|
self.profile_selector.setStyleSheet(combobox_style(self.font_style))
|
||||||
profiles = ProfileController.get_all()
|
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
||||||
if profiles:
|
if profiles:
|
||||||
for profile_id, profile in profiles.items():
|
for profile_id, profile in profiles.items():
|
||||||
self.profile_selector.addItem(
|
self.profile_selector.addItem(
|
||||||
|
|
@ -1038,8 +966,8 @@ class Settings(Page):
|
||||||
|
|
||||||
self.verification_profile_selector = QComboBox()
|
self.verification_profile_selector = QComboBox()
|
||||||
self.verification_profile_selector.setStyleSheet(
|
self.verification_profile_selector.setStyleSheet(
|
||||||
self.get_combobox_style())
|
combobox_style(self.font_style))
|
||||||
profiles = ProfileController.get_all()
|
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
||||||
if profiles:
|
if profiles:
|
||||||
for profile_id, profile in profiles.items():
|
for profile_id, profile in profiles.items():
|
||||||
self.verification_profile_selector.addItem(
|
self.verification_profile_selector.addItem(
|
||||||
|
|
@ -1134,7 +1062,9 @@ class Settings(Page):
|
||||||
self.endpoint_verification_checkbox = QCheckBox(page)
|
self.endpoint_verification_checkbox = QCheckBox(page)
|
||||||
self.endpoint_verification_checkbox.setGeometry(180, 415, 30, 30)
|
self.endpoint_verification_checkbox.setGeometry(180, 415, 30, 30)
|
||||||
self.endpoint_verification_checkbox.setChecked(
|
self.endpoint_verification_checkbox.setChecked(
|
||||||
ConfigurationController.get_endpoint_verification_enabled())
|
self._prepared_value(
|
||||||
|
"endpoint_verification_enabled",
|
||||||
|
ConfigurationController.get_endpoint_verification_enabled))
|
||||||
self.endpoint_verification_checkbox.setStyleSheet(
|
self.endpoint_verification_checkbox.setStyleSheet(
|
||||||
self.get_checkbox_style())
|
self.get_checkbox_style())
|
||||||
self.endpoint_verification_checkbox.show()
|
self.endpoint_verification_checkbox.show()
|
||||||
|
|
@ -1155,72 +1085,30 @@ class Settings(Page):
|
||||||
|
|
||||||
return page
|
return page
|
||||||
|
|
||||||
def truncate_key(self, text, max_length=50):
|
|
||||||
if not text or text == "N/A" or len(text) <= max_length:
|
|
||||||
return text
|
|
||||||
start_len = max_length // 2 - 2
|
|
||||||
end_len = max_length // 2 - 2
|
|
||||||
return text[:start_len] + "....." + text[-end_len:]
|
|
||||||
|
|
||||||
def update_verification_info(self, index):
|
def update_verification_info(self, index):
|
||||||
if index < 0:
|
if index < 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
profile_id = self.verification_profile_selector.itemData(index)
|
profile_id = self.verification_profile_selector.itemData(index)
|
||||||
if profile_id is None:
|
profile = None
|
||||||
for key, widget in self.verification_info.items():
|
if profile_id is not None:
|
||||||
widget.setText("N/A")
|
profile = ProfileController.get(profile_id)
|
||||||
self.verification_full_values[key] = "N/A"
|
|
||||||
if key in self.verification_checkmarks:
|
|
||||||
self.verification_checkmarks[key].hide()
|
|
||||||
return
|
|
||||||
|
|
||||||
profile = ProfileController.get(profile_id)
|
view = settings_data.build_verification_view(profile)
|
||||||
if not profile:
|
truncated_keys = (
|
||||||
for key, widget in self.verification_info.items():
|
"nostr_public_key",
|
||||||
widget.setText("N/A")
|
"hydraveil_public_key",
|
||||||
self.verification_full_values[key] = "N/A"
|
"nostr_attestation_event_reference",
|
||||||
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"
|
|
||||||
|
|
||||||
for key, widget in self.verification_info.items():
|
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:
|
if key in self.verification_checkmarks:
|
||||||
full_value = self.verification_full_values.get(key, "")
|
|
||||||
if full_value and full_value != "N/A":
|
if full_value and full_value != "N/A":
|
||||||
self.verification_checkmarks[key].show()
|
self.verification_checkmarks[key].show()
|
||||||
else:
|
else:
|
||||||
|
|
@ -1265,16 +1153,9 @@ class Settings(Page):
|
||||||
|
|
||||||
if profile and hasattr(profile, 'subscription') and profile.subscription:
|
if profile and hasattr(profile, 'subscription') and profile.subscription:
|
||||||
try:
|
try:
|
||||||
self.subscription_info["billing_code"].setText(
|
view = settings_data.build_subscription_view(profile)
|
||||||
str(profile.subscription.billing_code))
|
self.subscription_info["billing_code"].setText(view["billing_code"])
|
||||||
|
self.subscription_info["expires_at"].setText(view["expires_at"])
|
||||||
if hasattr(profile.subscription, 'expires_at') and profile.subscription.expires_at:
|
|
||||||
expires_at = profile.subscription.expires_at.strftime(
|
|
||||||
"%Y-%m-%d %H:%M:%S UTC")
|
|
||||||
self.subscription_info["expires_at"].setText(expires_at)
|
|
||||||
else:
|
|
||||||
self.subscription_info["expires_at"].setText(
|
|
||||||
"Not available")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating subscription info: {e}")
|
print(f"Error updating subscription info: {e}")
|
||||||
else:
|
else:
|
||||||
|
|
@ -1295,6 +1176,30 @@ class Settings(Page):
|
||||||
|
|
||||||
def showEvent(self, event):
|
def showEvent(self, event):
|
||||||
super().showEvent(event)
|
super().showEvent(event)
|
||||||
|
self._settings_refresh_seq += 1
|
||||||
|
seq = self._settings_refresh_seq
|
||||||
|
worker = PageDataWorker(
|
||||||
|
"settings", settings_data.prepare, self.custom_window.gui_config_file)
|
||||||
|
worker.data_ready.connect(
|
||||||
|
lambda name, payload, s=seq: self._on_settings_data_ready(payload, s))
|
||||||
|
worker.failed.connect(
|
||||||
|
lambda name, error: self._on_settings_data_failed(error))
|
||||||
|
worker.finished.connect(
|
||||||
|
lambda w=worker: self._cleanup_settings_worker(w))
|
||||||
|
self._settings_workers.add(worker)
|
||||||
|
worker.start()
|
||||||
|
|
||||||
|
def _on_settings_data_failed(self, error):
|
||||||
|
core_logger.warning(f"Settings data refresh failed: {error}")
|
||||||
|
|
||||||
|
def _cleanup_settings_worker(self, worker):
|
||||||
|
self._settings_workers.discard(worker)
|
||||||
|
worker.deleteLater()
|
||||||
|
|
||||||
|
def _on_settings_data_ready(self, payload, seq):
|
||||||
|
if seq != self._settings_refresh_seq:
|
||||||
|
return
|
||||||
|
self._prepared = payload
|
||||||
|
|
||||||
current_index = self.content_layout.currentIndex()
|
current_index = self.content_layout.currentIndex()
|
||||||
|
|
||||||
|
|
@ -1326,6 +1231,10 @@ class Settings(Page):
|
||||||
self.bwrap_page = self.create_bwrap_page()
|
self.bwrap_page = self.create_bwrap_page()
|
||||||
self.content_layout.addWidget(self.bwrap_page)
|
self.content_layout.addWidget(self.bwrap_page)
|
||||||
|
|
||||||
|
self.content_layout.removeWidget(self.connection_page)
|
||||||
|
self.connection_page = self.create_connection_page()
|
||||||
|
self.content_layout.addWidget(self.connection_page)
|
||||||
|
|
||||||
self.content_layout.removeWidget(self.delete_page)
|
self.content_layout.removeWidget(self.delete_page)
|
||||||
self.delete_page = self.create_delete_page()
|
self.delete_page = self.create_delete_page()
|
||||||
self.content_layout.addWidget(self.delete_page)
|
self.content_layout.addWidget(self.delete_page)
|
||||||
|
|
@ -1407,7 +1316,7 @@ class Settings(Page):
|
||||||
layout = QGridLayout(widget)
|
layout = QGridLayout(widget)
|
||||||
layout.setSpacing(10)
|
layout.setSpacing(10)
|
||||||
|
|
||||||
profiles = ProfileController.get_all()
|
profiles = self._prepared_value("profiles", ProfileController.get_all)
|
||||||
total_profiles = len(profiles)
|
total_profiles = len(profiles)
|
||||||
session_profiles = sum(1 for p in profiles.values()
|
session_profiles = sum(1 for p in profiles.values()
|
||||||
if isinstance(p, SessionProfile))
|
if isinstance(p, SessionProfile))
|
||||||
|
|
@ -1439,7 +1348,8 @@ class Settings(Page):
|
||||||
|
|
||||||
config_path = Constants.HV_CONFIG_HOME
|
config_path = Constants.HV_CONFIG_HOME
|
||||||
|
|
||||||
current_connection = self.update_status.get_current_connection()
|
current_connection = self._prepared_value(
|
||||||
|
"current_connection", self.update_status.get_current_connection)
|
||||||
|
|
||||||
if current_connection is not None:
|
if current_connection is not None:
|
||||||
current_connection = current_connection.capitalize()
|
current_connection = current_connection.capitalize()
|
||||||
|
|
@ -1539,8 +1449,9 @@ class Settings(Page):
|
||||||
self.verification_page = self.create_verification_page()
|
self.verification_page = self.create_verification_page()
|
||||||
self.systemwide_page = self.create_systemwide_page()
|
self.systemwide_page = self.create_systemwide_page()
|
||||||
self.bwrap_page = self.create_bwrap_page()
|
self.bwrap_page = self.create_bwrap_page()
|
||||||
self.logs_page = self.create_logs_page()
|
self.connection_page = self.create_connection_page()
|
||||||
self.delete_page = self.create_delete_page()
|
self.delete_page = self.create_delete_page()
|
||||||
|
self.logs_page = self.create_logs_page()
|
||||||
self.debug_page = self.create_debug_page()
|
self.debug_page = self.create_debug_page()
|
||||||
|
|
||||||
self.content_layout.addWidget(self.account_page)
|
self.content_layout.addWidget(self.account_page)
|
||||||
|
|
@ -1550,8 +1461,9 @@ class Settings(Page):
|
||||||
self.content_layout.addWidget(self.verification_page)
|
self.content_layout.addWidget(self.verification_page)
|
||||||
self.content_layout.addWidget(self.systemwide_page)
|
self.content_layout.addWidget(self.systemwide_page)
|
||||||
self.content_layout.addWidget(self.bwrap_page)
|
self.content_layout.addWidget(self.bwrap_page)
|
||||||
self.content_layout.addWidget(self.logs_page)
|
self.content_layout.addWidget(self.connection_page)
|
||||||
self.content_layout.addWidget(self.delete_page)
|
self.content_layout.addWidget(self.delete_page)
|
||||||
|
self.content_layout.addWidget(self.logs_page)
|
||||||
self.content_layout.addWidget(self.debug_page)
|
self.content_layout.addWidget(self.debug_page)
|
||||||
|
|
||||||
self.content_layout.setCurrentIndex(0)
|
self.content_layout.setCurrentIndex(0)
|
||||||
|
|
@ -1608,6 +1520,15 @@ class Settings(Page):
|
||||||
self.content_layout.setCurrentWidget(self.bwrap_page)
|
self.content_layout.setCurrentWidget(self.bwrap_page)
|
||||||
self._select_menu_button("Bwrap Permission")
|
self._select_menu_button("Bwrap Permission")
|
||||||
|
|
||||||
|
def show_connection_page(self):
|
||||||
|
core_logger.info("User navigated to Settings -> Connection")
|
||||||
|
if hasattr(self, "connection_gate_status"):
|
||||||
|
self.load_firewall_settings()
|
||||||
|
self.load_managed_dns_settings()
|
||||||
|
self.refresh_connection_settings_gate()
|
||||||
|
self.content_layout.setCurrentWidget(self.connection_page)
|
||||||
|
self._select_menu_button("Connections")
|
||||||
|
|
||||||
def show_logs_page(self):
|
def show_logs_page(self):
|
||||||
core_logger.info("User navigated to Settings -> Error Logs")
|
core_logger.info("User navigated to Settings -> Error Logs")
|
||||||
self.content_layout.setCurrentWidget(self.logs_page)
|
self.content_layout.setCurrentWidget(self.logs_page)
|
||||||
|
|
@ -1697,7 +1618,8 @@ class Settings(Page):
|
||||||
inventory_layout.addWidget(self.refresh_tickets_button)
|
inventory_layout.addWidget(self.refresh_tickets_button)
|
||||||
layout.addWidget(inventory_group)
|
layout.addWidget(inventory_group)
|
||||||
|
|
||||||
saved_failure = self.update_status.get_ticket_verification_failure()
|
saved_failure = self._prepared_value(
|
||||||
|
"ticket_failure", self.update_status.get_ticket_verification_failure)
|
||||||
has_failure = saved_failure is not None
|
has_failure = saved_failure is not None
|
||||||
|
|
||||||
recovery_group = QGroupBox("Verification Failure Recovery")
|
recovery_group = QGroupBox("Verification Failure Recovery")
|
||||||
|
|
@ -1705,7 +1627,7 @@ class Settings(Page):
|
||||||
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
|
f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}")
|
||||||
recovery_layout = QVBoxLayout(recovery_group)
|
recovery_layout = QVBoxLayout(recovery_group)
|
||||||
|
|
||||||
self.ticket_recovery_status_label = QLabel(self._format_ticket_failure_status(saved_failure))
|
self.ticket_recovery_status_label = QLabel(settings_data.format_ticket_failure_status(saved_failure))
|
||||||
self.ticket_recovery_status_label.setStyleSheet(
|
self.ticket_recovery_status_label.setStyleSheet(
|
||||||
f"color: white; font-size: 12px; {self.font_style}")
|
f"color: white; font-size: 12px; {self.font_style}")
|
||||||
self.ticket_recovery_status_label.setWordWrap(True)
|
self.ticket_recovery_status_label.setWordWrap(True)
|
||||||
|
|
@ -1759,23 +1681,11 @@ class Settings(Page):
|
||||||
page_layout.addWidget(scroll_area)
|
page_layout.addWidget(scroll_area)
|
||||||
return page
|
return page
|
||||||
|
|
||||||
def _format_ticket_failure_status(self, failure):
|
|
||||||
if not failure:
|
|
||||||
return "No saved verification failure. If ticket preparation fails validation, recovery data will appear here."
|
|
||||||
failed_validations = failure.get("failed_validations", [])
|
|
||||||
how_many_failed = failure.get("how_many_failed", len(failed_validations))
|
|
||||||
updated_at = failure.get("updated_at", "unknown time")
|
|
||||||
failed_text = ", ".join(str(item) for item in failed_validations)
|
|
||||||
return (
|
|
||||||
f"Saved verification failure: {how_many_failed} failed. "
|
|
||||||
f"Failed validation indices: {failed_text}. Saved: {updated_at}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _refresh_ticket_recovery_controls(self):
|
def _refresh_ticket_recovery_controls(self):
|
||||||
failure = self.update_status.get_ticket_verification_failure()
|
failure = self.update_status.get_ticket_verification_failure()
|
||||||
has_failure = failure is not None
|
has_failure = failure is not None
|
||||||
if hasattr(self, 'ticket_recovery_status_label'):
|
if hasattr(self, 'ticket_recovery_status_label'):
|
||||||
self.ticket_recovery_status_label.setText(self._format_ticket_failure_status(failure))
|
self.ticket_recovery_status_label.setText(settings_data.format_ticket_failure_status(failure))
|
||||||
if hasattr(self, 'evaluate_public_key_button'):
|
if hasattr(self, 'evaluate_public_key_button'):
|
||||||
self.evaluate_public_key_button.setEnabled(has_failure)
|
self.evaluate_public_key_button.setEnabled(has_failure)
|
||||||
if hasattr(self, 'prepare_saved_blind_sigs_button'):
|
if hasattr(self, 'prepare_saved_blind_sigs_button'):
|
||||||
|
|
@ -1791,13 +1701,6 @@ class Settings(Page):
|
||||||
if hasattr(self, 'ticket_recovery_output'):
|
if hasattr(self, 'ticket_recovery_output'):
|
||||||
self.ticket_recovery_output.setPlainText(text)
|
self.ticket_recovery_output.setPlainText(text)
|
||||||
|
|
||||||
def _format_ticket_recovery_result(self, label, result):
|
|
||||||
try:
|
|
||||||
payload = json.dumps(result, indent=2, default=str)
|
|
||||||
except TypeError:
|
|
||||||
payload = str(result)
|
|
||||||
return f"{label}:\n{payload}"
|
|
||||||
|
|
||||||
def _get_ticket_failure_for_action(self):
|
def _get_ticket_failure_for_action(self):
|
||||||
failure = self.update_status.get_ticket_verification_failure()
|
failure = self.update_status.get_ticket_verification_failure()
|
||||||
if failure is None:
|
if failure is None:
|
||||||
|
|
@ -1859,7 +1762,7 @@ class Settings(Page):
|
||||||
self.ticket_recovery_worker.start()
|
self.ticket_recovery_worker.start()
|
||||||
|
|
||||||
def on_saved_blind_prep_done(self, result):
|
def on_saved_blind_prep_done(self, result):
|
||||||
self._write_ticket_recovery_output(self._format_ticket_recovery_result("Results of preparation", result))
|
self._write_ticket_recovery_output(settings_data.format_ticket_recovery_result("Results of preparation", result))
|
||||||
if isinstance(result, dict) and result.get('valid') is True:
|
if isinstance(result, dict) and result.get('valid') is True:
|
||||||
self.update_status.clear_ticket_verification_failure()
|
self.update_status.clear_ticket_verification_failure()
|
||||||
self.update_status.update_status("Tickets prepared from saved blind signatures.")
|
self.update_status.update_status("Tickets prepared from saved blind signatures.")
|
||||||
|
|
@ -1954,7 +1857,8 @@ class Settings(Page):
|
||||||
|
|
||||||
def load_logs_settings(self) -> None:
|
def load_logs_settings(self) -> None:
|
||||||
try:
|
try:
|
||||||
config = self.update_status._load_gui_config()
|
config = self._prepared_value(
|
||||||
|
"gui_config", self.update_status._load_gui_config)
|
||||||
if config and "logging" in config:
|
if config and "logging" in config:
|
||||||
self.enable_gui_logging.setChecked(
|
self.enable_gui_logging.setChecked(
|
||||||
config["logging"]["gui_logging_enabled"])
|
config["logging"]["gui_logging_enabled"])
|
||||||
|
|
@ -2103,7 +2007,8 @@ class Settings(Page):
|
||||||
layout.addLayout(button_layout)
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
self.load_systemwide_settings()
|
self.load_systemwide_settings(
|
||||||
|
self._prepared_value("systemwide_enabled", settings_data.read_systemwide_enabled))
|
||||||
return page
|
return page
|
||||||
|
|
||||||
def create_bwrap_page(self):
|
def create_bwrap_page(self):
|
||||||
|
|
@ -2155,17 +2060,13 @@ class Settings(Page):
|
||||||
layout.addLayout(button_layout)
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
self.load_bwrap_settings()
|
self.load_bwrap_settings(
|
||||||
|
self._prepared_value("bwrap_enabled", settings_data.read_bwrap_enabled))
|
||||||
return page
|
return page
|
||||||
|
|
||||||
def load_systemwide_settings(self):
|
def load_systemwide_settings(self, enabled=None):
|
||||||
enabled = False
|
if enabled is None:
|
||||||
try:
|
enabled = settings_data.read_systemwide_enabled()
|
||||||
privilege_policy = PolicyController.get('privilege')
|
|
||||||
if privilege_policy is not None:
|
|
||||||
enabled = PolicyController.is_instated(privilege_policy)
|
|
||||||
except Exception:
|
|
||||||
enabled = False
|
|
||||||
self.systemwide_toggle.setChecked(enabled)
|
self.systemwide_toggle.setChecked(enabled)
|
||||||
if enabled:
|
if enabled:
|
||||||
self.systemwide_status_value.setText("Enabled")
|
self.systemwide_status_value.setText("Enabled")
|
||||||
|
|
@ -2202,14 +2103,9 @@ class Settings(Page):
|
||||||
self.systemwide_status_value.setStyleSheet(
|
self.systemwide_status_value.setStyleSheet(
|
||||||
f"color: red; font-size: 14px; {self.font_style}")
|
f"color: red; font-size: 14px; {self.font_style}")
|
||||||
|
|
||||||
def load_bwrap_settings(self):
|
def load_bwrap_settings(self, enabled=None):
|
||||||
enabled = False
|
if enabled is None:
|
||||||
try:
|
enabled = settings_data.read_bwrap_enabled()
|
||||||
capability_policy = PolicyController.get('capability')
|
|
||||||
if capability_policy is not None:
|
|
||||||
enabled = PolicyController.is_instated(capability_policy)
|
|
||||||
except Exception:
|
|
||||||
enabled = False
|
|
||||||
self.bwrap_toggle.setChecked(enabled)
|
self.bwrap_toggle.setChecked(enabled)
|
||||||
if enabled:
|
if enabled:
|
||||||
self.bwrap_status_value.setText("Enabled")
|
self.bwrap_status_value.setText("Enabled")
|
||||||
|
|
@ -2220,6 +2116,35 @@ class Settings(Page):
|
||||||
self.bwrap_status_value.setStyleSheet(
|
self.bwrap_status_value.setStyleSheet(
|
||||||
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_firewall_settings(self, enabled=None):
|
||||||
|
if enabled is None:
|
||||||
|
enabled = settings_data.read_firewall_setting()
|
||||||
|
self.firewall_toggle.setChecked(enabled)
|
||||||
|
if enabled:
|
||||||
|
self.firewall_status_value.setText("Enabled")
|
||||||
|
self.firewall_status_value.setStyleSheet(
|
||||||
|
f"color: #2ecc71; font-size: 14px; {self.font_style}")
|
||||||
|
else:
|
||||||
|
self.firewall_status_value.setText("Disabled")
|
||||||
|
self.firewall_status_value.setStyleSheet(
|
||||||
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_managed_dns_settings(self, enabled=None):
|
||||||
|
if enabled is None:
|
||||||
|
enabled = settings_data.read_managed_dns_setting()
|
||||||
|
self.managed_dns_toggle.setChecked(enabled)
|
||||||
|
if enabled:
|
||||||
|
self.managed_dns_status_value.setText("Enabled")
|
||||||
|
self.managed_dns_status_value.setStyleSheet(
|
||||||
|
f"color: #2ecc71; font-size: 14px; {self.font_style}")
|
||||||
|
else:
|
||||||
|
self.managed_dns_status_value.setText("Disabled")
|
||||||
|
self.managed_dns_status_value.setStyleSheet(
|
||||||
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
||||||
|
|
||||||
|
|
||||||
def save_bwrap_settings(self):
|
def save_bwrap_settings(self):
|
||||||
enable = self.bwrap_toggle.isChecked()
|
enable = self.bwrap_toggle.isChecked()
|
||||||
try:
|
try:
|
||||||
|
|
@ -2246,9 +2171,93 @@ class Settings(Page):
|
||||||
self.bwrap_status_value.setStyleSheet(
|
self.bwrap_status_value.setStyleSheet(
|
||||||
f"color: red; font-size: 14px; {self.font_style}")
|
f"color: red; font-size: 14px; {self.font_style}")
|
||||||
|
|
||||||
|
|
||||||
|
def save_connection_settings(self):
|
||||||
|
can_modify, message, _ = self.connection_settings_gate()
|
||||||
|
if not can_modify:
|
||||||
|
self.connection_gate_status.setText(message)
|
||||||
|
self.connection_gate_status.setStyleSheet(
|
||||||
|
f"color: #ff6b6b; font-size: 12px; {self.font_style}")
|
||||||
|
self.set_connection_controls_enabled(False)
|
||||||
|
self.update_status.update_status(message)
|
||||||
|
return
|
||||||
|
|
||||||
|
firewall_enabled = self.firewall_toggle.isChecked()
|
||||||
|
managed_dns_enabled = self.managed_dns_toggle.isChecked()
|
||||||
|
try:
|
||||||
|
ConfigurationController.change_firewall(firewall_enabled)
|
||||||
|
ConfigurationController.change_dns(managed_dns_enabled)
|
||||||
|
self._prepared["firewall_setting"] = firewall_enabled
|
||||||
|
self._prepared["managed_dns_setting"] = managed_dns_enabled
|
||||||
|
self.load_firewall_settings(firewall_enabled)
|
||||||
|
self.load_managed_dns_settings(managed_dns_enabled)
|
||||||
|
self.update_status.update_status(
|
||||||
|
"Connection settings updated")
|
||||||
|
except CommandNotFoundError as e:
|
||||||
|
self.firewall_status_value.setText(str(e))
|
||||||
|
self.firewall_status_value.setStyleSheet(
|
||||||
|
f"color: red; font-size: 14px; {self.font_style}")
|
||||||
|
except Exception as e:
|
||||||
|
self.firewall_status_value.setText(f"Failed to update connection settings {e}")
|
||||||
|
self.firewall_status_value.setStyleSheet(
|
||||||
|
f"color: red; font-size: 14px; {self.font_style}")
|
||||||
|
|
||||||
|
|
||||||
|
def save_firewall_settings(self):
|
||||||
|
self.save_connection_settings()
|
||||||
|
|
||||||
|
|
||||||
|
def connection_settings_gate(self):
|
||||||
|
installed, install_message = self.networking_scripts_installed()
|
||||||
|
if not installed:
|
||||||
|
return False, f"Firewall and managed-DNS helpers are not installed. {install_message}", True
|
||||||
|
if self.has_active_systemwide_profile():
|
||||||
|
return False, "Disable the active systemwide profile before changing firewall or managed-DNS settings.", False
|
||||||
|
return True, "Firewall and managed-DNS helpers are installed. No active systemwide profile detected.", False
|
||||||
|
|
||||||
|
|
||||||
|
def networking_scripts_installed(self):
|
||||||
|
try:
|
||||||
|
result = test_if_in_sudo_folder()
|
||||||
|
if result.valid:
|
||||||
|
return True, "Installed."
|
||||||
|
return False, result.message
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def has_active_systemwide_profile(self):
|
||||||
|
try:
|
||||||
|
return SystemStateController.get() is not None
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def set_connection_controls_enabled(self, enabled):
|
||||||
|
for widget_name in ("firewall_toggle", "managed_dns_toggle", "connection_save_button"):
|
||||||
|
widget = getattr(self, widget_name, None)
|
||||||
|
if widget is not None:
|
||||||
|
widget.setEnabled(enabled)
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_connection_settings_gate(self):
|
||||||
|
can_modify, message, show_install = self.connection_settings_gate()
|
||||||
|
self.connection_gate_status.setText(message)
|
||||||
|
color = "#2ecc71" if can_modify else "#ff6b6b"
|
||||||
|
self.connection_gate_status.setStyleSheet(
|
||||||
|
f"color: {color}; font-size: 12px; {self.font_style}")
|
||||||
|
self.set_connection_controls_enabled(can_modify)
|
||||||
|
self.connection_install_button.setVisible(show_install)
|
||||||
|
|
||||||
|
|
||||||
|
def open_networking_installer(self):
|
||||||
|
self.custom_window.navigator.navigate("networking_setup")
|
||||||
|
|
||||||
|
|
||||||
def load_registrations_settings(self) -> None:
|
def load_registrations_settings(self) -> None:
|
||||||
try:
|
try:
|
||||||
config = self.update_status._load_gui_config()
|
config = self._prepared_value(
|
||||||
|
"gui_config", self.update_status._load_gui_config)
|
||||||
if config and "registrations" in config:
|
if config and "registrations" in config:
|
||||||
registrations = config["registrations"]
|
registrations = config["registrations"]
|
||||||
auto_sync = registrations.get("auto_sync_enabled", False)
|
auto_sync = registrations.get("auto_sync_enabled", False)
|
||||||
|
|
@ -2307,25 +2316,103 @@ class Settings(Page):
|
||||||
self.update_status.update_status(
|
self.update_status.update_status(
|
||||||
"Error saving registration settings")
|
"Error saving registration settings")
|
||||||
|
|
||||||
def is_auto_sync_enabled(self) -> bool:
|
|
||||||
try:
|
|
||||||
config = self.update_status._load_gui_config()
|
|
||||||
if config and "registrations" in config:
|
|
||||||
registrations = config["registrations"]
|
|
||||||
return registrations.get("auto_sync_enabled", False)
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error checking auto-sync setting: {str(e)}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_fast_registration_enabled(self) -> bool:
|
|
||||||
try:
|
def create_connection_page(self):
|
||||||
config = self.update_status._load_gui_config()
|
page = QWidget()
|
||||||
if config and "registrations" in config:
|
layout = QVBoxLayout(page)
|
||||||
registrations = config["registrations"]
|
layout.setSpacing(16)
|
||||||
return registrations.get("fast_registration_enabled", False)
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
return False
|
|
||||||
except Exception as e:
|
title = QLabel("CONNECTION PAGE")
|
||||||
logging.error(
|
title.setStyleSheet(
|
||||||
f"Error checking fast registration setting: {str(e)}")
|
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
|
||||||
return False
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
description = QLabel(
|
||||||
|
"Control firewall and managed-DNS behavior for systemwide profiles.")
|
||||||
|
description.setWordWrap(True)
|
||||||
|
description.setStyleSheet(
|
||||||
|
f"color: white; font-size: 14px; {self.font_style}")
|
||||||
|
layout.addWidget(description)
|
||||||
|
|
||||||
|
self.connection_gate_status = QLabel("")
|
||||||
|
self.connection_gate_status.setWordWrap(True)
|
||||||
|
self.connection_gate_status.setStyleSheet(
|
||||||
|
f"color: #e67e22; font-size: 12px; {self.font_style}")
|
||||||
|
layout.addWidget(self.connection_gate_status)
|
||||||
|
|
||||||
|
self.connection_install_button = QPushButton("Install networking helpers")
|
||||||
|
self.connection_install_button.setFixedSize(190, 38)
|
||||||
|
self.connection_install_button.setStyleSheet(f"""
|
||||||
|
QPushButton {{
|
||||||
|
background: #007AFF;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
{self.font_style}
|
||||||
|
}}
|
||||||
|
QPushButton:hover {{
|
||||||
|
background: #0056CC;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
self.connection_install_button.clicked.connect(self.open_networking_installer)
|
||||||
|
layout.addWidget(self.connection_install_button)
|
||||||
|
|
||||||
|
firewall_status_layout = QHBoxLayout()
|
||||||
|
firewall_status_label = QLabel("Firewall status:")
|
||||||
|
firewall_status_label.setStyleSheet(
|
||||||
|
f"color: white; font-size: 14px; {self.font_style}")
|
||||||
|
self.firewall_status_value = QLabel("")
|
||||||
|
self.firewall_status_value.setStyleSheet(
|
||||||
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
||||||
|
firewall_status_layout.addWidget(firewall_status_label)
|
||||||
|
firewall_status_layout.addWidget(self.firewall_status_value)
|
||||||
|
firewall_status_layout.addStretch()
|
||||||
|
layout.addLayout(firewall_status_layout)
|
||||||
|
|
||||||
|
dns_status_layout = QHBoxLayout()
|
||||||
|
dns_status_label = QLabel("Managed-DNS status:")
|
||||||
|
dns_status_label.setStyleSheet(
|
||||||
|
f"color: white; font-size: 14px; {self.font_style}")
|
||||||
|
self.managed_dns_status_value = QLabel("")
|
||||||
|
self.managed_dns_status_value.setStyleSheet(
|
||||||
|
f"color: #e67e22; font-size: 14px; {self.font_style}")
|
||||||
|
dns_status_layout.addWidget(dns_status_label)
|
||||||
|
dns_status_layout.addWidget(self.managed_dns_status_value)
|
||||||
|
dns_status_layout.addStretch()
|
||||||
|
layout.addLayout(dns_status_layout)
|
||||||
|
|
||||||
|
firewall_toggle_layout = QHBoxLayout()
|
||||||
|
self.firewall_toggle = QCheckBox("Enable Firewall Automatically when Systemwide Profiles are Activated")
|
||||||
|
self.firewall_toggle.setStyleSheet(self.get_checkbox_style())
|
||||||
|
firewall_toggle_layout.addWidget(self.firewall_toggle)
|
||||||
|
firewall_toggle_layout.addStretch()
|
||||||
|
layout.addLayout(firewall_toggle_layout)
|
||||||
|
|
||||||
|
dns_toggle_layout = QHBoxLayout()
|
||||||
|
self.managed_dns_toggle = QCheckBox("Enable Managed-DNS Automatically when Systemwide Profiles are Activated")
|
||||||
|
self.managed_dns_toggle.setStyleSheet(self.get_checkbox_style())
|
||||||
|
dns_toggle_layout.addWidget(self.managed_dns_toggle)
|
||||||
|
dns_toggle_layout.addStretch()
|
||||||
|
layout.addLayout(dns_toggle_layout)
|
||||||
|
|
||||||
|
self.connection_save_button = QPushButton()
|
||||||
|
self.connection_save_button.setFixedSize(75, 46)
|
||||||
|
self.connection_save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png")))
|
||||||
|
self.connection_save_button.setIconSize(QSize(75, 46))
|
||||||
|
self.connection_save_button.clicked.connect(self.save_connection_settings)
|
||||||
|
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
button_layout.addWidget(self.connection_save_button)
|
||||||
|
button_layout.addStretch()
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.load_firewall_settings(
|
||||||
|
self._prepared_value("firewall_setting", settings_data.read_firewall_setting))
|
||||||
|
self.load_managed_dns_settings(
|
||||||
|
self._prepared_value("managed_dns_setting", settings_data.read_managed_dns_setting))
|
||||||
|
self.refresh_connection_settings_gate()
|
||||||
|
return page
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,13 @@ from PyQt6.QtGui import QIcon
|
||||||
from PyQt6.QtCore import QSize
|
from PyQt6.QtCore import QSize
|
||||||
from PyQt6 import QtCore
|
from PyQt6 import QtCore
|
||||||
|
|
||||||
|
from core.services.prepare_tickets.ticket_tracker import delete_ticket_data
|
||||||
from core.controllers.tickets.TicketPayController import check_if_paid
|
from core.controllers.tickets.TicketPayController import check_if_paid
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
|
||||||
from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer
|
from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
|
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
|
||||||
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -77,7 +80,13 @@ class TicketCryptoPickerPage(Page):
|
||||||
clipboard = QApplication.clipboard()
|
clipboard = QApplication.clipboard()
|
||||||
clipboard.setText(temp_billing_code)
|
clipboard.setText(temp_billing_code)
|
||||||
not_paid_msg = f"The billing code is not yet showing paid for {temp_billing_code}. Right now, your clipboard has the billing code, to paste it in any text editor. If you did pay, either wait longer for blockchain confirmation, or contact customer support with the code in your clipboard now."
|
not_paid_msg = f"The billing code is not yet showing paid for {temp_billing_code}. Right now, your clipboard has the billing code, to paste it in any text editor. If you did pay, either wait longer for blockchain confirmation, or contact customer support with the code in your clipboard now."
|
||||||
QMessageBox.information(None, "Not Paid", not_paid_msg)
|
info = QMessageBox(self)
|
||||||
|
info.setWindowTitle("Not Paid")
|
||||||
|
info.setText(not_paid_msg)
|
||||||
|
info.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||||
|
style_message_box(info)
|
||||||
|
mark_confirm_button(info.button(QMessageBox.StandardButton.Ok))
|
||||||
|
info.exec()
|
||||||
|
|
||||||
def start_initiate_payment(self, currency):
|
def start_initiate_payment(self, currency):
|
||||||
self.update_status.update_status("Initiating payment...")
|
self.update_status.update_status("Initiating payment...")
|
||||||
|
|
@ -96,34 +105,46 @@ class TicketCryptoPickerPage(Page):
|
||||||
self.update_status.update_status("Could not initiate payment.")
|
self.update_status.update_status("Could not initiate payment.")
|
||||||
return
|
return
|
||||||
|
|
||||||
error_code = getattr(invoice, 'error_code', None)
|
if not invoice.valid:
|
||||||
if error_code == 'already_exists' and not self.bypass_existing:
|
return self._handle_api_errors(invoice)
|
||||||
|
|
||||||
|
# error_code = getattr(invoice, 'error_code', None)
|
||||||
|
self.custom_window.navigator.navigate("payment_details")
|
||||||
|
payment_page = self.custom_window.navigator.get_cached("payment_details")
|
||||||
|
if payment_page is not None:
|
||||||
|
payment_page.set_ticket_invoice(invoice.data, self.selected_plan)
|
||||||
|
|
||||||
|
def _handle_api_errors(self, invoice: Result):
|
||||||
|
error_code = invoice.error_type
|
||||||
|
if error_code == ResultError.ALREADY_EXISTS and not self.bypass_existing:
|
||||||
self._prompt_wipe_existing(invoice)
|
self._prompt_wipe_existing(invoice)
|
||||||
return
|
return
|
||||||
if error_code == 'billing_code_exists' and not self.bypass_existing:
|
elif error_code == ResultError.BILLING_CODE_EXISTS and not self.bypass_existing:
|
||||||
temp_billing_code = getattr(invoice, 'temp_billing_code', None)
|
temp_billing_code = getattr(invoice, 'temp_billing_code', None)
|
||||||
print(f"temp_billing_code is {temp_billing_code}")
|
print(f"temp_billing_code is {temp_billing_code}")
|
||||||
if temp_billing_code:
|
if temp_billing_code:
|
||||||
self._prompt_wipe_billingcode(temp_billing_code)
|
self._prompt_wipe_billingcode(temp_billing_code)
|
||||||
return
|
return
|
||||||
if error_code:
|
else:
|
||||||
msg = getattr(invoice, 'final_error_msg', None) or error_code
|
self._prompt_wipe_billingcode("NONE")
|
||||||
self.update_status.update_status(f"Payment error: {msg}")
|
return
|
||||||
|
else:
|
||||||
|
error_msg = invoice.message
|
||||||
|
# msg = getattr(invoice, 'final_error_msg', None) or error_code
|
||||||
|
self.update_status.update_status(error_msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.custom_window.navigator.navigate("payment_details")
|
|
||||||
payment_page = self.custom_window.navigator.get_cached("payment_details")
|
|
||||||
if payment_page is not None:
|
|
||||||
payment_page.set_ticket_invoice(invoice, self.selected_plan)
|
|
||||||
|
|
||||||
def _prompt_wipe_existing(self, invoice):
|
def _prompt_wipe_existing(self, invoice):
|
||||||
msg = QMessageBox(self)
|
msg = QMessageBox(self)
|
||||||
msg.setWindowTitle("Existing tickets found")
|
msg.setWindowTitle("Existing tickets found")
|
||||||
msg.setText("You already have ticket data. Wipe it and start over?")
|
msg.setText("You already have ticket data. Wipe it and start over?")
|
||||||
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
style_message_box(msg)
|
||||||
|
mark_confirm_button(msg.button(QMessageBox.StandardButton.Yes))
|
||||||
result = msg.exec()
|
result = msg.exec()
|
||||||
if result == QMessageBox.StandardButton.Yes:
|
if result == QMessageBox.StandardButton.Yes:
|
||||||
self.bypass_existing = True
|
self.bypass_existing = True
|
||||||
|
delete_ticket_data()
|
||||||
currency_btn = self.buttonGroup.checkedButton()
|
currency_btn = self.buttonGroup.checkedButton()
|
||||||
if currency_btn:
|
if currency_btn:
|
||||||
self.start_initiate_payment(currency_btn.property('currency'))
|
self.start_initiate_payment(currency_btn.property('currency'))
|
||||||
|
|
@ -135,12 +156,15 @@ class TicketCryptoPickerPage(Page):
|
||||||
msg.setWindowTitle("Existing billing code found")
|
msg.setWindowTitle("Existing billing code found")
|
||||||
msg.setText("You already have a ticket billing code. Do you want to use it? Only hit YES if you ALREADY paid.")
|
msg.setText("You already have a ticket billing code. Do you want to use it? Only hit YES if you ALREADY paid.")
|
||||||
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||||
|
style_message_box(msg)
|
||||||
|
mark_confirm_button(msg.button(QMessageBox.StandardButton.Yes))
|
||||||
result = msg.exec()
|
result = msg.exec()
|
||||||
if result == QMessageBox.StandardButton.Yes:
|
if result == QMessageBox.StandardButton.Yes:
|
||||||
self.update_status.update_status("Reusing same code")
|
self.update_status.update_status("Reusing same code")
|
||||||
self.check_if_paid_for_existing(temp_billing_code)
|
self.check_if_paid_for_existing(temp_billing_code)
|
||||||
else:
|
else:
|
||||||
self.bypass_existing = True
|
self.bypass_existing = True
|
||||||
|
delete_ticket_data()
|
||||||
currency_btn = self.buttonGroup.checkedButton()
|
currency_btn = self.buttonGroup.checkedButton()
|
||||||
if currency_btn:
|
if currency_btn:
|
||||||
self.start_initiate_payment(currency_btn.property('currency'))
|
self.start_initiate_payment(currency_btn.property('currency'))
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem, QPushButton
|
from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem, QPushButton
|
||||||
from PyQt6 import QtCore
|
from PyQt6 import QtCore
|
||||||
|
|
||||||
from core.controllers.ProfileController import ProfileController
|
|
||||||
from core.controllers.tickets.UseTicketController import get_unused_tickets
|
from core.controllers.tickets.UseTicketController import get_unused_tickets
|
||||||
|
|
||||||
from gui.v2.actions.locations import location_candidates
|
|
||||||
from gui.v2.infrastructure.setup_observers import ticket_observer
|
from gui.v2.infrastructure.setup_observers import ticket_observer
|
||||||
from gui.v2.ui.pages.Page import Page
|
from gui.v2.ui.pages.Page import Page
|
||||||
|
|
||||||
|
|
@ -91,24 +89,20 @@ class TicketOrBillingChoicePage(Page):
|
||||||
self.status_label.setText("Pick a ticket from the list first.")
|
self.status_label.setText("Pick a ticket from the list first.")
|
||||||
return
|
return
|
||||||
which_ticket = item.data(QtCore.Qt.ItemDataRole.UserRole)
|
which_ticket = item.data(QtCore.Qt.ItemDataRole.UserRole)
|
||||||
profile_id = self.update_status.current_profile_id
|
|
||||||
try:
|
try:
|
||||||
profile = ProfileController.get(int(profile_id))
|
profile_id = int(self.update_status.current_profile_id)
|
||||||
except Exception:
|
except (TypeError, ValueError):
|
||||||
profile = None
|
self.status_label.setText("Could not determine profile.")
|
||||||
candidates = location_candidates(profile)
|
|
||||||
if not candidates:
|
|
||||||
self.status_label.setText("Could not determine profile location.")
|
|
||||||
return
|
return
|
||||||
profile_data = {
|
profile_data = {
|
||||||
'id': int(profile_id),
|
'id': profile_id,
|
||||||
'use_ticket': which_ticket,
|
'use_ticket': which_ticket
|
||||||
'ticket_location': candidates[0],
|
|
||||||
}
|
}
|
||||||
menu_page = self.custom_window.navigator.get_cached("menu")
|
menu_page = self.custom_window.navigator.get_cached("menu")
|
||||||
if menu_page:
|
if menu_page:
|
||||||
self.update_status.update_status(f"Using ticket #{which_ticket}...")
|
self.update_status.update_status(f"Using ticket #{which_ticket}...")
|
||||||
menu_page.enabling_profile(profile_data)
|
menu_page.enabling_profile(profile_data)
|
||||||
|
self.custom_window.navigator.navigate("menu")
|
||||||
|
|
||||||
def on_use_billing(self):
|
def on_use_billing(self):
|
||||||
self.custom_window.navigator.navigate("id")
|
self.custom_window.navigator.navigate("id")
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,11 @@ class TorPage(Page):
|
||||||
QPixmap(os.path.join(self.btn_path, "browser only.png")))
|
QPixmap(os.path.join(self.btn_path, "browser only.png")))
|
||||||
self.display0.lower()
|
self.display0.lower()
|
||||||
|
|
||||||
self.button_go.clicked.connect(self.go_selected)
|
self.replace_click_handler(self.button_go, self.go_selected)
|
||||||
|
|
||||||
self.button_reverse.setVisible(True)
|
self.button_reverse.setVisible(True)
|
||||||
self.button_reverse.clicked.connect(self.reverse_selected)
|
self.replace_click_handler(self.button_reverse, self.reverse_selected)
|
||||||
|
self.replace_click_handler(self.button_back, self.reverse_selected)
|
||||||
|
|
||||||
self.label = QLabel(self)
|
self.label = QLabel(self)
|
||||||
self.label.setGeometry(440, 370, 86, 130)
|
self.label.setGeometry(440, 370, 86, 130)
|
||||||
|
|
@ -72,6 +73,8 @@ class TorPage(Page):
|
||||||
self.update_status.write_data(inserted_data)
|
self.update_status.write_data(inserted_data)
|
||||||
|
|
||||||
def reverse_selected(self):
|
def reverse_selected(self):
|
||||||
|
self.limpiar()
|
||||||
|
self.button_go.setVisible(False)
|
||||||
self.custom_window.navigator.navigate("residential")
|
self.custom_window.navigator.navigate("residential")
|
||||||
|
|
||||||
def go_selected(self):
|
def go_selected(self):
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class WelcomePage(Page):
|
||||||
self.btn_path = main_window.btn_path
|
self.btn_path = main_window.btn_path
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
self.ui_elements = []
|
self.ui_elements = []
|
||||||
self.button_next.clicked.connect(self.go_to_install)
|
self.button_next.clicked.connect(self.go_to_networking_setup)
|
||||||
self.button_next.setVisible(True)
|
self.button_next.setVisible(True)
|
||||||
self._setup_welcome_ui()
|
self._setup_welcome_ui()
|
||||||
self._setup_stats_display()
|
self._setup_stats_display()
|
||||||
|
|
@ -30,7 +30,7 @@ class WelcomePage(Page):
|
||||||
|
|
||||||
welcome_msg = QLabel(
|
welcome_msg = QLabel(
|
||||||
"Before we begin your journey, we need to set up a few essential components. "
|
"Before we begin your journey, we need to set up a few essential components. "
|
||||||
"Click 'Next' to take you to the installation page.", self)
|
"Click 'Next' to review the optional networking setup.", self)
|
||||||
welcome_msg.setGeometry(40, 100, 720, 80)
|
welcome_msg.setGeometry(40, 100, 720, 80)
|
||||||
welcome_msg.setWordWrap(True)
|
welcome_msg.setWordWrap(True)
|
||||||
welcome_msg.setStyleSheet("""
|
welcome_msg.setStyleSheet("""
|
||||||
|
|
@ -95,7 +95,7 @@ class WelcomePage(Page):
|
||||||
"font-size: 16px; font-weight: bold; color: #2c3e50;")
|
"font-size: 16px; font-weight: bold; color: #2c3e50;")
|
||||||
title_layout.addWidget(title_label)
|
title_layout.addWidget(title_label)
|
||||||
|
|
||||||
status_indicator = QLabel("●")
|
status_indicator = QLabel(chr(9679))
|
||||||
status_indicator.setStyleSheet("color: #2ecc71; font-size: 16px;")
|
status_indicator.setStyleSheet("color: #2ecc71; font-size: 16px;")
|
||||||
title_layout.addWidget(status_indicator)
|
title_layout.addWidget(status_indicator)
|
||||||
|
|
||||||
|
|
@ -117,8 +117,5 @@ class WelcomePage(Page):
|
||||||
|
|
||||||
grid_layout.addWidget(stat_widget, row, col)
|
grid_layout.addWidget(stat_widget, row, col)
|
||||||
|
|
||||||
def go_to_install(self):
|
def go_to_networking_setup(self):
|
||||||
self.custom_window.navigator.navigate("install_system_package")
|
self.custom_window.navigator.navigate("networking_setup")
|
||||||
install_page = self.custom_window.navigator.get_cached("install_system_package")
|
|
||||||
if install_page is not None:
|
|
||||||
install_page.configure(package_name='all', distro='debian')
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ class WireGuardPage(Page):
|
||||||
self.selected_protocol = None
|
self.selected_protocol = None
|
||||||
self.selected_protocol_icon = None
|
self.selected_protocol_icon = None
|
||||||
self.button_back.setVisible(True)
|
self.button_back.setVisible(True)
|
||||||
self.button_go.clicked.connect(self.go_selected)
|
self.replace_click_handler(self.button_back, self.reverse)
|
||||||
|
self.replace_click_handler(self.button_go, self.go_selected)
|
||||||
self.additional_labels = []
|
self.additional_labels = []
|
||||||
self.title.setGeometry(585, 40, 185, 40)
|
self.title.setGeometry(585, 40, 185, 40)
|
||||||
self.title.setText("Pick a Protocol")
|
self.title.setText("Pick a Protocol")
|
||||||
|
|
|
||||||
290
gui/v2/ui/popups/Database_version.py
Executable file
|
|
@ -0,0 +1,290 @@
|
||||||
|
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 = "Upgrade Time!"
|
||||||
|
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
|
||||||
163
gui/v2/ui/popups/generic_choice.py
Executable file
|
|
@ -0,0 +1,163 @@
|
||||||
|
import sys
|
||||||
|
from PyQt6.QtWidgets import QApplication, QDialog, QWidget, QVBoxLayout, QLabel, QPushButton
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtGui import QFont, QColor
|
||||||
|
from PyQt6.QtWidgets import QGraphicsDropShadowEffect
|
||||||
|
|
||||||
|
class GenericOptionMenu(QDialog):
|
||||||
|
def __init__(self, title, option_one, option_two, option_three):
|
||||||
|
super().__init__()
|
||||||
|
self.selected_option = None
|
||||||
|
self.init_ui(title, option_one, option_two, option_three)
|
||||||
|
|
||||||
|
def init_ui(self, title, option_one, option_two, option_three):
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
self.setGeometry(100, 100, 500, 600)
|
||||||
|
self.setModal(True)
|
||||||
|
|
||||||
|
# Main container
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setSpacing(25)
|
||||||
|
layout.setContentsMargins(40, 50, 40, 50)
|
||||||
|
|
||||||
|
# Apply dark cyberpunk background
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QDialog {
|
||||||
|
background-color: #0a0e27;
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Title Label
|
||||||
|
title_label = QLabel(title)
|
||||||
|
title_label.setFont(QFont("Courier New", 16, QFont.Weight.Bold))
|
||||||
|
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
title_label.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: #00ff88;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid #00ff88;
|
||||||
|
border-radius: 5px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Add glow effect to title
|
||||||
|
title_shadow = QGraphicsDropShadowEffect()
|
||||||
|
title_shadow.setBlurRadius(15)
|
||||||
|
title_shadow.setColor(QColor(0, 255, 136, 200))
|
||||||
|
title_shadow.setOffset(0, 0)
|
||||||
|
title_label.setGraphicsEffect(title_shadow)
|
||||||
|
|
||||||
|
layout.addWidget(title_label)
|
||||||
|
layout.addSpacing(30)
|
||||||
|
|
||||||
|
# Button 1
|
||||||
|
btn1 = self.create_button(option_one, 1)
|
||||||
|
layout.addWidget(btn1)
|
||||||
|
|
||||||
|
# Button 2
|
||||||
|
btn2 = self.create_button(option_two, 2)
|
||||||
|
layout.addWidget(btn2)
|
||||||
|
|
||||||
|
# Button 3
|
||||||
|
btn3 = self.create_button(option_three, 3)
|
||||||
|
layout.addWidget(btn3)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
# Status label
|
||||||
|
status_label = QLabel("▓░ READY FOR INPUT ░▓")
|
||||||
|
status_label.setFont(QFont("Courier New", 10))
|
||||||
|
status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
status_label.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: #ff0088;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 10px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
layout.addWidget(status_label)
|
||||||
|
|
||||||
|
def create_button(self, text, button_num):
|
||||||
|
button = QPushButton(text)
|
||||||
|
button.setFont(QFont("Courier New", 12, QFont.Weight.Bold))
|
||||||
|
button.setMinimumHeight(60)
|
||||||
|
button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
|
||||||
|
# Color variations for each button
|
||||||
|
if button_num == 1:
|
||||||
|
neon_color = "#00ff88" # Cyan green
|
||||||
|
rgb_color = QColor(0, 255, 136)
|
||||||
|
elif button_num == 2:
|
||||||
|
neon_color = "#00d4ff" # Cyan blue
|
||||||
|
rgb_color = QColor(0, 212, 255)
|
||||||
|
else:
|
||||||
|
neon_color = "#ff0088" # Magenta
|
||||||
|
rgb_color = QColor(255, 0, 136)
|
||||||
|
|
||||||
|
button.setStyleSheet(f"""
|
||||||
|
QPushButton {{
|
||||||
|
background-color: #1a1f3a;
|
||||||
|
color: {neon_color};
|
||||||
|
border: 2px solid {neon_color};
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}}
|
||||||
|
QPushButton:hover {{
|
||||||
|
background-color: {neon_color};
|
||||||
|
color: #0a0e27;
|
||||||
|
border: 2px solid {neon_color};
|
||||||
|
}}
|
||||||
|
QPushButton:pressed {{
|
||||||
|
background-color: #0a0e27;
|
||||||
|
border: 3px solid {neon_color};
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Add drop shadow glow effect
|
||||||
|
shadow = QGraphicsDropShadowEffect()
|
||||||
|
shadow.setBlurRadius(20)
|
||||||
|
shadow.setColor(rgb_color)
|
||||||
|
shadow.setOffset(0, 0)
|
||||||
|
button.setGraphicsEffect(shadow)
|
||||||
|
|
||||||
|
button.clicked.connect(lambda: self.on_button_clicked(button_num))
|
||||||
|
return button
|
||||||
|
|
||||||
|
def on_button_clicked(self, button_num):
|
||||||
|
self.selected_option = button_num
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def show_generic_options(
|
||||||
|
title: str,
|
||||||
|
option_one: str,
|
||||||
|
option_two: str,
|
||||||
|
option_three: str
|
||||||
|
) -> int:
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# Create the menu with custom title and options
|
||||||
|
menu = GenericOptionMenu(
|
||||||
|
title=title,
|
||||||
|
option_one=option_one,
|
||||||
|
option_two=option_two,
|
||||||
|
option_three=option_three
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show the menu and get the result
|
||||||
|
result = menu.exec()
|
||||||
|
|
||||||
|
if result == QDialog.DialogCode.Accepted:
|
||||||
|
print(f"User selected option: {menu.selected_option}")
|
||||||
|
return menu.selected_option
|
||||||
|
else:
|
||||||
|
print("User cancelled")
|
||||||
|
return None
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
10
gui/v2/ui/popups/generic_error_popup.py
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMessageBox
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def show_error(error_text, title="Error"):
|
||||||
|
app = QApplication.instance()
|
||||||
|
if app is None:
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
QMessageBox.critical(None, title, error_text)
|
||||||
|
app.quit()
|
||||||
18
gui/v2/ui/popups/message_box.py
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
MESSAGE_BOX_QSS = (
|
||||||
|
"QMessageBox { background-color: white; }"
|
||||||
|
"QMessageBox QLabel { color: black; }"
|
||||||
|
"QMessageBox QPushButton { color: black; padding: 5px 14px; }"
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIRM_BUTTON_QSS = "color: #2e7d32; font-weight: bold; padding: 5px 14px;"
|
||||||
|
|
||||||
|
|
||||||
|
def style_message_box(box):
|
||||||
|
box.setStyleSheet(MESSAGE_BOX_QSS)
|
||||||
|
|
||||||
|
|
||||||
|
def mark_confirm_button(button):
|
||||||
|
if button is None:
|
||||||
|
return
|
||||||
|
button.setText(f"✓ {button.text()}")
|
||||||
|
button.setStyleSheet(CONFIRM_BUTTON_QSS)
|
||||||
200
gui/v2/ui/popups/operation_result_popup.py
Executable file
|
|
@ -0,0 +1,200 @@
|
||||||
|
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
|
||||||
|
from PyQt6.QtGui import QFont, QGuiApplication
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QDialog,
|
||||||
|
QFrame,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QPushButton,
|
||||||
|
QScrollArea,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from gui.v2.ui.styles.styles import (
|
||||||
|
POPUP_ACTION_BUTTON_RED_QSS,
|
||||||
|
POPUP_BG_QSS,
|
||||||
|
POPUP_CANCEL_BUTTON_QSS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OperationResultPopup(QDialog):
|
||||||
|
action_selected = pyqtSignal(bool)
|
||||||
|
|
||||||
|
def __init__(self, parent=None, message="", title="Operation Failed", action_button_text="Yes", cancel_button_text="No", action_result=True):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent_window = parent
|
||||||
|
self.title_text = title or "Operation Failed"
|
||||||
|
self.message = message or "The operation could not be completed."
|
||||||
|
self.action_button_text = action_button_text
|
||||||
|
self.cancel_button_text = cancel_button_text
|
||||||
|
self.action_result = action_result
|
||||||
|
self._completed = False
|
||||||
|
self._use_parent_local_geometry = QGuiApplication.platformName().lower().startswith("wayland")
|
||||||
|
self.initUI()
|
||||||
|
|
||||||
|
def initUI(self):
|
||||||
|
self.setFixedSize(560, 300)
|
||||||
|
if self._use_parent_local_geometry:
|
||||||
|
if self.parent_window is not None:
|
||||||
|
self.setParent(self.parent_window)
|
||||||
|
self.setWindowFlags(Qt.WindowType.Widget)
|
||||||
|
else:
|
||||||
|
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
outer = QVBoxLayout(self)
|
||||||
|
outer.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
bg_widget = QWidget(self)
|
||||||
|
bg_widget.setStyleSheet(POPUP_BG_QSS)
|
||||||
|
outer.addWidget(bg_widget)
|
||||||
|
|
||||||
|
content_layout = QVBoxLayout(bg_widget)
|
||||||
|
content_layout.setContentsMargins(22, 14, 22, 18)
|
||||||
|
content_layout.setSpacing(12)
|
||||||
|
|
||||||
|
top_row = QHBoxLayout()
|
||||||
|
top_row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
top_row.setSpacing(10)
|
||||||
|
|
||||||
|
title_label = QLabel(self.title_text)
|
||||||
|
title_label.setFont(QFont("Arial", 18, QFont.Weight.Bold))
|
||||||
|
title_label.setStyleSheet("color: #d62828; background: transparent; border: none;")
|
||||||
|
title_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
title_label.setMinimumWidth(0)
|
||||||
|
top_row.addWidget(title_label, 1)
|
||||||
|
|
||||||
|
close_button = QPushButton("X")
|
||||||
|
close_button.setFixedSize(36, 36)
|
||||||
|
close_button.setFont(QFont("Arial", 14, QFont.Weight.Bold))
|
||||||
|
close_button.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #444444;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
color: #d62828;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
close_button.clicked.connect(lambda: self._choose(False))
|
||||||
|
top_row.addWidget(close_button, 0, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop)
|
||||||
|
|
||||||
|
content_layout.addLayout(top_row)
|
||||||
|
|
||||||
|
scroll_area = QScrollArea()
|
||||||
|
scroll_area.setWidgetResizable(True)
|
||||||
|
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
|
scroll_area.setStyleSheet("background: transparent; border: none;")
|
||||||
|
content_layout.addWidget(scroll_area, 1)
|
||||||
|
|
||||||
|
scroll_content = QWidget()
|
||||||
|
scroll_content.setStyleSheet("background: transparent; border: none;")
|
||||||
|
scroll_layout = QVBoxLayout(scroll_content)
|
||||||
|
scroll_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
message_label = QLabel(self.message)
|
||||||
|
message_label.setFont(QFont("Arial", 12))
|
||||||
|
message_label.setStyleSheet("color: #333333; background: transparent; border: none;")
|
||||||
|
message_label.setWordWrap(True)
|
||||||
|
message_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||||
|
message_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
||||||
|
scroll_layout.addWidget(message_label)
|
||||||
|
scroll_layout.addStretch()
|
||||||
|
scroll_area.setWidget(scroll_content)
|
||||||
|
|
||||||
|
button_row = QHBoxLayout()
|
||||||
|
button_row.addStretch()
|
||||||
|
|
||||||
|
if self.cancel_button_text is not None:
|
||||||
|
cancel_button = QPushButton(self.cancel_button_text)
|
||||||
|
cancel_button.setFixedSize(max(110, min(180, 22 + len(self.cancel_button_text) * 8)), 42)
|
||||||
|
cancel_button.setFont(QFont("Arial", 11, QFont.Weight.Bold))
|
||||||
|
cancel_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
cancel_button.setStyleSheet(POPUP_CANCEL_BUTTON_QSS)
|
||||||
|
cancel_button.clicked.connect(lambda: self._choose(False))
|
||||||
|
button_row.addWidget(cancel_button)
|
||||||
|
|
||||||
|
action_button = QPushButton(self.action_button_text)
|
||||||
|
action_button.setFixedSize(max(110, min(180, 22 + len(self.action_button_text) * 8)), 42)
|
||||||
|
action_button.setFont(QFont("Arial", 11, QFont.Weight.Bold))
|
||||||
|
action_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
action_button.setStyleSheet(POPUP_ACTION_BUTTON_RED_QSS)
|
||||||
|
action_button.clicked.connect(lambda: self._choose(self.action_result))
|
||||||
|
button_row.addWidget(action_button)
|
||||||
|
content_layout.addLayout(button_row)
|
||||||
|
|
||||||
|
if not self._use_parent_local_geometry:
|
||||||
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||||
|
|
||||||
|
def _choose(self, accepted):
|
||||||
|
if self._completed:
|
||||||
|
return
|
||||||
|
self._completed = True
|
||||||
|
self.action_selected.emit(accepted)
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
if not self._completed:
|
||||||
|
self._completed = True
|
||||||
|
self.action_selected.emit(False)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
self._center_on_parent()
|
||||||
|
super().show()
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
super().showEvent(event)
|
||||||
|
self._center_on_parent()
|
||||||
|
QTimer.singleShot(0, self._center_on_parent)
|
||||||
|
|
||||||
|
def _center_on_parent(self):
|
||||||
|
parent = self.parent_window
|
||||||
|
if parent is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if not hasattr(parent, 'mapToGlobal') or not hasattr(parent, 'rect'):
|
||||||
|
return
|
||||||
|
parent_local_rect = parent.rect()
|
||||||
|
if parent_local_rect.isEmpty():
|
||||||
|
return
|
||||||
|
my_size = self.size()
|
||||||
|
if my_size.isEmpty() or my_size.width() <= 0 or my_size.height() <= 0:
|
||||||
|
my_size = self.sizeHint()
|
||||||
|
if self._use_parent_local_geometry:
|
||||||
|
target_x = parent_local_rect.x() + (parent_local_rect.width() - my_size.width()) // 2
|
||||||
|
target_y = parent_local_rect.y() + (parent_local_rect.height() - my_size.height()) // 2
|
||||||
|
target_x = max(parent_local_rect.left(), min(target_x, parent_local_rect.right() - my_size.width() + 1))
|
||||||
|
target_y = max(parent_local_rect.top(), min(target_y, parent_local_rect.bottom() - my_size.height() + 1))
|
||||||
|
else:
|
||||||
|
parent_center = parent.mapToGlobal(parent_local_rect.center())
|
||||||
|
target_x = parent_center.x() - my_size.width() // 2
|
||||||
|
target_y = parent_center.y() - my_size.height() // 2
|
||||||
|
screen = None
|
||||||
|
if hasattr(parent, 'screen'):
|
||||||
|
try:
|
||||||
|
screen = parent.screen()
|
||||||
|
except Exception:
|
||||||
|
screen = None
|
||||||
|
if screen is not None:
|
||||||
|
avail = screen.availableGeometry()
|
||||||
|
target_x = max(avail.left(), min(target_x, avail.right() - my_size.width()))
|
||||||
|
target_y = max(avail.top(), min(target_y, avail.bottom() - my_size.height()))
|
||||||
|
self.setGeometry(target_x, target_y, my_size.width(), my_size.height())
|
||||||
|
if self._use_parent_local_geometry:
|
||||||
|
self.raise_()
|
||||||
|
self.setFocus(Qt.FocusReason.PopupFocusReason)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
self.oldPos = event.globalPosition().toPoint()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
delta = event.globalPosition().toPoint() - self.oldPos
|
||||||
|
self.move(self.x() + delta.x(), self.y() + delta.y())
|
||||||
|
self.oldPos = event.globalPosition().toPoint()
|
||||||
50
gui/v2/ui/popups/pick_folder_to_move.py
Executable file
|
|
@ -0,0 +1,50 @@
|
||||||
|
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)
|
||||||
281
gui/v2/ui/popups/terminal_threading.py
Executable file
|
|
@ -0,0 +1,281 @@
|
||||||
|
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
|
||||||
|
|
||||||
|
from core.observers.ConnectionObserver import ConnectionObserver
|
||||||
|
from core.observers.ClientObserver import ClientObserver
|
||||||
|
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
|
||||||
|
from core.controllers.SyncController import new_sync
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTextEdit,
|
||||||
|
QLabel, QPushButton, QApplication)
|
||||||
|
from PyQt6.QtGui import QFont, QColor, QTextCursor
|
||||||
|
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, pyqtSlot
|
||||||
|
from PyQt6.QtWidgets import QGraphicsDropShadowEffect
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from typing import Callable, Any
|
||||||
|
from PyQt6.QtGui import QTextCharFormat
|
||||||
|
|
||||||
|
import threading
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTerminalUI(QDialog):
|
||||||
|
def __init__(self, connection_observer: ConnectionObserver, client_observer: ClientObserver):
|
||||||
|
super().__init__()
|
||||||
|
self.mainthread = ThreadSafetyTool()
|
||||||
|
|
||||||
|
self.connection_observer = connection_observer
|
||||||
|
self.client_observer = client_observer
|
||||||
|
self.synchronized = False
|
||||||
|
self.worker_thread = None
|
||||||
|
self.task_success = False
|
||||||
|
|
||||||
|
self.connection_observer.subscribe("message",
|
||||||
|
lambda msg: self.add_notification("UPDATE", msg, "#00ff88"))
|
||||||
|
|
||||||
|
self.connection_observer.subscribe("connecting",
|
||||||
|
lambda msg: self.add_notification("CONNECT", msg, "#00ff88"))
|
||||||
|
|
||||||
|
self.connection_observer.subscribe("tor_bootstrapping",
|
||||||
|
lambda msg: self.add_notification("BOOTSTRAP", msg, "#00d4ff"))
|
||||||
|
|
||||||
|
self.connection_observer.subscribe("tor_bootstrap_progressing",
|
||||||
|
lambda msg: self.add_notification("PROGRESS", msg, "#ff0088"))
|
||||||
|
|
||||||
|
self.connection_observer.subscribe("tor_bootstrapped",
|
||||||
|
lambda msg: self.add_notification("SUCCESS", msg, "#00ff88"))
|
||||||
|
|
||||||
|
self.connection_observer.subscribe("custom_message",
|
||||||
|
lambda msg: self.add_notification("INFO", msg, "#ffff00"))
|
||||||
|
|
||||||
|
# Subscribe to client_observer events
|
||||||
|
self.client_observer.subscribe("synchronizing",
|
||||||
|
lambda event: self.add_notification("SYNC",
|
||||||
|
event.subject if event.subject else "Sync in progress...", "#00d4ff"))
|
||||||
|
|
||||||
|
self.client_observer.subscribe("synchronized",
|
||||||
|
lambda event: self.on_synchronized())
|
||||||
|
|
||||||
|
self.client_observer.subscribe("updating",
|
||||||
|
lambda event: self.add_notification("UPDATE", "Updating client...", "#ff0088"))
|
||||||
|
|
||||||
|
self.client_observer.subscribe("update_progressing",
|
||||||
|
lambda event: self.add_notification("PROGRESS",
|
||||||
|
f'Current progress: {event.meta.get("progress", 0):.2f}%', "#ffff00"))
|
||||||
|
|
||||||
|
self.client_observer.subscribe("updated",
|
||||||
|
lambda event: self.add_notification("COMPLETE",
|
||||||
|
"Restart client to apply update.", "#00ff88"))
|
||||||
|
|
||||||
|
self.client_observer.subscribe("custom_message",
|
||||||
|
lambda event: self.add_notification("ERROR",
|
||||||
|
event.subject if event.subject else "Error, check logs", "#ff0088"))
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
self.setWindowTitle("SYNC PROCESS")
|
||||||
|
self.setGeometry(100, 100, 700, 600)
|
||||||
|
self.setModal(True)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setSpacing(15)
|
||||||
|
layout.setContentsMargins(30, 30, 30, 30)
|
||||||
|
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QDialog {
|
||||||
|
background-color: #0a0e27;
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Title Label
|
||||||
|
title_label = QLabel("▓░ NOTIFICATION TERMINAL ░▓")
|
||||||
|
title_label.setFont(QFont("Courier New", 14, QFont.Weight.Bold))
|
||||||
|
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
title_label.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: #00ff88;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 15px;
|
||||||
|
border: 2px solid #00ff88;
|
||||||
|
border-radius: 5px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
title_shadow = QGraphicsDropShadowEffect()
|
||||||
|
title_shadow.setBlurRadius(15)
|
||||||
|
title_shadow.setColor(QColor(0, 255, 136, 200))
|
||||||
|
title_shadow.setOffset(0, 0)
|
||||||
|
title_label.setGraphicsEffect(title_shadow)
|
||||||
|
layout.addWidget(title_label)
|
||||||
|
|
||||||
|
# Scrollable text display
|
||||||
|
self.output_text = QTextEdit()
|
||||||
|
self.output_text.setReadOnly(True)
|
||||||
|
self.output_text.setFont(QFont("Courier New", 10))
|
||||||
|
self.output_text.setMinimumHeight(400)
|
||||||
|
self.output_text.setStyleSheet("""
|
||||||
|
QTextEdit {
|
||||||
|
background-color: #1a1f3a;
|
||||||
|
color: #00ff88;
|
||||||
|
border: 2px solid #00ff88;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 16pt;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
selection-background-color: #ff0088;
|
||||||
|
}
|
||||||
|
QScrollBar:vertical {
|
||||||
|
background-color: #0a0e27;
|
||||||
|
width: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
QScrollBar::handle:vertical {
|
||||||
|
background-color: #00ff88;
|
||||||
|
border-radius: 6px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
QScrollBar::handle:vertical:hover {
|
||||||
|
background-color: #00d4ff;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
layout.addWidget(self.output_text)
|
||||||
|
|
||||||
|
# Status label
|
||||||
|
self.status_label = QLabel("◄ WAITING FOR TASK START ►")
|
||||||
|
self.status_label.setFont(QFont("Courier New", 9))
|
||||||
|
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.status_label.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: #ff0088;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 8px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
# Button layout
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
clear_btn = QPushButton("CLEAR")
|
||||||
|
clear_btn.setFont(QFont("Courier New", 10, QFont.Weight.Bold))
|
||||||
|
clear_btn.setMinimumHeight(40)
|
||||||
|
clear_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
|
clear_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #1a1f3a;
|
||||||
|
color: #00d4ff;
|
||||||
|
border: 2px solid #00d4ff;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #00d4ff;
|
||||||
|
color: #0a0e27;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #0a0e27;
|
||||||
|
border: 3px solid #00d4ff;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
clear_btn.clicked.connect(self.clear_output)
|
||||||
|
shadow = QGraphicsDropShadowEffect()
|
||||||
|
shadow.setBlurRadius(15)
|
||||||
|
shadow.setColor(QColor(0, 212, 255))
|
||||||
|
shadow.setOffset(0, 0)
|
||||||
|
clear_btn.setGraphicsEffect(shadow)
|
||||||
|
button_layout.addWidget(clear_btn)
|
||||||
|
button_layout.addStretch()
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
def add_notification(self, prefix: str, message: str, color: str = "#00ff88"):
|
||||||
|
"""Queue to main thread safely."""
|
||||||
|
self.mainthread(lambda: self._add_notification_impl(prefix, message, color))()
|
||||||
|
|
||||||
|
def _add_notification_impl(self, prefix: str, message: str, color: str):
|
||||||
|
"""Actual implementation on main thread."""
|
||||||
|
cursor = self.output_text.textCursor()
|
||||||
|
cursor.movePosition(cursor.MoveOperation.End)
|
||||||
|
fmt = QTextCharFormat()
|
||||||
|
fmt.setForeground(QColor(color))
|
||||||
|
cursor.insertText(f"[{prefix}] {message}\n", fmt)
|
||||||
|
self.output_text.setTextCursor(cursor)
|
||||||
|
self.output_text.ensureCursorVisible()
|
||||||
|
|
||||||
|
def set_status(self, text: str):
|
||||||
|
"""This runs on the main thread, safely."""
|
||||||
|
self.status_label.setText(text)
|
||||||
|
|
||||||
|
def on_synchronized(self):
|
||||||
|
"""Handle synchronized notification and exit."""
|
||||||
|
self.add_notification("SYNC", "Sync complete ✓", "#00ff88")
|
||||||
|
self.set_status("◄ SYNCHRONIZED - CLOSING ►")
|
||||||
|
self.synchronized = True
|
||||||
|
self.task_success = True
|
||||||
|
QTimer.singleShot(500, self.accept)
|
||||||
|
|
||||||
|
def run_task(self, task_func: Callable):
|
||||||
|
"""
|
||||||
|
Purpose:
|
||||||
|
Insert any function for a pure python background thread.
|
||||||
|
Which then pushes to the main UI via ThreadSafety
|
||||||
|
"""
|
||||||
|
if self.worker_thread is not None and self.worker_thread.is_alive():
|
||||||
|
self.add_notification("SYSTEM", "Task already running!", "#ff0088")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.set_status("◄ TASK RUNNING ►")
|
||||||
|
self.add_notification("SYSTEM", "Background task started", "#00d4ff")
|
||||||
|
|
||||||
|
# Start in pure Python thread (daemon=True so it doesn't block exit)
|
||||||
|
self.worker_thread = Thread(target=self._run_task_wrapper, args=(task_func,), daemon=True)
|
||||||
|
self.worker_thread.start()
|
||||||
|
|
||||||
|
def _run_task_wrapper(self, task_func: Callable):
|
||||||
|
"""Wrapper that runs task and handles errors."""
|
||||||
|
try:
|
||||||
|
task_func()
|
||||||
|
self.task_success = True
|
||||||
|
except Exception as e:
|
||||||
|
self.add_notification("ERROR", f"Task failed: {str(e)}", "#ff0088")
|
||||||
|
self.set_status("◄ TASK FAILED ►")
|
||||||
|
self.task_success = False
|
||||||
|
|
||||||
|
def clear_output(self):
|
||||||
|
"""Clear the terminal output."""
|
||||||
|
self.output_text.clear()
|
||||||
|
self.add_notification("SYSTEM", "Terminal cleared", "#00d4ff")
|
||||||
|
|
||||||
|
def get_result(self) -> bool:
|
||||||
|
"""Return True if synchronized, False otherwise."""
|
||||||
|
return self.synchronized and self.task_success
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Ensure thread is properly cleaned up on close."""
|
||||||
|
if self.worker_thread is not None and self.worker_thread.is_alive():
|
||||||
|
self.worker_thread.join(timeout=1)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
def show_terminal(
|
||||||
|
do_this_function: Callable[[Any, Any], None],
|
||||||
|
connection_observer: ConnectionObserver,
|
||||||
|
client_observer: ClientObserver
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
app = QApplication.instance() or QApplication(sys.argv)
|
||||||
|
|
||||||
|
# Create the UI
|
||||||
|
dialog = NotificationTerminalUI(connection_observer=connection_observer, client_observer=client_observer)
|
||||||
|
|
||||||
|
# Start the background task
|
||||||
|
dialog.run_task(do_this_function)
|
||||||
|
|
||||||
|
# Show dialog and wait
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
|
# Return success
|
||||||
|
return dialog.get_result()
|
||||||
22
gui/v2/ui/styles/css/main_ui_css.py
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
|
||||||
|
forced_sync_popup_style = """
|
||||||
|
QPushButton {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
QPushButton[text="Cancel"] {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
}
|
||||||
|
QPushButton[text="Cancel"]:hover {
|
||||||
|
background-color: #c0392b;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
@ -116,6 +116,42 @@ POPUP_ACTION_BUTTON_RED_QSS = """
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def combobox_style(font_style=""):
|
||||||
|
return f"""
|
||||||
|
QComboBox {{
|
||||||
|
color: black;
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 5px 30px 5px 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 120px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
{font_style}
|
||||||
|
}}
|
||||||
|
QComboBox:disabled {{
|
||||||
|
color: #666;
|
||||||
|
background: #e0e0e0;
|
||||||
|
}}
|
||||||
|
QComboBox::drop-down {{
|
||||||
|
border: none;
|
||||||
|
width: 30px;
|
||||||
|
}}
|
||||||
|
QComboBox::down-arrow {{
|
||||||
|
image: url(assets/down_arrow.png);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}}
|
||||||
|
QComboBox QAbstractItemView {{
|
||||||
|
color: black;
|
||||||
|
background: white;
|
||||||
|
selection-background-color: #007bff;
|
||||||
|
selection-color: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
{font_style}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def checkbox_style(font_style=""):
|
def checkbox_style(font_style=""):
|
||||||
return f"""
|
return f"""
|
||||||
QCheckBox {{
|
QCheckBox {{
|
||||||
|
|
|
||||||
19
gui/v2/workers/page_data_worker.py
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from PyQt6.QtCore import QThread, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
class PageDataWorker(QThread):
|
||||||
|
data_ready = pyqtSignal(str, object)
|
||||||
|
failed = pyqtSignal(str, str)
|
||||||
|
|
||||||
|
def __init__(self, name, fn, *args):
|
||||||
|
super().__init__()
|
||||||
|
self.name = name
|
||||||
|
self.fn = fn
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
payload = self.fn(*self.args)
|
||||||
|
self.data_ready.emit(self.name, payload)
|
||||||
|
except Exception as error:
|
||||||
|
self.failed.emit(self.name, f"{type(error).__name__}: {error}")
|
||||||
|
|
@ -7,7 +7,6 @@ from core.controllers.tickets.FailedVerificationController import (
|
||||||
evaluate_if_its_the_key,
|
evaluate_if_its_the_key,
|
||||||
prepare_tickets_with_saved_blind_sigs,
|
prepare_tickets_with_saved_blind_sigs,
|
||||||
)
|
)
|
||||||
from core.controllers.tickets.UseTicketController import use_ticket
|
|
||||||
|
|
||||||
from gui.v2.infrastructure.setup_observers import (
|
from gui.v2.infrastructure.setup_observers import (
|
||||||
connection_observer,
|
connection_observer,
|
||||||
|
|
@ -22,7 +21,6 @@ class TicketingWorkerThread(QThread):
|
||||||
not_paid = pyqtSignal()
|
not_paid = pyqtSignal()
|
||||||
paid_check_failed = pyqtSignal(str)
|
paid_check_failed = pyqtSignal(str)
|
||||||
prep_done = pyqtSignal(object)
|
prep_done = pyqtSignal(object)
|
||||||
use_done = pyqtSignal(object)
|
|
||||||
failed_verification_evaluated = pyqtSignal(object)
|
failed_verification_evaluated = pyqtSignal(object)
|
||||||
saved_blind_prep_done = pyqtSignal(object)
|
saved_blind_prep_done = pyqtSignal(object)
|
||||||
error = pyqtSignal(str)
|
error = pyqtSignal(str)
|
||||||
|
|
@ -71,13 +69,5 @@ class TicketingWorkerThread(QThread):
|
||||||
elif self.action == 'PREPARE_SAVED_BLIND_SIGS':
|
elif self.action == 'PREPARE_SAVED_BLIND_SIGS':
|
||||||
result = prepare_tickets_with_saved_blind_sigs(ticket_observer, connection_observer)
|
result = prepare_tickets_with_saved_blind_sigs(ticket_observer, connection_observer)
|
||||||
self.saved_blind_prep_done.emit(result)
|
self.saved_blind_prep_done.emit(result)
|
||||||
elif self.action == 'USE_TICKET':
|
|
||||||
result = use_ticket(
|
|
||||||
self.params['which_ticket'],
|
|
||||||
self.params['which_location'],
|
|
||||||
ticket_observer,
|
|
||||||
connection_observer,
|
|
||||||
)
|
|
||||||
self.use_done.emit(result)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error.emit(str(e))
|
self.error.emit(str(e))
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ from core.controllers.tickets.UseTicketController import (
|
||||||
)
|
)
|
||||||
from core.models.session.SessionProfile import SessionProfile
|
from core.models.session.SessionProfile import SessionProfile
|
||||||
from core.models.system.SystemProfile import SystemProfile
|
from core.models.system.SystemProfile import SystemProfile
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
from core.errors.exceptions import SudoScript, MissingPreReqs, FirewallError
|
||||||
from core.Errors import (
|
from core.Errors import (
|
||||||
CommandNotFoundError,
|
CommandNotFoundError,
|
||||||
EndpointVerificationError,
|
EndpointVerificationError,
|
||||||
|
|
@ -22,7 +24,10 @@ from core.Errors import (
|
||||||
UnsupportedApplicationVersionError,
|
UnsupportedApplicationVersionError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from gui.v2.actions.locations import location_candidates
|
from gui.v2.actions.database_health import GuiStorageDatabaseError
|
||||||
|
from gui.v2.actions.operation_result_dispatch import dispatch_result_action
|
||||||
|
from gui.v2.actions.operation_results import result_from_exception
|
||||||
|
from gui.v2.infrastructure.screen_size import get_max_screensize
|
||||||
from gui.v2.infrastructure.setup_observers import (
|
from gui.v2.infrastructure.setup_observers import (
|
||||||
application_version_observer,
|
application_version_observer,
|
||||||
connection_observer,
|
connection_observer,
|
||||||
|
|
@ -35,6 +40,7 @@ class Worker(QObject):
|
||||||
update_signal = pyqtSignal(str, bool, int, int, str)
|
update_signal = pyqtSignal(str, bool, int, int, str)
|
||||||
change_page = pyqtSignal(str, bool)
|
change_page = pyqtSignal(str, bool)
|
||||||
ticket_data_loss = pyqtSignal(str, str)
|
ticket_data_loss = pyqtSignal(str, str)
|
||||||
|
operation_failed = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, profile_data):
|
def __init__(self, profile_data):
|
||||||
self.profile_data = profile_data
|
self.profile_data = profile_data
|
||||||
|
|
@ -46,13 +52,29 @@ class Worker(QObject):
|
||||||
self.profile_type = None
|
self.profile_type = None
|
||||||
self._ticket_error_emitted = False
|
self._ticket_error_emitted = False
|
||||||
self._consumed_ticket = None
|
self._consumed_ticket = None
|
||||||
|
self._pending_operation_result = None
|
||||||
|
self._pending_operation_kwargs = {}
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.profile = ProfileController.get(int(self.profile_data['id']))
|
try:
|
||||||
|
self.profile = ProfileController.get(int(self.profile_data['id']))
|
||||||
|
except GuiStorageDatabaseError:
|
||||||
|
self.update_signal.emit(
|
||||||
|
"Local storage database could not be read. Restart and recover storage.db.", False, None, None, None)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
self.update_signal.emit(
|
||||||
|
"Could not load profile data. Sync or restart and try again.", False, None, None, None)
|
||||||
|
return
|
||||||
|
|
||||||
|
incomplete_reason = self._profile_incomplete_reason()
|
||||||
|
if incomplete_reason:
|
||||||
|
self.update_signal.emit(incomplete_reason, False, None, None, None)
|
||||||
|
return
|
||||||
|
|
||||||
if 'use_ticket' in self.profile_data:
|
if 'use_ticket' in self.profile_data:
|
||||||
ticket_billing_code = self._consume_ticket(
|
ticket_billing_code = self._consume_ticket(
|
||||||
self.profile_data['use_ticket'],
|
self.profile_data['use_ticket'])
|
||||||
self.profile_data.get('ticket_location'))
|
|
||||||
if ticket_billing_code is None:
|
if ticket_billing_code is None:
|
||||||
return
|
return
|
||||||
self.profile_data['billing_code'] = ticket_billing_code
|
self.profile_data['billing_code'] = ticket_billing_code
|
||||||
|
|
@ -65,18 +87,20 @@ class Worker(QObject):
|
||||||
self.profile_data['billing_code'] = ticket_billing_code
|
self.profile_data['billing_code'] = ticket_billing_code
|
||||||
|
|
||||||
if 'billing_code' in self.profile_data:
|
if 'billing_code' in self.profile_data:
|
||||||
subscription = SubscriptionController.get(
|
try:
|
||||||
self.profile_data['billing_code'], connection_observer=connection_observer)
|
subscription = SubscriptionController.get(
|
||||||
|
self.profile_data['billing_code'], connection_observer=connection_observer)
|
||||||
|
except Exception as e:
|
||||||
|
if self._consumed_ticket is not None:
|
||||||
|
self._emit_subscription_lookup_failure(exception=e)
|
||||||
|
return
|
||||||
|
subscription = None
|
||||||
if subscription is not None:
|
if subscription is not None:
|
||||||
ProfileController.attach_subscription(
|
ProfileController.attach_subscription(
|
||||||
self.profile, subscription)
|
self.profile, subscription)
|
||||||
else:
|
else:
|
||||||
if self._consumed_ticket is not None:
|
if self._consumed_ticket is not None:
|
||||||
self._ticket_error_emitted = True
|
self._emit_subscription_lookup_failure()
|
||||||
self.ticket_data_loss.emit(
|
|
||||||
self._consumed_ticket,
|
|
||||||
str(self.profile_data.get('billing_code', '')),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
self.change_page.emit('The billing code is invalid.', True)
|
self.change_page.emit('The billing code is invalid.', True)
|
||||||
return
|
return
|
||||||
|
|
@ -88,15 +112,22 @@ class Worker(QObject):
|
||||||
if self.profile_data.get('ignore_profile_state_conflict', False):
|
if self.profile_data.get('ignore_profile_state_conflict', False):
|
||||||
ignore_exceptions.append(ProfileStateConflictError)
|
ignore_exceptions.append(ProfileStateConflictError)
|
||||||
ignore_tuple = tuple(ignore_exceptions)
|
ignore_tuple = tuple(ignore_exceptions)
|
||||||
ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer,
|
max_resolution = get_max_screensize()
|
||||||
application_version_observer=application_version_observer,
|
enable_result = ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer,
|
||||||
connection_observer=connection_observer)
|
application_version_observer=application_version_observer,
|
||||||
|
connection_observer=connection_observer, ticket_observer=ticket_observer, max_resolution=max_resolution)
|
||||||
|
if isinstance(enable_result, Result) and not enable_result.valid:
|
||||||
|
self._emit_operation_failure(enable_result)
|
||||||
|
return
|
||||||
except EndpointVerificationError:
|
except EndpointVerificationError:
|
||||||
self.update_signal.emit(
|
self.update_signal.emit(
|
||||||
"ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None)
|
"ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None)
|
||||||
except (InvalidSubscriptionError, MissingSubscriptionError) as e:
|
except (InvalidSubscriptionError, MissingSubscriptionError) as e:
|
||||||
self.change_page.emit(
|
if self._consumed_ticket is not None:
|
||||||
f"Subscription missing or invalid for profile {self.profile_data['id']}", True)
|
self._emit_subscription_lookup_failure(exception=e)
|
||||||
|
else:
|
||||||
|
self.change_page.emit(
|
||||||
|
f"Subscription missing or invalid for profile {self.profile_data['id']}", True)
|
||||||
except ProfileActivationError:
|
except ProfileActivationError:
|
||||||
self.update_signal.emit(
|
self.update_signal.emit(
|
||||||
"The profile could not be enabled", False, None, None, None)
|
"The profile could not be enabled", False, None, None, None)
|
||||||
|
|
@ -114,6 +145,12 @@ class Worker(QObject):
|
||||||
"PROFILE_STATE_CONFLICT_ERROR", False, self.profile_data['id'], None, None)
|
"PROFILE_STATE_CONFLICT_ERROR", False, self.profile_data['id'], None, None)
|
||||||
except CommandNotFoundError as e:
|
except CommandNotFoundError as e:
|
||||||
self.update_signal.emit(str(e.subject), False, -1, None, None)
|
self.update_signal.emit(str(e.subject), False, -1, None, None)
|
||||||
|
except SudoScript as e:
|
||||||
|
self.update_signal.emit(str(e), False, None, None, None)
|
||||||
|
except MissingPreReqs as e:
|
||||||
|
self.update_signal.emit(str(e), False, None, None, None)
|
||||||
|
except FirewallError as e:
|
||||||
|
self.update_signal.emit(str(e), False, None, None, None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
self.update_signal.emit(
|
self.update_signal.emit(
|
||||||
|
|
@ -123,8 +160,26 @@ class Worker(QObject):
|
||||||
self.update_signal.emit(
|
self.update_signal.emit(
|
||||||
f"No profile found with ID: {self.profile_data['id']}", False, None, None, None)
|
f"No profile found with ID: {self.profile_data['id']}", False, None, None, None)
|
||||||
|
|
||||||
def _location_candidates(self, preferred=None):
|
def _profile_incomplete_reason(self):
|
||||||
return location_candidates(self.profile, preferred)
|
if self.profile is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
connection = getattr(self.profile, 'connection', None)
|
||||||
|
if not getattr(connection, 'code', None):
|
||||||
|
return "Profile connection data is incomplete. Sync the database and try again."
|
||||||
|
|
||||||
|
location = getattr(self.profile, 'location', None)
|
||||||
|
if location is None or isinstance(location, dict):
|
||||||
|
return "Profile location data is incomplete. Sync the database and try again."
|
||||||
|
|
||||||
|
if isinstance(self.profile, SessionProfile):
|
||||||
|
application_version = getattr(self.profile, 'application_version', None)
|
||||||
|
if isinstance(application_version, dict) or application_version is None:
|
||||||
|
return "Profile browser data is incomplete. Sync the database and try again."
|
||||||
|
if not getattr(application_version, 'application_code', None) or not getattr(application_version, 'version_number', None):
|
||||||
|
return "Profile browser data is incomplete. Sync the database and try again."
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def _profile_has_valid_subscription(self):
|
def _profile_has_valid_subscription(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -148,51 +203,143 @@ class Worker(QObject):
|
||||||
print(error_msg)
|
print(error_msg)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if self.profile.assassin:
|
||||||
|
print("skipping random ticket use for the assassin, as it already has it baked in.")
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
|
print("using a random ticket from GUI...")
|
||||||
which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer)
|
which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
self._emit_ticket_failure(None, exception=e)
|
||||||
return None
|
return None
|
||||||
if error_msg:
|
if error_msg:
|
||||||
self._ticket_error_emitted = True
|
self._emit_ticket_failure(
|
||||||
self.change_page.emit(f'Ticket use failed: {error_msg}', True)
|
None,
|
||||||
|
result=Result(
|
||||||
|
valid=False,
|
||||||
|
error_type=ResultError.TICKET,
|
||||||
|
message=f'Ticket use failed: {error_msg}',
|
||||||
|
),
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if which_ticket is None or which_ticket == 'error':
|
if which_ticket is None or which_ticket == 'error':
|
||||||
return None
|
return None
|
||||||
return self._consume_ticket(which_ticket, None)
|
return self._consume_ticket(which_ticket)
|
||||||
|
|
||||||
def _consume_ticket(self, which_ticket, which_location):
|
def _ticket_location_id(self):
|
||||||
candidates = self._location_candidates(preferred=which_location)
|
location = getattr(self.profile, 'location', None)
|
||||||
if not candidates:
|
location_id = getattr(location, 'id', None)
|
||||||
self._ticket_error_emitted = True
|
if location_id is None:
|
||||||
self.change_page.emit('Could not determine profile location for ticket use.', True)
|
return None
|
||||||
|
if isinstance(location_id, str):
|
||||||
|
location_id = location_id.strip()
|
||||||
|
if location_id == '':
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(location_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
last_msg = None
|
def _emit_ticket_failure(self, which_ticket, result=None, message=None, exception=None):
|
||||||
for cand in candidates:
|
|
||||||
try:
|
|
||||||
outcome = use_ticket(which_ticket, cand, ticket_observer, connection_observer)
|
|
||||||
except Exception as e:
|
|
||||||
last_msg = str(e)
|
|
||||||
continue
|
|
||||||
if not isinstance(outcome, dict):
|
|
||||||
last_msg = 'invalid_response'
|
|
||||||
continue
|
|
||||||
billing_code = outcome.get('billing_code')
|
|
||||||
if outcome.get('valid') and billing_code:
|
|
||||||
self._consumed_ticket = str(which_ticket)
|
|
||||||
return billing_code
|
|
||||||
if billing_code:
|
|
||||||
self._consumed_ticket = str(which_ticket)
|
|
||||||
return billing_code
|
|
||||||
msg = outcome.get('message', 'failed')
|
|
||||||
last_msg = msg
|
|
||||||
if msg != 'invalid_location':
|
|
||||||
self._ticket_error_emitted = True
|
|
||||||
self.change_page.emit(f'Ticket use failed: {msg}', True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
self._ticket_error_emitted = True
|
self._ticket_error_emitted = True
|
||||||
self.change_page.emit(f'Ticket use failed: {last_msg or "no valid location"}', True)
|
if exception is not None:
|
||||||
|
result = result_from_exception(exception)
|
||||||
|
elif result is None:
|
||||||
|
result = Result(
|
||||||
|
valid=False,
|
||||||
|
error_type=ResultError.UNKNOWN,
|
||||||
|
message=message or 'Ticket use failed.',
|
||||||
|
)
|
||||||
|
self._emit_operation_failure(result, which_ticket=which_ticket)
|
||||||
|
|
||||||
|
def _emit_operation_failure(self, result, **kwargs):
|
||||||
|
self._pending_operation_result = result
|
||||||
|
self._pending_operation_kwargs = {
|
||||||
|
key: value for key, value in kwargs.items()
|
||||||
|
if value is not None
|
||||||
|
}
|
||||||
|
self.operation_failed.emit(result)
|
||||||
|
|
||||||
|
def handle_operation_popup_choice(self, accepted):
|
||||||
|
if not accepted:
|
||||||
|
return None
|
||||||
|
if not isinstance(self._pending_operation_result, Result):
|
||||||
|
return None
|
||||||
|
return dispatch_result_action(
|
||||||
|
self._pending_operation_result,
|
||||||
|
**self._pending_operation_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _emit_subscription_lookup_failure(self, exception=None):
|
||||||
|
message = (
|
||||||
|
"Ticket was accepted, but the subscription details could not be "
|
||||||
|
"retrieved. Check your connection and try again."
|
||||||
|
)
|
||||||
|
if exception is not None:
|
||||||
|
error_text = str(exception)
|
||||||
|
if error_text:
|
||||||
|
message = f"{message} {error_text}"
|
||||||
|
self._emit_ticket_failure(
|
||||||
|
self._consumed_ticket,
|
||||||
|
result=Result(
|
||||||
|
valid=False,
|
||||||
|
error_type=ResultError.CONNECTION,
|
||||||
|
message=message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _consume_ticket(self, which_ticket):
|
||||||
|
which_location = self._ticket_location_id()
|
||||||
|
if which_location is None:
|
||||||
|
self._emit_ticket_failure(
|
||||||
|
which_ticket,
|
||||||
|
result=Result(
|
||||||
|
valid=False,
|
||||||
|
error_type=ResultError.MISSING_DATA,
|
||||||
|
message='Could not determine profile location for ticket use.',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
outcome = use_ticket(
|
||||||
|
which_ticket=which_ticket,
|
||||||
|
which_location=which_location,
|
||||||
|
ticket_observer=ticket_observer,
|
||||||
|
connection_observer=connection_observer,
|
||||||
|
profile=self.profile
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._emit_ticket_failure(which_ticket, exception=e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(outcome, Result):
|
||||||
|
self._emit_ticket_failure(
|
||||||
|
which_ticket,
|
||||||
|
result=Result(
|
||||||
|
valid=False,
|
||||||
|
error_type=ResultError.INVALID_API_REPLY,
|
||||||
|
message=f'use_ticket returned {type(outcome).__name__}, expected Result.',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if outcome.valid:
|
||||||
|
billing_code = outcome.data
|
||||||
|
if not billing_code:
|
||||||
|
self._emit_ticket_failure(
|
||||||
|
which_ticket,
|
||||||
|
result=Result(
|
||||||
|
valid=False,
|
||||||
|
error_type=ResultError.INVALID_API_REPLY,
|
||||||
|
message='Ticket use succeeded but no billing code was returned.',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
self._consumed_ticket = str(which_ticket)
|
||||||
|
return billing_code
|
||||||
|
|
||||||
|
self._emit_ticket_failure(which_ticket, result=outcome)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def handle_profile_status(self, profile, is_enabled):
|
def handle_profile_status(self, profile, is_enabled):
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
|
# import inspect
|
||||||
|
import time
|
||||||
|
|
||||||
from PyQt6.QtCore import QThread, pyqtSignal
|
from PyQt6.QtCore import QThread, pyqtSignal
|
||||||
|
|
||||||
from core.controllers.ApplicationVersionController import ApplicationVersionController
|
from core.controllers.ApplicationVersionController import ApplicationVersionController
|
||||||
from core.controllers.ClientController import ClientController
|
from core.controllers.ClientController import ClientController
|
||||||
from core.controllers.ConfigurationController import ConfigurationController
|
from core.controllers.ConfigurationController import ConfigurationController, ConnectionChoice
|
||||||
|
|
||||||
|
from core.controllers.SyncController import new_sync
|
||||||
from core.controllers.InvoiceController import InvoiceController
|
from core.controllers.InvoiceController import InvoiceController
|
||||||
from core.controllers.LocationController import LocationController
|
from core.controllers.LocationController import LocationController
|
||||||
from core.controllers.ProfileController import ProfileController
|
from core.controllers.ProfileController import ProfileController
|
||||||
|
|
@ -14,13 +18,24 @@ from core.controllers.SubscriptionPlanController import SubscriptionPlanControll
|
||||||
from core.models.session.SessionConnection import SessionConnection
|
from core.models.session.SessionConnection import SessionConnection
|
||||||
from core.models.session.SessionProfile import SessionProfile
|
from core.models.session.SessionProfile import SessionProfile
|
||||||
from core.models.system.SystemConnection import SystemConnection
|
from core.models.system.SystemConnection import SystemConnection
|
||||||
|
from core.models.BaseProfile import ProfileType
|
||||||
from core.models.system.SystemProfile import SystemProfile
|
from core.models.system.SystemProfile import SystemProfile
|
||||||
|
from core.errors.exceptions import SudoScript, MissingPreReqs, FirewallError
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
|
from core.services.helpers.install_dependencies import setup_singbox_binary as install_singbox_binary
|
||||||
|
|
||||||
|
from gui.v2.actions.disable_profiles import (
|
||||||
|
filter_profiles_by_type,
|
||||||
|
disable_profile_via_controller
|
||||||
|
)
|
||||||
|
|
||||||
from gui.v2.infrastructure.setup_observers import (
|
from gui.v2.infrastructure.setup_observers import (
|
||||||
|
application_version_observer,
|
||||||
client_observer,
|
client_observer,
|
||||||
connection_observer,
|
connection_observer,
|
||||||
invoice_observer,
|
invoice_observer,
|
||||||
profile_observer,
|
profile_observer,
|
||||||
|
ticket_observer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -44,6 +59,18 @@ class WorkerThread(QThread):
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
self.is_disabling = False
|
self.is_disabling = False
|
||||||
|
|
||||||
|
# def _disable_profile(self, profile):
|
||||||
|
# kwargs = {
|
||||||
|
# 'profile_observer': profile_observer,
|
||||||
|
# 'ticket_observer': ticket_observer,
|
||||||
|
# 'connection_observer': connection_observer,
|
||||||
|
# }
|
||||||
|
# supported = inspect.signature(ProfileController.disable).parameters
|
||||||
|
# ProfileController.disable(
|
||||||
|
# profile,
|
||||||
|
# **{key: value for key, value in kwargs.items() if key in supported}
|
||||||
|
# )
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if self.action == 'LIST_PROFILES':
|
if self.action == 'LIST_PROFILES':
|
||||||
self.list_profiles()
|
self.list_profiles()
|
||||||
|
|
@ -63,6 +90,8 @@ class WorkerThread(QThread):
|
||||||
self.disable_all_profiles()
|
self.disable_all_profiles()
|
||||||
elif self.action == 'INSTALL_PACKAGE':
|
elif self.action == 'INSTALL_PACKAGE':
|
||||||
self.install_package()
|
self.install_package()
|
||||||
|
elif self.action == 'SETUP_SINGBOX_BINARY':
|
||||||
|
self.setup_singbox_binary()
|
||||||
elif self.action == 'CHECK_FOR_UPDATE':
|
elif self.action == 'CHECK_FOR_UPDATE':
|
||||||
self.check_for_update()
|
self.check_for_update()
|
||||||
elif self.action == 'DOWNLOAD_UPDATE':
|
elif self.action == 'DOWNLOAD_UPDATE':
|
||||||
|
|
@ -95,8 +124,7 @@ class WorkerThread(QThread):
|
||||||
|
|
||||||
def check_for_update(self):
|
def check_for_update(self):
|
||||||
self.text_output.emit("Checking for updates...")
|
self.text_output.emit("Checking for updates...")
|
||||||
ClientController.sync(client_observer=client_observer,
|
new_sync(client_observer=client_observer, connection_observer=connection_observer)
|
||||||
connection_observer=connection_observer)
|
|
||||||
update_available = ClientController.can_be_updated()
|
update_available = ClientController.can_be_updated()
|
||||||
if update_available:
|
if update_available:
|
||||||
self.text_output.emit("An update is available. Downloading...")
|
self.text_output.emit("An update is available. Downloading...")
|
||||||
|
|
@ -119,20 +147,72 @@ class WorkerThread(QThread):
|
||||||
f"An error occurred when installing {self.package_name}: {e}")
|
f"An error occurred when installing {self.package_name}: {e}")
|
||||||
self.finished.emit(False)
|
self.finished.emit(False)
|
||||||
|
|
||||||
def disable_all_profiles(self):
|
def setup_singbox_binary(self):
|
||||||
|
connection_error = "Connection problems downloading Singbox or related data. Please disable Tor or try again with a better connection."
|
||||||
try:
|
try:
|
||||||
for profile_id in self.profile_data:
|
setup_result = install_singbox_binary(application_version_observer, connection_observer)
|
||||||
profile = ProfileController.get(int(profile_id))
|
except ConnectionError as e:
|
||||||
if isinstance(profile, SessionProfile):
|
self.text_output.emit(f"{connection_error}: {str(e)}")
|
||||||
ProfileController.disable(
|
self.finished.emit(False)
|
||||||
profile, ignore=True, profile_observer=profile_observer)
|
return
|
||||||
for profile_id in self.profile_data:
|
except ValueError as e:
|
||||||
profile = ProfileController.get(int(profile_id))
|
self.text_output.emit(f"Your configuration files may be corrupted, or a server-side error gave bad data: {str(e)}")
|
||||||
if isinstance(profile, SystemProfile):
|
self.finished.emit(False)
|
||||||
ProfileController.disable(
|
return
|
||||||
profile, ignore=True, profile_observer=profile_observer)
|
except Exception as e:
|
||||||
|
self.text_output.emit(f"Unknown error: {str(e)}")
|
||||||
|
self.finished.emit(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
if setup_result.valid:
|
||||||
|
self.text_output.emit("Setup done. You're all set to proceed with Singbox.")
|
||||||
|
self.finished.emit(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
messages = {
|
||||||
|
ResultError.NEED_SYNC: "You must sync to find out which Singbox version is supported.",
|
||||||
|
ResultError.FILE_SYSTEM: "Please check the configuration file, disk space, permissions, and filesystem health.",
|
||||||
|
ResultError.CONNECTION: connection_error,
|
||||||
|
ResultError.INVALID_INPUT: "This is a rare bug. Check the error logs, then run the program again from the terminal with DEBUG=true.",
|
||||||
|
ResultError.PERMISSION: "Singbox requires sudo for setup. After that, the wrapper allows it to run without sudo on an ongoing basis.",
|
||||||
|
ResultError.MISSING_FILE: "Singbox download or file setup did not complete. Please try again.",
|
||||||
|
ResultError.UNKNOWN: f"Unknown error: {setup_result.message}",
|
||||||
|
}
|
||||||
|
self.text_output.emit(messages.get(setup_result.error_type, setup_result.message or "Unknown Singbox setup error"))
|
||||||
|
self.finished.emit(False)
|
||||||
|
|
||||||
|
def disable_all_profiles(self):
|
||||||
|
"""
|
||||||
|
Purpose:
|
||||||
|
Loop through all profiles in the class data,
|
||||||
|
Classify them, and disable them.
|
||||||
|
Why:
|
||||||
|
Session profiles must be disabled first before System profiles.
|
||||||
|
Called by:
|
||||||
|
Same Class run()
|
||||||
|
"""
|
||||||
|
session_profiles, system_profiles = filter_profiles_by_type(self.profile_data)
|
||||||
|
try:
|
||||||
|
# SESSION
|
||||||
|
for profile in session_profiles:
|
||||||
|
disable_profile_via_controller(profile)
|
||||||
|
print("finished with session profiles. now moving onto session profiles")
|
||||||
|
|
||||||
|
if session_profiles and system_profiles:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# SYSTEM
|
||||||
|
for profile in system_profiles:
|
||||||
|
disable_profile_via_controller(profile)
|
||||||
self.text_output.emit("All profiles were successfully disabled")
|
self.text_output.emit("All profiles were successfully disabled")
|
||||||
except Exception:
|
except SudoScript as e:
|
||||||
|
self.text_output.emit(str(e))
|
||||||
|
except FirewallError as e:
|
||||||
|
self.text_output.emit(str(e))
|
||||||
|
except MissingPreReqs as e:
|
||||||
|
self.text_output.emit(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {str(e)}")
|
||||||
self.text_output.emit("An error occurred when disabling profile")
|
self.text_output.emit("An error occurred when disabling profile")
|
||||||
finally:
|
finally:
|
||||||
self.finished.emit(True)
|
self.finished.emit(True)
|
||||||
|
|
@ -143,7 +223,7 @@ class WorkerThread(QThread):
|
||||||
|
|
||||||
if profile is not None:
|
if profile is not None:
|
||||||
try:
|
try:
|
||||||
ProfileController.destroy(profile)
|
ProfileController.destroy(profile, profile_observer, ticket_observer, connection_observer)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_name = type(e).__name__
|
error_name = type(e).__name__
|
||||||
error_text = str(e) or 'Unknown deletion error'
|
error_text = str(e) or 'Unknown deletion error'
|
||||||
|
|
@ -158,12 +238,19 @@ class WorkerThread(QThread):
|
||||||
self.finished.emit(False)
|
self.finished.emit(False)
|
||||||
|
|
||||||
def list_profiles(self):
|
def list_profiles(self):
|
||||||
profiles = ProfileController.get_all()
|
try:
|
||||||
|
profiles = ProfileController.get_all()
|
||||||
|
except Exception:
|
||||||
|
profiles = {}
|
||||||
|
self.text_output.emit("Could not load profiles. Sync or restart and try again.")
|
||||||
self.profiles_output.emit(profiles)
|
self.profiles_output.emit(profiles)
|
||||||
|
|
||||||
def create_profile(self, profile_type):
|
def create_profile(self, profile_type):
|
||||||
location = LocationController.get(
|
try:
|
||||||
self.profile_data['country_code'], self.profile_data['code'])
|
location = LocationController.get(
|
||||||
|
self.profile_data['country_code'], self.profile_data['code'])
|
||||||
|
except Exception:
|
||||||
|
location = None
|
||||||
if location is None:
|
if location is None:
|
||||||
self.text_output.emit(
|
self.text_output.emit(
|
||||||
f"Invalid location code: {self.profile_data['location_code']}")
|
f"Invalid location code: {self.profile_data['location_code']}")
|
||||||
|
|
@ -178,8 +265,11 @@ class WorkerThread(QThread):
|
||||||
|
|
||||||
application_details = self.profile_data['application'].split(
|
application_details = self.profile_data['application'].split(
|
||||||
':', 1)
|
':', 1)
|
||||||
application_version = ApplicationVersionController.get(
|
try:
|
||||||
application_details[0], application_details[1] if len(application_details) > 1 else None)
|
application_version = ApplicationVersionController.get(
|
||||||
|
application_details[0], application_details[1] if len(application_details) > 1 else None)
|
||||||
|
except Exception:
|
||||||
|
application_version = None
|
||||||
if application_version is None:
|
if application_version is None:
|
||||||
self.text_output.emit(
|
self.text_output.emit(
|
||||||
f"Invalid application: {self.profile_data['application']}")
|
f"Invalid application: {self.profile_data['application']}")
|
||||||
|
|
@ -189,12 +279,23 @@ class WorkerThread(QThread):
|
||||||
resolution = self.profile_data['resolution']
|
resolution = self.profile_data['resolution']
|
||||||
connection = SessionConnection(connection_type, mask_connection)
|
connection = SessionConnection(connection_type, mask_connection)
|
||||||
profile = SessionProfile(
|
profile = SessionProfile(
|
||||||
profile_id, name, None, location, resolution, application_version, connection)
|
id=profile_id,
|
||||||
|
name=name,
|
||||||
|
subscription=None,
|
||||||
|
type=ProfileType.SESSION,
|
||||||
|
location=location,
|
||||||
|
resolution=resolution,
|
||||||
|
application_version=application_version,
|
||||||
|
connection=connection)
|
||||||
elif profile_type == "system":
|
elif profile_type == "system":
|
||||||
connection = SystemConnection(connection_type)
|
connection = SystemConnection(connection_type)
|
||||||
profile = SystemProfile(
|
profile = SystemProfile(
|
||||||
profile_id, name, None, location, connection)
|
id=profile_id,
|
||||||
|
name=name,
|
||||||
|
type=ProfileType.SYSTEM,
|
||||||
|
subscription=None,
|
||||||
|
location=location,
|
||||||
|
connection=connection)
|
||||||
else:
|
else:
|
||||||
self.text_output.emit(f"Invalid profile type: {profile_type}")
|
self.text_output.emit(f"Invalid profile type: {profile_type}")
|
||||||
return
|
return
|
||||||
|
|
@ -212,11 +313,16 @@ class WorkerThread(QThread):
|
||||||
try:
|
try:
|
||||||
profile = ProfileController.get(int(self.profile_data['id']))
|
profile = ProfileController.get(int(self.profile_data['id']))
|
||||||
if profile:
|
if profile:
|
||||||
ProfileController.disable(
|
disable_profile_via_controller(profile)
|
||||||
profile, profile_observer=profile_observer)
|
|
||||||
else:
|
else:
|
||||||
self.text_output.emit(
|
self.text_output.emit(
|
||||||
f"No profile found with ID: {self.profile_data['id']}")
|
f"No profile found with ID: {self.profile_data['id']}")
|
||||||
|
except SudoScript as e:
|
||||||
|
self.text_output.emit(str(e))
|
||||||
|
except FirewallError as e:
|
||||||
|
self.text_output.emit(str(e))
|
||||||
|
except MissingPreReqs as e:
|
||||||
|
self.text_output.emit(str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.text_output.emit("An error occurred when disabling profile")
|
self.text_output.emit("An error occurred when disabling profile")
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -229,8 +335,12 @@ class WorkerThread(QThread):
|
||||||
else:
|
else:
|
||||||
ConfigurationController.set_connection('system')
|
ConfigurationController.set_connection('system')
|
||||||
self.check_for_update()
|
self.check_for_update()
|
||||||
|
|
||||||
locations = LocationController.get_all()
|
locations = LocationController.get_all()
|
||||||
|
|
||||||
|
|
||||||
browser = ApplicationVersionController.get_all()
|
browser = ApplicationVersionController.get_all()
|
||||||
|
# print('the browser is: ', browser)
|
||||||
all_browser_versions = [
|
all_browser_versions = [
|
||||||
f"{browser.application_code}:{browser.version_number}" for browser in browser if browser.supported]
|
f"{browser.application_code}:{browser.version_number}" for browser in browser if browser.supported]
|
||||||
all_location_codes = [
|
all_location_codes = [
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "sp-hydra-veil-gui"
|
name = "sp-hydra-veil-gui"
|
||||||
version = "2.3.1"
|
version = "2.4.8"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Simplified Privacy" },
|
{ name = "Simplified Privacy" },
|
||||||
]
|
]
|
||||||
|
|
@ -12,7 +12,7 @@ classifiers = [
|
||||||
"Operating System :: POSIX :: Linux",
|
"Operating System :: POSIX :: Linux",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sp-hydra-veil-core == 2.3.4",
|
"sp-hydra-veil-core == 2.6.5",
|
||||||
"pyperclip ~= 1.9.0",
|
"pyperclip ~= 1.9.0",
|
||||||
"pyqt6 ~= 6.7.1",
|
"pyqt6 ~= 6.7.1",
|
||||||
"qrcode[pil] ~= 8.2"
|
"qrcode[pil] ~= 8.2"
|
||||||
|
|
|
||||||