diff --git a/Changelog b/Changelog index 1df4d59..a6ee910 100644 --- a/Changelog +++ b/Changelog @@ -2,7 +2,20 @@ Changelog for LXTools installer ## [Unreleased] - - + - + + ### Added +14-06-2025 + + - Installer divided into several modules and added new MessageDialog module + + ### Added +4-06-2025 + + - replace modul path /usr/lib/python3/dist-packages/shared_libs + with /usr/local/share/shared_libs for better ensure that the shared libs are found + + - add ensure_shared_libs_pth_exists Script to install ### Added 4-06-2025 diff --git a/ensure_modules.py b/ensure_modules.py new file mode 100644 index 0000000..53419b4 --- /dev/null +++ b/ensure_modules.py @@ -0,0 +1,68 @@ +import sys +import os + +# ✅ Path to be added in the .pth file +SHARED_LIBS_PATH = "/usr/local/share/shared_libs" +PTH_FILE_NAME = "shared_libs.pth" + + +def ensure_shared_libs_pth_exists(): + """ + Checks if all site-packages directories have a `.pth` file with the correct path. + Creates or updates it if missing or incorrect. + """ + # Search for all site-packages directories (e.g., /usr/lib/python3.x/site-packages/) + for root, dirs, files in os.walk("/usr"): + if "site-packages" in dirs: + site_packages_dir = os.path.join(root, "site-packages") + + pth_file_path = os.path.join(site_packages_dir, PTH_FILE_NAME) + + # Check if the file exists and is correct + if not os.path.exists(pth_file_path): + print(f"⚠️ .pth file not found: {pth_file_path}. Creating...") + with open(pth_file_path, "w") as f: + f.write(SHARED_LIBS_PATH + "\n") + + else: + # Check if the correct path is in the file + with open(pth_file_path, "r") as f: + content = f.read().strip() + + if not content == SHARED_LIBS_PATH: + print(f"⚠️ .pth file exists but has incorrect content. Fixing...") + with open(pth_file_path, "w") as f: + f.write(SHARED_LIBS_PATH + "\n") + + print("✅ All .pth files checked and corrected.") + + +def main(): + try: + # Try to import the module + from shared_libs.wp_app_config import AppConfig + + print("✅ 'shared_libs' is correctly loaded. Starting the application...") + + # Your main program logic here... + except ModuleNotFoundError as e: + # Only handle errors related to missing .pth file + if "No module named 'shared_libs'" in str(e): + print("⚠️ Error: 'shared_libs' module not found. Checking .pth file...") + ensure_shared_libs_pth_exists() + + # Try again after fixing the .pth file + try: + from shared_libs.wp_app_config import AppConfig + + print("✅ After correcting the .pth file: Module loaded.") + # Your main program logic here... + except Exception as e2: + print(f"❌ Error after correcting the .pth file: {e2}") + else: + # For other errors, re-raise them + raise + + +if __name__ == "__main__": + main() diff --git a/lx-icons/128/error.png b/lx-icons/128/error.png index ae580f8..e94b55a 100644 Binary files a/lx-icons/128/error.png and b/lx-icons/128/error.png differ diff --git a/lx-icons/128/info.png b/lx-icons/128/info.png index ccba611..7974653 100644 Binary files a/lx-icons/128/info.png and b/lx-icons/128/info.png differ diff --git a/lx-icons/128/question_mark.png b/lx-icons/128/question_mark.png new file mode 100644 index 0000000..f16b838 Binary files /dev/null and b/lx-icons/128/question_mark.png differ diff --git a/lx-icons/128/warning.png b/lx-icons/128/warning.png new file mode 100644 index 0000000..1a5eff7 Binary files /dev/null and b/lx-icons/128/warning.png differ diff --git a/lx-icons/128/wg_vpn-stop.png b/lx-icons/128/wg_vpn-stop.png index ff02259..7698de3 100644 Binary files a/lx-icons/128/wg_vpn-stop.png and b/lx-icons/128/wg_vpn-stop.png differ diff --git a/lx-icons/256/error.png b/lx-icons/256/error.png index 7734e3a..e034b9a 100644 Binary files a/lx-icons/256/error.png and b/lx-icons/256/error.png differ diff --git a/lx-icons/256/info.png b/lx-icons/256/info.png index 8c1ca4f..fd2488f 100644 Binary files a/lx-icons/256/info.png and b/lx-icons/256/info.png differ diff --git a/lx-icons/256/question_mark.png b/lx-icons/256/question_mark.png new file mode 100644 index 0000000..0b781dd Binary files /dev/null and b/lx-icons/256/question_mark.png differ diff --git a/lx-icons/256/warning.png b/lx-icons/256/warning.png new file mode 100644 index 0000000..a30311f Binary files /dev/null and b/lx-icons/256/warning.png differ diff --git a/lx-icons/256/wg_vpn-stop.png b/lx-icons/256/wg_vpn-stop.png index a99bc5b..535d948 100644 Binary files a/lx-icons/256/wg_vpn-stop.png and b/lx-icons/256/wg_vpn-stop.png differ diff --git a/lx-icons/32/error.png b/lx-icons/32/error.png index 18e6c64..876f5d3 100644 Binary files a/lx-icons/32/error.png and b/lx-icons/32/error.png differ diff --git a/lx-icons/32/info.png b/lx-icons/32/info.png index f11ca97..5104d27 100644 Binary files a/lx-icons/32/info.png and b/lx-icons/32/info.png differ diff --git a/lx-icons/32/question_mark.png b/lx-icons/32/question_mark.png new file mode 100644 index 0000000..7003622 Binary files /dev/null and b/lx-icons/32/question_mark.png differ diff --git a/lx-icons/32/warning.png b/lx-icons/32/warning.png new file mode 100644 index 0000000..582dd54 Binary files /dev/null and b/lx-icons/32/warning.png differ diff --git a/lx-icons/32/wg_vpn-stop.png b/lx-icons/32/wg_vpn-stop.png index f29cbe6..a972a98 100644 Binary files a/lx-icons/32/wg_vpn-stop.png and b/lx-icons/32/wg_vpn-stop.png differ diff --git a/lx-icons/48/error.png b/lx-icons/48/error.png index 92a731a..d425d8e 100644 Binary files a/lx-icons/48/error.png and b/lx-icons/48/error.png differ diff --git a/lx-icons/48/info.png b/lx-icons/48/info.png index 52206e6..45fb93f 100644 Binary files a/lx-icons/48/info.png and b/lx-icons/48/info.png differ diff --git a/lx-icons/48/question_mark.png b/lx-icons/48/question_mark.png new file mode 100644 index 0000000..f10ca66 Binary files /dev/null and b/lx-icons/48/question_mark.png differ diff --git a/lx-icons/48/warning.png b/lx-icons/48/warning.png new file mode 100644 index 0000000..39cc0be Binary files /dev/null and b/lx-icons/48/warning.png differ diff --git a/lx-icons/48/wg_vpn-stop.png b/lx-icons/48/wg_vpn-stop.png index 30aa9cc..fb9fee6 100644 Binary files a/lx-icons/48/wg_vpn-stop.png and b/lx-icons/48/wg_vpn-stop.png differ diff --git a/lx-icons/64/error.png b/lx-icons/64/error.png index 5ac3858..a135878 100644 Binary files a/lx-icons/64/error.png and b/lx-icons/64/error.png differ diff --git a/lx-icons/64/info.png b/lx-icons/64/info.png index 4917814..bce3e28 100644 Binary files a/lx-icons/64/info.png and b/lx-icons/64/info.png differ diff --git a/lx-icons/64/question_mark.png b/lx-icons/64/question_mark.png new file mode 100644 index 0000000..9fd9d5f Binary files /dev/null and b/lx-icons/64/question_mark.png differ diff --git a/lx-icons/64/warning.png b/lx-icons/64/warning.png new file mode 100644 index 0000000..8b015d3 Binary files /dev/null and b/lx-icons/64/warning.png differ diff --git a/lx-icons/64/wg_vpn-stop.png b/lx-icons/64/wg_vpn-stop.png index 03a5044..353e2e4 100644 Binary files a/lx-icons/64/wg_vpn-stop.png and b/lx-icons/64/wg_vpn-stop.png differ diff --git a/lxtools_installerv3.py b/lxtools_installer.py similarity index 57% rename from lxtools_installerv3.py rename to lxtools_installer.py index f531367..9477eab 100755 --- a/lxtools_installerv3.py +++ b/lxtools_installer.py @@ -1,529 +1,28 @@ #!/usr/bin/python3 -import gettext -import locale import tkinter as tk -from tkinter import messagebox, ttk +from tkinter import ttk import os -import socket import subprocess +from datetime import datetime import tempfile import urllib.request import zipfile -import json -import stat -from pathlib import Path -from datetime import datetime - - -class LXToolsAppConfig: - VERSION = "1.1.4" - APP_NAME = "Lunix Tools Installer" - WINDOW_WIDTH = 450 - WINDOW_HEIGHT = 580 - - # Working directory - WORK_DIR = os.getcwd() - ICONS_DIR = os.path.join(WORK_DIR, "lx-icons") - THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes") - - # Locale settings - LOCALE_DIR = Path("/usr/share/locale/") - - # 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" - ) - - # Project configurations - PROJECTS = { - "wirepy": { - "name": "Wire-Py", - "description": "WireGuard VPN Manager with GUI", - "download_url": WIREPY_URL, - "api_url": WIREPY_API_URL, - "icon_key": "icon_vpn", - "main_executable": "wirepy.py", - "symlink_name": "wirepy", - "config_file": "wp_app_config.py", - "desktop_file": "Wire-Py.desktop", - "policy_file": "org.sslcrypt.policy", - "requires_ssl": True, - }, - "logviewer": { - "name": "LogViewer", - "description": "System Log Viewer with GUI", - "download_url": SHARED_LIBS_URL, - "api_url": SHARED_LIBS_API_URL, - "icon_key": "icon_log", - "main_executable": "logviewer.py", - "symlink_name": "logviewer", - "config_file": "logview_app_config.py", - "desktop_file": "LogViewer.desktop", - "policy_file": None, - "requires_ssl": False, - }, - } - - # 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"), - ("arch", "Arch Linux"), - ("ubuntu", "Ubuntu"), - ("debian", "Debian"), - ] - - # Package manager commands for TKinter installation - TKINTER_INSTALL_COMMANDS = { - "Ubuntu": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], - "Debian": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], - "Linux Mint": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], - "Pop!_OS": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], - "Fedora": ["dnf", "install", "-y", "tkinter"], - "Arch Linux": ["pacman", "-S", "--noconfirm", "tk"], - "Manjaro": ["pacman", "-S", "--noconfirm", "tk"], - "Garuda Linux": ["pacman", "-S", "--noconfirm", "tk"], - "EndeavourOS": ["pacman", "-S", "--noconfirm", "tk"], - "SUSE Tumbleweed": ["zypper", "install", "-y", "python314-tk"], - "SUSE Leap": ["zypper", "install", "-y", "python312-tk"], - } - - @staticmethod - def setup_translations(): - """Initialize translations and set the translation function""" - try: - locale.bindtextdomain( - LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR - ) - gettext.bindtextdomain( - LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR - ) - gettext.textdomain(LXToolsAppConfig.APP_NAME) - except: - pass - return gettext.gettext - +from manager import ( + OSDetector, + Theme, + LXToolsAppConfig, + System, + Image, + AppManager, + Center, +) +from network import NetworkChecker +from message import MessageDialog # Initialize translations _ = LXToolsAppConfig.setup_translations() -class ColorManager: - @staticmethod - def get_system_colors(root): - """Get system colors that work across different themes""" - try: - # Versuche system-spezifische Farben zu ermitteln - default_bg = root.cget("bg") - - # Teste verschiedene Farbnamen - test_colors = ["#f0f0f0", "#e1e1e1", "#d9d9d9", "lightgray"] - working_bg = default_bg - - for color in test_colors: - try: - # Teste ob die Farbe funktioniert - test_label = tk.Label(root, bg=color) - working_bg = color - test_label.destroy() - break - except tk.TclError: - continue - - return { - "default_bg": working_bg, - "card_bg": "#f8f9fa", # Hellerer Hintergrund für Cards - "hover_bg": "#e6f3ff", - "selected_bg": "#cce7ff", - "border_color": "#cccccc", - "header_bg": "#2c3e50", # Dunkler Header - "header_fg": "white", # Weiße Schrift im Header - "progress_bg": "#f1f3f4", # Wie Progress Label - } - except: - # Fallback Farben - return { - "default_bg": "#f0f0f0", - "card_bg": "#f8f9fa", - "hover_bg": "#e6f3ff", - "selected_bg": "#cce7ff", - "border_color": "#cccccc", - "header_bg": "#2c3e50", - "header_fg": "white", - "progress_bg": "#f1f3f4", - } - - -class ThemeManager: - @staticmethod - def apply_light_theme(root): - """Apply light theme""" - try: - theme_dir = LXToolsAppConfig.THEMES_DIR - water_theme_path = os.path.join(theme_dir, "water.tcl") - - if os.path.exists(water_theme_path): - try: - root.tk.call("source", water_theme_path) - root.tk.call("set_theme", "light") - return True - except tk.TclError: - pass - - # System theme fallback - try: - style = ttk.Style() - if "clam" in style.theme_names(): - style.theme_use("clam") - return True - except: - pass - - except Exception: - pass - - return False - - -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" - - @staticmethod - def check_tkinter_available(): - """Check if tkinter is available""" - try: - import tkinter - - return True - except ImportError: - return False - - @staticmethod - def install_tkinter(): - """Install tkinter based on detected OS""" - detected_os = OSDetector.detect_os() - - if detected_os in LXToolsAppConfig.TKINTER_INSTALL_COMMANDS: - commands = LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os] - - print(f"Installing tkinter for {detected_os}...") - print(f"Command: {' '.join(commands)}") - - try: - # Use pkexec for privilege escalation - full_command = ["pkexec", "bash", "-c", " ".join(commands)] - result = subprocess.run( - full_command, capture_output=True, text=True, timeout=300 - ) - - if result.returncode == 0: - print("TKinter installation completed successfully!") - return True - else: - print(f"TKinter installation failed: {result.stderr}") - return False - - except subprocess.TimeoutExpired: - print("TKinter installation timed out") - return False - except Exception as e: - print(f"Error installing tkinter: {e}") - return False - else: - print(f"No tkinter installation command defined for {detected_os}") - return False - - -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 - - -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] - - # Define image paths based on key - image_paths = { - "app_icon": [ - "./lx-icons/48/wg_vpn.png", - "/usr/share/icons/lx-icons/48/wg_vpn.png", - ], - "download_icon": [ - "./lx-icons/32/download.png", - "/usr/share/icons/lx-icons/32/download.png", - ], - "download_error_icon": [ - "./lx-icons/32/download_error.png", - "/usr/share/icons/lx-icons/32/download_error.png", - ], - "success_icon": [ - "./lx-icons/32/download.png", - "/usr/share/icons/lx-icons/32/download.png", - ], - "icon_vpn": [ - "./lx-icons/48/wg_vpn.png", - "/usr/share/icons/lx-icons/48/wg_vpn.png", - ], - "icon_log": [ - "./lx-icons/48/log.png", - "/usr/share/icons/lx-icons/48/log.png", - ], - } - - # Get paths to try - paths_to_try = image_paths.get(image_key, []) - - # Add fallback paths if provided - 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 - return None - - -class GiteaUpdate: - @staticmethod - def api_down(url, current_version=""): - """Get latest version from Gitea API""" - try: - with urllib.request.urlopen(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") # Remove 'v' prefix if present - return "Unknown" - except Exception as e: - print(f"API Error: {e}") - return "Unknown" - - -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", timeout=5): - """Check if repository is accessible""" - try: - urllib.request.urlopen(url, timeout=timeout) - return True - except: - return False - - -class DownloadManager: - @staticmethod - def download_and_extract(url, extract_to, progress_callback=None): - """Download and extract ZIP file""" - try: - if progress_callback: - progress_callback(f"Downloading from {url}...") - - 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) - return True - - except Exception as e: - if progress_callback: - progress_callback(f"Download failed: {str(e)}") - return False - - -class AppManager: - def __init__(self): - self.projects = LXToolsAppConfig.PROJECTS - - def get_project_info(self, project_key): - """Get project information by key""" - return self.projects.get(project_key) - - def get_all_projects(self): - """Get all project configurations""" - return self.projects - - def is_installed(self, project_key): - """Check if project is installed with better detection""" - if project_key == "wirepy": - # Check for wirepy symlink - return os.path.exists("/usr/local/bin/wirepy") and os.path.islink( - "/usr/local/bin/wirepy" - ) - - elif project_key == "logviewer": - # Check for logviewer symlink AND executable file - symlink_exists = os.path.exists("/usr/local/bin/logviewer") - executable_exists = os.path.exists( - "/usr/lib/python3/dist-packages/shared_libs/logviewer.py" - ) - executable_is_executable = False - - if executable_exists: - try: - # Check if file is executable - file_stat = os.stat( - "/usr/lib/python3/dist-packages/shared_libs/logviewer.py" - ) - executable_is_executable = bool(file_stat.st_mode & stat.S_IEXEC) - except: - executable_is_executable = False - - # LogViewer is installed if symlink exists AND executable file exists AND is executable - is_installed = ( - symlink_exists and executable_exists and executable_is_executable - ) - - # Debug logging - print(f"LogViewer installation check:") - print(f" Symlink exists: {symlink_exists}") - print(f" Executable exists: {executable_exists}") - print(f" Is executable: {executable_is_executable}") - print(f" Final result: {is_installed}") - - return is_installed - - return False - - 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: - return "Unknown" - - 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""" - project_info = self.get_project_info(project_key) - if not project_info: - return "Unknown" - - return GiteaUpdate.api_down(project_info["api_url"]) - - def check_other_apps_installed(self, exclude_key): - """Check if other apps are still installed""" - return any( - self.is_installed(key) for key in self.projects.keys() if key != exclude_key - ) - - class InstallationManager: def __init__( self, app_manager, progress_callback=None, icon_callback=None, log_callback=None @@ -532,17 +31,17 @@ class InstallationManager: self.progress_callback = progress_callback self.icon_callback = icon_callback self.log_callback = log_callback - self.system_manager = SystemManager() + self.system_manager = System() self.download_manager = DownloadManager() def install_project(self, project_key): """Install or update project""" project_info = self.app_manager.get_project_info(project_key) if not project_info: - raise Exception(f"Unknown project: {project_key}") + raise Exception(_(f"Unknown project: {project_key}")) - self.update_progress(f"Starting installation of {project_info['name']}...") - self.log(f"=== Installing {project_info['name']} ===") + self.update_progress(_(f"Starting installation of {project_info['name']}...")) + self.log(_(f"=== Installing {project_info['name']} ===")) try: # Create installation script @@ -552,17 +51,17 @@ class InstallationManager: self._execute_install_script(script_content) self.update_progress( - f"{project_info['name']} installation completed successfully!" + _(f"{project_info['name']} installation completed successfully!") ) - self.log(f"=== {project_info['name']} installed successfully ===") + self.log(_(f"=== {project_info['name']} installed successfully ===")) # Set success icon self.update_icon("success") except Exception as e: - self.log(f"ERROR: Installation failed: {e}") + self.log(_(f"ERROR: Installation failed: {e}")) self.update_icon("error") - raise Exception(f"Installation failed: {e}") + raise Exception(_(f"Installation failed: {e}")) def _create_install_script(self, project_key): """Create installation script based on project""" @@ -571,7 +70,7 @@ class InstallationManager: elif project_key == "logviewer": return self._create_logviewer_install_script() else: - raise Exception(f"Unknown project: {project_key}") + raise Exception(_(f"Unknown project: {project_key}")) def _create_wirepy_install_script(self): """Create Wire-Py installation script""" @@ -581,7 +80,7 @@ set -e echo "=== Wire-Py Installation ===" # Create necessary directories -mkdir -p /usr/lib/python3/dist-packages/shared_libs +mkdir -p /usr/local/share/shared_libs mkdir -p /usr/share/icons/lx-icons mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/applications @@ -617,23 +116,23 @@ done # Install config if [ -f "$WIREPY_DIR/wp_app_config.py" ]; then - cp -f "$WIREPY_DIR/wp_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ + cp -f "$WIREPY_DIR/wp_app_config.py" /usr/local/share/shared_libs/ echo "Installed wp_app_config.py" fi # Install shared libraries echo "Installing shared libraries..." -for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do +for file in common_tools.py message.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do if [ -f "$SHARED_DIR/$file" ]; then - cp -f "$SHARED_DIR/$file" /usr/lib/python3/dist-packages/shared_libs/ + cp -f "$SHARED_DIR/$file" /usr/local/share/shared_libs/ echo "Installed shared lib: $file" fi done # Install LogViewer executable if [ -f "$SHARED_DIR/logviewer.py" ]; then - cp -f "$SHARED_DIR/logviewer.py" /usr/lib/python3/dist-packages/shared_libs/ - chmod 755 /usr/lib/python3/dist-packages/shared_libs/logviewer.py + cp -f "$SHARED_DIR/logviewer.py" /usr/local/share/shared_libs/ + chmod 755 /usr/local/share/shared_libs/logviewer.py echo "Installed logviewer.py (executable)" fi @@ -670,7 +169,7 @@ fi # Create symlink for Wirepy ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy # Create symlink for LogViewer -ln -sf /usr/lib/python3/dist-packages/shared_libs/logviewer.py /usr/local/bin/logviewer +ln -sf /usr/local/share/shared_libs/logviewer.py /usr/local/bin/logviewer echo "Created Wirepy and LogViewer symlink" # Install language files if available @@ -703,7 +202,7 @@ set -e echo "=== LogViewer Installation ===" # Create necessary directories -mkdir -p /usr/lib/python3/dist-packages/shared_libs +mkdir -p /usr/local/share/shared_libs mkdir -p /usr/share/icons/lx-icons mkdir -p /usr/share/locale/de/LC_MESSAGES mkdir -p /usr/share/applications @@ -741,17 +240,17 @@ fi # Install shared libraries echo "Installing shared libraries..." -for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do +for file in common_tools.py message.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do if [ -f "$SHARED_DIR/$file" ]; then - cp -f "$SHARED_DIR/$file" /usr/lib/python3/dist-packages/shared_libs/ + cp -f "$SHARED_DIR/$file" /usr/local/share/shared_libs/ echo "Installed shared lib: $file" fi done # Install LogViewer executable if [ -f "$SHARED_DIR/logviewer.py" ]; then - cp -f "$SHARED_DIR/logviewer.py" /usr/lib/python3/dist-packages/shared_libs/ - chmod 755 /usr/lib/python3/dist-packages/shared_libs/logviewer.py + cp -f "$SHARED_DIR/logviewer.py" /usr/local/share/shared_libs/ + chmod 755 /usr/local/share/shared_libs/logviewer.py echo "Installed logviewer.py (executable)" fi @@ -772,7 +271,7 @@ EOF echo "Created LogViewer desktop file" # Create symlink for LogViewer -ln -sf /usr/lib/python3/dist-packages/shared_libs/logviewer.py /usr/local/bin/logviewer +ln -sf /usr/local/share/shared_libs/logviewer.py /usr/local/bin/logviewer echo "Created LogViewer symlink" # Install language files if available @@ -800,7 +299,7 @@ echo "LogViewer installation completed!" # Make script executable os.chmod(script_file.name, 0o755) - self.log(f"Created install script: {script_file.name}") + self.log(_(f"Created install script: {script_file.name}")) # Execute with pkexec result = subprocess.run( @@ -820,12 +319,12 @@ echo "LogViewer installation completed!" os.unlink(script_file.name) if result.returncode != 0: - raise Exception(f"Installation script failed: {result.stderr}") + raise Exception(_(f"Installation script failed: {result.stderr}")) except subprocess.TimeoutExpired: - raise Exception("Installation timed out") + raise Exception(_("Installation timed out")) except subprocess.CalledProcessError as e: - raise Exception(f"Installation script failed: {e}") + raise Exception(_(f"Installation script failed: {e}")) def update_progress(self, message): if self.progress_callback: @@ -853,10 +352,10 @@ class UninstallationManager: 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.") + raise Exception(_(f"{project_info['name']} is not installed.")) - self.update_progress(f"Uninstalling {project_info['name']}...") - self.log(f"=== Uninstalling {project_info['name']} ===") + self.update_progress(_(f"Uninstalling {project_info['name']}...")) + self.log(_(f"=== Uninstalling {project_info['name']} ===")) try: # Create uninstallation script @@ -865,12 +364,12 @@ class UninstallationManager: # Execute uninstallation self._execute_uninstall_script(script_content) - self.update_progress(f"{project_info['name']} uninstalled successfully!") - self.log(f"=== {project_info['name']} uninstalled successfully ===") + self.update_progress(_(f"{project_info['name']} uninstalled successfully!")) + self.log(_(f"=== {project_info['name']} uninstalled successfully ===")) except Exception as e: - self.log(f"ERROR: Uninstallation failed: {e}") - raise Exception(f"Uninstallation failed: {e}") + self.log(_(f"ERROR: Uninstallation failed: {e}")) + raise Exception(_(f"Uninstallation failed: {e}")) def _create_uninstall_script(self, project_key): """Create uninstallation script based on project""" @@ -879,7 +378,7 @@ class UninstallationManager: elif project_key == "logviewer": return self._create_logviewer_uninstall_script() else: - raise Exception(f"Unknown project: {project_key}") + raise Exception(_(f"Unknown project: {project_key}")) def _create_wirepy_uninstall_script(self): """Create Wire-Py uninstallation script""" @@ -900,7 +399,7 @@ rm -f /usr/local/bin/wirepy echo "Removed wirepy symlink" # Remove config -rm -f /usr/lib/python3/dist-packages/shared_libs/wp_app_config.py +rm -f /usr/local/share/shared_libs/wp_app_config.py echo "Removed wp_app_config.py" # Remove desktop file @@ -934,11 +433,11 @@ if [ ! -f /usr/local/bin/logviewer ] || [ ! -L /usr/local/bin/logviewer ]; then # Remove shared libs for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py logviewer.py; do - rm -f /usr/lib/python3/dist-packages/shared_libs/$file + rm -f /usr/local/share/shared_libs/$file done # Try to remove shared_libs directory if empty - rmdir /usr/lib/python3/dist-packages/shared_libs 2>/dev/null || true + rmdir /usr/local/share/shared_libs 2>/dev/null || true else echo "LogViewer still installed, keeping shared resources" fi @@ -984,19 +483,19 @@ if [ ! -f /usr/local/bin/wirepy ] || [ ! -L /usr/local/bin/wirepy ]; then # Remove shared libs (but keep logviewer.py if we're only uninstalling logviewer) for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do - rm -f /usr/lib/python3/dist-packages/shared_libs/$file + rm -f /usr/local/share/shared_libs/$file done # Remove logviewer.py last - rm -f /usr/lib/python3/dist-packages/shared_libs/logviewer.py + rm -f /usr/local/share/shared_libs/logviewer.py # Try to remove shared_libs directory if empty - rmdir /usr/lib/python3/dist-packages/shared_libs 2>/dev/null || true + rmdir /usr/local/share/shared_libs 2>/dev/null || true else echo "Wire-Py still installed, keeping shared resources" # Only remove logviewer-specific files - rm -f /usr/lib/python3/dist-packages/shared_libs/logview_app_config.py - rm -f /usr/lib/python3/dist-packages/shared_libs/logviewer.py + rm -f /usr/local/share/shared_libs/logview_app_config.py + rm -f /usr/local/share/shared_libs/logviewer.py fi echo "LogViewer uninstallation completed!" @@ -1014,7 +513,7 @@ echo "LogViewer uninstallation completed!" # Make script executable os.chmod(script_file.name, 0o755) - self.log(f"Created uninstall script: {script_file.name}") + self.log(_(f"Created uninstall script: {script_file.name}")) # Execute with pkexec result = subprocess.run( @@ -1034,12 +533,12 @@ echo "LogViewer uninstallation completed!" os.unlink(script_file.name) if result.returncode != 0: - raise Exception(f"Uninstallation script failed: {result.stderr}") + raise Exception(_(f"Uninstallation script failed: {result.stderr}")) except subprocess.TimeoutExpired: - raise Exception("Uninstallation timed out") + raise Exception(_("Uninstallation timed out")) except subprocess.CalledProcessError as e: - raise Exception(f"Uninstallation script failed: {e}") + raise Exception(_(f"Uninstallation script failed: {e}")) def update_progress(self, message): if self.progress_callback: @@ -1050,6 +549,32 @@ echo "LogViewer uninstallation completed!" self.log_callback(message) +class DownloadManager: + @staticmethod + def download_and_extract(url, extract_to, progress_callback=None): + """Download and extract ZIP file""" + try: + if progress_callback: + progress_callback(_(f"Downloading from {url}...")) + + 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) + return True + + except Exception as e: + if progress_callback: + progress_callback(_(f"Download failed: {str(e)}")) + return False + + class LXToolsGUI: def __init__(self): self.root = None @@ -1061,7 +586,6 @@ class LXToolsGUI: self.project_frames = {} self.status_labels = {} self.version_labels = {} - # Managers self.app_manager = AppManager() self.installation_manager = InstallationManager( @@ -1073,7 +597,7 @@ class LXToolsGUI: self.uninstallation_manager = UninstallationManager( self.app_manager, self.update_progress, self.log_message ) - self.image_manager = ImageManager() + self.image_manager = Image() # Detect OS self.detected_os = OSDetector.detect_os() @@ -1094,9 +618,11 @@ class LXToolsGUI: self.root = tk.Tk() self.root.title(f"{LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}") self.root.geometry( - f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}" + f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}+100+100" + ) + Center.center_window_cross_platform( + self.root, LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT ) - self.root.configure(bg=self.colors["bg"]) # Try to set icon @@ -1107,7 +633,7 @@ class LXToolsGUI: except: pass self.root.minsize(LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT) - ThemeManager.apply_light_theme(self.root) + Theme.apply_light_theme(self.root) # Create header self._create_header() @@ -1141,19 +667,19 @@ class LXToolsGUI: content = tk.Frame(header_frame, bg="#2c3e50") content.pack(fill="both", expand=True, padx=15, pady=12) - # LINKE SEITE: Icon + App Info + # LEFT SIDE: Icon + App Info left_side = tk.Frame(content, bg="#2c3e50") left_side.pack(side="left", anchor="w") icon_text_frame = tk.Frame(left_side, bg="#2c3e50") icon_text_frame.pack(anchor="w") - # Werkzeug-Icon + # Tool-Icon tk.Label( icon_text_frame, text="🔧", font=("Helvetica", 18), bg="#2c3e50", fg="white" ).pack(side="left", padx=(0, 8)) - # App Name und Version + # App Name and Version text_frame = tk.Frame(icon_text_frame, bg="#2c3e50") text_frame.pack(side="left") @@ -1168,13 +694,13 @@ class LXToolsGUI: tk.Label( text_frame, - text=f"v {LXToolsAppConfig.VERSION} • Linux Application Installer", + text=f"v {LXToolsAppConfig.VERSION} • Linux App Installer", font=("Helvetica", 9), fg="#bdc3c7", bg="#2c3e50", ).pack(anchor="w") - # RECHTE SEITE: System + Dynamischer Status + # RIGHT SIDE: System + Dynamic Status right_side = tk.Frame(content, bg="#2c3e50") right_side.pack(side="right", anchor="e") @@ -1186,13 +712,13 @@ class LXToolsGUI: bg="#2c3e50", ).pack(anchor="e") - # DYNAMISCHER Status (anfangs leer) + # DYNAMIC Status (begin empty) self.header_status_label = tk.Label( - right_side, text="", font=("Helvetica", 10), bg="#2c3e50" # Anfangs leer + right_side, text="", font=("Helvetica", 10), bg="#2c3e50" # begin empty ) self.header_status_label.pack(anchor="e", pady=(2, 0)) - # Trennlinie + # Separator separator = tk.Frame(self.root, height=1, bg="#34495e") separator.pack(fill="x", pady=0) @@ -1208,18 +734,18 @@ class LXToolsGUI: repo_ok = NetworkChecker.check_repository_access() if internet_ok and repo_ok: - self.update_header_status("Ready for installation", "#1abc9c") # Grün + self.update_header_status(_("Ready for installation"), "#1abc9c") # Green elif not internet_ok: - self.update_header_status("No internet connection", "#e74c3c") # Rot + self.update_header_status(_("No internet connection"), "#e74c3c") # Red elif not repo_ok: - self.update_header_status("Repository unavailable", "#f39c12") # Orange + self.update_header_status(_("Repository unavailable"), "#f39c12") # Orange else: - self.update_header_status("System checking...", "#3498db") # Blau + self.update_header_status(_("System checking..."), "#3498db") # Blue def _create_projects_tab(self): """Create projects tab with project cards""" projects_frame = ttk.Frame(self.notebook) - self.notebook.add(projects_frame, text="Projects") + self.notebook.add(projects_frame, text=_("Applications")) # Scrollable frame canvas = tk.Canvas(projects_frame, bg=self.colors["bg"]) @@ -1415,7 +941,7 @@ class LXToolsGUI: def _create_log_tab(self): """Create log tab""" log_frame = ttk.Frame(self.notebook) - self.notebook.add(log_frame, text="Installation Log") + self.notebook.add(log_frame, text=_("Installation Log")) # Log text with scrollbar log_container = tk.Frame(log_frame) @@ -1445,7 +971,7 @@ class LXToolsGUI: # Clear log button clear_log_btn = ttk.Button( - log_controls, text="Clear Log", command=self.clear_log + log_controls, text=_("Clear Log"), command=self.clear_log ) clear_log_btn.pack(side="right") @@ -1453,14 +979,14 @@ class LXToolsGUI: self.log_message( f"=== {LXToolsAppConfig.APP_NAME} v {LXToolsAppConfig.VERSION} ===" ) - self.log_message(f"Working directory: {LXToolsAppConfig.WORK_DIR}") - self.log_message(f"Icons directory: {LXToolsAppConfig.ICONS_DIR}") - self.log_message(f"Detected OS: {self.detected_os}") - self.log_message("Ready for installation...") + self.log_message(_(f"Working directory: {LXToolsAppConfig.WORK_DIR}")) + self.log_message(_(f"Icons directory: {LXToolsAppConfig.ICONS_DIR}")) + self.log_message(_(f"Detected OS: {self.detected_os}")) + self.log_message(_("Ready for installation...")) def _create_progress_section(self): """Create progress section with download icon""" - progress_frame = ttk.LabelFrame(self.root, text="Progress", padding=10) + progress_frame = ttk.LabelFrame(self.root, text=_("Progress"), padding=10) progress_frame.pack(fill="x", padx=15, pady=10) # Container for Icon and Progress @@ -1474,7 +1000,7 @@ class LXToolsGUI: # Progress Text (right, expandable) self.progress_label = tk.Label( progress_container, - text="Ready for installation...", + text=_("Ready for installation..."), font=("Helvetica", 10), fg="blue", anchor="w", @@ -1518,7 +1044,7 @@ class LXToolsGUI: # Create buttons install_btn = ttk.Button( button_frame, - text="Install/Update", + text=_("Install/Update"), command=self.install_selected, style="Install.TButton", padding=8, @@ -1527,7 +1053,7 @@ class LXToolsGUI: uninstall_btn = ttk.Button( button_frame, - text="Uninstall", + text=_("Uninstall"), command=self.uninstall_selected, style="Uninstall.TButton", padding=8, @@ -1536,7 +1062,7 @@ class LXToolsGUI: refresh_btn = ttk.Button( button_frame, - text="Refresh Status", + text=_("Refresh Status"), command=self.refresh_status, style="Refresh.TButton", padding=8, @@ -1585,23 +1111,23 @@ class LXToolsGUI: def refresh_status(self): """Refresh application status and version information""" - self.update_progress("Refreshing status and checking versions...") + self.update_progress(_("Refreshing status and checking versions...")) self._reset_download_icon() - self.log_message("=== Refreshing Status ===") + 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] - self.log_message(f"Checking {project_info['name']}...") + self.log_message(_(f"Checking {project_info['name']}...")) if self.app_manager.is_installed(project_key): installed_version = self.app_manager.get_installed_version(project_key) status_label.config( - text=f"✅ Installed ({installed_version})", fg="green" + text=_(f"✅ Installed ({installed_version})"), fg="green" ) self.log_message( - f"{project_info['name']}: Installed {installed_version}" + _(f"{project_info['name']}: Installed {installed_version}") ) # Get latest version from API @@ -1610,11 +1136,15 @@ class LXToolsGUI: if latest_version != "Unknown": if installed_version != f"v. {latest_version}": version_label.config( - text=f"Latest: v. {latest_version} (Update available)", + text=_( + f"Latest: v. {latest_version} (Update available)" + ), fg="orange", ) self.log_message( - f"{project_info['name']}: Update available v. {latest_version}" + _( + f"{project_info['name']}: Update available v. {latest_version}" + ) ) else: version_label.config( @@ -1634,135 +1164,116 @@ class LXToolsGUI: ) else: status_label.config(text="❌ Not installed", fg="red") - self.log_message(f"{project_info['name']}: Not installed") + self.log_message(_(f"{project_info['name']}: Not installed")) # Still show latest available version try: latest_version = self.app_manager.get_latest_version(project_key) if latest_version != "Unknown": version_label.config( - text=f"Available: v {latest_version}", fg="blue" + text=_(f"Available: v {latest_version}"), fg="blue" ) self.log_message( - f"{project_info['name']}: Available v {latest_version}" + _(f"{project_info['name']}: Available v {latest_version}") ) else: - version_label.config(text="Available: Unknown", fg="gray") + version_label.config(text=_("Available: Unknown"), fg="gray") except Exception as e: - version_label.config(text="Available: Check failed", fg="gray") + version_label.config(text=_("Available: Check failed"), fg="gray") self.log_message( - f"{project_info['name']}: Version check failed: {e}" + _(f"{project_info['name']}: Version check failed: {e}") ) - self.update_progress("Status refresh completed.") - self.log_message("=== Status refresh completed ===") + self.update_progress(_("Status refresh completed.")) + self.log_message(_("=== Status refresh completed ===")) self.check_ready_status() def install_selected(self): """Handle install button click""" if not self.selected_project: - messagebox.showwarning("Warning", "Please select a project to install.") + MessageDialog("error", _("Please select a project to install.")) + self.root.focus_set() 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.", + MessageDialog( + "error", + _( + "Network Error", + "No internet connection available.\nPlease check your network connection.", + ), ) + self.root.focus_set() return if not NetworkChecker.check_repository_access(): self.update_download_icon("error") - messagebox.showerror( - "Repository Error", "Cannot access repository.\nPlease try again later." + MessageDialog( + "error", + _( + "Repository Error", + "Cannot access repository.\nPlease try again later.", + ), ) + self.root.focus_set() return - self.root.focus_set() # Reset download icon self._reset_download_icon() project_info = self.app_manager.get_project_info(self.selected_project) - # Check if already installed - if self.app_manager.is_installed(self.selected_project): - installed_version = self.app_manager.get_installed_version( - self.selected_project - ) - latest_version = self.app_manager.get_latest_version(self.selected_project) - - dialog_text = ( - f"{project_info['name']} is already installed.\n\n" - f"Installed version: {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_selected() - return - else: # Update - self.update_progress("Updating application...") - try: self.update_download_icon("downloading") self.installation_manager.install_project(self.selected_project) self.update_download_icon("success") - messagebox.showinfo( - "Success", - f"{project_info['name']} has been successfully installed/updated.", + MessageDialog( + "info", + _(f"{project_info['name']} has been successfully installed/updated."), ) self.refresh_status() except Exception as e: self.update_download_icon("error") - messagebox.showerror("Error", f"Installation failed: {e}") + MessageDialog("error", _(f"{e}")) + + self.root.focus_set() def uninstall_selected(self): """Handle uninstall button click""" + if not self.selected_project: - messagebox.showwarning("Warning", "Please select a project to uninstall.") + MessageDialog("error", _("Please select a project to uninstall.")) + self.root.focus_set() return project_info = self.app_manager.get_project_info(self.selected_project) if not self.app_manager.is_installed(self.selected_project): - 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: + MessageDialog("error", _(f"{project_info['name']} is not installed.")) + self.root.focus_set() return try: self.uninstallation_manager.uninstall_project(self.selected_project) - messagebox.showinfo( - "Success", f"{project_info['name']} has been successfully uninstalled." + MessageDialog( + "info", + _( + f"{project_info['name']} has been successfully uninstalled.", + ), ) self.refresh_status() + self.root.focus_set() except Exception as e: - messagebox.showerror("Error", f"Uninstallation failed: {e}") - self.root.focus_set() + MessageDialog("error", _(f"Uninstallation failed: {e}")) + self.root.focus_set() def update_progress(self, message): """Update progress message""" if self.progress_label: self.progress_label.config(text=message) self.progress_label.update() - print(f"Progress: {message}") + print(_(f"Progress: {message}")) def log_message(self, message): """Add message to log""" @@ -1772,13 +1283,13 @@ class LXToolsGUI: self.log_text.insert(tk.END, log_entry) self.log_text.see(tk.END) self.log_text.update() - print(f"Log: {message}") + print(_(f"Log: {message}")) def clear_log(self): """Clear the log""" if self.log_text: self.log_text.delete(1.0, tk.END) - self.log_message("Log cleared") + self.log_message(_("Log cleared")) def run(self): """Start the GUI application""" @@ -1786,67 +1297,24 @@ class LXToolsGUI: root.mainloop() -def check_and_install_tkinter(): - """Check if tkinter is available and install if needed""" - if not OSDetector.check_tkinter_available(): - print("TKinter is not available on this system.") - detected_os = OSDetector.detect_os() - print(f"Detected OS: {detected_os}") - - response = input("Would you like to install TKinter? (y/n): ").lower().strip() - if response in ["y", "yes"]: - print("Installing TKinter...") - if OSDetector.install_tkinter(): - print("TKinter installed successfully!") - print("Please restart the application.") - return False - else: - print("Failed to install TKinter.") - print("Please install TKinter manually:") - - if detected_os in ["Ubuntu", "Debian", "Linux Mint", "Pop!_OS"]: - print("sudo apt update && sudo apt install python3-tk") - elif detected_os == "Fedora": - print("sudo dnf install tkinter") - elif detected_os in [ - "Arch Linux", - "Manjaro", - "Garuda Linux", - "EndeavourOS", - ]: - print("sudo pacman -S tk") - elif "SUSE" in detected_os or "openSUSE" in detected_os: - print("sudo zypper install python3-tk") - else: - print("Please check your distribution's package manager.") - - return False - else: - print("TKinter is required to run this application.") - return False - - return True - - def main(): """Main function to start the application""" print(f"=== {LXToolsAppConfig.APP_NAME} v {LXToolsAppConfig.VERSION} ===") - print(f"Working directory: {os.getcwd()}") - - # Check and install tkinter if needed - if not check_and_install_tkinter(): - return + print(_(f"Working directory: {os.getcwd()}")) try: # Create and run the GUI app = LXToolsGUI() + app.run() except KeyboardInterrupt: - print("\nApplication interrupted by user.") + print(_("\nApplication interrupted by user.")) except Exception as e: - print(f"Fatal error: {e}") + print(_(f"Fatal error: {e}")) try: - messagebox.showerror("Fatal Error", f"Application failed to start: {e}") + MessageDialog( + "error", _("Fatal Error", f"Application failed to start: {e}") + ) except: pass diff --git a/manager.py b/manager.py new file mode 100644 index 0000000..6448fa4 --- /dev/null +++ b/manager.py @@ -0,0 +1,493 @@ +import locale +import gettext +import tkinter as tk +from pathlib import Path +from tkinter import ttk +import os +import subprocess +import stat +from network import GiteaUpdate + + +class LXToolsAppConfig: + VERSION = "1.1.4" + APP_NAME = "Lunix Tools Installer" + WINDOW_WIDTH = 450 + WINDOW_HEIGHT = 580 + + # Working directory + WORK_DIR = os.getcwd() + ICONS_DIR = os.path.join(WORK_DIR, "lx-icons") + THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes") + + # Locale settings + LOCALE_DIR = Path("/usr/share/locale/") + + # 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" + ) + + # Project configurations + PROJECTS = { + "wirepy": { + "name": "Wire-Py", + "description": "WireGuard VPN Manager with GUI", + "download_url": WIREPY_URL, + "api_url": WIREPY_API_URL, + "icon_key": "icon_vpn", + "main_executable": "wirepy.py", + "symlink_name": "wirepy", + "config_file": "wp_app_config.py", + "desktop_file": "Wire-Py.desktop", + "policy_file": "org.sslcrypt.policy", + "requires_ssl": True, + }, + "logviewer": { + "name": "LogViewer", + "description": "System Log Viewer with GUI", + "download_url": SHARED_LIBS_URL, + "api_url": SHARED_LIBS_API_URL, + "icon_key": "icon_log", + "main_executable": "logviewer.py", + "symlink_name": "logviewer", + "config_file": "logview_app_config.py", + "desktop_file": "LogViewer.desktop", + "policy_file": None, + "requires_ssl": False, + }, + } + + # 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"), + ("arch", "Arch Linux"), + ("ubuntu", "Ubuntu"), + ("debian", "Debian"), + ] + + # Package manager commands for TKinter installation + TKINTER_INSTALL_COMMANDS = { + "Ubuntu": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], + "Debian": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], + "Linux Mint": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], + "Pop!_OS": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], + "Fedora": ["dnf", "install", "-y", "tkinter"], + "Arch Linux": ["pacman", "-S", "--noconfirm", "tk"], + "Manjaro": ["pacman", "-S", "--noconfirm", "tk"], + "Garuda Linux": ["pacman", "-S", "--noconfirm", "tk"], + "EndeavourOS": ["pacman", "-S", "--noconfirm", "tk"], + "SUSE Tumbleweed": ["zypper", "install", "-y", "python314-tk"], + "SUSE Leap": ["zypper", "install", "-y", "python312-tk"], + } + + @staticmethod + def setup_translations(): + """Initialize translations and set the translation function""" + try: + locale.bindtextdomain( + LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR + ) + gettext.bindtextdomain( + LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR + ) + gettext.textdomain(LXToolsAppConfig.APP_NAME) + except: + pass + return gettext.gettext + + +# Initialize translations +_ = LXToolsAppConfig.setup_translations() + + +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" + + @staticmethod + def check_tkinter_available(): + """Check if tkinter is available""" + try: + import tkinter + + return True + except ImportError: + return False + + @staticmethod + def install_tkinter(): + """Install tkinter based on detected OS""" + detected_os = OSDetector.detect_os() + + if detected_os in LXToolsAppConfig.TKINTER_INSTALL_COMMANDS: + commands = LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os] + + print(f"Installing tkinter for {detected_os}...") + print(_(f"Command: {' '.join(commands)}")) + + try: + # Use pkexec for privilege escalation + full_command = ["pkexec", "bash", "-c", " ".join(commands)] + result = subprocess.run( + full_command, capture_output=True, text=True, timeout=300 + ) + + if result.returncode == 0: + print(_("TKinter installation completed successfully!")) + return True + else: + print(_(f"TKinter installation failed: {result.stderr}")) + return False + + except subprocess.TimeoutExpired: + print(_("TKinter installation timed out")) + return False + except Exception as e: + print(_(f"Error installing tkinter: {e}")) + return False + else: + print(_(f"No tkinter installation command defined for {detected_os}")) + return False + + +class Theme: + @staticmethod + def apply_light_theme(root): + """Apply light theme""" + try: + theme_dir = LXToolsAppConfig.THEMES_DIR + water_theme_path = os.path.join(theme_dir, "water.tcl") + + if os.path.exists(water_theme_path): + try: + root.tk.call("source", water_theme_path) + root.tk.call("set_theme", "light") + return True + except tk.TclError: + pass + + # System theme fallback + try: + style = ttk.Style() + if "clam" in style.theme_names(): + style.theme_use("clam") + return True + except: + pass + + except Exception: + pass + + return False + + +class System: + @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 + + +class Image: + 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] + + # Define image paths based on key + image_paths = { + "app_icon": [ + "./lx-icons/48/wg_vpn.png", + "/usr/share/icons/lx-icons/48/wg_vpn.png", + ], + "download_icon": [ + "./lx-icons/32/download.png", + "/usr/share/icons/lx-icons/32/download.png", + ], + "download_error_icon": [ + "./lx-icons/32/download_error.png", + "/usr/share/icons/lx-icons/32/download_error.png", + ], + "success_icon": [ + "./lx-icons/32/download.png", + "/usr/share/icons/lx-icons/32/download.png", + ], + "icon_vpn": [ + "./lx-icons/48/wg_vpn.png", + "/usr/share/icons/lx-icons/48/wg_vpn.png", + ], + "icon_log": [ + "./lx-icons/48/log.png", + "/usr/share/icons/lx-icons/48/log.png", + ], + } + + # Get paths to try + paths_to_try = image_paths.get(image_key, []) + + # Add fallback paths if provided + 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 + return None + + +class AppManager: + def __init__(self): + self.projects = LXToolsAppConfig.PROJECTS + + def get_project_info(self, project_key): + """Get project information by key""" + return self.projects.get(project_key) + + def get_all_projects(self): + """Get all project configurations""" + return self.projects + + def is_installed(self, project_key): + """Check if project is installed with better detection""" + if project_key == "wirepy": + # Check for wirepy symlink + return os.path.exists("/usr/local/bin/wirepy") and os.path.islink( + "/usr/local/bin/wirepy" + ) + + elif project_key == "logviewer": + # Check for logviewer symlink AND executable file + symlink_exists = os.path.exists("/usr/local/bin/logviewer") + executable_exists = os.path.exists( + "/usr/local/share/shared_libs/logviewer.py" + ) + executable_is_executable = False + + if executable_exists: + try: + # Check if file is executable + file_stat = os.stat("/usr/local/share/shared_libs/logviewer.py") + executable_is_executable = bool(file_stat.st_mode & stat.S_IEXEC) + except: + executable_is_executable = False + + # LogViewer is installed if symlink exists AND executable file exists AND is executable + is_installed = ( + symlink_exists and executable_exists and executable_is_executable + ) + + # Debug logging + print(_("LogViewer installation check:")) + print(_(f" Symlink exists: {symlink_exists}")) + print(_(f" Executable exists: {executable_exists}")) + print(_(f" Is executable: {executable_is_executable}")) + print(_(f" Final result: {is_installed}")) + + return is_installed + + return False + + def get_installed_version(self, project_key): + """Get installed version from config file""" + try: + if project_key == "wirepy": + config_file = "/usr/local/share/shared_libs/wp_app_config.py" + elif project_key == "logviewer": + config_file = "/usr/local/share/shared_libs/logview_app_config.py" + else: + return "Unknown" + + 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""" + project_info = self.get_project_info(project_key) + if not project_info: + return "Unknown" + + return GiteaUpdate.api_down(project_info["api_url"]) + + def check_other_apps_installed(self, exclude_key): + """Check if other apps are still installed""" + return any( + self.is_installed(key) for key in self.projects.keys() if key != exclude_key + ) + + +class Center: + @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}") diff --git a/message.py b/message.py new file mode 100644 index 0000000..342483b --- /dev/null +++ b/message.py @@ -0,0 +1,384 @@ +import os +from typing import List, Optional, Dict +import tkinter as tk +from tkinter import ttk +from manager import Center + +""" +#################################################### +Attention! MessageDialog returns different values. +From 3 buttons with Cancel, Cancel and the Close (x) +None returns. otherwise always False. +#################################################### +Usage Examples +1. Basic Info Dialog +from tkinter import Tk + +root = Tk() +dialog = MessageDialog( + message_type="info", + text="This is an information message.", + buttons=["OK"], + master=root, +) +result = dialog.show() +print("User clicked OK:", result) + +----------------------------------------------------- +My Favorite Example, +for simply information message: + +MessageDialog(text="This is an information message.") +result = MessageDialog(text="This is an information message.").show() +----------------------------------------------------- +Explanation: if you need the return value e.g. in the vaiable result, +you need to add .show(). otherwise only if no root.mainloop z.b is used to test the window. +##################################################### + +2. Error Dialog with Custom Icon and Command +def on_cancel(): + print("User canceled the operation.") + +root = Tk() +result = MessageDialog( + message_type="error", + text="An error occurred during processing.", + buttons=["Retry", "Cancel"], + commands=[None, on_cancel], + icon="/path/to/custom/error_icon.png", + title="Critical Error" +).show() + +print("User clicked Retry:", result) + +----------------------------------------------------- +My Favorite Example, +for simply Error message: + +MessageDialog( + "error", + text="An error occurred during processing.", +).show() + +##################################################### + +3. Confirmation Dialog with Yes/No Buttons +def on_confirm(): + print("User confirmed the action.") + +root = Tk() +dialog = MessageDialog( + message_type="ask", + text="Are you sure you want to proceed?", + buttons=["Yes", "No"], + commands=[on_confirm, None], # Either use comando or work with the values True and False +) +result = dialog.show() +print("User confirmed:", result) +----------------------------------------------------- + +My Favorite Example, +for simply Question message: + +dialog = MessageDialog( + "ask", + text="Are you sure you want to proceed?", + buttons=["Yes", "No"] + ).show() +##################################################### + +4. Warning Dialog with Custom Title + +root = Tk() +dialog = MessageDialog( + message_type="warning", + text="This action cannot be undone.", + buttons=["Proceed", "Cancel"], + title="Warning: Irreversible Action" +) +result = dialog.show() +print("User proceeded:", result) +----------------------------------------------------- +And a special example for a "open link" button: +Be careful not to forget to import it into the script in which this dialog is used!!! +import webbrowser +from functools import partial + +dialog = MessageDialog( + "ask", + text="Are you sure you want to proceed?", + buttons=["Yes", "Go to Exapmle"], + commands=[ + None, # Default on "OK" + partial(webbrowser.open, "https://exapmle.com"), + ], + icon="/pathh/to/custom/icon.png", + title="Example", +).show() + + +In all dialogues, a font can also be specified as a tuple. With font=("ubuntu", 11) +and wraplength=300, the text is automatically wrapped. +""" + + +class MessageDialog: + """ + A customizable message dialog window using tkinter. + + This class creates modal dialogs for displaying information, warnings, errors, + or questions to the user. It supports multiple button configurations and custom + icons. The dialog is centered on the screen and handles user interactions. + + Attributes: + message_type (str): Type of message ("info", "error", "warning", "ask"). + text (str): Main message content. + buttons (List[str]): List of button labels (e.g., ["OK", "Cancel"]). + result (bool): True if the user clicked a positive button (like "Yes" or "OK"), else False. + icons: Dictionary mapping message types to tkinter.PhotoImage objects. + + Parameters: + message_type: Type of message dialog (default: "info"). + text: Message content to display. + buttons: List of button labels (default: ["OK"]). + master: Parent tkinter window (optional). + commands: List of callables for each button (default: [None]). + icon: Custom icon path (overrides default icons if provided). + title: Window title (default: derived from message_type). + + Methods: + _get_title(): Returns the default window title based on message type. + _load_icons(): Loads icons from system paths or fallback locations. + _on_button_click(button_text): Sets result and closes the dialog. + show(): Displays the dialog and waits for user response (returns self.result). + """ + + DEFAULT_ICON_PATH = "/usr/share/icons/lx-icons" + + def __init__( + self, + message_type: str = "info", + text: str = "", + buttons: List[str] = ["OK"], + master: Optional[tk.Tk] = None, + commands: List[Optional[callable]] = [None], + icon: str = None, + title: str = None, + font: tuple = None, + wraplength: int = None, + ): + self.message_type = message_type or "info" # Default is "info" + self.text = text + self.buttons = buttons + self.master = master + self.result: bool = False # Default is False + + self.icon_path = self._get_icon_path() + self.icon = icon + self.title = title + # Window creation + self.window = tk.Toplevel(master) + self.window.grab_set() + self.window.resizable(False, False) + ttk.Style().configure("TButton", font=("Helvetica", 11), padding=5) + self.buttons_widgets = [] + self.current_button_index = 0 + self._load_icons() + + # Window title and icon + self.window.title(self._get_title() if not self.title else self.title) + self.window.iconphoto(False, self.icons[self.message_type]) + + # Layout + frame = ttk.Frame(self.window) + frame.pack(expand=True, fill="both") + + # Grid-Configuration + frame.grid_rowconfigure(0, weight=1) + frame.grid_columnconfigure(0, weight=1) + frame.grid_columnconfigure(1, weight=3) + + # Icon and Text + icon_label = ttk.Label(frame, image=self.icons[self.message_type]) + pady_value = 5 if self.icon is not None else 15 + icon_label.grid( + row=0, column=0, padx=(20, 10), pady=(pady_value, 15), sticky="nsew" + ) + + text_label = tk.Label( + frame, + text=text, + wraplength=wraplength if wraplength else 300, + justify="left", + anchor="center", + font=font if font else ("Helvetica", 12), + pady=20, + ) + text_label.grid( + row=0, + column=1, + padx=(10, 20), + pady=(8, 20), + sticky="nsew", + ) + + # Create button frame + self.button_frame = ttk.Frame(frame) + self.button_frame.grid(row=1, columnspan=2, pady=(15, 10)) + + for i, btn_text in enumerate(buttons): + if commands and len(commands) > i and commands[i] is not None: + # Button with individual command + btn = ttk.Button( + self.button_frame, + text=btn_text, + command=commands[i], + ) + else: + # Default button set self.result and close window + btn = ttk.Button( + self.button_frame, + text=btn_text, + command=lambda t=btn_text: self._on_button_click(t), + ) + + padx_value = 50 if self.icon is not None and len(buttons) == 2 else 10 + btn.pack(side="left" if i == 0 else "right", padx=padx_value, pady=15) + btn.focus_set() if i == 0 else None # Set focus on first button + self.buttons_widgets.append(btn) + + self.window.bind("", lambda event: self._on_enter_pressed()) + self.window.bind("", lambda event: self._navigate_left()) + self.window.bind("", lambda event: self._navigate_right()) + self.window.update_idletasks() + self.window.attributes("-alpha", 0.0) # 100% Transparencence + self.window.after(200, lambda: self.window.attributes("-alpha", 100.0)) + self.window.update() # Window update before centering! + Center.center_window_cross_platform( + self.window, self.window.winfo_width(), self.window.winfo_height() + ) + + # Close Window on Cancel + self.window.protocol( + "WM_DELETE_WINDOW", lambda: self._on_button_click("Cancel") + ) + + def _get_title(self) -> str: + return { + "error": "Error", + "info": "Info", + "ask": "Question", + "warning": "Warning", + }[self.message_type] + + def _load_icons(self): + # Try to load the icon from the provided path + self.icons = {} + icon_paths: Dict[str, str] = { + "error": os.path.join(self.icon_path, "64/error.png"), + "info": os.path.join(self.icon_path, "64/info.png"), + "warning": os.path.join(self.icon_path, "64/warning.png"), + "ask": os.path.join(self.icon_path, "64/question_mark.png"), + } + + fallback_paths: Dict[str, str] = { + "error": "./lx-icons/64/error.png", + "info": "./lx-icons/64/info.png", + "warning": "./lx-icons/64/warning.png", + "ask": "./lx-icons/64/question_mark.png", + } + + for key in icon_paths: + try: + # Check if an individual icon is provided + if ( + self.message_type == key + and self.icon is not None + and os.path.exists(self.icon) + ): + try: + self.icons[key] = tk.PhotoImage(file=self.icon) + except Exception as e: + print( + f"Erro on loading individual icon '{key}': {e}\n", + "Try to use the default icon", + ) + + else: + # Check for standard path + if os.path.exists(icon_paths[key]): + self.icons[key] = tk.PhotoImage(file=icon_paths[key]) + else: + self.icons[key] = tk.PhotoImage(file=fallback_paths[key]) + except Exception as e: + print(f"Error on load Icon '{[key]}': {e}") + self.icons[key] = tk.PhotoImage() + print(f"⚠️ No Icon found for '{key}'. Use standard Tkinter icon.") + + return self.icons + + def _get_icon_path(self) -> str: + """Get the path to the default icon.""" + if os.path.exists(self.DEFAULT_ICON_PATH): + return self.DEFAULT_ICON_PATH + else: + # Fallback to the directory of the script + return os.path.dirname(os.path.abspath(__file__)) + + def _navigate_left(self): + if not self.buttons_widgets: + return + self.current_button_index = (self.current_button_index - 1) % len( + self.buttons_widgets + ) + self.buttons_widgets[self.current_button_index].focus_set() + + def _navigate_right(self): + if not self.buttons_widgets: + return + self.current_button_index = (self.current_button_index + 1) % len( + self.buttons_widgets + ) + self.buttons_widgets[self.current_button_index].focus_set() + + def _on_enter_pressed(self): + focused = self.window.focus_get() + if isinstance(focused, ttk.Button): + focused.invoke() + + def _on_button_click(self, button_text: str) -> None: + """ + Sets `self.result` based on the clicked button. + - Returns `None` if the button is "Cancel", "Abort", or "Exit" **and** there are 3 or more buttons. + - Returns `True` if the button is "Yes", "Ok", "Continue", "Next", or "Start". + - Returns `False` in all other cases (e.g., "No", closing with X, or fewer than 3 buttons). + """ + # Check: If there are 3+ buttons and the button text matches "Cancel", "Abort", or "Exit" + if len(self.buttons) >= 3 and button_text.lower() in [ + "cancel", + "abort", + "exit", + ]: + self.result = None + # Check: Button text is "Yes", "Ok", "Continue", "Next", or "Start" + elif button_text.lower() in ["yes", "ok", "continue", "next", "start"]: + self.result = True + else: + # Fallback for all other cases (e.g., "No", closing with X, or fewer than 3 buttons) + self.result = False + + self.window.destroy() + + def show(self) -> Optional[bool]: + """ + Displays the dialog window and waits for user interaction. + + Returns: + bool or None: + - `True` if "Yes", "Ok", etc. was clicked. + - `False` if "No" was clicked, or the window was closed with X (when there are 2 buttons). + - `None` if "Cancel", "Abort", or "Exit" was clicked **and** there are 3+ buttons, + or the window was closed with X (when there are 3+ buttons). + """ + self.window.wait_window() + return self.result diff --git a/network.py b/network.py new file mode 100644 index 0000000..ba60639 --- /dev/null +++ b/network.py @@ -0,0 +1,40 @@ +import socket +import urllib.request +import json + + +class GiteaUpdate: + @staticmethod + def api_down(url, current_version=""): + """Get latest version from Gitea API""" + try: + with urllib.request.urlopen(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") # Remove 'v' prefix if present + return "Unknown" + except Exception as e: + print(f"API Error: {e}") + return "Unknown" + + +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", timeout=5): + """Check if repository is accessible""" + try: + urllib.request.urlopen(url, timeout=timeout) + return True + except: + return False