forked from Support/sp-hydra-veil-gui
New sync workflow. Sync goes to a new function. Forced DB Sync at startup with an empty database
This commit is contained in:
parent
b502f9130d
commit
1d886728e0
5 changed files with 590 additions and 64 deletions
179
gui/__main__.py
179
gui/__main__.py
|
|
@ -6,12 +6,15 @@ from core.Constants import Constants
|
||||||
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
|
from core.models.DatabaseOperation import DatabaseOperation, DBErrorType
|
||||||
from core.models.manage.migrations import migrate_sql
|
from core.models.manage.migrations import migrate_sql
|
||||||
from core.models.manage.clear_sql_model import clear_sql_model
|
from core.models.manage.clear_sql_model import clear_sql_model
|
||||||
|
from core.controllers.SyncController import new_sync
|
||||||
|
from essentials.observers.ConnectionObserver import ConnectionObserver
|
||||||
|
from core.observers.ClientObserver import ClientObserver
|
||||||
|
|
||||||
# generic
|
# generic
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# UTIL FUNCTIONS AND RECOVERY UI
|
# UTIL FUNCTIONS AND RECOVERY UI
|
||||||
|
|
@ -65,6 +68,43 @@ def recovery_dialog(custom_error, db_version_you_have):
|
||||||
logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.")
|
logger.error(f"[DB MANAGEMENT] User opted to close the app WITHOUT wiping the database, even though they need to.")
|
||||||
sys.exit()
|
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 or option == 2:
|
||||||
|
# # setup the function to do:
|
||||||
|
do_this_function = partial(new_sync, client_observer, connection_observer)
|
||||||
|
|
||||||
|
sync_worked = show_terminal(
|
||||||
|
do_this_function=do_this_function,
|
||||||
|
connection_observer=connection_observer,
|
||||||
|
client_observer=client_observer
|
||||||
|
)
|
||||||
|
|
||||||
|
if sync_worked:
|
||||||
|
print("it worked")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# BEGIN PROGRAM
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ASSETS FOLDER
|
# ASSETS FOLDER
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -100,8 +140,28 @@ except:
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
migration_happened = False
|
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
|
||||||
|
start_ui(force_sync=False)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LEGACY VERSION. MIGRATE
|
||||||
|
# ============================================================================
|
||||||
# are they upgrading from a legacy version? that would mean the table existed, but not the version table,
|
# are they upgrading from a legacy version? that would mean the table existed, but not the version table,
|
||||||
if not version_table_exists and main_db_exists:
|
if not version_table_exists:
|
||||||
logger.info(f"[DB MANAGEMENT] We are dealing with a legacy database, we need to transition the user.")
|
logger.info(f"[DB MANAGEMENT] We are dealing with a legacy database, we need to transition the user.")
|
||||||
|
|
||||||
migration = migrate_sql()
|
migration = migrate_sql()
|
||||||
|
|
@ -122,6 +182,7 @@ if not version_table_exists and main_db_exists:
|
||||||
if not version_table_exists:
|
if not version_table_exists:
|
||||||
create_ONLY_db_version_table()
|
create_ONLY_db_version_table()
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# COMPARE DB VERISONS
|
# COMPARE DB VERISONS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -131,66 +192,9 @@ 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}")
|
logger.info(f"[DB MANAGEMENT] The result of the check is {is_compatable} and the reason is {reason}")
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# IF THE VERSIONS MATCH
|
# IF the versions do NOT match
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
if is_compatable:
|
if not is_compatable:
|
||||||
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.")
|
|
||||||
|
|
||||||
# Load GUI either way:
|
|
||||||
from gui.main_ui import start_ui
|
|
||||||
|
|
||||||
# If the user lacks a database, we want to force them to sync the new data, to avoid NoneType errors when enabling existing profiles,
|
|
||||||
if reason == "no_database":
|
|
||||||
logger.info(f"[DB MANAGEMENT] User had no database to begin with, so now we're entering GUI with sync on..")
|
|
||||||
force_sync = True
|
|
||||||
else:
|
|
||||||
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)
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# but IF the versions do NOT match
|
|
||||||
# ============================================================================
|
|
||||||
else:
|
|
||||||
logger.error(f"[DB MANAGEMENT] The App is expecting a different DB version than the real database. The reason is {reason}")
|
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")
|
db_version_you_have = compatability_dict.get("old_db_version", "Error getting the Version")
|
||||||
|
|
||||||
|
|
@ -202,4 +206,57 @@ else:
|
||||||
|
|
||||||
# THEN DISPLAY CHOICES AND RECOVERY UI
|
# THEN DISPLAY CHOICES AND RECOVERY UI
|
||||||
recovery_dialog(custom_error, db_version_you_have)
|
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.")
|
||||||
|
|
||||||
|
# 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
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']
|
||||||
163
gui/v2/ui/popups/generic_choice.py
Normal file
163
gui/v2/ui/popups/generic_choice.py
Normal 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)
|
||||||
281
gui/v2/ui/popups/terminal_threading.py
Normal file
281
gui/v2/ui/popups/terminal_threading.py
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
from essentials.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
|
||||||
|
|
||||||
|
|
||||||
|
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
|
||||||
|
|
||||||
|
import threading
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
# mainthread = ThreadSafetyTool()
|
||||||
|
|
||||||
|
|
||||||
|
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("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()
|
||||||
|
|
@ -5,7 +5,9 @@ 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
|
||||||
|
|
@ -98,8 +100,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...")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue