Files
lxtools_installer/lxtools_installer.py
2025-06-18 18:36:34 +02:00

1353 lines
46 KiB
Python
Executable File

#!/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(
"<Configure>", 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("<Button-1>", on_click)
widget.bind("<Enter>", on_enter)
widget.bind("<Leave>", 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()