#!/usr/bin/python3 import tkinter as tk from tkinter import ttk import os import sys import subprocess from typing import Optional, List import hashlib import getpass from datetime import datetime import tempfile import urllib.request import zipfile from manager import ( Detector, Theme, LocaleStrings, LXToolsAppConfig, System, Image, AppManager, LxTools, Locale, ) from network import NetworkChecker, GiteaUpdater, GPGManager from message import MessageDialog class InstallationManager: def __init__( self, app_manager, progress_callback=None, icon_callback=None, log_callback=None ): self.app_manager = app_manager self.progress_callback = progress_callback self.icon_callback = icon_callback self.log_callback = log_callback self.system_manager = System() self.download_manager = DownloadManager() def check_gpg(self) -> bool: if GPGManager.is_key_already_imported( LXToolsAppConfig.EXPECTED_FINGERPRINT[-8:] ): return True all_keys_valid: bool = True # 1. Check reachability of OpenPGP keyserver and process key if reachable openpgp_reachable: bool = GPGManager.is_url_reachable( LXToolsAppConfig.KEY_URL_OPENPGP ) if openpgp_reachable: self.log(LocaleStrings.MSGGPG["keyserver_reachable"]) self.update_progress(LocaleStrings.MSGGPG["keyserver_reachable"]) if not GPGManager.import_key_from_url( key_url=LXToolsAppConfig.KEY_URL_OPENPGP, expected_fingerprint=LXToolsAppConfig.EXPECTED_FINGERPRINT, filename="public_key1.asc", ): all_keys_valid = False else: self.log(LocaleStrings.MSGGPG["keyserver_unreachable"]) self.update_progress(LocaleStrings.MSGGPG["keyserver_unreachable"]) # 2. Always download and verify Git.ilunix public key if not GPGManager.import_key_from_url( key_url=LXToolsAppConfig.KEY_URL_GITILUNIX, expected_fingerprint=LXToolsAppConfig.EXPECTED_FINGERPRINT, filename="public_key2.asc", ): all_keys_valid = False # Final step: Proceed only if all fingerprints match or OpenPGP was unreachable if all_keys_valid: self.log(LocaleStrings.MSGGPG["all_keys_valid"]) self.update_progress(LocaleStrings.MSGGPG["all_keys_valid"]) key_id: str = LXToolsAppConfig.EXPECTED_FINGERPRINT[-8:] if not GPGManager.update_gpg_trust_level(key_id, trust_level=5): self.log(LocaleStrings.MSGGPG["error_updating_trust_level"], key_id) self.update_progress( LocaleStrings.MSGGPG["error_updating_trust_level"], key_id ) return False else: self.log(f"{LocaleStrings.MSGGPG['set_trust_level']}{key_id}.") self.update_progress( f"{LocaleStrings.MSGGPG['set_trust_level']}{key_id}." ) return True else: self.log(LocaleStrings.MSGGPG["not_all_keys_are_valid"]) self.update_progress(LocaleStrings.MSGGPG["not_all_keys_are_valid"]) def check_appimage(self) -> bool | None | str: """ Checks the current version of the lxtools_installer AppImage by: 1. Downloading it from Gitea (with checksum and signature verification). 2. Renaming and making it executable. 3. Comparing its SHA-256 hash with an existing reference file at `/usr/local/bin/lxtools_installer`. 4. Returns `True` if the hashes match, otherwise `False`. Returns: bool: True if AppImages are identical; False if they differ or an error occurred. """ appimage_path: Optional[str] = GiteaUpdater.download_appimage( verify_checksum=True, verify_signature=True ) tests: List[str] = [ "Checksum file not found", "Signature file not found", "Checksum mismatch", "Signature verification failed", ] if appimage_path is None or appimage_path in tests: print(f"{LocaleStrings.MSGA['error']}{appimage_path}") return None else: try: if os.path.isfile(appimage_path): new_filename = "/tmp/portinstaller/lxtools_installer" result = subprocess.run( ["mv", appimage_path, new_filename], capture_output=True, text=True, check=False, ) if result.returncode == 0: self.update_progress(LocaleStrings.MSGA["appimage_renamed"]) self.log(LocaleStrings.MSGA["appimage_renamed"]) else: self.update_progress( f"{LocaleStrings.MSGA['appimage_rename_error']}{result.stderr}" ) self.log( f"{LocaleStrings.MSGA['appimage_rename_error']}{result.stderr}" ) result = subprocess.run( ["chmod", "+x", new_filename], capture_output=True, text=True, check=False, ) if result.returncode == 0: self.update_progress( LocaleStrings.MSGA["appimage_executable_success"] ) self.log(LocaleStrings.MSGA["appimage_executable_success"]) else: self.update_progress( f"{LocaleStrings.MSGA['appimage_executable_error']}{result.stderr}" ) self.log( f"{LocaleStrings.MSGA['appimage_executable_error']}{result.stderr}" ) # Define reference path reference_appimage_path = "/usr/local/bin/lxtools_installer" if os.path.isfile(reference_appimage_path): def calculate_sha256(file_path: str) -> str: """ Calculates the SHA-256 hash of a file. Args: file_path (str): Path to the file. Returns: str: Hexdigest of the SHA-256 hash. """ hash_obj = hashlib.sha256() with open(file_path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_obj.update(chunk) return hash_obj.hexdigest() # Calculate hashes new_checksum = calculate_sha256(new_filename) reference_checksum = calculate_sha256(reference_appimage_path) # Compare and decide action if new_checksum == reference_checksum: return "True" # String as a compatibility solution to network.py else: return False else: return False else: self.update_progress(LocaleStrings.MSGA["appimage_not_exist"]) self.log(LocaleStrings.MSGA["appimage_not_exist"]) return False except Exception as e: self.update_progress(f"Exception occurred: {str(e)}") self.log(f"Exception occurred: {str(e)}") return None def install_project(self, project_key): """Install or update project""" project_info = self.app_manager.get_project_info(project_key) if not project_info: raise Exception(f"{LocaleStrings.MSG['unknow_project']}{project_key}") self.update_progress( f"{LocaleStrings.MSGI['start_install']}{project_info['name']}..." ) self.log(f"=== {LocaleStrings.MSGI['install']}{project_info['name']} ===") try: # Create installation script script_content = self._create_install_script(project_key) # Execute installation self._execute_install_script(script_content) self.update_progress( f"{project_info['name']}{LocaleStrings.MSGI['install_success']}" ) self.log( f"=== {project_info['name']}{LocaleStrings.MSGI['install_success']} ===" ) # Set success icon self.update_icon("success") except Exception as e: self.log(f"ERROR: {LocaleStrings.MSGI['install_failed']}{e}") self.update_icon("error") raise Exception(f"{LocaleStrings.MSGI['install_failed']}{e}") def _create_install_script(self, project_key): """Create installation script based on project""" if project_key == "wirepy": return self._create_wirepy_install_script() elif project_key == "logviewer": return self._create_logviewer_install_script() else: raise Exception(f"{LocaleStrings.MSGI['unknow_project']}{project_key}") def _create_wirepy_install_script(self): gpg_checker = self.check_gpg() result_appimage = self.check_appimage() if not gpg_checker: print(gpg_checker) return if result_appimage is True or result_appimage is None: return detected_os = Detector.get_os() if detected_os == "Arch Linux": result_unzip = Detector.get_unzip() result_wget = Detector.get_wget() result_requests = Detector.get_requests() else: result_unzip = None result_wget = None result_requests = None """Create Wire-Py installation script""" script = f"""#!/bin/bash set -e if [ "{result_appimage}" = "False" ]; then cp -f /tmp/portinstaller/lxtools_installer /usr/local/bin/lxtools_installer fi if [ "{detected_os}" = "Arch Linux" ]; then if [ "{result_unzip}" = "False" ]; then pacman -S --noconfirm unzip fi if [ "{result_wget}" = "False" ]; then pacman -S --noconfirm wget fi if [ "{result_requests}" = "False" ]; then pacman -S --noconfirm python-requests fi fi {LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]} 2>&1 | grep -v "apt does not have a stable CLI interface" # Create necessary directories mkdir -p {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]} mkdir -p /usr/share/icons/lx-icons mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/applications mkdir -p /usr/local/etc/ssl mkdir -p /usr/share/polkit-1/actions mkdir -p /usr/share/TK-Themes # Download and extract Wire-Py cd /tmp rm -rf wirepy_install mkdir wirepy_install cd wirepy_install echo "Downloading Wire-Py..." wget -q "{LXToolsAppConfig.WIREPY_URL}" -O wirepy.zip unzip -q wirepy.zip WIREPY_DIR=$(find . -name "wire-py" -type d | head -1) echo "Downloading shared libraries..." wget -q "{LXToolsAppConfig.SHARED_LIBS_URL}" -O shared.zip unzip -q shared.zip SHARED_DIR=$(find . -name "shared_libs" -type d | head -1) # Install Wire-Py files echo "Installing Wire-Py executables..." for file in wirepy.py start_wg.py ssl_encrypt.py ssl_decrypt.py match_found.py tunnel.py; do if [ -f "$WIREPY_DIR/$file" ]; then cp -f "$WIREPY_DIR/$file" /usr/local/bin/ chmod 755 /usr/local/bin/$file echo "Installed $file" fi done # Install config if [ -f "$WIREPY_DIR/wp_app_config.py" ]; then cp -f "$WIREPY_DIR/wp_app_config.py" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/ echo "Installed wp_app_config.py" fi # Install shared libraries echo "Installing shared libraries..." for file in common_tools.py message.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do if [ -f "$SHARED_DIR/$file" ]; then cp -f "$SHARED_DIR/$file" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/ echo "Installed shared lib: $file" fi done # Install LogViewer executable if [ -f "$SHARED_DIR/logviewer.py" ]; then cp -f "$SHARED_DIR/logviewer.py" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/ chmod 755 {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py echo "Installed logviewer.py (executable)" fi # Install icons if [ -d "$WIREPY_DIR/lx-icons" ]; then echo "Installing icons..." cp -rf "$WIREPY_DIR/lx-icons"/* /usr/share/icons/lx-icons/ fi # Install TK-Themes if [ -d "$WIREPY_DIR/TK-Themes" ]; then echo "Installing TK-Themes..." cp -rf "$WIREPY_DIR/TK-Themes"/* /usr/share/TK-Themes/ fi # Install desktop file if [ -f "$WIREPY_DIR/Wire-Py.desktop" ]; then cp -f "$WIREPY_DIR/Wire-Py.desktop" /usr/share/applications/ echo "Installed desktop file" fi # Install language files if [ -d "$WIREPY_DIR/languages/de" ]; then echo "Installing language files..." cp -f "$WIREPY_DIR/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true fi # Install policy file if [ -f "$WIREPY_DIR/org.sslcrypt.policy" ]; then cp -f "$WIREPY_DIR/org.sslcrypt.policy" /usr/share/polkit-1/actions/ echo "Installed policy file" fi # Create symlink for Wirepy ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy # Create symlink for LogViewer ln -sf {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py /usr/local/bin/logviewer echo "Created Wirepy and LogViewer symlink" # Install language files if available if [ -d "$SHARED_DIR/languages/de" ]; then echo "Installing language files..." cp "$SHARED_DIR/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true fi echo "Created symlink" # Create SSL key if not exists if [ ! -f /usr/local/etc/ssl/pwgk.pem ]; then echo "Creating SSL key..." openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 chmod 600 /usr/local/etc/ssl/pwgk.pem fi # Cleanup cd /tmp rm -rf wirepy_install portinstaller echo "Wire-Py installation completed!" """ return script def _create_logviewer_install_script(self): gpg_checker = self.check_gpg() result_appimage = self.check_appimage() if not gpg_checker: print(gpg_checker) return if result_appimage is True or result_appimage is None: return detected_os = Detector.get_os() if detected_os == "Arch Linux": result_unzip = Detector.get_unzip() result_wget = Detector.get_wget() result_requests = Detector.get_requests() else: result_unzip = None result_wget = None result_requests = None """Create LogViewer installation script""" script = f"""#!/bin/bash set -e {LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]} 2>&1 | grep -v "apt does not have a stable CLI interface" if [ "{result_appimage}" = "False" ]; then cp -f /tmp/portinstaller/lxtools_installer /usr/local/bin/lxtools_installer fi if [ "{detected_os}" = "Arch Linux" ]; then if [ "{result_unzip}" = "False" ]; then pacman -S --noconfirm unzip fi if [ "{result_wget}" = "False" ]; then pacman -S --noconfirm wget fi if [ "{result_requests}" = "False" ]; then pacman -S --noconfirm python-requests fi fi # Create necessary directories mkdir -p {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]} mkdir -p /usr/share/icons/lx-icons mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/applications mkdir -p /usr/share/TK-Themes # Download and extract shared libraries (contains LogViewer) cd /tmp rm -rf logviewer_install mkdir logviewer_install cd logviewer_install echo "Downloading LogViewer and shared libraries..." wget -q "{LXToolsAppConfig.SHARED_LIBS_URL}" -O shared.zip unzip -q shared.zip SHARED_DIR=$(find . -name "shared_libs" -type d | head -1) # Check if TK-Themes exists, if not download Wire-Py for themes if [ ! -d "/usr/share/TK-Themes" ] || [ -z "$(ls -A /usr/share/TK-Themes 2>/dev/null)" ]; then echo "TK-Themes not found, downloading from Wire-Py..." wget -q "{LXToolsAppConfig.WIREPY_URL}" -O wirepy.zip unzip -q wirepy.zip WIREPY_DIR=$(find . -name "Wire-Py" -type d | head -1) if [ -d "$WIREPY_DIR/TK-Themes" ]; then echo "Installing TK-Themes..." cp -rf "$WIREPY_DIR/TK-Themes"/* /usr/share/TK-Themes/ fi # Also install icons from Wire-Py if not present if [ -d "$WIREPY_DIR/lx-icons" ] && [ ! -d "/usr/share/icons/lx-icons" ]; then echo "Installing icons from Wire-Py..." cp -rf "$WIREPY_DIR/lx-icons"/* /usr/share/icons/lx-icons/ fi fi # Install shared libraries echo "Installing shared libraries..." for file in common_tools.py message.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do if [ -f "$SHARED_DIR/$file" ]; then cp -f "$SHARED_DIR/$file" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/ echo "Installed shared lib: $file" fi done # Install LogViewer executable if [ -f "$SHARED_DIR/logviewer.py" ]; then cp -f "$SHARED_DIR/logviewer.py" {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/ chmod 755 {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py echo "Installed logviewer.py (executable)" fi # Create symlink for LogViewer ln -sf {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py /usr/local/bin/logviewer echo "Created LogViewer symlink" # Install language files if available if [ -d "$SHARED_DIR/languages/de" ]; then echo "Installing language files..." cp "$SHARED_DIR/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true fi # Cleanup cd /tmp rm -rf logviewer_install portinstaller echo "LogViewer installation completed!" """ return script def _execute_install_script(self, script_content): """Execute installation script with pkexec""" try: with tempfile.NamedTemporaryFile( mode="w", suffix=".sh", delete=False ) as script_file: script_file.write(script_content) script_file.flush() # Make script executable os.chmod(script_file.name, 0o755) self.log(f"{LocaleStrings.MSGI['install_create']}{script_file.name}") # Execute with pkexec result = subprocess.run( ["pkexec", "bash", script_file.name], capture_output=True, text=True, timeout=300, # 5 minutes timeout ) # Log output if result.stdout: self.log((result.stdout).strip("Log\n")) if result.stderr: self.log(f"STDERR: {result.stderr}") # Clean up os.unlink(script_file.name) if result.returncode != 0: raise Exception( f"{LocaleStrings.MSGI['install_script_failed']}{result.stderr}" ) except subprocess.TimeoutExpired: raise Exception(LocaleStrings.MSGI["install_timeout"]) except subprocess.CalledProcessError as e: raise Exception(f"{LocaleStrings.MSGI['install_script_failed']}{e}") def update_progress(self, message): if self.progress_callback: self.progress_callback(message) def update_icon(self, status): if self.icon_callback: self.icon_callback(status) def log(self, message): if self.log_callback: self.log_callback(message) class UninstallationManager: def __init__(self, app_manager, progress_callback=None, log_callback=None): self.app_manager = app_manager self.progress_callback = progress_callback self.log_callback = log_callback def uninstall_project(self, project_key): """Uninstall project""" project_info = self.app_manager.get_project_info(project_key) if not project_info: raise Exception(f"{LocaleStrings.MSGO['unknow_project']}{project_key}") if not self.app_manager.is_installed(project_key): raise Exception( f"{project_info['name']}{LocaleStrings.MSGO['not_installed']}" ) self.update_progress( f"{LocaleStrings.MSGU['uninstall']}{project_info['name']}..." ) self.log(f"=== {LocaleStrings.MSGU['uninstall']}{project_info['name']} ===") try: # Create uninstallation script script_content = self._create_uninstall_script(project_key) # Execute uninstallation self._execute_uninstall_script(script_content) self.update_progress( f"{project_info['name']}{LocaleStrings.MSGU['uninstall_success']}" ) self.log( f"=== {project_info['name']}{LocaleStrings.MSGU['uninstall_success']} ===" ) except Exception as e: self.log(f"ERROR: {LocaleStrings.MSGU['uninstall_failed']}{e}") raise Exception(f"{LocaleStrings.MSGU['uninstall_failed']}{e}") def _create_uninstall_script(self, project_key): """Create uninstallation script based on project""" if project_key == "wirepy": return self._create_wirepy_uninstall_script() elif project_key == "logviewer": return self._create_logviewer_uninstall_script() else: raise Exception(f"{LocaleStrings.MSGO['unknow_project']}{project_key}") def _create_wirepy_uninstall_script(self): detected_os = Detector.get_os() """Create Wire-Py uninstallation script""" script = f"""#!/bin/bash set -e echo "=== Wire-Py Uninstallation ===" # Remove Wire-Py executables echo "Removing Wire-Py executables..." for file in wirepy.py start_wg.py ssl_encrypt.py ssl_decrypt.py match_found.py tunnel.py; do rm -f /usr/local/bin/$file echo "Removed $file" done # Remove symlink rm -f /usr/local/bin/wirepy echo "Removed wirepy symlink" # Remove config rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/wp_app_config.py echo "Removed wp_app_config.py" # Remove desktop file rm -f /usr/share/applications/Wire-Py.desktop echo "Removed desktop file" # Remove policy file rm -f /usr/share/polkit-1/actions/org.sslcrypt.policy echo "Removed policy file" # Remove language files rm -f /usr/share/locale/de/LC_MESSAGES/wirepy.mo echo "Removed language files" # Remove user config directory if [ -d /home/{getpass.getuser()}/.config/wire_py ]; then rm -rf /home/{getpass.getuser()}/.config/wire_py echo "Removed user config directory" fi # Remove ssl private key rm -rf /usr/local/etc/ssl echo "Removed ssl private key" # Remove log file rm -f /home/{getpass.getuser()}/.local/share/lxlogs/wirepy.log echo "Removed log file" # Check if LogViewer is still installed before removing shared resources if [ ! -f /usr/local/bin/logviewer ] || [ ! -L /usr/local/bin/logviewer ]; then echo "No other LX apps found, removing shared resources..." rm -rf /usr/share/icons/lx-icons rm -rf /usr/share/TK-Themes rm -rf /usr/local/bin/lxtools_installer # Remove shared libs for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py logviewer.py; do rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/$file done # Try to remove shared_libs directory if empty rmdir {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]} 2>/dev/null || true else echo "LogViewer still installed, keeping shared resources" fi echo "Wire-Py uninstallation completed!" """ return script def _create_logviewer_uninstall_script(self): detected_os = Detector.get_os() """Create LogViewer uninstallation script""" script = f"""#!/bin/bash set -e echo "=== LogViewer Uninstallation ===" # Remove LogViewer symlink rm -f /usr/local/bin/logviewer echo "Removed logviewer symlink" # Remove language files rm -f /usr/share/locale/de/LC_MESSAGES/logviewer.mo echo "Removed language files" # Remove user config directory if [ -d /home/{getpass.getuser()}/.config/logviewer ]; then rm -rf /home/{getpass.getuser()}/.config/logviewer echo "Removed user config directory" fi # Remove log file rm -f /home/{getpass.getuser()}/.local/share/lxlogs/logviewer.log echo "Removed log file" # Check if Wire-Py is still installed before removing shared resources if [ ! -f /usr/local/bin/wirepy ] || [ ! -L /usr/local/bin/wirepy ]; then echo "No other LX apps found, removing shared resources..." rm -rf /usr/share/icons/lx-icons rm -rf /usr/share/TK-Themes rm -rf /usr/local/bin/lxtools_installer # Remove shared libs (but keep logviewer.py if we're only uninstalling logviewer) for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/$file done # Remove logviewer.py last rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py # Try to remove shared_libs directory if empty rmdir LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os] 2>/dev/null || true else echo "Wire-Py still installed, keeping shared resources" # Only remove logviewer-specific files rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logview_app_config.py rm -f {LXToolsAppConfig.SHARED_LIBS_DESTINATION[detected_os]}/logviewer.py fi echo "LogViewer uninstallation completed!" """ return script def _execute_uninstall_script(self, script_content): """Execute uninstallation script with pkexec""" try: with tempfile.NamedTemporaryFile( mode="w", suffix=".sh", delete=False ) as script_file: script_file.write(script_content) script_file.flush() # Make script executable os.chmod(script_file.name, 0o755) self.log(f"{LocaleStrings.MSGU['uninstall_create']}{script_file.name}") # Execute with pkexec result = subprocess.run( ["pkexec", "bash", script_file.name], capture_output=True, text=True, timeout=120, ) # Log output if result.stdout: self.log(result.stdout) if result.stderr: self.log(f"STDERR: {result.stderr}") # Clean up os.unlink(script_file.name) if result.returncode != 0: raise Exception( f"{LocaleStrings.MSGU['uninstall_script_failed']}{result.stderr}" ) except subprocess.TimeoutExpired: raise Exception(LocaleStrings.MSGU["uninstall_timeout"]) except subprocess.CalledProcessError as e: raise Exception(f"{LocaleStrings.MSGU['uninstall_script_failed']}{e}") def update_progress(self, message): if self.progress_callback: self.progress_callback(message) def log(self, message): if self.log_callback: self.log_callback(message) class DownloadManager: @staticmethod def download_and_extract(url, extract_to, progress_callback=None): """Download and extract ZIP file""" try: if progress_callback: progress_callback(f"{LocaleStrings.MSGO['download_from']}{url}...") with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file: urllib.request.urlretrieve(url, tmp_file.name) if progress_callback: progress_callback(LocaleStrings.MSGO["extract_files"]) with zipfile.ZipFile(tmp_file.name, "r") as zip_ref: zip_ref.extractall(extract_to) os.unlink(tmp_file.name) return True except Exception as e: if progress_callback: progress_callback(f"{LocaleStrings.MSGO['download_failed']}{e}") return False class LXToolsGUI: def __init__(self): self.root = None self.notebook = None self.progress_label = None self.download_icon_label = None self.log_text = None self.selected_project = None self.polkit_ok = Detector.get_polkit() self.networkmanager_ok = Detector.get_networkmanager() self.project_frames = {} self.status_labels = {} self.version_labels = {} # Managers self.app_manager = AppManager() self.installation_manager = InstallationManager( self.app_manager, self.update_progress, self.update_download_icon, self.log_message, ) self.uninstallation_manager = UninstallationManager( self.app_manager, self.update_progress, self.log_message ) self.image_manager = Image() # Detect OS self.detected_os = Detector.get_os() # Color scheme self.colors = { "bg": "#f8f9fa", "card_bg": "#ffffff", "hover_bg": "#e3f2fd", "selected_bg": "#bbdefb", "progress_bg": "#f8f9fa", "text": "#2c3e50", "accent": "#3498db", } def create_gui(self): """Create the main GUI""" self.root = tk.Tk() self.root.title(f"{Locale.APP_NAME} v{LXToolsAppConfig.VERSION}") self.root.geometry( f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}+100+100" ) LxTools.center_window_cross_platform( self.root, LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT ) self.root.configure(bg=self.colors["bg"]) # Try to set icon try: icon = self.image_manager.load_image("download_icon") if icon: self.root.iconphoto(False, icon) except: pass self.root.minsize(LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT) Theme.apply_light_theme(self.root) # Create header self._create_header() # Create notebook (tabs) self.notebook = ttk.Notebook(self.root) self.notebook.pack(fill="both", expand=True, padx=15, pady=(10, 10)) # Create tabs self._create_projects_tab() self._create_log_tab() # Create progress section self._create_progress_section() # Create buttons self._create_modern_buttons() # Initial status refresh self.root.after(100, self.refresh_status) return self.root def _create_header(self): """Create clean header""" # HEADER header_frame = tk.Frame(self.root, bg="#2c3e50", height=70) header_frame.pack(fill="x", pady=(0, 0)) header_frame.pack_propagate(False) # Content content = tk.Frame(header_frame, bg="#2c3e50") content.pack(fill="both", expand=True, padx=15, pady=12) # LEFT SIDE: Icon + App Info left_side = tk.Frame(content, bg="#2c3e50") left_side.pack(side="left", anchor="w") icon_text_frame = tk.Frame(left_side, bg="#2c3e50") icon_text_frame.pack(anchor="w") # Tool-Icon (versuche echtes Icon, dann Fallback) try: icon = self.image_manager.load_image("header_image") if icon: # Resize icon für Header icon_label = tk.Label(icon_text_frame, image=icon, bg="#2c3e50") icon_label.image = icon # Referenz behalten icon_label.pack(side="left", padx=(0, 8)) else: raise Exception("Kein Icon gefunden") except: # Fallback: Unicode-Symbol statt Emoji tk.Label( icon_text_frame, text="⚙", font=("Helvetica", 18), bg="#2c3e50", fg="white", ).pack(side="left", padx=(0, 8)) # App Name and Version text_frame = tk.Frame(icon_text_frame, bg="#2c3e50") text_frame.pack(side="left") tk.Label( text_frame, text="Lx Tools Installer", font=("Helvetica", 14, "bold"), fg="white", bg="#2c3e50", pady=4, ).pack(anchor="w") tk.Label( text_frame, text=f"v {LXToolsAppConfig.VERSION} • {LocaleStrings.MSGO['head_string3']}", font=("Helvetica", 9), fg="#bdc3c7", bg="#2c3e50", ).pack(anchor="w") # RIGHT SIDE: System + Dynamic Status right_side = tk.Frame(content, bg="#2c3e50") right_side.pack(side="right", anchor="e") tk.Label( right_side, text=f"{LocaleStrings.MSGO['head_string2']}{self.detected_os}", font=("Helvetica", 11), fg="#ecf0f1", bg="#2c3e50", ).pack(anchor="e") # DYNAMIC Status (begin empty) self.header_status_label = tk.Label( right_side, text="", font=("Helvetica", 10), bg="#2c3e50" # begin empty ) self.header_status_label.pack(anchor="e", pady=(2, 0)) # Separator separator = tk.Frame(self.root, height=1, bg="#34495e") separator.pack(fill="x", pady=0) def update_header_status(self, message="", color="#1abc9c"): """Update status in header""" if hasattr(self, "header_status_label"): self.header_status_label.config(text=message, fg=color) def check_ready_status(self): """Check if system is ready for installation""" # Führe alle Checks durch internet_ok = NetworkChecker.check_internet_connection() repo_ok = NetworkChecker.check_repository_access() python_result = Detector.get_host_python_version() # Sammle alle Probleme issues = [] if not self.polkit_ok: issues.append(("PolicyKit", "#e74c3c", LocaleStrings.MSGO["polkit_check"])) if not self.networkmanager_ok: issues.append( ( "NetworkManager", "#e74c3c", LocaleStrings.MSGO["networkmanager_check"], ) ) if not internet_ok: issues.append(("Internet", "#e74c3c", LocaleStrings.MSGO["no_internet"])) if not repo_ok: issues.append( ("Repository", "#f39c12", LocaleStrings.MSGO["repo_unavailable"]) ) if python_result is None: issues.append(("Python", "#e74c3c", LocaleStrings.MSGO["python_check"])) # Bestimme Hauptstatus basierend auf Priorität if issues: # Zeige das wichtigste Problem (Internet > Python > Repository > Services) for priority_check in [ "Internet", "Python", "Repository", "PolicyKit", "NetworkManager", ]: for issue_name, color, message in issues: if issue_name == priority_check: print(f"DEBUG: Zeige Problem: {issue_name} - {message}") self.update_header_status(message, color) return else: self.update_header_status(LocaleStrings.MSGO["ready"], "#1abc9c") # Green def _create_projects_tab(self): """Create projects tab with project cards""" projects_frame = ttk.Frame(self.notebook) self.notebook.add(projects_frame, text=LocaleStrings.MSGO["applications"]) # Scrollable frame canvas = tk.Canvas(projects_frame, bg=self.colors["bg"]) scrollbar = ttk.Scrollbar( projects_frame, orient="vertical", command=canvas.yview ) scrollable_frame = tk.Frame(canvas, bg=self.colors["bg"]) scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # Create project cards for project_key, project_info in self.app_manager.get_all_projects().items(): self._create_project_card(scrollable_frame, project_key, project_info) def _create_project_card(self, parent, project_key, project_info): """Create a project card""" # Main card frame card_frame = tk.Frame(parent, bg=self.colors["bg"]) card_frame.pack(fill="x", padx=10, pady=5) # Content frame (the actual card) content_frame = tk.Frame( card_frame, bg=self.colors["card_bg"], relief="solid", bd=1 ) content_frame.pack(fill="x", padx=5, pady=2) # Store frame reference self.project_frames[project_key] = card_frame # Make entire card clickable self._make_clickable(content_frame, content_frame, project_key) # Header with icon and title header_frame = tk.Frame(content_frame, bg=self.colors["card_bg"]) header_frame.pack(fill="x", padx=15, pady=(15, 5)) # Icon icon_label = tk.Label(header_frame, bg=self.colors["card_bg"]) icon_label.pack(side="left", padx=(0, 10)) # Try to load project icon icon = self.image_manager.load_image( project_info.get("icon_key", "default_icon") ) if icon: icon_label.config(image=icon) icon_label.image = icon else: # Use emoji based on project if project_key == "wirepy": icon_label.config(text="🔒", font=("Helvetica", 24)) elif project_key == "logviewer": icon_label.config(text="📋", font=("Helvetica", 24)) else: icon_label.config(text="📦", font=("Helvetica", 24)) # Title and description title_frame = tk.Frame(header_frame, bg=self.colors["card_bg"]) title_frame.pack(side="left", fill="x", expand=True) title_label = tk.Label( title_frame, text=project_info["name"], font=("Helvetica", 14, "bold"), bg=self.colors["card_bg"], fg=self.colors["text"], anchor="w", ) title_label.pack(fill="x") desc_label = tk.Label( title_frame, text=project_info["description"], font=("Helvetica", 10), bg=self.colors["card_bg"], fg="#7f8c8d", anchor="w", wraplength=300, ) desc_label.pack(fill="x") # Status section status_frame = tk.Frame(content_frame, bg=self.colors["card_bg"]) status_frame.pack(fill="x", padx=15, pady=(5, 15)) # Status label status_label = tk.Label( status_frame, text=f"❓ {LocaleStrings.MSGC['checking']}", font=("Helvetica", 10), bg=self.colors["card_bg"], fg="#95a5a6", anchor="w", ) status_label.pack(fill="x") # Version label version_label = tk.Label( status_frame, text=LocaleStrings.MSGC["version_check"], font=("Helvetica", 9), bg=self.colors["card_bg"], fg="#95a5a6", anchor="w", ) version_label.pack(fill="x") # Store label references self.status_labels[project_key] = status_label self.version_labels[project_key] = version_label # Make all elements clickable for widget in [ header_frame, title_frame, title_label, desc_label, status_frame, status_label, version_label, ]: self._make_clickable(widget, content_frame, project_key) # Make icon clickable too self._make_clickable(icon_label, content_frame, project_key) def _make_clickable(self, widget, main_frame, project_key): """Make widget clickable with hover effects""" def on_click(event): self.select_project(project_key) def on_enter(event): if self.selected_project == project_key: main_frame.config(bg=self.colors["selected_bg"]) self._update_frame_children_bg(main_frame, self.colors["selected_bg"]) else: main_frame.config(bg=self.colors["hover_bg"]) self._update_frame_children_bg(main_frame, self.colors["hover_bg"]) def on_leave(event): if self.selected_project == project_key: main_frame.config(bg=self.colors["selected_bg"]) self._update_frame_children_bg(main_frame, self.colors["selected_bg"]) else: main_frame.config(bg=self.colors["card_bg"]) self._update_frame_children_bg(main_frame, self.colors["card_bg"]) widget.bind("", on_click) widget.bind("", on_enter) widget.bind("", on_leave) def _update_frame_children_bg(self, frame, bg_color): """Recursively update background color of all children""" try: for child in frame.winfo_children(): if isinstance(child, (tk.Frame, tk.Label)): child.config(bg=bg_color) if isinstance(child, tk.Frame): self._update_frame_children_bg(child, bg_color) except tk.TclError: # Ignore color errors pass def select_project(self, project_key): """Select a project""" # Reset previous selection if self.selected_project and self.selected_project in self.project_frames: old_frame = self.project_frames[self.selected_project] old_content = old_frame.winfo_children()[0] # content_frame old_content.config(bg=self.colors["card_bg"]) self._update_frame_children_bg(old_content, self.colors["card_bg"]) # Set new selection self.selected_project = project_key if project_key in self.project_frames: new_frame = self.project_frames[project_key] new_content = new_frame.winfo_children()[0] # content_frame new_content.config(bg=self.colors["selected_bg"]) self._update_frame_children_bg(new_content, self.colors["selected_bg"]) project_info = self.app_manager.get_project_info(project_key) self.log_message(f"{LocaleStrings.MSGL['selected_app']}{project_info['name']}") def _create_log_tab(self): """Create log tab""" log_frame = ttk.Frame(self.notebook) self.notebook.add(log_frame, text=LocaleStrings.MSGL["log_name"]) # Log text with scrollbar log_container = tk.Frame(log_frame) log_container.pack(fill="both", expand=True, padx=10, pady=10) # Important! pack_propagate(False) must be set here to display # the Clear Log button correctly log_container.pack_propagate(False) self.log_text = tk.Text( log_container, wrap=tk.WORD, font=("Consolas", 9), bg="#1e1e1e", fg="#d4d4d4", insertbackground="white", selectbackground="#264f78", ) log_scrollbar = ttk.Scrollbar( log_container, orient="vertical", command=self.log_text.yview ) self.log_text.configure(yscrollcommand=log_scrollbar.set) self.log_text.pack(side="left", fill="both", expand=True) log_scrollbar.pack(side="right", fill="y") # Log controls log_controls = tk.Frame(log_frame) log_controls.pack(fill="x", padx=10, pady=(5, 0)) # Clear log button clear_log_btn = ttk.Button( log_controls, text=LocaleStrings.MSGB["clear_log"], command=self.clear_log ) clear_log_btn.pack(side="right", pady=(0, 10)) # Initial log message self.log_message(f"=== {Locale.APP_NAME} v {LXToolsAppConfig.VERSION} ===") self.log_message(f"{LocaleStrings.MSGL['work_dir']}{LXToolsAppConfig.WORK_DIR}") self.log_message( f"{LocaleStrings.MSGL['icons_dir']}{LXToolsAppConfig.ICONS_DIR}" ) self.log_message(f"{LocaleStrings.MSGL['detected_os']}{self.detected_os}") self.log_message(f"{LocaleStrings.MSGO['polkit_check_log']}{self.polkit_ok}") self.log_message( f"{LocaleStrings.MSGO['networkmanager_check_log']}{self.networkmanager_ok}" ) self.log_message(f"{LocaleStrings.MSGO['ready']}...") def _create_progress_section(self): """Create progress section with download icon""" progress_frame = ttk.LabelFrame( self.root, text=LocaleStrings.MSGO["progress"], padding=10 ) progress_frame.pack(fill="x", padx=15, pady=10) # Container for Icon and Progress progress_container = tk.Frame(progress_frame) progress_container.pack(fill="x") # Download Icon (left) self.download_icon_label = tk.Label(progress_container, text="", width=50) self.download_icon_label.pack(side="left", padx=(0, 10)) # Progress Text (right, expandable) self.progress_label = tk.Label( progress_container, text=f"{LocaleStrings.MSGO['ready']}...", font=("Helvetica", 10), fg="blue", anchor="w", ) self.progress_label.pack(side="left", fill="x", expand=True) # Initial icon load (neutral) self._reset_download_icon() def _create_modern_buttons(self): """Create modern styled buttons""" button_frame = tk.Frame(self.root, bg=self.colors["bg"]) button_frame.pack(fill="x", padx=15, pady=(5, 10)) # Button style configuration (mit Error-Handling für Linux Mint) try: style = ttk.Style() # Install button (green) style.configure( "Install.TButton", foreground="#27ae60", font=("Helvetica", 11) ) style.map( "Install.TButton", foreground=[("active", "#14542f"), ("pressed", "#1e8449")], ) # Uninstall button (red) style.configure( "Uninstall.TButton", foreground="#e74c3c", font=("Helvetica", 11) ) style.map( "Uninstall.TButton", foreground=[("active", "#7d3b34"), ("pressed", "#c0392b")], ) # Refresh button (blue) style.configure( "Refresh.TButton", foreground="#3498db", font=("Helvetica", 11) ) style.map( "Refresh.TButton", foreground=[("active", "#1e3747"), ("pressed", "#2980b9")], ) except Exception as e: print(f"Style-Konfiguration fehlgeschlagen: {e}") # Fallback: verwende Standard-Styles # Create buttons (mit Fallback für Style-Probleme) try: install_btn = ttk.Button( button_frame, text=LocaleStrings.MSGB["install"], command=self.install_selected, style="Install.TButton", padding=8, ) except: install_btn = ttk.Button( button_frame, text=LocaleStrings.MSGB["install"], command=self.install_selected, ) install_btn.pack(side="left", padx=(0, 10)) try: uninstall_btn = ttk.Button( button_frame, text=LocaleStrings.MSGB["uninstall"], command=self.uninstall_selected, style="Uninstall.TButton", padding=8, ) except: uninstall_btn = ttk.Button( button_frame, text=LocaleStrings.MSGB["uninstall"], command=self.uninstall_selected, ) uninstall_btn.pack(side="left", padx=(0, 10)) try: refresh_btn = ttk.Button( button_frame, text=LocaleStrings.MSGB["refresh"], command=self.refresh_status, style="Refresh.TButton", width=30, padding=8, ) except: refresh_btn = ttk.Button( button_frame, text=LocaleStrings.MSGB["refresh"], command=self.refresh_status, ) refresh_btn.pack(side="right") def update_download_icon(self, status): """Update download icon based on status""" if not self.download_icon_label: return if status == "downloading": icon = self.image_manager.load_image("download_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="⬇️", font=("Helvetica", 16)) elif status == "error": icon = self.image_manager.load_image("download_error_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="❌", font=("Helvetica", 16)) elif status == "success": icon = self.image_manager.load_image("success_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="✅", font=("Helvetica", 16)) self.download_icon_label.update() def _reset_download_icon(self): """Reset download icon to neutral state""" icon = self.image_manager.load_image("download_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="📥", font=("Helvetica", 16)) def refresh_status(self): """Refresh application status and version information""" self.update_progress(LocaleStrings.MSGI["refresh_and_check"]) self._reset_download_icon() self.log_message(f"=== {LocaleStrings.MSGB['refresh']} ===") self.root.focus_set() for project_key, project_info in self.app_manager.get_all_projects().items(): status_label = self.status_labels[project_key] version_label = self.version_labels[project_key] self.log_message(f"{LocaleStrings.MSGC['checking']} {project_info['name']}") if self.app_manager.is_installed(project_key): installed_version = self.app_manager.get_installed_version(project_key) status_label.config( text=f"✅ {LocaleStrings.MSGI['installed']}({installed_version})", fg="green", ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGI['installed']}({installed_version})" ) # Get latest version from API try: latest_version = self.app_manager.get_latest_version(project_key) if latest_version != "Unknown": if installed_version != f"v. {latest_version}": version_label.config( text=f"{LocaleStrings.MSGC['update_on']}(v. {latest_version}) {LocaleStrings.MSGC['available_lower']}", fg="orange", ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGC['update_on']}(v. {latest_version} {LocaleStrings.MSGC['available_lower']})" ) else: version_label.config( text=f"(v. {latest_version}) {LocaleStrings.MSGC['up_to_date']}", fg="green", ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGC['up_to_date']}", ) else: version_label.config( text=LocaleStrings.MSGC["latest_unknown"], fg="gray" ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGC['could_not_check']}" ) except Exception as e: version_label.config( text=LocaleStrings.MSGC["check_last_failed"], fg="gray" ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGC['version_check_failed']}: {e}" ) else: status_label.config( text=f"❌ {LocaleStrings.MSGC['not_installed']}", fg="red" ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGC['not_installed']}" ) # Still show latest available version try: latest_version = self.app_manager.get_latest_version(project_key) if latest_version != "Unknown": version_label.config( text=f"{project_info['name']} {LocaleStrings.MSGC['available']}: v {latest_version}", fg="blue", ) self.log_message( f"{project_info['name']} {LocaleStrings.MSGC['available']}: v {latest_version}" ) else: version_label.config( text=LocaleStrings.MSGC["available_unknown"], fg="gray" ) except Exception as e: version_label.config( text=LocaleStrings.MSGC["available_check_unknown"], fg="gray" ) self.log_message( f" {project_info['name']}: {LocaleStrings.MSGC['version_check_failed']}: {e}" ) self.update_progress(LocaleStrings.MSGO["refresh2"]) self.log_message(f"=== {LocaleStrings.MSGO['refresh2']} ===") self.check_ready_status() def install_selected(self): """Handle install button click""" if not self.selected_project: MessageDialog("error", LocaleStrings.MSGM["please_select"]) self.root.focus_set() return # Check internet connection if not NetworkChecker.check_internet_connection(): self.update_download_icon("error") MessageDialog("error", LocaleStrings.MSGM["network_error"]) self.root.focus_set() return if not NetworkChecker.check_repository_access(): self.update_download_icon("error") MessageDialog("error", LocaleStrings.MSGM["repo_error"]) self.root.focus_set() return # Reset download icon self._reset_download_icon() project_info = self.app_manager.get_project_info(self.selected_project) try: self.update_download_icon("downloading") self.installation_manager.install_project(self.selected_project) self.update_download_icon("success") MessageDialog( "info", f"{project_info['name']} {LocaleStrings.MSGM['has_success_update']}", wraplength=400, ) self.refresh_status() except Exception as e: self.update_download_icon("error") MessageDialog("error", f"{e}") self.root.focus_set() def uninstall_selected(self): """Handle uninstall button click""" if not self.selected_project: MessageDialog("error", LocaleStrings.MSGM["please_select_uninstall"]) self.root.focus_set() return project_info = self.app_manager.get_project_info(self.selected_project) if not self.app_manager.is_installed(self.selected_project): MessageDialog( "error", f"{project_info['name']} {LocaleStrings.MSGO['not_installed']}" ) self.root.focus_set() return try: self.uninstallation_manager.uninstall_project(self.selected_project) MessageDialog( "info", f"{project_info['name']} {LocaleStrings.MSGU['uninstall_success']}", wraplength=400, ) self.refresh_status() self.root.focus_set() except Exception as e: MessageDialog("error", f"{LocaleStrings.MSGU['uninstall_failed']}: {e}") self.root.focus_set() def update_progress(self, message): """Update progress message""" if self.progress_label: self.progress_label.config(text=message) self.progress_label.update() print(f"{LocaleStrings.MSGO['progress']}: {message}") def log_message(self, message): """Add message to log""" if self.log_text: timestamp = datetime.now().strftime("%H:%M:%S") log_entry = f"[{timestamp}] {message}\n" self.log_text.insert(tk.END, log_entry) self.log_text.see(tk.END) self.log_text.update() print(f"Log: {message}") def clear_log(self): """Clear the log""" if self.log_text: self.log_text.delete(1.0, tk.END) self.log_message(LocaleStrings.MSGL["log_cleared"]) def run(self): """Start the GUI application""" root = self.create_gui() root.mainloop() def main(): """Main function to start the application""" print(f"=== {Locale.APP_NAME} v {LXToolsAppConfig.VERSION} ===") print(f"{LocaleStrings.MSGL['working_dir']}{os.getcwd()}") try: LxTools.sigi(LXToolsAppConfig.TEMP_DIR) # Create and run the GUI app = LXToolsGUI() app.run() except KeyboardInterrupt: print(LocaleStrings.MSGL["user_interrupt"]) except Exception as e: print(f"{LocaleStrings.MSGL['fatal_error']}: {e}") try: MessageDialog("error", f"{LocaleStrings.MSGL['fatal_app_error']}: {e}") except: pass if __name__ == "__main__": main() LxTools.remove_lxtools_files() LxTools.clean_files(LXToolsAppConfig.TEMP_DIR) sys.exit(0)