#!/usr/bin/python3 import tkinter as tk from tkinter import ttk import os import subprocess from datetime import datetime import tempfile import urllib.request import zipfile from manager import ( OSDetector, Theme, LocaleStrings, LXToolsAppConfig, System, Image, AppManager, LxTools, ) from network import NetworkChecker from message import MessageDialog class InstallationManager: def __init__( self, app_manager, progress_callback=None, icon_callback=None, log_callback=None ): self.app_manager = app_manager self.progress_callback = progress_callback self.icon_callback = icon_callback self.log_callback = log_callback self.system_manager = System() self.download_manager = DownloadManager() def install_project(self, project_key): """Install or update project""" project_info = self.app_manager.get_project_info(project_key) if not project_info: raise Exception(f"{LocaleStrings.MSG["unknow_project"]}{project_key}") self.update_progress( f"{LocaleStrings.MSGI["start_install"]}{project_info['name']}..." ) self.log(f"=== {LocaleStrings.MSGI["install"]}{project_info['name']} ===") try: # Create installation script script_content = self._create_install_script(project_key) # Execute installation self._execute_install_script(script_content) self.update_progress( f"{project_info["name"]}{LocaleStrings.MSGI["install_success"]}" ) self.log( f"=== {project_info["name"]}{LocaleStrings.MSGI["install_success"]} ===" ) # Set success icon self.update_icon("success") except Exception as e: self.log(f"ERROR: {LocaleStrings.MSGI["install_failed"]}{e}") self.update_icon("error") raise Exception(f"{LocaleStrings.MSGI["install_failed"]}{e}") def _create_install_script(self, project_key): """Create installation script based on project""" if project_key == "wirepy": return self._create_wirepy_install_script() elif project_key == "logviewer": return self._create_logviewer_install_script() else: raise Exception(f"{LocaleStrings.MSGI["unknow_project"]}{project_key}") def _create_wirepy_install_script(self): """Create Wire-Py installation script""" script = f"""#!/bin/bash set -e echo "=== Wire-Py Installation ===" # Create necessary directories 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 mkdir -p /usr/local/etc/ssl mkdir -p /usr/share/polkit-1/actions mkdir -p /usr/share/TK-Themes # Download and extract Wire-Py cd /tmp rm -rf wirepy_install mkdir wirepy_install cd wirepy_install echo "Downloading Wire-Py..." wget -q "{LXToolsAppConfig.WIREPY_URL}" -O wirepy.zip unzip -q wirepy.zip WIREPY_DIR=$(find . -name "wire-py" -type d | head -1) echo "Downloading shared libraries..." wget -q "{LXToolsAppConfig.SHARED_LIBS_URL}" -O shared.zip unzip -q shared.zip SHARED_DIR=$(find . -name "shared_libs" -type d | head -1) # Install Wire-Py files echo "Installing Wire-Py executables..." for file in wirepy.py start_wg.py ssl_encrypt.py ssl_decrypt.py match_found.py tunnel.py; do if [ -f "$WIREPY_DIR/$file" ]; then cp -f "$WIREPY_DIR/$file" /usr/local/bin/ chmod 755 /usr/local/bin/$file echo "Installed $file" fi done # Install config if [ -f "$WIREPY_DIR/wp_app_config.py" ]; then cp -f "$WIREPY_DIR/wp_app_config.py" /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 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/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/local/share/shared_libs/ chmod 755 /usr/local/share/shared_libs/logviewer.py echo "Installed logviewer.py (executable)" fi # Install icons if [ -d "$WIREPY_DIR/lx-icons" ]; then echo "Installing icons..." cp -rf "$WIREPY_DIR/lx-icons"/* /usr/share/icons/lx-icons/ fi # Install TK-Themes if [ -d "$WIREPY_DIR/TK-Themes" ]; then echo "Installing TK-Themes..." cp -rf "$WIREPY_DIR/TK-Themes"/* /usr/share/TK-Themes/ fi # Install desktop file if [ -f "$WIREPY_DIR/Wire-Py.desktop" ]; then cp -f "$WIREPY_DIR/Wire-Py.desktop" /usr/share/applications/ echo "Installed desktop file" fi # Install language files if [ -d "$WIREPY_DIR/languages/de" ]; then echo "Installing language files..." cp -f "$WIREPY_DIR/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true fi # Install policy file if [ -f "$WIREPY_DIR/org.sslcrypt.policy" ]; then cp -f "$WIREPY_DIR/org.sslcrypt.policy" /usr/share/polkit-1/actions/ echo "Installed policy file" fi # Create symlink for Wirepy ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy # Create symlink for LogViewer ln -sf /usr/local/share/shared_libs/logviewer.py /usr/local/bin/logviewer echo "Created Wirepy and LogViewer symlink" # Install language files if available if [ -d "$SHARED_DIR/languages/de" ]; then echo "Installing language files..." cp "$SHARED_DIR/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true fi echo "Created symlink" # Create SSL key if not exists if [ ! -f /usr/local/etc/ssl/pwgk.pem ]; then echo "Creating SSL key..." openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 chmod 600 /usr/local/etc/ssl/pwgk.pem fi # Cleanup cd /tmp rm -rf wirepy_install echo "Wire-Py installation completed!" """ return script def _create_logviewer_install_script(self): """Create LogViewer installation script""" script = f"""#!/bin/bash set -e echo "=== LogViewer Installation ===" # Create necessary directories 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 mkdir -p /usr/share/TK-Themes # Download and extract shared libraries (contains LogViewer) cd /tmp rm -rf logviewer_install mkdir logviewer_install cd logviewer_install echo "Downloading LogViewer and shared libraries..." wget -q "{LXToolsAppConfig.SHARED_LIBS_URL}" -O shared.zip unzip -q shared.zip SHARED_DIR=$(find . -name "shared_libs" -type d | head -1) # Check if TK-Themes exists, if not download Wire-Py for themes if [ ! -d "/usr/share/TK-Themes" ] || [ -z "$(ls -A /usr/share/TK-Themes 2>/dev/null)" ]; then echo "TK-Themes not found, downloading from Wire-Py..." wget -q "{LXToolsAppConfig.WIREPY_URL}" -O wirepy.zip unzip -q wirepy.zip WIREPY_DIR=$(find . -name "Wire-Py" -type d | head -1) if [ -d "$WIREPY_DIR/TK-Themes" ]; then echo "Installing TK-Themes..." cp -rf "$WIREPY_DIR/TK-Themes"/* /usr/share/TK-Themes/ fi # Also install icons from Wire-Py if not present if [ -d "$WIREPY_DIR/lx-icons" ] && [ ! -d "/usr/share/icons/lx-icons" ]; then echo "Installing icons from Wire-Py..." cp -rf "$WIREPY_DIR/lx-icons"/* /usr/share/icons/lx-icons/ fi fi # Install shared libraries echo "Installing shared libraries..." for file in common_tools.py message.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do if [ -f "$SHARED_DIR/$file" ]; then cp -f "$SHARED_DIR/$file" /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/local/share/shared_libs/ chmod 755 /usr/local/share/shared_libs/logviewer.py echo "Installed logviewer.py (executable)" fi # Create LogViewer desktop file cat > /usr/share/applications/LogViewer.desktop << 'EOF' [Desktop Entry] Version=1.0 Type=Application Name=LogViewer Comment=System Log Viewer Exec=/usr/local/bin/logviewer Icon=/usr/share/icons/lx-icons/48/log.png Terminal=false Categories=System;Utility; StartupNotify=true EOF echo "Created LogViewer desktop file" # Create symlink for LogViewer ln -sf /usr/local/share/shared_libs/logviewer.py /usr/local/bin/logviewer echo "Created LogViewer symlink" # Install language files if available if [ -d "$SHARED_DIR/languages/de" ]; then echo "Installing language files..." cp "$SHARED_DIR/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true fi # Cleanup cd /tmp rm -rf logviewer_install echo "LogViewer installation completed!" """ return script def _execute_install_script(self, script_content): """Execute installation script with pkexec""" try: with tempfile.NamedTemporaryFile( mode="w", suffix=".sh", delete=False ) as script_file: script_file.write(script_content) script_file.flush() # Make script executable os.chmod(script_file.name, 0o755) self.log(f"{LocaleStrings.MSGI["install_create"]}{script_file.name}") # Execute with pkexec result = subprocess.run( ["pkexec", "bash", script_file.name], capture_output=True, text=True, timeout=300, # 5 minutes timeout ) # Log output if result.stdout: self.log(f"STDOUT: {result.stdout}") if result.stderr: self.log(f"STDERR: {result.stderr}") # Clean up os.unlink(script_file.name) if result.returncode != 0: raise Exception( f"{LocaleStrings.MSGI["install_script_failed"]}{result.stderr}" ) except subprocess.TimeoutExpired: raise Exception(LocaleStrings.MSGI["install_timeout"]) except subprocess.CalledProcessError as e: raise Exception(f"{LocaleStrings.MSGI["install_script_failed"]}{e}") def update_progress(self, message): if self.progress_callback: self.progress_callback(message) def update_icon(self, status): if self.icon_callback: self.icon_callback(status) def log(self, message): if self.log_callback: self.log_callback(message) class UninstallationManager: def __init__(self, app_manager, progress_callback=None, log_callback=None): self.app_manager = app_manager self.progress_callback = progress_callback self.log_callback = log_callback def uninstall_project(self, project_key): """Uninstall project""" project_info = self.app_manager.get_project_info(project_key) if not project_info: raise Exception(f"{LocaleStrings.MSGO["unknow_project"]}{project_key}") if not self.app_manager.is_installed(project_key): raise Exception( f"{project_info["name"]}{LocaleStrings.MSGO["not_installed"]}" ) self.update_progress( f"{LocaleStrings.MSGU["uninstall"]}{project_info['name']}..." ) self.log(f"=== {LocaleStrings.MSGU["uninstall"]}{project_info['name']} ===") try: # Create uninstallation script script_content = self._create_uninstall_script(project_key) # Execute uninstallation self._execute_uninstall_script(script_content) self.update_progress( f"{project_info['name']}{LocaleStrings.MSGU["uninstall_success"]}" ) self.log( f"=== {project_info['name']}{LocaleStrings.MSGU["uninstall_success"]} ===" ) except Exception as e: self.log(f"ERROR: {LocaleStrings.MSGU['uninstall_failed']}{e}") raise Exception(f"{LocaleStrings.MSGU["uninstall_failed"]}{e}") def _create_uninstall_script(self, project_key): """Create uninstallation script based on project""" if project_key == "wirepy": return self._create_wirepy_uninstall_script() elif project_key == "logviewer": return self._create_logviewer_uninstall_script() else: raise Exception(f"{LocaleStrings.MSGO["unknow_project"]}{project_key}") def _create_wirepy_uninstall_script(self): """Create Wire-Py uninstallation script""" script = """#!/bin/bash set -e echo "=== Wire-Py Uninstallation ===" # Remove Wire-Py executables echo "Removing Wire-Py executables..." for file in wirepy.py start_wg.py ssl_encrypt.py ssl_decrypt.py match_found.py tunnel.py; do rm -f /usr/local/bin/$file echo "Removed $file" done # Remove symlink rm -f /usr/local/bin/wirepy echo "Removed wirepy symlink" # Remove config rm -f /usr/local/share/shared_libs/wp_app_config.py echo "Removed wp_app_config.py" # Remove desktop file rm -f /usr/share/applications/Wire-Py.desktop echo "Removed desktop file" # Remove policy file rm -f /usr/share/polkit-1/actions/org.sslcrypt.policy echo "Removed policy file" # Remove language files rm -f /usr/share/locale/de/LC_MESSAGES/wirepy.mo echo "Removed language files" # Remove user config directory if [ -d "$HOME/.config/wire_py" ]; then rm -rf "$HOME/.config/wire_py" echo "Removed user config directory" fi # Remove log file rm -f "$HOME/.local/share/lxlogs/wirepy.log" echo "Removed log file" # Check if LogViewer is still installed before removing shared resources if [ ! -f /usr/local/bin/logviewer ] || [ ! -L /usr/local/bin/logviewer ]; then echo "No other LX apps found, removing shared resources..." rm -rf /usr/share/icons/lx-icons rm -rf /usr/share/TK-Themes rm -rf /usr/local/etc/ssl # 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/local/share/shared_libs/$file done # Try to remove shared_libs directory if empty rmdir /usr/local/share/shared_libs 2>/dev/null || true else echo "LogViewer still installed, keeping shared resources" fi echo "Wire-Py uninstallation completed!" """ return script def _create_logviewer_uninstall_script(self): """Create LogViewer uninstallation script""" script = """#!/bin/bash set -e echo "=== LogViewer Uninstallation ===" # Remove LogViewer symlink rm -f /usr/local/bin/logviewer echo "Removed logviewer symlink" # Remove desktop file rm -f /usr/share/applications/LogViewer.desktop echo "Removed desktop file" # Remove language files rm -f /usr/share/locale/de/LC_MESSAGES/logviewer.mo echo "Removed language files" # Remove user config directory if [ -d "$HOME/.config/logviewer" ]; then rm -rf "$HOME/.config/logviewer" echo "Removed user config directory" fi # Remove log file rm -f "$HOME/.local/share/lxlogs/logviewer.log" echo "Removed log file" # Check if Wire-Py is still installed before removing shared resources if [ ! -f /usr/local/bin/wirepy ] || [ ! -L /usr/local/bin/wirepy ]; then echo "No other LX apps found, removing shared resources..." rm -rf /usr/share/icons/lx-icons rm -rf /usr/share/TK-Themes # 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/local/share/shared_libs/$file done # Remove logviewer.py last rm -f /usr/local/share/shared_libs/logviewer.py # Try to remove shared_libs directory if empty 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/local/share/shared_libs/logview_app_config.py rm -f /usr/local/share/shared_libs/logviewer.py fi echo "LogViewer uninstallation completed!" """ return script def _execute_uninstall_script(self, script_content): """Execute uninstallation script with pkexec""" try: with tempfile.NamedTemporaryFile( mode="w", suffix=".sh", delete=False ) as script_file: script_file.write(script_content) script_file.flush() # Make script executable os.chmod(script_file.name, 0o755) self.log(f"{LocaleStrings.MSGU["uninstall_create"]}{script_file.name}") # Execute with pkexec result = subprocess.run( ["pkexec", "bash", script_file.name], capture_output=True, text=True, timeout=120, ) # Log output if result.stdout: self.log(f"STDOUT: {result.stdout}") if result.stderr: self.log(f"STDERR: {result.stderr}") # Clean up os.unlink(script_file.name) if result.returncode != 0: raise Exception( f"{LocaleStrings.MSGU["uninstall_script_failed"]}{result.stderr}" ) except subprocess.TimeoutExpired: raise Exception(LocaleStrings.MSGU["uninstall_timeout"]) except subprocess.CalledProcessError as e: raise Exception(f"{LocaleStrings.MSGU["uninstall_script_failed"]}{e}") def update_progress(self, message): if self.progress_callback: self.progress_callback(message) def log(self, message): if self.log_callback: self.log_callback(message) class DownloadManager: @staticmethod def download_and_extract(url, extract_to, progress_callback=None): """Download and extract ZIP file""" try: if progress_callback: progress_callback(f"{LocaleStrings.MSGO["download_from"]}{url}...") with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file: urllib.request.urlretrieve(url, tmp_file.name) if progress_callback: progress_callback(LocaleStrings.MSGO["extract_files"]) with zipfile.ZipFile(tmp_file.name, "r") as zip_ref: zip_ref.extractall(extract_to) os.unlink(tmp_file.name) return True except Exception as e: if progress_callback: progress_callback(f"{LocaleStrings.MSGO["download_failed"]}{e}") return False class LXToolsGUI: def __init__(self): self.root = None self.notebook = None self.progress_label = None self.download_icon_label = None self.log_text = None self.selected_project = None self.project_frames = {} self.status_labels = {} self.version_labels = {} # Managers self.app_manager = AppManager() self.installation_manager = InstallationManager( self.app_manager, self.update_progress, self.update_download_icon, self.log_message, ) self.uninstallation_manager = UninstallationManager( self.app_manager, self.update_progress, self.log_message ) self.image_manager = Image() # Detect OS self.detected_os = OSDetector.detect_os() # Color scheme self.colors = { "bg": "#f8f9fa", "card_bg": "#ffffff", "hover_bg": "#e3f2fd", "selected_bg": "#bbdefb", "progress_bg": "#f8f9fa", "text": "#2c3e50", "accent": "#3498db", } def create_gui(self): """Create the main GUI""" self.root = tk.Tk() self.root.title(f"{LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}") self.root.geometry( f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}+100+100" ) LxTools.center_window_cross_platform( self.root, LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT ) self.root.configure(bg=self.colors["bg"]) # Try to set icon try: icon = self.image_manager.load_image("download_icon") if icon: self.root.iconphoto(False, icon) except: pass self.root.minsize(LXToolsAppConfig.WINDOW_WIDTH, LXToolsAppConfig.WINDOW_HEIGHT) Theme.apply_light_theme(self.root) # Create header self._create_header() # Create notebook (tabs) self.notebook = ttk.Notebook(self.root) self.notebook.pack(fill="both", expand=True, padx=15, pady=(10, 10)) # Create tabs self._create_projects_tab() self._create_log_tab() # Create progress section self._create_progress_section() # Create buttons self._create_modern_buttons() # Initial status refresh self.root.after(100, self.refresh_status) return self.root def _create_header(self): """Create clean header""" # HEADER header_frame = tk.Frame(self.root, bg="#2c3e50", height=70) header_frame.pack(fill="x", pady=(0, 0)) header_frame.pack_propagate(False) # Content content = tk.Frame(header_frame, bg="#2c3e50") content.pack(fill="both", expand=True, padx=15, pady=12) # LEFT SIDE: Icon + App Info left_side = tk.Frame(content, bg="#2c3e50") left_side.pack(side="left", anchor="w") icon_text_frame = tk.Frame(left_side, bg="#2c3e50") icon_text_frame.pack(anchor="w") # Tool-Icon tk.Label( icon_text_frame, text="🔧", font=("Helvetica", 18), bg="#2c3e50", fg="white" ).pack(side="left", padx=(0, 8)) # App Name and Version text_frame = tk.Frame(icon_text_frame, bg="#2c3e50") text_frame.pack(side="left") tk.Label( text_frame, text="Lx Tools Installer", font=("Helvetica", 14, "bold"), fg="white", bg="#2c3e50", pady=4, ).pack(anchor="w") tk.Label( text_frame, text=f"v {LXToolsAppConfig.VERSION} • {LocaleStrings.MSGO["head_string3"]}", font=("Helvetica", 9), fg="#bdc3c7", bg="#2c3e50", ).pack(anchor="w") # RIGHT SIDE: System + Dynamic Status right_side = tk.Frame(content, bg="#2c3e50") right_side.pack(side="right", anchor="e") tk.Label( right_side, text=f"{LocaleStrings.MSGO["head_string2"]}{self.detected_os}", font=("Helvetica", 11), fg="#ecf0f1", bg="#2c3e50", ).pack(anchor="e") # DYNAMIC Status (begin empty) self.header_status_label = tk.Label( right_side, text="", font=("Helvetica", 10), bg="#2c3e50" # begin empty ) self.header_status_label.pack(anchor="e", pady=(2, 0)) # Separator separator = tk.Frame(self.root, height=1, bg="#34495e") separator.pack(fill="x", pady=0) def update_header_status(self, message="", color="#1abc9c"): """Update status in header""" if hasattr(self, "header_status_label"): self.header_status_label.config(text=message, fg=color) def check_ready_status(self): """Check if system is ready for installation""" # Checks: internet_ok = NetworkChecker.check_internet_connection() repo_ok = NetworkChecker.check_repository_access() if internet_ok and repo_ok: self.update_header_status(LocaleStrings.MSGO["ready"], "#1abc9c") # Green elif not internet_ok: self.update_header_status( LocaleStrings.MSGO["no_internet"], "#e74c3c" ) # Red elif not repo_ok: self.update_header_status( LocaleStrings.MSGO["repo_unavailable"], "#f39c12" ) # Orange else: self.update_header_status( LocaleStrings.MSGO["system_check"], "#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=LocaleStrings.MSGO["applications"]) # Scrollable frame canvas = tk.Canvas(projects_frame, bg=self.colors["bg"]) scrollbar = ttk.Scrollbar( projects_frame, orient="vertical", command=canvas.yview ) scrollable_frame = tk.Frame(canvas, bg=self.colors["bg"]) scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # Create project cards for project_key, project_info in self.app_manager.get_all_projects().items(): self._create_project_card(scrollable_frame, project_key, project_info) def _create_project_card(self, parent, project_key, project_info): """Create a project card""" # Main card frame card_frame = tk.Frame(parent, bg=self.colors["bg"]) card_frame.pack(fill="x", padx=10, pady=5) # Content frame (the actual card) content_frame = tk.Frame( card_frame, bg=self.colors["card_bg"], relief="solid", bd=1 ) content_frame.pack(fill="x", padx=5, pady=2) # Store frame reference self.project_frames[project_key] = card_frame # Make entire card clickable self._make_clickable(content_frame, content_frame, project_key) # Header with icon and title header_frame = tk.Frame(content_frame, bg=self.colors["card_bg"]) header_frame.pack(fill="x", padx=15, pady=(15, 5)) # Icon icon_label = tk.Label(header_frame, bg=self.colors["card_bg"]) icon_label.pack(side="left", padx=(0, 10)) # Try to load project icon icon = self.image_manager.load_image( project_info.get("icon_key", "default_icon") ) if icon: icon_label.config(image=icon) icon_label.image = icon else: # Use emoji based on project if project_key == "wirepy": icon_label.config(text="🔒", font=("Helvetica", 24)) elif project_key == "logviewer": icon_label.config(text="📋", font=("Helvetica", 24)) else: icon_label.config(text="📦", font=("Helvetica", 24)) # Title and description title_frame = tk.Frame(header_frame, bg=self.colors["card_bg"]) title_frame.pack(side="left", fill="x", expand=True) title_label = tk.Label( title_frame, text=project_info["name"], font=("Helvetica", 14, "bold"), bg=self.colors["card_bg"], fg=self.colors["text"], anchor="w", ) title_label.pack(fill="x") desc_label = tk.Label( title_frame, text=project_info["description"], font=("Helvetica", 10), bg=self.colors["card_bg"], fg="#7f8c8d", anchor="w", wraplength=300, ) desc_label.pack(fill="x") # Status section status_frame = tk.Frame(content_frame, bg=self.colors["card_bg"]) status_frame.pack(fill="x", padx=15, pady=(5, 15)) # Status label status_label = tk.Label( status_frame, text=f"❓ {LocaleStrings.MSGC["checking"]}", font=("Helvetica", 10), bg=self.colors["card_bg"], fg="#95a5a6", anchor="w", ) status_label.pack(fill="x") # Version label version_label = tk.Label( status_frame, text=LocaleStrings.MSGC["version_check"], font=("Helvetica", 9), bg=self.colors["card_bg"], fg="#95a5a6", anchor="w", ) version_label.pack(fill="x") # Store label references self.status_labels[project_key] = status_label self.version_labels[project_key] = version_label # Make all elements clickable for widget in [ header_frame, title_frame, title_label, desc_label, status_frame, status_label, version_label, ]: self._make_clickable(widget, content_frame, project_key) # Make icon clickable too self._make_clickable(icon_label, content_frame, project_key) def _make_clickable(self, widget, main_frame, project_key): """Make widget clickable with hover effects""" def on_click(event): self.select_project(project_key) def on_enter(event): if self.selected_project == project_key: main_frame.config(bg=self.colors["selected_bg"]) self._update_frame_children_bg(main_frame, self.colors["selected_bg"]) else: main_frame.config(bg=self.colors["hover_bg"]) self._update_frame_children_bg(main_frame, self.colors["hover_bg"]) def on_leave(event): if self.selected_project == project_key: main_frame.config(bg=self.colors["selected_bg"]) self._update_frame_children_bg(main_frame, self.colors["selected_bg"]) else: main_frame.config(bg=self.colors["card_bg"]) self._update_frame_children_bg(main_frame, self.colors["card_bg"]) widget.bind("", on_click) widget.bind("", on_enter) widget.bind("", on_leave) def _update_frame_children_bg(self, frame, bg_color): """Recursively update background color of all children""" try: for child in frame.winfo_children(): if isinstance(child, (tk.Frame, tk.Label)): child.config(bg=bg_color) if isinstance(child, tk.Frame): self._update_frame_children_bg(child, bg_color) except tk.TclError: # Ignore color errors pass def select_project(self, project_key): """Select a project""" # Reset previous selection if self.selected_project and self.selected_project in self.project_frames: old_frame = self.project_frames[self.selected_project] old_content = old_frame.winfo_children()[0] # content_frame old_content.config(bg=self.colors["card_bg"]) self._update_frame_children_bg(old_content, self.colors["card_bg"]) # Set new selection self.selected_project = project_key if project_key in self.project_frames: new_frame = self.project_frames[project_key] new_content = new_frame.winfo_children()[0] # content_frame new_content.config(bg=self.colors["selected_bg"]) self._update_frame_children_bg(new_content, self.colors["selected_bg"]) project_info = self.app_manager.get_project_info(project_key) self.log_message(f"{LocaleStrings.MSGL["selected_app"]}{project_info["name"]}") def _create_log_tab(self): """Create log tab""" log_frame = ttk.Frame(self.notebook) self.notebook.add(log_frame, text=LocaleStrings.MSGL["log_name"]) # Log text with scrollbar log_container = tk.Frame(log_frame) log_container.pack(fill="both", expand=True, padx=10, pady=10) # Important! pack_propagate(False) must be set here to display # the Clear Log button correctly log_container.pack_propagate(False) self.log_text = tk.Text( log_container, wrap=tk.WORD, font=("Consolas", 9), bg="#1e1e1e", fg="#d4d4d4", insertbackground="white", selectbackground="#264f78", ) log_scrollbar = ttk.Scrollbar( log_container, orient="vertical", command=self.log_text.yview ) self.log_text.configure(yscrollcommand=log_scrollbar.set) self.log_text.pack(side="left", fill="both", expand=True) log_scrollbar.pack(side="right", fill="y") # Log controls log_controls = tk.Frame(log_frame) log_controls.pack(fill="x", padx=10, pady=(5, 0)) # Clear log button clear_log_btn = ttk.Button( log_controls, text=LocaleStrings.MSGB["clear_log"], command=self.clear_log ) clear_log_btn.pack(side="right", pady=(0, 10)) # Initial log message self.log_message( f"=== {LXToolsAppConfig.APP_NAME} v {LXToolsAppConfig.VERSION} ===" ) self.log_message(f"{LocaleStrings.MSGL["work_dir"]}{LXToolsAppConfig.WORK_DIR}") self.log_message( f"{LocaleStrings.MSGL["icons_dir"]}{LXToolsAppConfig.ICONS_DIR}" ) self.log_message(f"{LocaleStrings.MSGL["detected_os"]}{self.detected_os}") self.log_message(f"{LocaleStrings.MSGO["ready"]}...") def _create_progress_section(self): """Create progress section with download icon""" progress_frame = ttk.LabelFrame( self.root, text=LocaleStrings.MSGO["progress"], padding=10 ) progress_frame.pack(fill="x", padx=15, pady=10) # Container for Icon and Progress progress_container = tk.Frame(progress_frame) progress_container.pack(fill="x") # Download Icon (left) self.download_icon_label = tk.Label(progress_container, text="", width=50) self.download_icon_label.pack(side="left", padx=(0, 10)) # Progress Text (right, expandable) self.progress_label = tk.Label( progress_container, text=f"{LocaleStrings.MSGO["ready"]}...", font=("Helvetica", 10), fg="blue", anchor="w", ) self.progress_label.pack(side="left", fill="x", expand=True) # Initial icon load (neutral) self._reset_download_icon() def _create_modern_buttons(self): """Create modern styled buttons""" button_frame = tk.Frame(self.root, bg=self.colors["bg"]) button_frame.pack(fill="x", padx=15, pady=(5, 10)) # Button style configuration style = ttk.Style() # Install button (green) style.configure("Install.TButton", foreground="#27ae60", font=("Helvetica", 11)) style.map( "Install.TButton", foreground=[("active", "#14542f"), ("pressed", "#1e8449")], ) # Uninstall button (red) style.configure( "Uninstall.TButton", foreground="#e74c3c", font=("Helvetica", 11) ) style.map( "Uninstall.TButton", foreground=[("active", "#7d3b34"), ("pressed", "#c0392b")], ) # Refresh button (blue) style.configure("Refresh.TButton", foreground="#3498db", font=("Helvetica", 11)) style.map( "Refresh.TButton", foreground=[("active", "#1e3747"), ("pressed", "#2980b9")], ) # Create buttons install_btn = ttk.Button( button_frame, text=LocaleStrings.MSGB["install"], command=self.install_selected, style="Install.TButton", padding=8, ) install_btn.pack(side="left", padx=(0, 10)) uninstall_btn = ttk.Button( button_frame, text=LocaleStrings.MSGB["uninstall"], command=self.uninstall_selected, style="Uninstall.TButton", padding=8, ) uninstall_btn.pack(side="left", padx=(0, 10)) refresh_btn = ttk.Button( button_frame, text=LocaleStrings.MSGB["refresh"], command=self.refresh_status, style="Refresh.TButton", padding=8, ) refresh_btn.pack(side="right") def update_download_icon(self, status): """Update download icon based on status""" if not self.download_icon_label: return if status == "downloading": icon = self.image_manager.load_image("download_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="⬇️", font=("Helvetica", 16)) elif status == "error": icon = self.image_manager.load_image("download_error_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="❌", font=("Helvetica", 16)) elif status == "success": icon = self.image_manager.load_image("success_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="✅", font=("Helvetica", 16)) self.download_icon_label.update() def _reset_download_icon(self): """Reset download icon to neutral state""" icon = self.image_manager.load_image("download_icon") if icon: self.download_icon_label.config(image=icon, text="") self.download_icon_label.image = icon else: self.download_icon_label.config(text="📥", font=("Helvetica", 16)) def refresh_status(self): """Refresh application status and version information""" self.update_progress(LocaleStrings.MSGI["refresh_and_check"]) self._reset_download_icon() self.log_message(f"=== {LocaleStrings.MSGB["refresh"]} ===") self.root.focus_set() for project_key, project_info in self.app_manager.get_all_projects().items(): status_label = self.status_labels[project_key] version_label = self.version_labels[project_key] self.log_message(f"{LocaleStrings.MSGC["checking"]} {project_info['name']}") if self.app_manager.is_installed(project_key): installed_version = self.app_manager.get_installed_version(project_key) status_label.config( text=f"✅ {LocaleStrings.MSGI["installed"]}({installed_version})", fg="green", ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGI["installed"]}({installed_version})" ) # Get latest version from API try: latest_version = self.app_manager.get_latest_version(project_key) if latest_version != "Unknown": if installed_version != f"v. {latest_version}": version_label.config( text=f"{LocaleStrings.MSGC["latest"]}(v. {latest_version}) {LocaleStrings.MSGC["update_available"]}", fg="orange", ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGC["update_available"]}(v. {latest_version})" ) else: version_label.config( text=f"{LocaleStrings.MSGC["latest"]}: (v. {latest_version}) {LocaleStrings.MSGC["up_to_date"]}", fg="green", ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGC["up_to_date"]}", ) else: version_label.config( text=LocaleStrings.MSGC["latest_unknown"], fg="gray" ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGC["could_not_check"]}" ) except Exception as e: version_label.config( text=LocaleStrings.MSGC["check_last_failed"], fg="gray" ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGC["version_check_failed"]}: {e}" ) else: status_label.config( text=f"❌ {LocaleStrings.MSGC["not_installed"]}", fg="red" ) self.log_message( f"{project_info['name']}: {LocaleStrings.MSGC["not_installed"]}" ) # Still show latest available version try: latest_version = self.app_manager.get_latest_version(project_key) if latest_version != "Unknown": version_label.config( text=f"{project_info['name']} {LocaleStrings.MSGC["available"]}: v {latest_version}", fg="blue", ) self.log_message( f"{project_info['name']} {LocaleStrings.MSGC["available"]}: v {latest_version}" ) else: version_label.config( text=LocaleStrings.MSGC["available_unknown"], fg="gray" ) except Exception as e: version_label.config( text=LocaleStrings.MSGC["available_check_unknown"], fg="gray" ) self.log_message( f" {project_info['name']}: {LocaleStrings.MSGC["version_check_failed"]}: {e}" ) self.update_progress(LocaleStrings.MSGO["refresh2"]) self.log_message(f"=== {LocaleStrings.MSGO["refresh2"]} ===") self.check_ready_status() def install_selected(self): """Handle install button click""" if not self.selected_project: MessageDialog("error", LocaleStrings.MSGM["please_select"]) self.root.focus_set() return # Check internet connection if not NetworkChecker.check_internet_connection(): self.update_download_icon("error") MessageDialog("error", LocaleStrings.MSGM["network_error"]) self.root.focus_set() return if not NetworkChecker.check_repository_access(): self.update_download_icon("error") MessageDialog("error", LocaleStrings.MSGM["repo_error"]) self.root.focus_set() return # Reset download icon self._reset_download_icon() project_info = self.app_manager.get_project_info(self.selected_project) try: self.update_download_icon("downloading") self.installation_manager.install_project(self.selected_project) self.update_download_icon("success") MessageDialog( "info", f"{project_info["name"]} {LocaleStrings.MSGM["has_success_update"]}", ) self.refresh_status() except Exception as e: self.update_download_icon("error") MessageDialog("error", f"{e}") self.root.focus_set() def uninstall_selected(self): """Handle uninstall button click""" if not self.selected_project: MessageDialog("error", LocaleStrings.MSGM["please_select_uninstall"]) self.root.focus_set() return project_info = self.app_manager.get_project_info(self.selected_project) if not self.app_manager.is_installed(self.selected_project): MessageDialog( "error", f"{project_info["name"]} {LocaleStrings.MSGO["not_installed"]}" ) self.root.focus_set() return try: self.uninstallation_manager.uninstall_project(self.selected_project) MessageDialog( "info", f"{project_info["name"]} {LocaleStrings.MSGU["uninstall_success"]}", ) self.refresh_status() self.root.focus_set() except Exception as e: MessageDialog("error", f"{LocaleStrings.MSGU["uninstall_failed"]}: {e}") self.root.focus_set() def update_progress(self, message): """Update progress message""" if self.progress_label: self.progress_label.config(text=message) self.progress_label.update() print(f"{LocaleStrings.MSGO["progress"]}: {message}") def log_message(self, message): """Add message to log""" if self.log_text: timestamp = datetime.now().strftime("%H:%M:%S") log_entry = f"[{timestamp}] {message}\n" self.log_text.insert(tk.END, log_entry) self.log_text.see(tk.END) self.log_text.update() print(f"Log: {message}") def clear_log(self): """Clear the log""" if self.log_text: self.log_text.delete(1.0, tk.END) self.log_message(LocaleStrings.MSGL["log_cleared"]) def run(self): """Start the GUI application""" root = self.create_gui() root.mainloop() def main(): """Main function to start the application""" print(f"=== {LXToolsAppConfig.APP_NAME} v {LXToolsAppConfig.VERSION} ===") print(f"{LocaleStrings.MSGL["working_dir"]}{os.getcwd()}") try: # Create and run the GUI app = LXToolsGUI() app.run() except KeyboardInterrupt: print(LocaleStrings.MSGL["user_interrupt"]) except Exception as e: print(f"{LocaleStrings.MSGL["fatal_error"]}: {e}") try: MessageDialog("error", f"{LocaleStrings.MSGL['fatal_app_error']}: {e}") except: pass if __name__ == "__main__": main()