From d7a1cd056948c6590a2a4fdfd059e603c23a89fc Mon Sep 17 00:00:00 2001 From: Kleidis <167202775+kleidis@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:05:59 +0200 Subject: [PATCH] [VERY WIP] refactor: Use lazy importing throughout the codebase --- main.py | 157 +++++++++++++------------------------------ ui/install_page.py | 14 ++-- ui/progress_bar.py | 4 +- ui/selection_page.py | 84 ++++++++++++----------- ui/ui_main.py | 137 +++++++++++++++++++++++++++++++++++++ ui/welcome.py | 43 ++++++++++++ 6 files changed, 279 insertions(+), 160 deletions(-) create mode 100644 ui/ui_main.py create mode 100644 ui/welcome.py diff --git a/main.py b/main.py index 49fd8a5..46cb2ed 100644 --- a/main.py +++ b/main.py @@ -1,58 +1,46 @@ -from imports import * -from ui_init import * - -class Online: - def init (self): - self.troppical_database = self.fetch_data() - - def fetch_data(self): - url = "https://raw.githubusercontent.com/kleidis/test/main/troppical-data.json" - response = requests.get(url) - if response.status_code == 200: - all_data = response.json() - self.troppical_database = [item for item in all_data if item.get('emulator_platform') != 'android'] - return self.troppical_database - else: - print("Failed to fetch data:", response.status_code) - - def get_latest_git_tag(): - tag = "1.0" - github_token = os.getenv("GITHUB_TOKEN", "") - try: - command = f"GH_TOKEN={github_token} gh release list --limit 1 --json tagName --jq '.[0].tagName'" - process = subprocess.Popen(['bash', '-c', command], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = process.communicate() - if process.returncode == 0: - tag = out.decode('utf-8').strip() - if tag.startswith("v"): - tag = tag[1:] - else: - print(f"Failed to get latest GitHub release tag: {err.decode('utf-8')}") - except Exception as e: - print(f"Failed to get latest GitHub release tag: {e}") - return tag - -class Logic: +from PyQt6.QtWidgets import QApplication, QMessageBox, QFileDialog, QInputDialog +from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot +import requests +import os +import subprocess +import sys +from zipfile import ZipFile +import shutil +import tempfile +from icons import styledark_rc +import win32com.client +import winreg +from pathlib import Path +from init_instances import inst + +class Main(): def __init__(self): self.regvalue = None self.install_mode = None self.emulator = None + def initialize_app(self): + version = inst.online.get_latest_git_tag() + app = QApplication(sys.argv) + ui_main = inst.ui + ui_main.show() + sys.exit(app.exec()) + # Set which emulator to use for the installer depeanding on the selected emulator - def set_emulator(self): - selected_item = MainWindow.instances().selection_page.emulatorTreeWidget.currentItem() - print (selected_item) + def set_emulator(self, selection_page): + selected_item = selection_page.emulatorTreeWidget.currentItem() + print(selected_item) if not selected_item or not selected_item.parent(): - QMessageBox.warning(SelectionPage.window, "Selection Error", "Please select an emulator.") - print ("Please select an emulator.") + QMessageBox.warning(selection_page.window, "Selection Error", "Please select an emulator.") + print("Please select an emulator.") return emulator_name = selected_item.text(0) if self.emulator != emulator_name: # Clear previous emulator settings - qtui.labeldown.setText("Downloading: ") - qtui.labelext.setText("Extracting: ") - qtui.welcomerLabel.setText("") + inst.progress_bar_page_instance.labeldown.setText("Downloading: ") + inst.ui.labelext.setText("Extracting: ") + inst.ui.welcomerLabel.setText("") # Set new emulator self.emulator = emulator_name @@ -64,48 +52,27 @@ def set_emulator(self): # Update UI components with new emulator settings reg_result = self.checkreg() installed_emulator = "Not Installed" if reg_result is None else reg_result[1] - qtui.installationPathLineEdit.setText(os.path.join(os.environ['LOCALAPPDATA'], self.emulator)) - qtui.labeldown.setText("Downloading: " + self.emulator) - qtui.labelext.setText("Extracting: " + self.emulator) - qtui.welcomerLabel.setText(f'Your currently selected emulator is {self.emulator} and current version is {installed_emulator}.') + inst.ui.installationPathLineEdit.setText(os.path.join(os.environ['LOCALAPPDATA'], self.emulator)) + inst.ui.labeldown.setText("Downloading: " + self.emulator) + inst.ui.labelext.setText("Extracting: " + self.emulator) + inst.ui.welcomerLabel.setText(f'Your currently selected emulator is {self.emulator} and current version is {installed_emulator}.') - print (self.emulator) + print(self.emulator) self.checkreg() self.disable_qt_buttons_if_installed() - qtui.layout.setCurrentIndex(1) + inst.ui.layout.setCurrentIndex(1) # Disable buttons depanding on if it the program is already installed def disable_qt_buttons_if_installed(self): regvalue = self.checkreg() if regvalue is None: - qtui.installButton.setEnabled(True) - qtui.updateButton.setEnabled(False) - qtui.uninstallButton.setEnabled(False) + inst.ui.installButton.setEnabled(True) + inst.ui.updateButton.setEnabled(False) + inst.ui.uninstallButton.setEnabled(False) else: - qtui.installButton.setEnabled(False) - qtui.updateButton.setEnabled(True) - qtui.uninstallButton.setEnabled(True) - - # Button clinking function - def qt_button_click(self): - button = qtui.sender() - if button is qtui.installButton: - self.install_mode = "Install" - qtui.layout.setCurrentIndex(2) - self.Add_releases_to_combobox() - elif button is qtui.updateButton: - self.emulator_updates() - elif button is qtui.uninstallButton: - self.install_mode = "Uninstall" # Unused for now - self.uninstall() - if button is qtui.install_emu_button: - qtui.layout.setCurrentIndex(3) - self.Prepare_Download() - if button is qtui.backButton: - current_index = qtui.layout.currentIndex() - if current_index > 0: - qtui.layout.setCurrentIndex(current_index - 1) - return self.install_mode + inst.ui.installButton.setEnabled(False) + inst.ui.updateButton.setEnabled(True) + inst.ui.uninstallButton.setEnabled(True) # Select installation path function def InstallPath(self): @@ -394,40 +361,6 @@ def uninstall(self): QMessageBox.critical(qtui, "Error",("Failed to read the registry key. Try and reinstall again!")) qtui.layout.setCurrentIndex(1) -# Download Worker class to download the files -class DownloadWorker(QThread): - progress = pyqtSignal(int) - - def __init__(self, url, dest): - super().__init__() - self.url = url - self.dest = dest - - - @pyqtSlot() - def do_download(self): - try: - response = requests.get(self.url, stream=True) - total_size = int(response.headers.get('content-length', 0)) - if total_size == 0: - print("The content-length of the response is zero.") - return - - downloaded_size = 0 - with open(self.dest, 'wb') as file: - for data in response.iter_content(1024): - downloaded_size += len(data) - file.write(data) - progress_percentage = (downloaded_size / total_size) * 100 - self.progress.emit(int(progress_percentage)) - self.finished.emit() - except Exception as e: - QMessageBox.critical(QtUi, "Error",("Error doing download.")) - self.finished.emit() - if __name__ == "__main__": - version = Online.get_latest_git_tag() - app = QApplication(sys.argv) - ui_window = MainWindow() - ui_window.show() - sys.exit(app.exec()) + main = Main() + main.initialize_app() diff --git a/ui/install_page.py b/ui/install_page.py index b61e8be..cdbfb66 100644 --- a/ui/install_page.py +++ b/ui/install_page.py @@ -1,7 +1,7 @@ - # Install page -from imports import * -from main import Logic as main -from header import Header +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QHBoxLayout, QGroupBox, QComboBox, QCheckBox, QLineEdit +from PyQt6.QtCore import Qt +from init_instances import inst +from ui.header import Header class InstallPage(QWidget): def __init__(self): @@ -23,10 +23,8 @@ def __init__(self): self.startMenuShortcutCheckbox = QCheckBox("Create a start menu shortcut") self.installationPathLineEdit = QLineEdit() # Browse for installation path widget self.browseButton = QPushButton("Browse") - self.browseButton.clicked.connect(main.InstallPath) - self.install_emu_button = QPushButton('Install') # Install button - self.install_emu_button.clicked.connect(main.qt_button_click) - ## Add widgets / layouts + self.browseButton.clicked.connect(inst.main.InstallPath) + self.install_emu_button = QPushButton('Install') # Install button ## Add widgets / layouts installLayout.addLayout(self.header) ### Icon self.header installLayout.addWidget(InstalOpt) ### Instalation Option Label installLayout.addWidget(self.installationSourceComboBox) ## Install Sorce Widget diff --git a/ui/progress_bar.py b/ui/progress_bar.py index 44575ba..e2b1349 100644 --- a/ui/progress_bar.py +++ b/ui/progress_bar.py @@ -1,5 +1,5 @@ -from imports import * -from header import Header +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QProgressBar, QLabel +from ui.header import Header # Progress bar page class ProgressBarPage(QWidget): diff --git a/ui/selection_page.py b/ui/selection_page.py index a628767..7aa2162 100644 --- a/ui/selection_page.py +++ b/ui/selection_page.py @@ -1,13 +1,13 @@ -from imports import * -from main import Online -from main import Logic +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QGroupBox, QTreeWidget, QTreeWidgetItem, QPushButton, QMessageBox +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import Qt +from init_instances import inst - # Emulator Select Page +# Emulator Select Page class SelectionPage(QWidget): def __init__(self): super().__init__() - self.troppical_database = Online.fetch_data(self) self.emulatorSelectPage = QWidget() emulatorSelectLayout = QVBoxLayout() emulatorSelectGroup = QGroupBox("Select your emulator from the list") @@ -20,35 +20,48 @@ def __init__(self): emulatorSelectGroupLayout.addWidget(self.emulatorTreeWidget) - # Keep track of emulator systems - system_items = {} - - for troppical_api_data in self.troppical_database: - emulator_system = troppical_api_data['emulator_system'] - emulator_name = troppical_api_data['emulator_name'] - emulator_desc = troppical_api_data.get('emulator_desc', '') - - # Fetch and decode the logo - logo_url = troppical_api_data['emulator_logo'] - response = requests.get(logo_url) - if response.status_code == 200: - image_bytes = response.content - qimage = QImage.fromData(QByteArray(image_bytes)) - pixmap = QPixmap.fromImage(qimage).scaled(32, 32, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) - icon = QIcon(pixmap) - print(logo_url) - else: - QMessageBox.critical(self, "Failed to fetch logo", f"Failed to fetch logo for {emulator_name}. Status code: {response.status_code}") - icon = QIcon() + # Set layout for the group and add to the main layout + emulatorSelectGroup.setLayout(emulatorSelectGroupLayout) + emulatorSelectLayout.addWidget(emulatorSelectGroup) + self.emulatorSelectPage.setLayout(emulatorSelectLayout) # Set the layout for the emulator selection page + + # Next button to confirm selection + self.nextButton = QPushButton("Next") + self.nextButton.clicked.connect(lambda: inst.main_instance.set_emulator(self)) + emulatorSelectLayout.addWidget(self.nextButton) + + # Show initializing message + self.initializing_msg = QMessageBox(self) + self.initializing_msg.setWindowTitle("Troppical API") + self.initializing_msg.setText("Getting emulator data...") + self.initializing_msg.setStandardButtons(QMessageBox.StandardButton.NoButton) + self.initializing_msg.show() + + # Start the secondary thread with the task and callback + inst.ui.start_secondary_thread(inst.online.filter_emulator_data, self.populate_emulator_tree) + + def populate_emulator_tree(self, emulator_data): + # Iterate over each emulator item and add it to the tree + for emulator_name, data in emulator_data.items(): + emulator_system = data['system'] + emulator_desc = data['description'] + icon = data['icon'] + print(f"Fetching data for {emulator_name}") # Check if the emulator system already has a tree item, if not create one - if emulator_system not in system_items: + system_item = None + for i in range(self.emulatorTreeWidget.topLevelItemCount()): + item = self.emulatorTreeWidget.topLevelItem(i) + if item.text(0) == emulator_system: + system_item = item + break + + if not system_item: system_item = QTreeWidgetItem(self.emulatorTreeWidget) system_item.setText(0, emulator_system) - system_item.setExpanded(True) # Uncollapse the category by default - system_items[emulator_system] = system_item - else: - system_item = system_items[emulator_system] + system_item.setExpanded(True) + # Make the system item unselectable + system_item.setFlags(system_item.flags() & ~Qt.ItemFlag.ItemIsSelectable) # Add the emulator to the appropriate tree item emulator_item = QTreeWidgetItem(system_item) @@ -56,21 +69,16 @@ def __init__(self): emulator_item.setIcon(0, icon) emulator_item.setToolTip(0, emulator_desc) + # Close the initializing message + self.initializing_msg.hide() + # Sort the systems and emulators alphabetically self.emulatorTreeWidget.sortItems(0, Qt.SortOrder.AscendingOrder) for i in range(self.emulatorTreeWidget.topLevelItemCount()): system_item = self.emulatorTreeWidget.topLevelItem(i) system_item.sortChildren(0, Qt.SortOrder.AscendingOrder) - # Set layout for the group and add to the main layout - emulatorSelectGroup.setLayout(emulatorSelectGroupLayout) - emulatorSelectLayout.addWidget(emulatorSelectGroup) - self.emulatorSelectPage.setLayout(emulatorSelectLayout) # Set the layout for the emulator selection page - # Next button to confirm selection - self.nextButton = QPushButton("Next") - self.nextButton.clicked.connect(Logic.set_emulator) - emulatorSelectLayout.addWidget(self.nextButton) def get_selected_emulator(self): selected_item = self.emulatorTreeWidget.currentItem() diff --git a/ui/ui_main.py b/ui/ui_main.py new file mode 100644 index 0000000..a54ec27 --- /dev/null +++ b/ui/ui_main.py @@ -0,0 +1,137 @@ +# Page imports +from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QStackedLayout, QMessageBox, QLabel +from stylesheet import Style +import requests +from PyQt6.QtGui import QIcon, QImage, QPixmap +from PyQt6.QtCore import Qt, QByteArray, QObject, QThread, pyqtSignal +from init_instances import inst + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle(f'Troppical - {"version"}') # Window name with version + self.setCentralWidget(QWidget(self)) # Set a central widget + self.layout = QStackedLayout(self.centralWidget()) # Set the layout on the central widget + self.setMaximumSize(1000, 720) # Set the maximum window size to 1280x720 + self.setMinimumSize(1000, 720) # Set the minimum window size to 800x600 + # Set the window icon + # icon_path = os.path.join(sys._MEIPASS, 'icon.ico') + # self.setWindowIcon(QIcon(icon_path)) + self.load_stylesheet() + # Initialize the first page + self.layout.addWidget(inst.wel.welcomePage) + + # Connect buttons after initialization + self.connect_buttons() + + self.shared_thread = None # Initialize shared_thread to None + + def connect_buttons(self): + # Connect buttons to the qt_button_click method + inst.wel.manageButton.clicked.connect(self.qt_button_click) + inst.act.installButton.clicked.connect(self.qt_button_click) + inst.act.updateButton.clicked.connect(self.qt_button_click) + inst.act.uninstallButton.clicked.connect(self.qt_button_click) + inst.install.install_emu_button.clicked.connect(self.qt_button_click) + inst.act.backButton.clicked.connect(self.qt_button_click) + + def widget_2_layout(self): + # Add actual pages instead of placeholders + self.layout.addWidget(inst.sel.emulatorSelectPage) + self.layout.addWidget(inst.wel.welcomePage) + self.layout.addWidget(inst.install.installPage) + self.layout.addWidget(inst.bar.progressBarPage) + self.layout.addWidget(inst.finish.finishPage) + + def initialize_page(self, index): + # Mapping of index to page attributes and instances + page_map = { + 1: ('selection_page', inst.sel, 'emulatorSelectPage'), + 2: ('welcome_page', inst.wel, 'welcomePage'), + 3: ('install_page', inst.install, 'installPage'), + 4: ('progress_bar_page', inst.bar, 'progressBarPage'), + 5: ('finish_page', inst.finish, 'finishPage') + } + + # Initialize and replace the widget for the given index + if index in page_map: + attr_name, instance, widget_name = page_map[index] + setattr(self, attr_name, instance) + widget = getattr(instance, widget_name) + self.layout.replaceWidget(self.layout.widget(index), widget) + self.layout.setCurrentIndex(index) + print(f"Page {index} initialized: {attr_name}") + + def load_stylesheet(self): + self.setStyleSheet(Style.dark_stylesheet) + + def qt_button_click(self): + self.widget_2_layout() + button = self.sender() + + # Define a mapping of buttons to their actions + button_actions = { + inst.wel.manageButton: 1, # Assuming index 1 is the manage page + inst.act.installButton: 2, + inst.act.updateButton: inst.main.emulator_updates, + inst.act.uninstallButton: self.handle_uninstall, + inst.install.install_emu_button: 3, + inst.act.backButton: self.handle_back + } + + # Execute the corresponding action if the button is in the mapping + action = button_actions.get(button) + if isinstance(action, int): + self.initialize_page(action) + elif callable(action): + action() + + def handle_manage(self): + self.layout.setCurrentIndex(1) # Assuming index 1 is the desired page + def handle_install(self): + inst.main.install_mode = "Install" + self.layout.setCurrentIndex(2) + inst.main.Add_releases_to_combobox() + def handle_uninstall(self): + inst.main.install_mode = "Uninstall" # Unused for now + inst.main.uninstall() + def handle_install_emu(self): + self.layout.setCurrentIndex(3) + inst.main.Prepare_Download() + def handle_back(self): + current_index = self.layout.currentIndex() + if current_index > 0: + self.layout.setCurrentIndex(current_index - 1) + + def start_secondary_thread(self, task, callback, *args, **kwargs): + if self.shared_thread is not None: + self.shared_thread.quit() + self.shared_thread.wait() + + self.shared_thread = QThread() + worker = inst.secondary_thread + worker.set_task(task, *args, **kwargs) + worker.moveToThread(self.shared_thread) + worker.finished.connect(callback) + self.shared_thread.started.connect(worker.run) + self.shared_thread.start() + +class Worker(QObject): + finished = pyqtSignal(dict) + + def __init__(self): + super().__init__() + self.task = None + self.args = () + self.kwargs = {} + + def set_task(self, task, *args, **kwargs): + self.task = task + self.args = args + self.kwargs = kwargs + + def run(self): + if self.task is not None: + result = self.task(*self.args, **self.kwargs) + self.finished.emit(result) diff --git a/ui/welcome.py b/ui/welcome.py new file mode 100644 index 0000000..c412742 --- /dev/null +++ b/ui/welcome.py @@ -0,0 +1,43 @@ +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QHBoxLayout +from PyQt6.QtCore import Qt +from ui.header import Header +from init_instances import inst + +# Welcome page +class InitPage(QWidget): + def __init__(self): + super().__init__() + self.welcomePage = QWidget() + + # Layout and groups + welcomeLayout = QVBoxLayout() + self.welcomePage.setLayout(welcomeLayout) + + # Add header + # self.Header = Header().header() # Initialize the header + # welcomeLayout.addLayout(self.Header) + + # Welcome label + welcomeLabel = QLabel("Welcome to Troppical Installer!") + welcomeLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) # Center the text horizontally + welcomeLayout.addWidget(welcomeLabel) + + # Create a horizontal layout for the buttons + buttonLayout = QHBoxLayout() + + # Manage button + self.manageButton = QPushButton("Manage") + self.manageButton.setFixedSize(200, 100) # Set size for large rectangular shape + + # Configure button + self.configureButton = QPushButton("Configure") + self.configureButton.setFixedSize(200, 100) # Set size for large rectangular shape + self.configureButton.setToolTip("WIP: Coming soon!") + self.configureButton.setEnabled(False) # Set the button to be grayed out + + # Add buttons to the layout + buttonLayout.addWidget(self.manageButton) + buttonLayout.addWidget(self.configureButton) + + # Add button layout to the main layout + welcomeLayout.addLayout(buttonLayout) \ No newline at end of file