#!/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()