diff --git a/common_tools.py b/common_tools.py deleted file mode 100755 index ae495a1..0000000 --- a/common_tools.py +++ /dev/null @@ -1,573 +0,0 @@ -""" Classes Method and Functions for lx Apps """ - -import logging -import signal -import base64 -from subprocess import CompletedProcess, run -import re -import sys -import shutil -import tkinter as tk -from typing import Optional, Dict, Any, NoReturn -from pathlib import Path -from tkinter import ttk, Toplevel - - -class CryptoUtil: - """ - This class is for the creation of the folders and files - required by Wire-Py, as well as for decryption - the tunnel from the user's home directory - """ - - @staticmethod - def decrypt(user, path) -> None: - """ - Starts SSL dencrypt - """ - if len([file.stem for file in path.glob("*.dat")]) == 0: - pass - else: - process: CompletedProcess[str] = run( - ["pkexec", "/usr/local/bin/ssl_decrypt.py", "--user", user], - capture_output=True, - text=True, - check=False, - ) - - # Output from Openssl Error - if process.stderr: - logging.error(process.stderr, exc_info=True) - - if process.returncode == 0: - logging.info("Files successfully decrypted...", exc_info=True) - else: - logging.error( - f"Error process decrypt: Code {process.returncode}", exc_info=True - ) - - @staticmethod - def encrypt(user) -> None: - """ - Starts SSL encryption - """ - process: CompletedProcess[str] = run( - ["pkexec", "/usr/local/bin/ssl_encrypt.py", "--user", user], - capture_output=True, - text=True, - check=False, - ) - - # Output from Openssl Error - if process.stderr: - logging.error(process.stderr, exc_info=True) - - if process.returncode == 0: - logging.info("Files successfully encrypted...", exc_info=True) - else: - logging.error( - f"Error process encrypt: Code {process.returncode}", exc_info=True - ) - - @staticmethod - def find_key(key: str = "") -> bool: - """ - Checks if the private key already exists in the system using an external script. - Returns True only if the full key is found exactly (no partial match). - """ - process: CompletedProcess[str] = run( - ["pkexec", "/usr/local/bin/match_found.py", key], - capture_output=True, - text=True, - check=False, - ) - if "True" in process.stdout: - return True - elif "False" in process.stdout: - return False - logging.error( - f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}", - exc_info=True, - ) - return False - - @staticmethod - def is_valid_base64(key: str) -> bool: - """ - Validates if the input is a valid Base64 string (WireGuard private key format). - Returns True only for non-empty strings that match the expected length. - """ - # Check for empty string - if not key or key.strip() == "": - return False - - # Regex pattern to validate Base64: [A-Za-z0-9+/]+={0,2} - base64_pattern = r"^[A-Za-z0-9+/]+={0,2}$" - if not re.match(base64_pattern, key): - return False - - try: - # Decode and check length (WireGuard private keys are 32 bytes long) - decoded = base64.b64decode(key) - if len(decoded) != 32: # 32 bytes = 256 bits - return False - except Exception as e: - logging.error(f"Error on decode Base64: {e}", exc_info=True) - return False - - return True - - -class LxTools: - """ - Class LinuxTools methods that can also be used for other apps - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - @staticmethod - def center_window_cross_platform(window, width, height): - """ - Centers a window on the primary monitor in a way that works on both X11 and Wayland - - Args: - window: The tkinter window to center - width: Window width - height: Window height - """ - # Calculate the position before showing the window - - # First attempt: Try to use GDK if available (works on both X11 and Wayland) - try: - import gi - - gi.require_version("Gdk", "3.0") - from gi.repository import Gdk - - display = Gdk.Display.get_default() - monitor = display.get_primary_monitor() or display.get_monitor(0) - geometry = monitor.get_geometry() - scale_factor = monitor.get_scale_factor() - - # Calculate center position on the primary monitor - x = geometry.x + (geometry.width - width // scale_factor) // 2 - y = geometry.y + (geometry.height - height // scale_factor) // 2 - - # Set window geometry - window.geometry(f"{width}x{height}+{x}+{y}") - return - except (ImportError, AttributeError): - pass - - # Second attempt: Try xrandr for X11 - try: - import subprocess - - output = subprocess.check_output( - ["xrandr", "--query"], universal_newlines=True - ) - - # Parse the output to find the primary monitor - primary_info = None - for line in output.splitlines(): - if "primary" in line: - parts = line.split() - for part in parts: - if "x" in part and "+" in part: - primary_info = part - break - break - - if primary_info: - # Parse the geometry: WIDTH x HEIGHT+X+Y - geometry = primary_info.split("+") - dimensions = geometry[0].split("x") - primary_width = int(dimensions[0]) - primary_height = int(dimensions[1]) - primary_x = int(geometry[1]) - primary_y = int(geometry[2]) - - # Calculate center position on the primary monitor - x = primary_x + (primary_width - width) // 2 - y = primary_y + (primary_height - height) // 2 - - # Set window geometry - window.geometry(f"{width}x{height}+{x}+{y}") - return - except (ImportError, IndexError, ValueError): - pass - - # Final fallback: Use standard Tkinter method - screen_width = window.winfo_screenwidth() - screen_height = window.winfo_screenheight() - - # Try to make an educated guess for multi-monitor setups - # If screen width is much larger than height, assume multiple monitors side by side - if ( - screen_width > screen_height * 1.8 - ): # Heuristic for detecting multiple monitors - # Assume the primary monitor is on the left half - screen_width = screen_width // 2 - - x = (screen_width - width) // 2 - y = (screen_height - height) // 2 - window.geometry(f"{width}x{height}+{x}+{y}") - - @staticmethod - def clean_files(tmp_dir: Path = None, file: Path = None) -> None: - """ - Deletes temporary files and directories for cleanup when exiting the application. - - This method safely removes an optional directory defined by `AppConfig.TEMP_DIR` - and a single file to free up resources at the end of the program's execution. - All operations are performed securely, and errors such as `FileNotFoundError` - are ignored if the target files or directories do not exist. - :param tmp_dir: (Path, optional): Path to the temporary directory that should be deleted. - If `None`, the value of `AppConfig.TEMP_DIR` is used. - :param file: (Path, optional): Path to the file that should be deleted. - If `None`, no additional file will be deleted. - - Returns: - None: The method does not return any value. - """ - - if tmp_dir is not None: - shutil.rmtree(tmp_dir, ignore_errors=True) - try: - if file is not None: - Path.unlink(file) - - except FileNotFoundError: - pass - - @staticmethod - def msg_window( - image_path: Path, - image_path2: Path, - w_title: str, - w_txt: str, - txt2: Optional[str] = None, - com: Optional[str] = None, - ) -> None: - """ - Creates message windows - - :param image_path2: - :param image_path: - AppConfig.IMAGE_PATHS["icon_info"] = Image for TK window which is displayed to the left of the text - AppConfig.IMAGE_PATHS["icon_vpn"] = Image for Task Icon - :argument w_title = Windows Title - :argument w_txt = Text for Tk Window - :argument txt2 = Text for Button two - :argument com = function for Button command - """ - msg: tk.Toplevel = tk.Toplevel() - msg.resizable(width=False, height=False) - msg.title(w_title) - msg.configure(pady=15, padx=15) - - # load first image for a window - try: - msg.img = tk.PhotoImage(file=image_path) - msg.i_window = tk.Label(msg, image=msg.img) - except Exception as e: - logging.error(f"Error on load Window Image: {e}", exc_info=True) - msg.i_window = tk.Label(msg, text="Image not found") - - label: tk.Label = tk.Label(msg, text=w_txt) - label.grid(column=1, row=0) - - if txt2 is not None and com is not None: - label.config(font=("Ubuntu", 11), padx=15, justify="left") - msg.i_window.grid(column=0, row=0, sticky="nw") - button2: ttk.Button = ttk.Button( - msg, text=f"{txt2}", command=com, padding=4 - ) - button2.grid(column=0, row=1, sticky="e", columnspan=2) - button: ttk.Button = ttk.Button( - msg, text="OK", command=msg.destroy, padding=4 - ) - button.grid(column=0, row=1, sticky="w", columnspan=2) - else: - label.config(font=("Ubuntu", 11), padx=15) - msg.i_window.grid(column=0, row=0) - button: ttk.Button = ttk.Button( - msg, text="OK", command=msg.destroy, padding=4 - ) - button.grid(column=0, columnspan=2, row=1) - - try: - icon = tk.PhotoImage(file=image_path2) - msg.iconphoto(True, icon) - except Exception as e: - logging.error(f"Error loading the window icon: {e}", exc_info=True) - - msg.columnconfigure(0, weight=1) - msg.rowconfigure(0, weight=1) - msg.winfo_toplevel() - - @staticmethod - def sigi(file_path: Optional[Path] = None, file: Optional[Path] = None) -> None: - """ - Function for cleanup after a program interruption - - :param file: Optional - File to be deleted - :param file_path: Optional - Directory to be deleted - """ - - def signal_handler(signum: int, frame: Any) -> NoReturn: - """ - Determines clear text names for signal numbers and handles signals - - Args: - signum: The signal number - frame: The current stack frame - - Returns: - NoReturn since the function either exits the program or continues execution - """ - - signals_to_names_dict: Dict[int, str] = dict( - (getattr(signal, n), n) - for n in dir(signal) - if n.startswith("SIG") and "_" not in n - ) - - signal_name: str = signals_to_names_dict.get( - signum, f"Unnamed signal: {signum}" - ) - - # End program for certain signals, report to others only reception - if signum in (signal.SIGINT, signal.SIGTERM): - exit_code: int = 1 - logging.error( - f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.", - exc_info=True, - ) - LxTools.clean_files(file_path, file) - logging.info("Breakdown by user...") - sys.exit(exit_code) - else: - logging.info(f"Signal {signum} received and ignored.") - LxTools.clean_files(file_path, file) - logging.error("Process unexpectedly ended...") - - # Register signal handlers for various signals - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGHUP, signal_handler) - - -# ConfigManager with caching -class ConfigManager: - """ - Universal class for managing configuration files with caching support. - - This class provides a general solution to load, save, and manage configuration - files across different projects. It uses a caching system to optimize access efficiency. - The `init()` method initializes the configuration file path, while `load()` and `save()` - synchronize data between the file and internal memory structures. - - Key Features: - - Caching to minimize I/O operations. - - Default values for missing or corrupted configuration files. - - Reusability across different projects and use cases. - - The class is designed for central application configuration management, working closely - with `ThemeManager` to dynamically manage themes or other settings. - """ - - _config = None - _config_file = None - - @classmethod - def init(cls, config_file): - """Initial the Configmanager with the given config file""" - cls._config_file = config_file - cls._config = None # Reset the cache - - @classmethod - def load(cls): - """Load the config file and return the config as dict""" - if not cls._config: - try: - lines = Path(cls._config_file).read_text(encoding="utf-8").splitlines() - cls._config = { - "updates": lines[1].strip(), - "theme": lines[3].strip(), - "tooltips": lines[5].strip() - == "True", # is converted here to boolean!!! - "autostart": lines[7].strip() if len(lines) > 7 else "off", - "logfile": lines[9].strip(), - } - except (IndexError, FileNotFoundError): - # DeDefault values in case of error - cls._config = { - "updates": "on", - "theme": "light", - "tooltips": "True", # Default Value as string! - "autostart": "off", - "logfile": LOG_FILE_PATH, - } - return cls._config - - @classmethod - def save(cls): - """Save the config to the config file""" - if cls._config: - lines = [ - "# Configuration\n", - f"{cls._config['updates']}\n", - "# Theme\n", - f"{cls._config['theme']}\n", - "# Tooltips\n", - f"{str(cls._config['tooltips'])}\n", - "# Autostart\n", - f"{cls._config['autostart']}\n", - "# Logfile\n", - f"{cls._config['logfile']}\n", - ] - Path(cls._config_file).write_text("".join(lines), encoding="utf-8") - - @classmethod - def set(cls, key, value): - """Sets a configuration value and saves the change""" - cls.load() - cls._config[key] = value - cls.save() - - @classmethod - def get(cls, key, default=None): - """Returns a configuration value""" - config = cls.load() - return config.get(key, default) - - -class ThemeManager: - """ - Class for central theme management and UI customization. - - This static class allows dynamic adjustment of the application's appearance. - The method `change_theme()` updates the current theme and saves - the selection in the configuration file via `ConfigManager`. - It ensures a consistent visual design across the entire project. - - Key Features: - - Central control over themes. - - Automatic saving of theme settings to the configuration file. - - Tight integration with `ConfigManager` for persistent storage of preferences. - - The class is designed to apply themes consistently throughout the application, - ensuring that changes are traceable and uniform across all parts of the project. - """ - - @staticmethod - def change_theme(root, theme_in_use, theme_name=None): - """Change application theme centrally""" - root.tk.call("set_theme", theme_in_use) - if theme_in_use == theme_name: - ConfigManager.set("theme", theme_in_use) - - -class Tooltip: - """Class for Tooltip - from common_tools.py import Tooltip - example: Tooltip(label, "Show tooltip on label") - example: Tooltip(button, "Show tooltip on button") - example: Tooltip(widget, "Text", state_var=tk.BooleanVar()) - example: Tooltip(widget, "Text", state_var=tk.BooleanVar(), x_offset=20, y_offset=10) - - info: label and button are parent widgets. - NOTE: When using with state_var, pass the tk.BooleanVar object directly, - NOT its value. For example: use state_var=my_bool_var, NOT state_var=my_bool_var.get() - """ - - def __init__( - self, - widget: Any, - text: str, - state_var: Optional[tk.BooleanVar] = None, - x_offset: int = 65, - y_offset: int = 40, - ) -> None: - """Tooltip Class""" - self.widget: Any = widget - self.text: str = text - self.tooltip_window: Optional[Toplevel] = None - self.state_var = state_var - self.x_offset = x_offset - self.y_offset = y_offset - - # Initial binding based on the current state - self.update_bindings() - - # Add trace to the state_var if provided - if self.state_var is not None: - self.state_var.trace_add("write", self.update_bindings) - - def update_bindings(self, *args) -> None: - """Updates the bindings based on the current state""" - # Remove existing bindings first - self.widget.unbind("") - self.widget.unbind("") - - # Add new bindings if tooltips are enabled - if self.state_var is None or self.state_var.get(): - self.widget.bind("", self.show_tooltip) - self.widget.bind("", self.hide_tooltip) - - def show_tooltip(self, event: Optional[Any] = None) -> None: - """Shows the tooltip""" - if self.tooltip_window or not self.text: - return - - x: int - y: int - cx: int - cy: int - - x, y, cx, cy = self.widget.bbox("insert") - x += self.widget.winfo_rootx() + self.x_offset - y += self.widget.winfo_rooty() + self.y_offset - - self.tooltip_window = tw = tk.Toplevel(self.widget) - tw.wm_overrideredirect(True) - tw.wm_geometry(f"+{x}+{y}") - - label: tk.Label = tk.Label( - tw, - text=self.text, - background="lightgreen", - foreground="black", - relief="solid", - borderwidth=1, - padx=5, - pady=5, - ) - label.grid() - - self.tooltip_window.after(2200, lambda: tw.destroy()) - - def hide_tooltip(self, event: Optional[Any] = None) -> None: - """Hides the tooltip""" - if self.tooltip_window: - self.tooltip_window.destroy() - self.tooltip_window = None - - -class LogConfig: - @staticmethod - def logger(file_path) -> None: - - file_handler = logging.FileHandler( - filename=f"{file_path}", - mode="a", - encoding="utf-8", - ) - formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") - file_handler.setFormatter(formatter) - file_handler.setLevel(logging.DEBUG) - - logger = logging.getLogger() - logger.addHandler(file_handler) diff --git a/lxtools_installer.py b/lxtools_installer.py deleted file mode 100644 index 8cb19e7..0000000 --- a/lxtools_installer.py +++ /dev/null @@ -1,1264 +0,0 @@ -#!/usr/bin/python3 -import gettext -import locale -import tkinter as tk -from tkinter import messagebox, ttk -import shutil -import os -import socket -import subprocess -import tempfile -import urllib.request -import zipfile -import json -from pathlib import Path - - -# ---------------------------- -# LXTools App Configuration -# ---------------------------- -class LXToolsAppConfig: - VERSION = "1.0.4" - APP_NAME = "LXTools Installer" - WINDOW_WIDTH = 500 - WINDOW_HEIGHT = 600 - - # Locale settings - LOCALE_DIR = Path("/usr/share/locale/") - - # Images and icons paths - IMAGE_PATHS = { - "icon_vpn": "./lx-icons/32/wg_vpn.png", - "icon_vpn2": "./lx-icons/48/wg_vpn.png", - "icon_msg": "./lx-icons/48/wg_msg.png", - "icon_info": "./lx-icons/64/info.png", - "icon_error": "./lx-icons/64/error.png", - "icon_log": "./lx-icons/32/log.png", - "icon_log2": "./lx-icons/48/log.png", - "icon_download": "./lx-icons/32/download.png", - "icon_download_error": "./lx-icons/32/download_error.png", - } - - # System-dependent paths - SYSTEM_PATHS = { - "tcl_path": "/usr/share/TK-Themes", - } - - # Download URLs - WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip" - SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip" - - # API URLs for version checking - WIREPY_API_URL = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases" - SHARED_LIBS_API_URL = ( - "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases" - ) - - # OS Detection List (order matters - specific first, generic last) - OS_DETECTION = [ - ("mint", "Linux Mint"), - ("pop", "Pop!_OS"), - ("manjaro", "Manjaro"), - ("garuda", "Garuda Linux"), - ("endeavouros", "EndeavourOS"), - ("fedora", "Fedora"), - ("tumbleweed", "SUSE Tumbleweed"), - ("leap", "SUSE Leap"), - ("suse", "openSUSE"), - ("arch", "Arch Linux"), - ("ubuntu", "Ubuntu"), - ("debian", "Debian"), - ] - - @staticmethod - def setup_translations(): - """Initialize translations and set the translation function""" - locale.bindtextdomain(LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR) - gettext.bindtextdomain(LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR) - gettext.textdomain(LXToolsAppConfig.APP_NAME) - return gettext.gettext - - -# Initialize translations -_ = LXToolsAppConfig.setup_translations() - - -# ---------------------------- -# Image Manager Class -# ---------------------------- -class ImageManager: - def __init__(self): - self.images = {} - - def load_image(self, image_key, fallback_paths=None): - """Load PNG image using tk.PhotoImage with fallback options""" - if image_key in self.images: - return self.images[image_key] - - # Primary path from config - primary_path = LXToolsAppConfig.IMAGE_PATHS.get(image_key) - paths_to_try = [] - - if primary_path: - paths_to_try.append(primary_path) - - # Add fallback paths - if fallback_paths: - paths_to_try.extend(fallback_paths) - - # Try to load image from paths - for path in paths_to_try: - try: - if os.path.exists(path): - photo = tk.PhotoImage(file=path) - self.images[image_key] = photo - return photo - except tk.TclError as e: - print(f"Failed to load image from {path}: {e}") - continue - - # Return None if no image found (we'll handle this in GUI) - return None - - -# ---------------------------- -# Gitea API Handler -# ---------------------------- -class GiteaUpdate: - @staticmethod - def api_down(url, current_version=""): - """Get latest version from Gitea API""" - try: - with urllib.request.urlopen(url) as response: - data = json.loads(response.read().decode()) - if data and len(data) > 0: - latest_version = data[0].get("tag_name", "Unknown") - return latest_version.lstrip("v") # Remove 'v' prefix if present - return "Unknown" - except Exception as e: - print(f"API Error: {e}") - return "Unknown" - - -# ---------------------------- -# OS Detection Class -# ---------------------------- -class OSDetector: - @staticmethod - def detect_os(): - """Detect operating system using ordered list""" - try: - with open("/etc/os-release", "r") as f: - content = f.read().lower() - - # Check each OS in order (specific first) - for keyword, os_name in LXToolsAppConfig.OS_DETECTION: - if keyword in content: - return os_name - - return "Unknown System" - except FileNotFoundError: - return "File not found" - - -# ---------------------------- -# Network Checker Class -# ---------------------------- -class NetworkChecker: - @staticmethod - def check_internet_connection(host="8.8.8.8", port=53, timeout=3): - """Check if internet connection is available""" - try: - socket.setdefaulttimeout(timeout) - socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) - return True - except socket.error: - return False - - @staticmethod - def check_repository_access(url="https://git.ilunix.de"): - """Check if repository is accessible""" - try: - urllib.request.urlopen(url, timeout=5) - return True - except: - return False - - -# ---------------------------- -# Application Configuration Class -# ---------------------------- -class AppConfig: - def __init__( - self, - key, - name, - files, - config, - desktop, - icon, - symlink, - config_dir, - log_file, - languages, - api_url, - icon_key, - policy_file=None, - ): - self.key = key - self.name = name - self.files = files - self.config = config - self.desktop = desktop - self.icon = icon - self.symlink = symlink - self.config_dir = config_dir - self.log_file = log_file - self.languages = languages - self.api_url = api_url - self.icon_key = icon_key # Key for ImageManager - self.policy_file = policy_file - - def is_installed(self): - """Check if application is installed""" - return os.path.exists(f"/usr/local/bin/{self.symlink}") - - def get_installed_version(self): - """Get installed version from config file""" - try: - config_file = f"/usr/lib/python3/dist-packages/shared_libs/{self.config}" - if os.path.exists(config_file): - with open(config_file, "r") as f: - content = f.read() - for line in content.split("\n"): - if "VERSION" in line and "=" in line: - return line.split("=")[1].strip().strip("\"'") - return "Unknown" - except: - return "Unknown" - - def get_latest_version(self): - """Get latest version from API""" - return GiteaUpdate.api_down(self.api_url) - - -# ---------------------------- -# Application Manager Class -# ---------------------------- -class AppManager: - def __init__(self): - self.apps = { - "wirepy": AppConfig( - key="wirepy", - name="Wire-Py", - files=[ - "wirepy.py", - "start_wg.py", - "ssl_encrypt.py", - "ssl_decrypt.py", - "match_found.py", - "tunnel.py", - ], - config="wp_app_config.py", - desktop="Wire-Py.desktop", - icon="wg_vpn.png", - symlink="wirepy", - config_dir="~/.config/wire_py", - log_file="~/.local/share/lxlogs/wirepy.log", - languages=["wirepy.mo"], - api_url=LXToolsAppConfig.WIREPY_API_URL, - icon_key="icon_vpn", - policy_file="org.sslcrypt.policy", - ), - "logviewer": AppConfig( - key="logviewer", - name="LogViewer", - files=["logviewer.py"], - config="logview_app_config.py", - desktop="LogViewer.desktop", - icon="log.png", - symlink="logviewer", - config_dir="~/.config/logviewer", - log_file="~/.local/share/lxlogs/logviewer.log", - languages=["logviewer.mo"], - api_url=LXToolsAppConfig.SHARED_LIBS_API_URL, - icon_key="icon_log", - policy_file=None, - ), - } - - self.shared_files = [ - "common_tools.py", - "file_and_dir_ensure.py", - "gitea.py", - "__init__.py", - "logview_app_config.py", - "logviewer.py", - ] - - def get_app(self, key): - """Get application configuration by key""" - return self.apps.get(key) - - def get_all_apps(self): - """Get all application configurations""" - return self.apps - - def check_other_apps_installed(self, exclude_key): - """Check if other apps are still installed""" - return any( - app.is_installed() for key, app in self.apps.items() if key != exclude_key - ) - - -# ---------------------------- -# Download Manager Class -# ---------------------------- -class DownloadManager: - @staticmethod - def download_and_extract( - url, extract_to, progress_callback=None, icon_callback=None - ): - """Download and extract ZIP file with icon status""" - try: - if progress_callback: - progress_callback(f"Downloading from {url}...") - - # Set download icon - if icon_callback: - icon_callback("downloading") - - with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file: - urllib.request.urlretrieve(url, tmp_file.name) - - if progress_callback: - progress_callback("Extracting files...") - - with zipfile.ZipFile(tmp_file.name, "r") as zip_ref: - zip_ref.extractall(extract_to) - - os.unlink(tmp_file.name) - - # Set success icon - if icon_callback: - icon_callback("success") - - return True - - except Exception as e: - if progress_callback: - progress_callback(f"Download failed: {str(e)}") - - # Set error icon - if icon_callback: - icon_callback("error") - - return False - - -# ---------------------------- -# System Manager Class -# ---------------------------- -class SystemManager: - @staticmethod - def create_directories(directories): - """Create system directories using pkexec""" - for directory in directories: - subprocess.run(["pkexec", "mkdir", "-p", directory], check=True) - - @staticmethod - def copy_file(src, dest, make_executable=False): - """Copy file using pkexec""" - subprocess.run(["pkexec", "cp", src, dest], check=True) - if make_executable: - subprocess.run(["pkexec", "chmod", "755", dest], check=True) - - @staticmethod - def copy_directory(src, dest): - """Copy directory using pkexec""" - subprocess.run(["pkexec", "cp", "-r", src, dest], check=True) - - @staticmethod - def remove_file(path): - """Remove file using pkexec""" - subprocess.run(["pkexec", "rm", "-f", path], check=False) - - @staticmethod - def remove_directory(path): - """Remove directory using pkexec""" - subprocess.run(["pkexec", "rm", "-rf", path], check=False) - - @staticmethod - def create_symlink(target, link_name): - """Create symbolic link using pkexec""" - subprocess.run(["pkexec", "rm", "-f", link_name], check=False) - subprocess.run(["pkexec", "ln", "-sf", target, link_name], check=True) - - @staticmethod - def create_ssl_key(pem_file): - """Create SSL key using pkexec""" - try: - subprocess.run( - ["pkexec", "openssl", "genrsa", "-out", pem_file, "4096"], check=True - ) - subprocess.run(["pkexec", "chmod", "600", pem_file], check=True) - return True - except subprocess.CalledProcessError: - return False - - -# ---------------------------- -# Installation Manager Class -# ---------------------------- -class InstallationManager: - def __init__(self, app_manager, progress_callback=None, icon_callback=None): - self.app_manager = app_manager - self.progress_callback = progress_callback - self.icon_callback = icon_callback - self.system_manager = SystemManager() - self.download_manager = DownloadManager() - - def update_progress(self, message): - """Update progress message""" - if self.progress_callback: - self.progress_callback(message) - - def install_app(self, app_key): - """Install or update application""" - app = self.app_manager.get_app(app_key) - if not app: - raise Exception(f"Unknown application: {app_key}") - - self.update_progress(f"Starting installation of {app.name}...") - - try: - # Create temporary directory - with tempfile.TemporaryDirectory() as temp_dir: - # Download application source - if app_key == "wirepy": - if not self.download_manager.download_and_extract( - LXToolsAppConfig.WIREPY_URL, - temp_dir, - self.update_progress, - self.icon_callback, - ): - raise Exception("Failed to download Wire-Py") - source_dir = os.path.join(temp_dir, "Wire-Py") - else: - if not self.download_manager.download_and_extract( - LXToolsAppConfig.SHARED_LIBS_URL, - temp_dir, - self.update_progress, - self.icon_callback, - ): - raise Exception("Failed to download LogViewer") - source_dir = os.path.join(temp_dir, "shared_libs") - - # Download shared libraries - shared_temp = os.path.join(temp_dir, "shared") - if not self.download_manager.download_and_extract( - LXToolsAppConfig.SHARED_LIBS_URL, - shared_temp, - self.update_progress, - self.icon_callback, - ): - raise Exception("Failed to download shared libraries") - shared_source = os.path.join(shared_temp, "shared_libs") - - # Create necessary directories - self.update_progress("Creating directories...") - directories = [ - "/usr/lib/python3/dist-packages/shared_libs", - "/usr/share/icons/lx-icons/48", - "/usr/share/icons/lx-icons/64", - "/usr/share/locale/de/LC_MESSAGES", - "/usr/share/applications", - "/usr/local/etc/ssl", - "/usr/share/polkit-1/actions", - ] - self.system_manager.create_directories(directories) - - # Install shared libraries - self.update_progress("Installing shared libraries...") - self._install_shared_libraries(shared_source) - - # Install application files - self.update_progress(f"Installing {app.name} files...") - self._install_app_files(app, source_dir) - - # Install additional resources - self._install_app_resources(app, source_dir) - - # Install policy file if exists - if app.policy_file: - self._install_policy_file(app, source_dir) - - # Create symlink - self.update_progress("Creating symlink...") - main_file = app.files[0] # First file is usually the main file - self.system_manager.create_symlink( - f"/usr/local/bin/{main_file}", f"/usr/local/bin/{app.symlink}" - ) - - # Special handling for Wire-Py SSL key - if app_key == "wirepy": - self._create_ssl_key() - - self.update_progress(f"{app.name} installation completed successfully!") - return True - - except subprocess.CalledProcessError as e: - self.update_progress("Error: pkexec command failed") - raise Exception( - f"Installation failed (pkexec): {e}\n\nPermission might have been denied." - ) - except Exception as e: - self.update_progress(f"Error: {str(e)}") - raise - - def _install_policy_file(self, app, source_dir): - """Install polkit policy file""" - if app.policy_file: - self.update_progress(f"Installing policy file {app.policy_file}...") - policy_src = os.path.join(source_dir, app.policy_file) - if os.path.exists(policy_src): - policy_dest = f"/usr/share/polkit-1/actions/{app.policy_file}" - self.system_manager.copy_file(policy_src, policy_dest) - self.update_progress( - f"Policy file {app.policy_file} installed successfully." - ) - else: - self.update_progress( - f"Warning: Policy file {app.policy_file} not found in source." - ) - - def _install_shared_libraries(self, shared_source): - """Install shared library files""" - for shared_file in self.app_manager.shared_files: - src = os.path.join(shared_source, shared_file) - if os.path.exists(src): - dest = f"/usr/lib/python3/dist-packages/shared_libs/{shared_file}" - self.system_manager.copy_file(src, dest) - - def _install_app_files(self, app, source_dir): - """Install application executable files""" - for app_file in app.files: - src = os.path.join(source_dir, app_file) - if os.path.exists(src): - dest = f"/usr/local/bin/{app_file}" - self.system_manager.copy_file(src, dest, make_executable=True) - - # Install app config - config_src = os.path.join(source_dir, app.config) - if os.path.exists(config_src): - config_dest = f"/usr/lib/python3/dist-packages/shared_libs/{app.config}" - self.system_manager.copy_file(config_src, config_dest) - - def _install_app_resources(self, app, source_dir): - """Install icons, desktop files, and language files""" - # Install icons - self.update_progress("Installing icons...") - icons_src = os.path.join(source_dir, "lx-icons") - if os.path.exists(icons_src): - # Copy all icon subdirectories - for item in os.listdir(icons_src): - item_path = os.path.join(icons_src, item) - if os.path.isdir(item_path): - dest_path = f"/usr/share/icons/lx-icons/{item}" - self.system_manager.copy_directory(item_path, dest_path) - - # Install desktop file - desktop_src = os.path.join(source_dir, app.desktop) - if os.path.exists(desktop_src): - self.system_manager.copy_file( - desktop_src, f"/usr/share/applications/{app.desktop}" - ) - - # Install language files - self.update_progress("Installing language files...") - lang_dir = os.path.join(source_dir, "languages", "de") - if os.path.exists(lang_dir): - for lang_file in app.languages: - lang_src = os.path.join(lang_dir, lang_file) - if os.path.exists(lang_src): - lang_dest = f"/usr/share/locale/de/LC_MESSAGES/{lang_file}" - self.system_manager.copy_file(lang_src, lang_dest) - - def _create_ssl_key(self): - """Create SSL key for Wire-Py""" - pem_file = "/usr/local/etc/ssl/pwgk.pem" - if not os.path.exists(pem_file): - self.update_progress("Creating SSL key...") - if not self.system_manager.create_ssl_key(pem_file): - self.update_progress( - "Warning: SSL key creation failed. OpenSSL might be missing." - ) - - def uninstall_app(self, app_key): - """Uninstall application""" - app = self.app_manager.get_app(app_key) - if not app: - raise Exception(f"Unknown application: {app_key}") - - if not app.is_installed(): - raise Exception(f"{app.name} is not installed.") - - try: - self.update_progress(f"Uninstalling {app.name}...") - - # Remove policy file if exists - if app.policy_file: - self.system_manager.remove_file( - f"/usr/share/polkit-1/actions/{app.policy_file}" - ) - - # Remove application files - for app_file in app.files: - self.system_manager.remove_file(f"/usr/local/bin/{app_file}") - - # Remove symlink - self.system_manager.remove_file(f"/usr/local/bin/{app.symlink}") - - # Remove app config - self.system_manager.remove_file( - f"/usr/lib/python3/dist-packages/shared_libs/{app.config}" - ) - - # Remove desktop file - self.system_manager.remove_file(f"/usr/share/applications/{app.desktop}") - - # Remove language files - for lang_file in app.languages: - self.system_manager.remove_file( - f"/usr/share/locale/de/LC_MESSAGES/{lang_file}" - ) - - # Remove user config directory - config_dir = os.path.expanduser(app.config_dir) - if os.path.exists(config_dir): - shutil.rmtree(config_dir) - - # Remove log file - log_file = os.path.expanduser(app.log_file) - if os.path.exists(log_file): - os.remove(log_file) - - # Check if other apps are still installed before removing shared resources - if not self.app_manager.check_other_apps_installed(app_key): - self.update_progress("Removing shared resources...") - self._remove_shared_resources() - - self.update_progress(f"{app.name} uninstalled successfully!") - return True - - except Exception as e: - self.update_progress(f"Error during uninstallation: {str(e)}") - raise - - def _remove_shared_resources(self): - """Remove shared resources when no apps are installed""" - # Remove shared libraries - for shared_file in self.app_manager.shared_files: - self.system_manager.remove_file( - f"/usr/lib/python3/dist-packages/shared_libs/{shared_file}" - ) - - # Remove icons and SSL directory - self.system_manager.remove_directory("/usr/share/icons/lx-icons") - self.system_manager.remove_directory("/usr/local/etc/ssl") - - # Remove shared_libs directory if empty - subprocess.run( - ["pkexec", "rmdir", "/usr/lib/python3/dist-packages/shared_libs"], - check=False, - ) - - -# ---------------------------- -# GUI Application Class (Erweiterte Version) -# ---------------------------- -class LXToolsGUI: - def __init__(self): - self.root = None - self.progress_label = None - self.download_icon_label = None - self.app_var = None - self.status_labels = {} - self.version_labels = {} - - # Initialize managers - self.app_manager = AppManager() - self.installation_manager = InstallationManager( - self.app_manager, self.update_progress, self.update_download_icon - ) - self.image_manager = ImageManager() - - # Detect OS - self.detected_os = OSDetector.detect_os() - - def create_gui(self): - """Create the main GUI""" - self.root = tk.Tk() - self.root.title(LXToolsAppConfig.APP_NAME) - self.root.geometry( - f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}" - ) - - self.root.resizable(True, True) - - # Apply theme - try: - self.root.tk.call("source", "TK-Themes/water.tcl") - self.root.tk.call("set_theme", "light") - except tk.TclError as e: - print(f"Theme loading failed: {e}") - - # Create GUI components - self._create_header() - self._create_system_info() - self._create_app_selection() # Diese wird durch erweiterte Version ersetzt - self._create_progress_section() - self._create_buttons() - self._create_info_section() - self._check_system_requirements() - - # Configure responsive layout - self._configure_responsive_layout() - - # Initial status refresh - self.refresh_status() - - return self.root - - def _create_header(self): - """Create header section""" - header_frame = tk.Frame(self.root, bg="lightblue", height=60) - header_frame.pack(fill="x", padx=10, pady=10) - header_frame.pack_propagate(False) - - title_label = tk.Label( - header_frame, - text=LXToolsAppConfig.APP_NAME, - font=("Helvetica", 18, "bold"), - bg="lightblue", - ) - title_label.pack(expand=True) - - version_label = tk.Label( - header_frame, - text=f"v{LXToolsAppConfig.VERSION}", - font=("Helvetica", 10), - bg="lightblue", - ) - version_label.pack(side="bottom") - - def _create_system_info(self): - """Create system information section""" - info_frame = tk.Frame(self.root) - info_frame.pack(pady=5) - - os_info = tk.Label( - info_frame, - text=f"{_('Detected System')}: {self.detected_os}", - font=("Helvetica", 11), - ) - os_info.pack(pady=2) - - def _create_app_selection(self): - """Create application selection section with improved Grid layout""" - selection_frame = ttk.LabelFrame( - self.root, text=_("Select Application"), padding=15 - ) - selection_frame.pack(fill="both", expand=True, padx=15, pady=10) - - self.app_var = tk.StringVar() - - # Haupt-Container mit Scrollbar (falls mehr Apps hinzukommen) - canvas = tk.Canvas(selection_frame, highlightthickness=0) - scrollbar = ttk.Scrollbar( - selection_frame, orient="vertical", command=canvas.yview - ) - scrollable_frame = ttk.Frame(canvas) - - 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) - - # Grid-Container für Apps - apps_container = tk.Frame(scrollable_frame) - apps_container.pack(fill="x", padx=5, pady=5) - - # Grid konfigurieren - 4 Spalten mit besserer Verteilung - apps_container.grid_columnconfigure(0, weight=0, minsize=60) # Icon - apps_container.grid_columnconfigure(1, weight=1, minsize=150) # App Name - apps_container.grid_columnconfigure(2, weight=0, minsize=120) # Status - apps_container.grid_columnconfigure(3, weight=0, minsize=150) # Version - - # Header-Zeile - header_font = ("Helvetica", 9, "bold") - tk.Label(apps_container, text="", font=header_font).grid( - row=0, column=0, sticky="w", padx=5, pady=2 - ) - tk.Label(apps_container, text=_("Application"), font=header_font).grid( - row=0, column=1, sticky="w", padx=5, pady=2 - ) - tk.Label(apps_container, text=_("Status"), font=header_font).grid( - row=0, column=2, sticky="w", padx=5, pady=2 - ) - tk.Label(apps_container, text=_("Version"), font=header_font).grid( - row=0, column=3, sticky="w", padx=5, pady=2 - ) - - # Trennlinie - separator = ttk.Separator(apps_container, orient="horizontal") - separator.grid(row=1, column=0, columnspan=4, sticky="ew", pady=5) - - row = 2 - for app_key, app in self.app_manager.get_all_apps().items(): - # Spalte 0: Icon - app_icon = self._load_app_icon(app) - if app_icon: - icon_label = tk.Label(apps_container, image=app_icon) - icon_label.grid(row=row, column=0, padx=5, pady=5, sticky="w") - icon_label.image = app_icon - else: - icon_text = "🔧" if app.icon_key == "icon_log" else "🔒" - icon_label = tk.Label( - apps_container, text=icon_text, font=("Helvetica", 16) - ) - icon_label.grid(row=row, column=0, padx=5, pady=5, sticky="w") - - # Spalte 1: Radio button mit App-Name - radio = ttk.Radiobutton( - apps_container, text=app.name, variable=self.app_var, value=app_key - ) - radio.grid(row=row, column=1, padx=5, pady=5, sticky="w") - - # Spalte 2: Status - status_label = tk.Label(apps_container, text="", font=("Helvetica", 9)) - status_label.grid(row=row, column=2, padx=5, pady=5, sticky="w") - self.status_labels[app_key] = status_label - - # Spalte 3: Version Info - version_label = tk.Label( - apps_container, text="", font=("Helvetica", 8), fg="gray" - ) - version_label.grid(row=row, column=3, padx=5, pady=5, sticky="w") - self.version_labels[app_key] = version_label - - row += 1 - - # Pack canvas and scrollbar - canvas.pack(side="left", fill="both", expand=True) - scrollbar.pack(side="right", fill="y") - - # Mouse wheel scrolling - def _on_mousewheel(event): - canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") - - canvas.bind_all("", _on_mousewheel) - - def _load_app_icon(self, app): - """Load icon for application using tk.PhotoImage""" - fallback_paths = [ - f"lx-icons/48/{app.icon}", - f"icons/{app.icon}", - f"./lx-icons/48/{app.icon}", - f"./icons/48/{app.icon}", - ( - f"lx-icons/48/wg_vpn.png" - if app.icon_key == "icon_vpn" - else f"lx-icons/48/log.png" - ), - ] - - return self.image_manager.load_image( - app.icon_key, fallback_paths=fallback_paths - ) - - def _create_progress_section(self): - """Create progress section with download icon using Grid""" - progress_frame = ttk.LabelFrame(self.root, text=_("Progress"), padding=10) - progress_frame.pack(fill="x", padx=15, pady=10) - - # Container für Icon und Progress mit Grid - progress_container = tk.Frame(progress_frame) - progress_container.pack(fill="x") - - # Grid konfigurieren - progress_container.grid_columnconfigure(1, weight=1) - - # Download Icon (Spalte 0) - self.download_icon_label = tk.Label( - progress_container, - text="", - width=3, - height=2, - relief="flat", - anchor="center", - ) - self.download_icon_label.grid(row=0, column=0, padx=(0, 10), pady=2, sticky="w") - - # Progress Text (Spalte 1) - self.progress_label = tk.Label( - progress_container, - text=_("Ready for installation..."), - font=("Helvetica", 10), - fg="blue", - anchor="w", - wraplength=400, - ) - self.progress_label.grid(row=0, column=1, pady=2, sticky="ew") - - # Initial icon laden (neutral) - self._reset_download_icon() - - def _configure_responsive_layout(self): - """Configure responsive layout for window resizing""" - - def on_window_resize(event): - # Adjust wraplength for progress label based on window width - if self.progress_label and event.widget == self.root: - new_width = max(300, event.width - 150) - self.progress_label.config(wraplength=new_width) - - self.root.bind("", on_window_resize) - - def _reset_download_icon(self): - """Reset download icon to neutral state""" - icon = self.image_manager.load_image( - "icon_download", - fallback_paths=["lx-icons/32/download.png", "./lx-icons/32/download.png"], - ) - if icon: - self.download_icon_label.config(image=icon, text="", compound="center") - self.download_icon_label.image = icon - else: - self.download_icon_label.config(text="📥", font=("Helvetica", 14), image="") - - 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( - "icon_download", - fallback_paths=[ - "lx-icons/32/download.png", - "./lx-icons/32/download.png", - ], - ) - if icon: - self.download_icon_label.config(image=icon, text="", compound="center") - self.download_icon_label.image = icon - else: - self.download_icon_label.config( - text="⬇️", font=("Helvetica", 14), image="" - ) - - elif status == "error": - icon = self.image_manager.load_image( - "icon_download_error", - fallback_paths=[ - "lx-icons/32/download_error.png", - "./lx-icons/32/download_error.png", - "/home/punix/Pyapps/installer-appimage/lx-icons/32/download_error.png", - ], - ) - if icon: - self.download_icon_label.config(image=icon, text="", compound="center") - self.download_icon_label.image = icon - else: - self.download_icon_label.config( - text="❌", font=("Helvetica", 14), image="" - ) - - elif status == "success": - icon = self.image_manager.load_image( - "icon_download", - fallback_paths=[ - "lx-icons/32/download.png", - "./lx-icons/32/download.png", - ], - ) - if icon: - self.download_icon_label.config(image=icon, text="", compound="center") - self.download_icon_label.image = icon - else: - self.download_icon_label.config( - text="✅", font=("Helvetica", 14), image="" - ) - - self.download_icon_label.update() - - def _create_buttons(self): - """Create button section using Grid""" - button_frame = tk.Frame(self.root) - button_frame.pack(pady=15) - - # Grid für Buttons - 3 Spalten - button_frame.grid_columnconfigure(0, weight=1) - button_frame.grid_columnconfigure(1, weight=1) - button_frame.grid_columnconfigure(2, weight=1) - - # Configure button styles - style = ttk.Style() - style.configure("Install.TButton", foreground="green") - style.configure("Uninstall.TButton", foreground="red") - style.configure("Refresh.TButton", foreground="blue") - - install_btn = ttk.Button( - button_frame, - text=_("Install / Update"), - command=self.install_app, - style="Install.TButton", - ) - install_btn.grid(row=0, column=0, padx=8, sticky="ew") - - uninstall_btn = ttk.Button( - button_frame, - text=_("Uninstall"), - command=self.uninstall_app, - style="Uninstall.TButton", - ) - uninstall_btn.grid(row=0, column=1, padx=8, sticky="ew") - - refresh_btn = ttk.Button( - button_frame, - text=_("Refresh Status"), - command=self.refresh_status, - style="Refresh.TButton", - ) - refresh_btn.grid(row=0, column=2, padx=8, sticky="ew") - - def _create_info_section(self): - """Create information section""" - info_text = tk.Label( - self.root, - text=_( - "Notes:\n" - "• Applications are downloaded automatically from the repository\n" - "• Root privileges are requested via pkexec when needed\n" - "• Shared libraries are managed automatically\n" - "• User configuration files are preserved during updates\n" - "• Policy files for pkexec are installed automatically" - ), - font=("Helvetica", 9), - fg="gray", - wraplength=450, - justify="left", - ) - info_text.pack(pady=15, padx=20) - - def _check_system_requirements(self): - """Check system requirements""" - try: - subprocess.run(["which", "pkexec"], check=True, capture_output=True) - except subprocess.CalledProcessError: - warning_label = tk.Label( - self.root, - text=_("⚠️ WARNING: pkexec is not available! Installation will fail."), - font=("Helvetica", 10, "bold"), - fg="red", - ) - warning_label.pack(pady=5) - - def update_progress(self, message): - """Update progress label""" - if self.progress_label: - self.progress_label.config(text=message) - self.progress_label.update() - - def refresh_status(self): - """Refresh application status and version information""" - self.update_progress(_("Refreshing status and checking versions...")) - self._reset_download_icon() - - for app_key, app in self.app_manager.get_all_apps().items(): - status_label = self.status_labels[app_key] - version_label = self.version_labels[app_key] - - if app.is_installed(): - installed_version = app.get_installed_version() - status_label.config( - text=f"✅ {_('Installed')} (v{installed_version})", fg="green" - ) - - # Get latest version from API - try: - latest_version = app.get_latest_version() - if latest_version != "Unknown": - if installed_version != latest_version: - version_label.config( - text=f"{_('Latest')}: v{latest_version} ({_('Update available')})", - fg="orange", - ) - else: - version_label.config( - text=f"{_('Latest')}: v{latest_version} ({_('Up to date')})", - fg="green", - ) - else: - version_label.config( - text=f"{_('Latest')}: {_('Unknown')}", fg="gray" - ) - except: - version_label.config( - text=f"{_('Latest')}: {_('Check failed')}", fg="gray" - ) - else: - status_label.config(text=f"❌ {_('Not installed')}", fg="red") - - # Still show latest available version - try: - latest_version = app.get_latest_version() - if latest_version != "Unknown": - version_label.config( - text=f"{_('Available')}: v{latest_version}", fg="blue" - ) - else: - version_label.config( - text=f"{_('Available')}: {_('Unknown')}", fg="gray" - ) - except: - version_label.config( - text=f"{_('Available')}: {_('Check failed')}", fg="gray" - ) - - self.update_progress(_("Status refresh completed.")) - - def install_app(self): - """Handle install button click""" - selected_app = self.app_var.get() - if not selected_app: - messagebox.showwarning( - _("Warning"), _("Please select an application to install.") - ) - return - - # Check internet connection - if not NetworkChecker.check_internet_connection(): - self.update_download_icon("error") - messagebox.showerror( - _("Network Error"), - _( - "No internet connection available.\nPlease check your network connection." - ), - ) - return - - if not NetworkChecker.check_repository_access(): - self.update_download_icon("error") - messagebox.showerror( - _("Repository Error"), - _("Cannot access repository.\nPlease try again later."), - ) - return - - # Reset download icon - self._reset_download_icon() - app = self.app_manager.get_app(selected_app) - - # Check if already installed - if app.is_installed(): - installed_version = app.get_installed_version() - latest_version = app.get_latest_version() - - dialog_text = ( - f"{app.name} {_('is already installed')}.\n\n" - f"{_('Installed version')}: v{installed_version}\n" - f"{_('Latest version')}: v{latest_version}\n\n" - f"{_('YES')} = {_('Update')} ({_('reinstall all files')})\n" - f"{_('NO')} = {_('Uninstall')}\n" - f"{_('Cancel')} = {_('Do nothing')}" - ) - - result = messagebox.askyesnocancel( - f"{app.name} {_('already installed')}", dialog_text - ) - - if result is None: # Cancel - self.update_progress(_("Installation cancelled.")) - return - elif not result: # Uninstall - self.uninstall_app(selected_app) - return - else: # Update - self.update_progress(_("Updating application...")) - - try: - self.installation_manager.install_app(selected_app) - messagebox.showinfo( - _("Success"), - f"{app.name} {_('has been successfully installed/updated')}.", - ) - self.refresh_status() - except Exception as e: - # Bei Fehler Error-Icon anzeigen - self.update_download_icon("error") - messagebox.showerror(_("Error"), f"{_('Installation failed')}: {e}") - - def uninstall_app(self, app_key=None): - """Handle uninstall button click""" - if app_key is None: - app_key = self.app_var.get() - - if not app_key: - messagebox.showwarning( - _("Warning"), _("Please select an application to uninstall.") - ) - return - - app = self.app_manager.get_app(app_key) - - if not app.is_installed(): - messagebox.showinfo(_("Info"), f"{app.name} {_('is not installed')}.") - return - - result = messagebox.askyesno( - _("Confirm Uninstall"), - f"{_('Are you sure you want to uninstall')} {app.name}?\n\n" - f"{_('This will remove all application files and user configurations')}.", - ) - if not result: - return - - try: - self.installation_manager.uninstall_app(app_key) - messagebox.showinfo( - _("Success"), f"{app.name} {_('has been successfully uninstalled')}." - ) - self.refresh_status() - except Exception as e: - messagebox.showerror(_("Error"), f"{_('Uninstallation failed')}: {e}") - - def run(self): - """Start the GUI application""" - root = self.create_gui() - root.mainloop() - - -# ---------------------------- -# Main Application Entry Point -# ---------------------------- -def main(): - """Main function to start the application""" - try: - # Create and run the GUI - app = LXToolsGUI() - app.run() - except KeyboardInterrupt: - print("\nApplication interrupted by user.") - except Exception as e: - print(f"Fatal error: {e}") - messagebox.showerror( - _("Fatal Error"), f"{_('Application failed to start')}: {e}" - ) - - -if __name__ == "__main__": - main() diff --git a/lxtools_installerv2.py b/lxtools_installerv2.py deleted file mode 100755 index 0ff6bc2..0000000 --- a/lxtools_installerv2.py +++ /dev/null @@ -1,1584 +0,0 @@ -#!/usr/bin/python3 -import tkinter as tk -from tkinter import messagebox, ttk -import os -import subprocess -import urllib.request -import json -import tempfile -import zipfile -import shutil -import sys -import socket - -# Add current directory to path for imports -current_dir = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, current_dir) - -# Try to import common_tools, but provide fallback -try: - from common_tools import LxTools, center_window_cross_platform - - USE_LXTOOLS = True - print("Using local common_tools from LxTools project") -except ImportError: - try: - from shared_libs.common_tools import LxTools, center_window_cross_platform - - USE_LXTOOLS = True - print("Using shared_libs.common_tools") - except ImportError: - print("Warning: common_tools not found, using integrated methods") - USE_LXTOOLS = False - - -# ---------------------------- -# App Configuration Class (korrigiert) -# ---------------------------- -class LXToolsAppConfig: - VERSION = "1.0.8" - APP_NAME = "LX Tools Installer" - WINDOW_WIDTH = 650 - WINDOW_HEIGHT = 600 - DEBUG_WINDOW_HEIGHT = 700 - - # LxTools Installer eigene Ressourcen - WORK_DIR = os.path.dirname(os.path.abspath(__file__)) - ICONS_DIR = os.path.join(WORK_DIR, "lx-icons") - THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes") - - # Download URLs für alle installierbaren Projekte - PROJECTS = { - "wirepy": { - "name": "Wire-Py", - "description": "🔐 WireGuard VPN Manager", - "download_url": "https://git.ilunix.de/punix/Wire-Py/archive/main.zip", - "api_url": "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases", - "archive_folder": "Wire-Py", - "icon_key": "wirepy_icon", - }, - "logviewer": { - "name": "LogViewer", - "description": "📋 System Log Viewer", - "download_url": "https://git.ilunix.de/punix/shared_libs/archive/main.zip", - "api_url": "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases", - "archive_folder": "shared_libs", - "icon_key": "logviewer_icon", - }, - } - - # Shared Libraries (für alle Projekte benötigt) - SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip" - SHARED_LIBS_API_URL = ( - "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases" - ) - - @staticmethod - def get_icon_path(icon_key): - """Get icon path with fallbacks - KORRIGIERT""" - base_paths = [ - LXToolsAppConfig.ICONS_DIR, - os.path.join(LXToolsAppConfig.WORK_DIR, "lx-icons"), - "./lx-icons", - "/usr/share/icons/lx-icons", - ] - - icon_mapping = { - "app_icon": ["64/download.png", "48/download.png", "32/download.png"], - "wirepy_icon": ["32/wg_vpn.png", "48/wg_vpn.png"], - "logviewer_icon": ["32/log.png", "48/log.png"], - "download_icon": ["32/download.png", "48/download.png"], - "download_error_icon": ["32/download_error.png", "48/error.png"], - "success_icon": ["32/download.png", "48/download.png"], - } - - if icon_key not in icon_mapping: - return None - - for base_path in base_paths: - if not os.path.exists(base_path): - continue - for icon_file in icon_mapping[icon_key]: - full_path = os.path.join(base_path, icon_file) - if os.path.exists(full_path): - print(f"Found icon: {full_path}") - return full_path - - print(f"Icon not found: {icon_key}") - return None - - -# ---------------------------- -# Integrierte LxTools Methoden (korrigiert) -# ---------------------------- -class IntegratedLxTools: - @staticmethod - def center_window_cross_platform(window): - """Center window on screen - works with multiple monitors""" - window.update_idletasks() - - # Get window dimensions - window_width = window.winfo_reqwidth() - window_height = window.winfo_reqheight() - - # Get screen dimensions - screen_width = window.winfo_screenwidth() - screen_height = window.winfo_screenheight() - - # Calculate position - pos_x = (screen_width // 2) - (window_width // 2) - pos_y = (screen_height // 2) - (window_height // 2) - - # Ensure window is not positioned off-screen - pos_x = max(0, pos_x) - pos_y = max(0, pos_y) - - window.geometry(f"{window_width}x{window_height}+{pos_x}+{pos_y}") - - @staticmethod - def msg_window(parent, title, message, msg_type="info", width=400, height=200): - """Custom message window with proper centering""" - msg_win = tk.Toplevel(parent) - msg_win.title(title) - msg_win.geometry(f"{width}x{height}") - msg_win.transient(parent) - msg_win.grab_set() - - # Configure grid - msg_win.grid_columnconfigure(0, weight=1) - msg_win.grid_rowconfigure(0, weight=1) - msg_win.grid_rowconfigure(1, weight=0) - - # Message frame - msg_frame = ttk.Frame(msg_win, padding=20) - msg_frame.grid(row=0, column=0, sticky="nsew") - msg_frame.grid_columnconfigure(0, weight=1) - msg_frame.grid_rowconfigure(0, weight=1) - - # Message text - msg_label = tk.Label( - msg_frame, - text=message, - wraplength=width - 40, - justify="left", - font=("Helvetica", 10), - ) - msg_label.grid(row=0, column=0, sticky="nsew") - - # Button frame - btn_frame = ttk.Frame(msg_win) - btn_frame.grid(row=1, column=0, sticky="ew", padx=20, pady=(0, 20)) - btn_frame.grid_columnconfigure(0, weight=1) - - # OK Button - ok_btn = ttk.Button(btn_frame, text="OK", command=msg_win.destroy) - ok_btn.grid(row=0, column=0) - - # Center the window - IntegratedLxTools.center_window_cross_platform(msg_win) - - # Focus - msg_win.focus_set() - ok_btn.focus_set() - - return msg_win - - -# ---------------------------- -# Theme Manager Class (korrigiert) -# ---------------------------- -class ThemeManager: - @staticmethod - def apply_light_theme(root): - """Apply light theme using your working method""" - try: - # Verwende TK-Themes aus dem aktuellen LxTools Projekt-Ordner - theme_dir = LXToolsAppConfig.THEMES_DIR - water_theme_path = os.path.join(theme_dir, "water.tcl") - - print(f"Looking for theme at: {water_theme_path}") - - if os.path.exists(water_theme_path): - try: - # DEINE funktionierende Methode: - root.tk.call("source", water_theme_path) - root.tk.call("set_theme", "light") - print("Successfully applied water theme with set_theme light") - return True - except tk.TclError as e: - print(f"Theme loading failed: {e}") - - # Fallback: Versuche ohne set_theme - try: - root.tk.call("source", water_theme_path) - style = ttk.Style() - available_themes = style.theme_names() - print(f"Available themes: {available_themes}") - - # Versuche verschiedene Theme-Namen - for theme_name in ["water", "Water", "light", "awlight"]: - if theme_name in available_themes: - style.theme_use(theme_name) - print(f"Applied theme: {theme_name}") - return True - - except Exception as e2: - print(f"Fallback theme loading failed: {e2}") - else: - print(f"Theme file not found: {water_theme_path}") - print(f"Current working directory: {os.getcwd()}") - print(f"Theme directory exists: {os.path.exists(theme_dir)}") - if os.path.exists(theme_dir): - print(f"Files in theme directory: {os.listdir(theme_dir)}") - - # System theme fallback - try: - style = ttk.Style() - if "clam" in style.theme_names(): - style.theme_use("clam") - print("Using fallback theme: clam") - return True - except: - pass - - except Exception as e: - print(f"Theme loading completely failed: {e}") - - return False - - -# ---------------------------- -# Image Manager Class (korrigiert) -# ---------------------------- -class ImageManager: - def __init__(self): - self.images = {} - - def load_image(self, icon_key, fallback_paths=None): - """Load PNG image using tk.PhotoImage with fallback options""" - if icon_key in self.images: - return self.images[icon_key] - - # Get primary path from config - primary_path = LXToolsAppConfig.get_icon_path(icon_key) - paths_to_try = [] - - if primary_path: - paths_to_try.append(primary_path) - - # Add fallback paths - if fallback_paths: - paths_to_try.extend(fallback_paths) - - # Try to load image from paths - for path in paths_to_try: - try: - if os.path.exists(path): - photo = tk.PhotoImage(file=path) - self.images[icon_key] = photo - print(f"Successfully loaded image: {path}") - return photo - except tk.TclError as e: - print(f"Failed to load image from {path}: {e}") - continue - - # Return None if no image found - print(f"No image found for key: {icon_key}") - return None - - -# ---------------------------- -# OS Detection Class (korrigiert) -# ---------------------------- -class OSDetector: - OS_DETECTION = [ - ("mint", "Linux Mint"), - ("pop", "Pop!_OS"), - ("manjaro", "Manjaro"), - ("garuda", "Garuda Linux"), - ("endeavouros", "EndeavourOS"), - ("fedora", "Fedora"), - ("tumbleweed", "SUSE Tumbleweed"), - ("leap", "SUSE Leap"), - ("suse", "openSUSE"), - ("arch", "Arch Linux"), - ("ubuntu", "Ubuntu"), - ("debian", "Debian"), - ] - - @staticmethod - def detect_os(): - """Detect operating system using ordered list""" - try: - with open("/etc/os-release", "r") as f: - content = f.read().lower() - - # Check each OS in order (specific first) - for keyword, os_name in OSDetector.OS_DETECTION: - if keyword in content: - return os_name - - return "Unknown System" - except FileNotFoundError: - return "File not found" - - -# ---------------------------- -# Network Checker Class (korrigiert) -# ---------------------------- -class NetworkChecker: - @staticmethod - def check_internet(host="8.8.8.8", port=53, timeout=3): # ← Korrigierter Name - """Check if internet connection is available""" - try: - socket.setdefaulttimeout(timeout) - socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) - return True - except socket.error: - return False - - @staticmethod - def check_repository(url="https://git.ilunix.de"): # ← Korrigierter Name - """Check if repository is accessible""" - try: - urllib.request.urlopen(url, timeout=5) - return True - except: - return False - - -# ---------------------------- -# App Manager Class (korrigiert) -# ---------------------------- -class AppManager: - def __init__(self): - self.projects = LXToolsAppConfig.PROJECTS - - def get_all_projects(self): - """Get all project configurations""" - return self.projects - - def get_project_info(self, project_key): - """Get project information by key""" - return self.projects.get(project_key) - - def is_installed(self, project_key): - """Check if project is installed""" - if project_key == "wirepy": - return os.path.exists("/usr/local/bin/wirepy") - elif project_key == "logviewer": - return os.path.exists("/usr/local/bin/logviewer") - else: - return os.path.exists(f"/usr/local/bin/{project_key}") - - def get_installed_version(self, project_key): - """Get installed version from config file""" - try: - if project_key == "wirepy": - config_file = ( - "/usr/lib/python3/dist-packages/shared_libs/wp_app_config.py" - ) - elif project_key == "logviewer": - config_file = ( - "/usr/lib/python3/dist-packages/shared_libs/logview_app_config.py" - ) - else: - config_file = f"/usr/lib/python3/dist-packages/shared_libs/{project_key}_app_config.py" - - if os.path.exists(config_file): - with open(config_file, "r") as f: - content = f.read() - for line in content.split("\n"): - if "VERSION" in line and "=" in line: - version = line.split("=")[1].strip().strip("\"'") - return version - return "Unknown" - except Exception as e: - print(f"Error getting version for {project_key}: {e}") - return "Unknown" - - def get_latest_version(self, project_key): - """Get latest version from API - KORRIGIERT""" - try: - project_info = self.get_project_info(project_key) - if not project_info: - return "Unknown" - - with urllib.request.urlopen( - project_info["api_url"], timeout=10 - ) as response: - data = json.loads(response.read().decode()) - if data and len(data) > 0: - latest_version = data[0].get("tag_name", "Unknown") - return latest_version.lstrip("v") - return "Unknown" # ← FIX: Korrigierte Syntax - except Exception as e: - print(f"API Error for {project_key}: {e}") - return "Unknown" # ← FIX: Korrigierte Syntax - - -# ---------------------------- -# Installation Manager Class (korrigiert) -# ---------------------------- -class InstallationManager: - def __init__( - self, - app_manager, - progress_callback=None, - icon_callback=None, - debug_callback=None, - ): - self.app_manager = app_manager - self.progress_callback = progress_callback - self.icon_callback = icon_callback - self.debug_callback = debug_callback - - def install_project(self, project_key): - """Install any project generically""" - project_info = self.app_manager.get_project_info(project_key) - if not project_info: - raise Exception(f"Unknown project: {project_key}") - - self.update_progress(f"Starting {project_info['name']} installation...") - self.update_icon("downloading") - - try: - with tempfile.TemporaryDirectory() as temp_dir: - # Download project - self.update_progress(f"Downloading {project_info['name']}...") - if not self._download_and_extract( - project_info["download_url"], temp_dir - ): - raise Exception(f"Failed to download {project_info['name']}") - - # Download shared libs - self.update_progress("Downloading shared libraries...") - shared_temp = os.path.join(temp_dir, "shared") - if not self._download_and_extract( - LXToolsAppConfig.SHARED_LIBS_URL, shared_temp - ): - raise Exception("Failed to download shared libraries") - - # Create installation script - self.update_progress("Preparing installation...") - script_path = self._create_install_script( - project_key, project_info, temp_dir - ) - - # Execute installation - self.update_progress("Installing...") - self._execute_install_script(script_path) - - self.update_progress(f"{project_info['name']} installation completed!") - self.update_icon("success") - return True - - except Exception as e: - self.update_icon("error") - raise Exception(f"Installation failed: {e}") - - def _download_and_extract(self, url, extract_to): - """Download and extract ZIP file""" - try: - with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file: - urllib.request.urlretrieve(url, tmp_file.name) - - 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: - self.debug_log(f"Download failed: {e}") - return False - - def _create_install_script(self, project_key, project_info, temp_dir): - """Create installation script for any project""" - with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f: - - project_source = os.path.join(temp_dir, project_info["archive_folder"]) - shared_source = os.path.join(temp_dir, "shared", "shared_libs") - - if project_key == "wirepy": - script_content = f"""#!/bin/bash -set -e - -echo "=== {project_info['name']} Installation Script ===" - -# Create directories -mkdir -p /usr/lib/python3/dist-packages/shared_libs -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 - -# Install shared libraries -echo "Installing shared libraries..." -if [ -d "{shared_source}" ]; then - cp -f "{shared_source}"/*.py /usr/lib/python3/dist-packages/shared_libs/ 2>/dev/null || true -fi - -# Install Wire-Py files -echo "Installing Wire-Py files..." -cp -f "{project_source}/wirepy.py" /usr/local/bin/ -cp -f "{project_source}/start_wg.py" /usr/local/bin/ -cp -f "{project_source}/ssl_encrypt.py" /usr/local/bin/ -cp -f "{project_source}/ssl_decrypt.py" /usr/local/bin/ -cp -f "{project_source}/match_found.py" /usr/local/bin/ -cp -f "{project_source}/tunnel.py" /usr/local/bin/ 2>/dev/null || true - -# Make executable -chmod 755 /usr/local/bin/wirepy.py -chmod 755 /usr/local/bin/start_wg.py -chmod 755 /usr/local/bin/ssl_encrypt.py -chmod 755 /usr/local/bin/ssl_decrypt.py -chmod 755 /usr/local/bin/match_found.py -chmod 755 /usr/local/bin/tunnel.py 2>/dev/null || true - -# Install config -cp -f "{project_source}/wp_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ - -# Install icons -echo "Installing icons..." -if [ -d "{project_source}/lx-icons" ]; then - cp -rf "{project_source}/lx-icons"/* /usr/share/icons/lx-icons/ -fi - -# Install desktop file -if [ -f "{project_source}/Wire-Py.desktop" ]; then - cp -f "{project_source}/Wire-Py.desktop" /usr/share/applications/ -fi - -# Install policy file -if [ -f "{project_source}/org.sslcrypt.policy" ]; then - cp -f "{project_source}/org.sslcrypt.policy" /usr/share/polkit-1/actions/ -fi - -# Install language files -echo "Installing language files..." -if [ -d "{project_source}/languages/de" ]; then - cp -f "{project_source}/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true -fi - -# Create symlink -rm -f /usr/local/bin/wirepy -ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy - -# 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 2>/dev/null || echo "Warning: SSL key creation failed" - chmod 600 /usr/local/etc/ssl/pwgk.pem 2>/dev/null || true -fi - -echo "=== {project_info['name']} installation completed ===" -""" - - elif project_key == "logviewer": - script_content = f"""#!/bin/bash -set -e - -echo "=== {project_info['name']} Installation Script ===" - -# Create directories -mkdir -p /usr/lib/python3/dist-packages/shared_libs -mkdir -p /usr/share/icons/lx-icons -mkdir -p /usr/share/locale/de/LC_MESSAGES -mkdir -p /usr/share/applications - -# Install shared libraries -echo "Installing shared libraries..." -if [ -d "{shared_source}" ]; then - cp -f "{shared_source}"/*.py /usr/lib/python3/dist-packages/shared_libs/ 2>/dev/null || true -fi - -# Install LogViewer -echo "Installing LogViewer..." -cp -f "{shared_source}/logviewer.py" /usr/local/bin/ -chmod 755 /usr/local/bin/logviewer.py - -# Install config -cp -f "{shared_source}/logview_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ - -# Install icons (if available) -if [ -d "{shared_source}/lx-icons" ]; then - cp -rf "{shared_source}/lx-icons"/* /usr/share/icons/lx-icons/ 2>/dev/null || true -fi - -# Install desktop file (if available) -if [ -f "{shared_source}/LogViewer.desktop" ]; then - cp -f "{shared_source}/LogViewer.desktop" /usr/share/applications/ -fi - -# Install language files (if available) -if [ -d "{shared_source}/languages/de" ]; then - cp -f "{shared_source}/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true -fi - -# Create symlink -rm -f /usr/local/bin/logviewer -ln -sf /usr/local/bin/logviewer.py /usr/local/bin/logviewer - -echo "=== {project_info['name']} installation completed ===" -""" - - else: - # Generisches Script für zukünftige Projekte - script_content = f"""#!/bin/bash -set -e - -echo "=== {project_info['name']} Installation Script ===" - -# Create directories -mkdir -p /usr/lib/python3/dist-packages/shared_libs -mkdir -p /usr/share/icons/lx-icons -mkdir -p /usr/share/locale/de/LC_MESSAGES -mkdir -p /usr/share/applications - -# Install shared libraries -echo "Installing shared libraries..." -if [ -d "{shared_source}" ]; then - cp -f "{shared_source}"/*.py /usr/lib/python3/dist-packages/shared_libs/ 2>/dev/null || true -fi - -# Install project files (generic approach) -echo "Installing {project_info['name']} files..." -if [ -f "{project_source}/{project_key}.py" ]; then - cp -f "{project_source}/{project_key}.py" /usr/local/bin/ - chmod 755 /usr/local/bin/{project_key}.py - - # Create symlink - rm -f /usr/local/bin/{project_key} - ln -sf /usr/local/bin/{project_key}.py /usr/local/bin/{project_key} -fi - -# Install config (if exists) -if [ -f "{project_source}/{project_key}_app_config.py" ]; then - cp -f "{project_source}/{project_key}_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ -fi - -# Install icons (if available) -if [ -d "{project_source}/lx-icons" ]; then - cp -rf "{project_source}/lx-icons"/* /usr/share/icons/lx-icons/ 2>/dev/null || true -fi - -# Install desktop file (if available) -if [ -f "{project_source}/{project_info['name']}.desktop" ]; then - cp -f "{project_source}/{project_info['name']}.desktop" /usr/share/applications/ -fi - -# Install language files (if available) -if [ -d "{project_source}/languages/de" ]; then - cp -f "{project_source}/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true -fi - -echo "=== {project_info['name']} installation completed ===" -""" - - f.write(script_content) - script_path = f.name - - # Make script executable - os.chmod(script_path, 0o755) - self.debug_log(f"Created install script: {script_path}") - return script_path - - def _execute_install_script(self, script_path): - """Execute installation script with pkexec""" - try: - result = subprocess.run( - ["pkexec", "bash", script_path], - capture_output=True, - text=True, - timeout=120, - ) - - if result.returncode != 0: - error_msg = f"Installation script failed: {result.stderr}" - self.debug_log(f"ERROR: {error_msg}") - self.debug_log(f"STDOUT: {result.stdout}") - raise Exception(error_msg) - - self.debug_log("Installation script output:") - self.debug_log(result.stdout) - - except subprocess.TimeoutExpired: - raise Exception("Installation timed out") - except subprocess.CalledProcessError as e: - raise Exception(f"Installation script failed: {e}") - - def update_progress(self, message): - if self.progress_callback: - self.progress_callback(message) - self.debug_log(f"Progress: {message}") - - def update_icon(self, status): - if self.icon_callback: - self.icon_callback(status) - - def debug_log(self, message): - if self.debug_callback: - self.debug_callback(message) - print(message) - - -# ---------------------------- -# Uninstallation Manager Class (korrigiert) -# ---------------------------- -class UninstallationManager: - def __init__(self, app_manager, progress_callback=None, debug_callback=None): - self.app_manager = app_manager - self.progress_callback = progress_callback - self.debug_callback = debug_callback - - def uninstall_project(self, project_key): - """Uninstall any project generically""" - project_info = self.app_manager.get_project_info(project_key) - if not project_info: - raise Exception(f"Unknown project: {project_key}") - - if not self.app_manager.is_installed(project_key): - raise Exception(f"{project_info['name']} is not installed.") - - self.update_progress(f"Starting {project_info['name']} uninstallation...") - - try: - # Create uninstallation script - script_path = self._create_uninstall_script(project_key, project_info) - - # Execute uninstallation - self.update_progress("Executing uninstallation...") - self._execute_uninstall_script(script_path) - - # Remove user config directories - self._cleanup_user_files(project_key) - - self.update_progress(f"{project_info['name']} uninstalled successfully!") - return True - - except Exception as e: - self.update_progress(f"Error during uninstallation: {str(e)}") - raise - - def _create_uninstall_script(self, project_key, project_info): - """Create uninstallation script""" - with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f: - - if project_key == "wirepy": - script_content = f"""#!/bin/bash -set -e - -echo "=== {project_info['name']} Uninstallation Script ===" - -# Remove Wire-Py files -rm -f /usr/local/bin/wirepy.py -rm -f /usr/local/bin/start_wg.py -rm -f /usr/local/bin/ssl_encrypt.py -rm -f /usr/local/bin/ssl_decrypt.py -rm -f /usr/local/bin/match_found.py -rm -f /usr/local/bin/tunnel.py -rm -f /usr/local/bin/wirepy - -# Remove config -rm -f /usr/lib/python3/dist-packages/shared_libs/wp_app_config.py - -# Remove desktop file -rm -f /usr/share/applications/Wire-Py.desktop - -# Remove policy file -rm -f /usr/share/polkit-1/actions/org.sslcrypt.policy - -# Remove language files -rm -f /usr/share/locale/de/LC_MESSAGES/wirepy.mo - -# Check if other projects are still installed -OTHER_PROJECTS_INSTALLED=false -if [ -f "/usr/local/bin/logviewer" ]; then - OTHER_PROJECTS_INSTALLED=true -fi - -# Remove shared resources only if no other projects -if [ "$OTHER_PROJECTS_INSTALLED" = false ]; then - echo "No other LX projects found, removing shared resources..." - rm -rf /usr/share/icons/lx-icons - rm -rf /usr/local/etc/ssl - rmdir /usr/lib/python3/dist-packages/shared_libs 2>/dev/null || true -fi - -echo "=== {project_info['name']} uninstallation completed ===" -""" - - elif project_key == "logviewer": - script_content = f"""#!/bin/bash -set -e - -echo "=== {project_info['name']} Uninstallation Script ===" - -# Remove LogViewer files -rm -f /usr/local/bin/logviewer.py -rm -f /usr/local/bin/logviewer - -# Remove config -rm -f /usr/lib/python3/dist-packages/shared_libs/logview_app_config.py - -# Remove desktop file -rm -f /usr/share/applications/LogViewer.desktop - -# Remove language files -rm -f /usr/share/locale/de/LC_MESSAGES/logviewer.mo - -# Check if other projects are still installed -OTHER_PROJECTS_INSTALLED=false -if [ -f "/usr/local/bin/wirepy" ]; then - OTHER_PROJECTS_INSTALLED=true -fi - -# Remove shared resources only if no other projects -if [ "$OTHER_PROJECTS_INSTALLED" = false ]; then - echo "No other LX projects found, removing shared resources..." - rm -rf /usr/share/icons/lx-icons - rmdir /usr/lib/python3/dist-packages/shared_libs 2>/dev/null || true -fi - -echo "=== {project_info['name']} uninstallation completed ===" -""" - - else: - # Generisches Uninstall-Script - script_content = f"""#!/bin/bash -set -e - -echo "=== {project_info['name']} Uninstallation Script ===" - -# Remove project files -rm -f /usr/local/bin/{project_key}.py -rm -f /usr/local/bin/{project_key} - -# Remove config -rm -f /usr/lib/python3/dist-packages/shared_libs/{project_key}_app_config.py - -# Remove desktop file -rm -f /usr/share/applications/{project_info['name']}.desktop - -echo "=== {project_info['name']} uninstallation completed ===" -""" - - f.write(script_content) - script_path = f.name - - # Make script executable - os.chmod(script_path, 0o755) - self.debug_log(f"Created uninstall script: {script_path}") - return script_path - - def _execute_uninstall_script(self, script_path): - """Execute uninstallation script with pkexec""" - try: - result = subprocess.run( - ["pkexec", "bash", script_path], - capture_output=True, - text=True, - timeout=60, - ) - - if result.returncode != 0: - error_msg = f"Uninstallation script failed: {result.stderr}" - self.debug_log(f"ERROR: {error_msg}") - self.debug_log(f"STDOUT: {result.stdout}") - raise Exception(error_msg) - - self.debug_log("Uninstallation script output:") - self.debug_log(result.stdout) - - except subprocess.TimeoutExpired: - raise Exception("Uninstallation timed out") - except subprocess.CalledProcessError as e: - raise Exception(f"Uninstallation script failed: {e}") - - def _cleanup_user_files(self, project_key): - """Clean up user configuration files""" - try: - if project_key == "wirepy": - config_dir = os.path.expanduser("~/.config/wire_py") - log_file = os.path.expanduser("~/.local/share/lxlogs/wirepy.log") - elif project_key == "logviewer": - config_dir = os.path.expanduser("~/.config/logviewer") - log_file = os.path.expanduser("~/.local/share/lxlogs/logviewer.log") - else: - config_dir = os.path.expanduser(f"~/.config/{project_key}") - log_file = os.path.expanduser( - f"~/.local/share/lxlogs/{project_key}.log" - ) - - # Remove user config directory - if os.path.exists(config_dir): - shutil.rmtree(config_dir) - self.debug_log(f"Removed user config: {config_dir}") - - # Remove log file - if os.path.exists(log_file): - os.remove(log_file) - self.debug_log(f"Removed log file: {log_file}") - - except Exception as e: - self.debug_log(f"Warning: Could not clean up user files: {e}") - - def update_progress(self, message): - if self.progress_callback: - self.progress_callback(message) - self.debug_log(f"Progress: {message}") - - def debug_log(self, message): - if self.debug_callback: - self.debug_callback(message) - print(message) - - -# ---------------------------- -# Main GUI Application Class (korrigiert) -# ---------------------------- -class LXToolsGUI: - def __init__(self): - self.root = None - self.progress_label = None - self.download_icon_label = None - self.project_var = None - self.status_labels = {} - self.version_labels = {} - self.debug_text = None - self.show_debug = False - - # Initialize managers - self.app_manager = AppManager() - self.installation_manager = InstallationManager( - self.app_manager, - self.update_progress, - self.update_download_icon, - self.debug_log, - ) - self.uninstallation_manager = UninstallationManager( - self.app_manager, self.update_progress, self.debug_log - ) - self.image_manager = ImageManager() - - # Detect OS - self.detected_os = OSDetector.detect_os() - - def create_gui(self): - """Create the main GUI""" - self.root = tk.Tk() - self.root.title(f"{LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}") - - # Set window size - window_height = ( - LXToolsAppConfig.DEBUG_WINDOW_HEIGHT - if self.show_debug - else LXToolsAppConfig.WINDOW_HEIGHT - ) - self.root.geometry(f"{LXToolsAppConfig.WINDOW_WIDTH}x{window_height}") - - # Apply theme - ThemeManager.apply_light_theme(self.root) - - # Configure main grid - self.root.grid_columnconfigure(0, weight=1) - self.root.grid_rowconfigure(0, weight=0) # Header - self.root.grid_rowconfigure(1, weight=0) # OS Info - self.root.grid_rowconfigure(2, weight=1) # Projects - self.root.grid_rowconfigure(3, weight=0) # Progress - self.root.grid_rowconfigure(4, weight=0) # Buttons - if self.show_debug: - self.root.grid_rowconfigure(5, weight=1) # Debug - - # Create GUI sections - self._create_header_section() - self._create_os_info_section() - self._create_projects_section() - self._create_progress_section() - self._create_buttons_section() - - if self.show_debug: - self._create_debug_section() - - # Load app icon - self._load_app_icon() - - # Center window - if USE_LXTOOLS: - center_window_cross_platform(self.root) - else: - IntegratedLxTools.center_window_cross_platform(self.root) - - # Initial status refresh - self.root.after(100, self.refresh_status) - - return self.root - - def _create_header_section(self): - """Create header section with title and version""" - header_frame = ttk.Frame(self.root, padding=15) - header_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) - header_frame.grid_columnconfigure(0, weight=1) - - # Title - title_label = tk.Label( - header_frame, - text=f"🚀 {LXToolsAppConfig.APP_NAME}", - font=("Helvetica", 16, "bold"), - fg="#2E86AB", - ) - title_label.grid(row=0, column=0, sticky="w") - - # Version - version_label = tk.Label( - header_frame, - text=f"Version {LXToolsAppConfig.VERSION}", - font=("Helvetica", 10), - fg="gray", - ) - version_label.grid(row=1, column=0, sticky="w") - - def _create_os_info_section(self): - """Create OS information section""" - os_frame = ttk.LabelFrame(self.root, text="System Information", padding=10) - os_frame.grid(row=1, column=0, sticky="ew", padx=15, pady=10) - os_frame.grid_columnconfigure(1, weight=1) - - # OS Detection - tk.Label(os_frame, text="Detected OS:", font=("Helvetica", 10, "bold")).grid( - row=0, column=0, sticky="w", padx=(0, 10) - ) - tk.Label(os_frame, text=self.detected_os, font=("Helvetica", 10)).grid( - row=0, column=1, sticky="w" - ) - - # Working Directory - tk.Label(os_frame, text="Working Dir:", font=("Helvetica", 10, "bold")).grid( - row=1, column=0, sticky="w", padx=(0, 10) - ) - tk.Label(os_frame, text=LXToolsAppConfig.WORK_DIR, font=("Helvetica", 9)).grid( - row=1, column=1, sticky="w" - ) - - def _create_projects_section(self): - """Create projects selection and status section""" - projects_frame = ttk.LabelFrame( - self.root, text="Available Projects", padding=10 - ) - projects_frame.grid(row=2, column=0, sticky="nsew", padx=15, pady=(0, 10)) - projects_frame.grid_columnconfigure(0, weight=1) - projects_frame.grid_rowconfigure(1, weight=1) - - # Project selection - selection_frame = ttk.Frame(projects_frame) - selection_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10)) - selection_frame.grid_columnconfigure(1, weight=1) - - tk.Label( - selection_frame, text="Select Project:", font=("Helvetica", 10, "bold") - ).grid(row=0, column=0, sticky="w", padx=(0, 10)) - - self.project_var = tk.StringVar() - project_combo = ttk.Combobox( - selection_frame, - textvariable=self.project_var, - values=list(self.app_manager.get_all_projects().keys()), - state="readonly", - width=20, - ) - project_combo.grid(row=0, column=1, sticky="w") - project_combo.bind("<>", self._on_project_selected) - - # Projects status frame with scrollbar - status_container = ttk.Frame(projects_frame) - status_container.grid(row=1, column=0, sticky="nsew") - status_container.grid_columnconfigure(0, weight=1) - status_container.grid_rowconfigure(0, weight=1) - - # Canvas for scrolling - canvas = tk.Canvas(status_container, height=200) - scrollbar = ttk.Scrollbar( - status_container, orient="vertical", command=canvas.yview - ) - scrollable_frame = ttk.Frame(canvas) - - 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.grid(row=0, column=0, sticky="nsew") - scrollbar.grid(row=0, column=1, sticky="ns") - - # Project status entries - scrollable_frame.grid_columnconfigure(1, weight=1) - - row = 0 - for project_key, project_info in self.app_manager.get_all_projects().items(): - # Project icon and name - project_frame = ttk.Frame(scrollable_frame, padding=5) - project_frame.grid(row=row, column=0, columnspan=3, sticky="ew", pady=2) - project_frame.grid_columnconfigure(1, weight=1) - - # Try to load project icon - icon = self.image_manager.load_image(project_info["icon_key"]) - if icon: - icon_label = tk.Label(project_frame, image=icon) - icon_label.image = icon # Keep reference - icon_label.grid(row=0, column=0, rowspan=2, padx=(0, 10)) - else: - # Fallback emoji - emoji_map = {"wirepy": "🔐", "logviewer": "📋"} - emoji = emoji_map.get(project_key, "📦") - tk.Label(project_frame, text=emoji, font=("Helvetica", 16)).grid( - row=0, column=0, rowspan=2, padx=(0, 10) - ) - - # Project name and description - tk.Label( - project_frame, - text=f"{project_info['name']} - {project_info['description']}", - font=("Helvetica", 11, "bold"), - ).grid(row=0, column=1, sticky="w") - - # Status label - status_label = tk.Label( - project_frame, text="Checking...", font=("Helvetica", 9) - ) - status_label.grid(row=1, column=1, sticky="w") - self.status_labels[project_key] = status_label - - # Version label - version_label = tk.Label(project_frame, text="", font=("Helvetica", 9)) - version_label.grid(row=2, column=1, sticky="w") - self.version_labels[project_key] = version_label - - row += 1 - - def _create_progress_section(self): - """Create progress section with download icon""" - progress_frame = ttk.LabelFrame(self.root, text="Progress", padding=10) - progress_frame.grid(row=3, column=0, sticky="ew", padx=15, pady=(0, 10)) - progress_frame.grid_columnconfigure(1, weight=1) - - # Download icon - self.download_icon_label = tk.Label(progress_frame, text="", width=4) - self.download_icon_label.grid(row=0, column=0, padx=(0, 10)) - - # Progress text - self.progress_label = tk.Label( - progress_frame, - text="Ready for installation...", - font=("Helvetica", 10), - fg="blue", - anchor="w", - ) - self.progress_label.grid(row=0, column=1, sticky="ew") - - # Reset download icon - self._reset_download_icon() - - def _create_buttons_section(self): - """Create buttons section""" - buttons_frame = ttk.Frame(self.root, padding=10) - buttons_frame.grid(row=4, column=0, sticky="ew", padx=15, pady=(0, 10)) - buttons_frame.grid_columnconfigure(0, weight=1) - - # Button container - btn_container = ttk.Frame(buttons_frame) - btn_container.grid(row=0, column=0) - - # Install/Update button - install_btn = ttk.Button( - btn_container, - text="📥 Install/Update", - command=self.install_project, - width=15, - ) - install_btn.grid(row=0, column=0, padx=(0, 10)) - - # Uninstall button - uninstall_btn = ttk.Button( - btn_container, text="🗑️ Uninstall", command=self.uninstall_project, width=15 - ) - uninstall_btn.grid(row=0, column=1, padx=(0, 10)) - - # Refresh button - refresh_btn = ttk.Button( - btn_container, text="Refresh", command=self.refresh_status, width=15 - ) - refresh_btn.grid(row=0, column=2, padx=(0, 10)) - - # Debug toggle button - debug_btn = ttk.Button( - btn_container, text="Debug", command=self.toggle_debug, width=15 - ) - debug_btn.grid(row=0, column=3) - - def _create_debug_section(self): - """Create debug section""" - debug_frame = ttk.LabelFrame(self.root, text="Debug Output", padding=10) - debug_frame.grid(row=5, column=0, sticky="nsew", padx=15, pady=(0, 10)) - debug_frame.grid_columnconfigure(0, weight=1) - debug_frame.grid_rowconfigure(0, weight=1) - - # Debug text with scrollbar - debug_container = ttk.Frame(debug_frame) - debug_container.grid(row=0, column=0, sticky="nsew") - debug_container.grid_columnconfigure(0, weight=1) - debug_container.grid_rowconfigure(0, weight=1) - - self.debug_text = tk.Text( - debug_container, height=8, font=("Courier", 9), bg="#f8f8f8", fg="#333333" - ) - self.debug_text.grid(row=0, column=0, sticky="nsew") - - # Scrollbar for debug text - debug_scrollbar = ttk.Scrollbar( - debug_container, orient="vertical", command=self.debug_text.yview - ) - debug_scrollbar.grid(row=0, column=1, sticky="ns") - self.debug_text.configure(yscrollcommand=debug_scrollbar.set) - - # Clear debug button - clear_debug_btn = ttk.Button( - debug_frame, text="Clear Debug", command=self.clear_debug - ) - clear_debug_btn.grid(row=1, column=0, sticky="e", pady=(5, 0)) - - def _load_app_icon(self): - """Load application icon""" - try: - icon_path = LXToolsAppConfig.get_icon_path("app_icon") - if icon_path and os.path.exists(icon_path): - icon = tk.PhotoImage(file=icon_path) - self.root.iconphoto(False, icon) - print(f"App icon loaded: {icon_path}") - else: - print("App icon not found, using default") - except Exception as e: - print(f"Failed to load app icon: {e}") - - 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: - # Fallback to emoji - self.download_icon_label.config(text="📥", font=("Helvetica", 16)) - - def _on_project_selected(self, event=None): - """Handle project selection change""" - selected = self.project_var.get() - if selected: - self.update_progress(f"Selected: {selected}") - - 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 update_progress(self, message): - """Update progress message""" - if self.progress_label: - self.progress_label.config(text=message) - self.progress_label.update() - print(f"Progress: {message}") - - def debug_log(self, message): - """Add message to debug log""" - if self.debug_text: - self.debug_text.insert(tk.END, f"{message}\n") - self.debug_text.see(tk.END) - self.debug_text.update() - print(f"Debug: {message}") - - def clear_debug(self): - """Clear debug text""" - if self.debug_text: - self.debug_text.delete(1.0, tk.END) - - def toggle_debug(self): - """Toggle debug window visibility""" - self.show_debug = not self.show_debug - - # Recreate window with new size - if self.root: - self.root.destroy() - - # Create new window - self.create_gui() - self.root.mainloop() - - def refresh_status(self): - """Refresh application status and version information""" - self.update_progress("Refreshing status and checking versions...") - self._reset_download_icon() - - 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] - - if self.app_manager.is_installed(project_key): - installed_version = self.app_manager.get_installed_version(project_key) - status_label.config( - text=f"✅ Installed (v{installed_version})", fg="green" - ) - - # Get latest version from API - try: - latest_version = self.app_manager.get_latest_version(project_key) - if latest_version != "Unknown": - if installed_version != latest_version: - version_label.config( - text=f"Latest: v{latest_version} (Update available)", - fg="orange", - ) - else: - version_label.config( - text=f"Latest: v{latest_version} (Up to date)", - fg="green", - ) - else: - version_label.config(text=f"Latest: Unknown", fg="gray") - except Exception as e: - version_label.config(text=f"Latest: Check failed", fg="gray") - self.debug_log(f"Version check failed for {project_key}: {e}") - else: - status_label.config(text=f"❌ Not installed", fg="red") - - # 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"Available: v{latest_version}", fg="blue" - ) - else: - version_label.config(text=f"Available: Unknown", fg="gray") - except Exception as e: - version_label.config(text=f"Available: Check failed", fg="gray") - self.debug_log(f"Version check failed for {project_key}: {e}") - - self.update_progress("Status refresh completed.") - - def install_project(self): - """Handle install button click""" - selected_project = self.project_var.get() - if not selected_project: - messagebox.showwarning("Warning", "Please select a project to install.") - return - - # Check internet connection - if not NetworkChecker.check_internet(): - self.update_download_icon("error") - messagebox.showerror( - "Network Error", - "No internet connection available.\nPlease check your network connection.", - ) - return - - if not NetworkChecker.check_repository(): - self.update_download_icon("error") - messagebox.showerror( - "Repository Error", "Cannot access repository.\nPlease try again later." - ) - return - - # Reset download icon - self._reset_download_icon() - project_info = self.app_manager.get_project_info(selected_project) - - # Check if already installed - if self.app_manager.is_installed(selected_project): - installed_version = self.app_manager.get_installed_version(selected_project) - latest_version = self.app_manager.get_latest_version(selected_project) - - dialog_text = ( - f"{project_info['name']} is already installed.\n\n" - f"Installed version: v{installed_version}\n" - f"Latest version: v{latest_version}\n\n" - f"YES = Update (reinstall all files)\n" - f"NO = Uninstall\n" - f"Cancel = Do nothing" - ) - - result = messagebox.askyesnocancel( - f"{project_info['name']} already installed", dialog_text - ) - - if result is None: # Cancel - self.update_progress("Installation cancelled.") - return - elif not result: # Uninstall - self.uninstall_project(selected_project) - return - else: # Update - self.update_progress("Updating application...") - - try: - self.installation_manager.install_project(selected_project) - - # Success message - if USE_LXTOOLS: - LxTools.msg_window( - self.root, - "Success", - f"{project_info['name']} has been successfully installed/updated.", - "info", - ) - else: - messagebox.showinfo( - "Success", - f"{project_info['name']} has been successfully installed/updated.", - ) - - self.refresh_status() - - except Exception as e: - # Show error icon - self.update_download_icon("error") - - error_msg = f"Installation failed: {e}" - self.debug_log(f"ERROR: {error_msg}") - - if USE_LXTOOLS: - LxTools.msg_window( - self.root, "Error", error_msg, "error", width=500, height=300 - ) - else: - messagebox.showerror("Error", error_msg) - - def uninstall_project(self, project_key=None): - """Handle uninstall button click""" - if project_key is None: - project_key = self.project_var.get() - - if not project_key: - messagebox.showwarning("Warning", "Please select a project to uninstall.") - return - - project_info = self.app_manager.get_project_info(project_key) - - if not self.app_manager.is_installed(project_key): - messagebox.showinfo("Info", f"{project_info['name']} is not installed.") - return - - result = messagebox.askyesno( - "Confirm Uninstall", - f"Are you sure you want to uninstall {project_info['name']}?\n\n" - f"This will remove all application files and user configurations.", - ) - if not result: - return - - try: - self.uninstallation_manager.uninstall_project(project_key) - - # Success message - if USE_LXTOOLS: - LxTools.msg_window( - self.root, - "Success", - f"{project_info['name']} has been successfully uninstalled.", - "info", - ) - else: - messagebox.showinfo( - "Success", - f"{project_info['name']} has been successfully uninstalled.", - ) - - self.refresh_status() - - except Exception as e: - error_msg = f"Uninstallation failed: {e}" - self.debug_log(f"ERROR: {error_msg}") - - if USE_LXTOOLS: - LxTools.msg_window( - self.root, "Error", error_msg, "error", width=500, height=300 - ) - else: - messagebox.showerror("Error", error_msg) - - def run(self): - """Start the GUI application""" - try: - print(f"Starting {LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}") - print(f"Working directory: {LXToolsAppConfig.WORK_DIR}") - print(f"Icons directory: {LXToolsAppConfig.ICONS_DIR}") - print(f"Using LxTools: {USE_LXTOOLS}") - - root = self.create_gui() - root.mainloop() - - except KeyboardInterrupt: - print("\nApplication interrupted by user.") - except Exception as e: - print(f"Fatal error: {e}") - if self.root: - messagebox.showerror("Fatal Error", f"Application failed to start: {e}") - - -# ---------------------------- -# Main Application Entry Point -# ---------------------------- -def main(): - """Main function to start the application""" - try: - # Check if running as root (not recommended) - if os.geteuid() == 0: - print("Warning: Running as root is not recommended!") - print("The installer will use pkexec for privilege escalation when needed.") - - # Create and run the GUI - app = LXToolsGUI() - app.run() - - except KeyboardInterrupt: - print("\nApplication interrupted by user.") - except Exception as e: - print(f"Fatal error: {e}") - try: - import tkinter.messagebox as mb - - mb.showerror("Fatal Error", f"Application failed to start: {e}") - except: - pass # If even tkinter fails, just print - - -if __name__ == "__main__": - main() diff --git a/lxtools_installerv3.py b/lxtools_installerv3.py index 2bfc91f..35c22fb 100755 --- a/lxtools_installerv3.py +++ b/lxtools_installerv3.py @@ -22,7 +22,7 @@ class LXToolsAppConfig: VERSION = "1.1.3" APP_NAME = "LX Tools Installer" WINDOW_WIDTH = 500 - WINDOW_HEIGHT = 700 + WINDOW_HEIGHT = 720 # Working directory WORK_DIR = os.getcwd() @@ -1110,6 +1110,7 @@ class LXToolsGUI: self.root.iconphoto(False, icon) except: pass + # self.root.minsize(LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT) ThemeManager.apply_light_theme(self.root) # Create header self._create_header() @@ -1493,36 +1494,32 @@ class LXToolsGUI: 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=(0, 15)) + button_frame.pack(fill="x", padx=15, pady=(5, 10)) # Button style configuration style = ttk.Style() # Install button (green) - style.configure( - "Install.TButton", foreground="#27ae60", font=("Helvetica", 10, "bold") - ) + style.configure("Install.TButton", foreground="#27ae60", font=("Helvetica", 11)) style.map( "Install.TButton", - foreground=[("active", "#2ecc71"), ("pressed", "#1e8449")], + foreground=[("active", "#14542f"), ("pressed", "#1e8449")], ) # Uninstall button (red) style.configure( - "Uninstall.TButton", foreground="#e74c3c", font=("Helvetica", 10, "bold") + "Uninstall.TButton", foreground="#e74c3c", font=("Helvetica", 11) ) style.map( "Uninstall.TButton", - foreground=[("active", "#ec7063"), ("pressed", "#c0392b")], + foreground=[("active", "#7d3b34"), ("pressed", "#c0392b")], ) # Refresh button (blue) - style.configure( - "Refresh.TButton", foreground="#3498db", font=("Helvetica", 10, "bold") - ) + style.configure("Refresh.TButton", foreground="#3498db", font=("Helvetica", 11)) style.map( "Refresh.TButton", - foreground=[("active", "#5dade2"), ("pressed", "#2980b9")], + foreground=[("active", "#1e3747"), ("pressed", "#2980b9")], ) # Create buttons @@ -1531,6 +1528,7 @@ class LXToolsGUI: text="Install/Update Selected", command=self.install_selected, style="Install.TButton", + padding=8, ) install_btn.pack(side="left", padx=(0, 10)) @@ -1539,6 +1537,7 @@ class LXToolsGUI: text="Uninstall Selected", command=self.uninstall_selected, style="Uninstall.TButton", + padding=8, ) uninstall_btn.pack(side="left", padx=(0, 10)) @@ -1547,6 +1546,7 @@ class LXToolsGUI: text="Refresh Status", command=self.refresh_status, style="Refresh.TButton", + padding=8, ) refresh_btn.pack(side="right") @@ -1596,7 +1596,7 @@ class LXToolsGUI: self.update_progress("Refreshing status and checking versions...") self._reset_download_icon() self.log_message("=== Refreshing Status ===") - + 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] @@ -1688,6 +1688,7 @@ class LXToolsGUI: "Repository Error", "Cannot access repository.\nPlease try again later." ) return + self.root.focus_set() # Reset download icon self._reset_download_icon() @@ -1763,6 +1764,7 @@ class LXToolsGUI: self.refresh_status() except Exception as e: messagebox.showerror("Error", f"Uninstallation failed: {e}") + self.root.focus_set() ################### Teil 18 - GUI Helper Methods ################### def update_progress(self, message):