Files
lxtools_installer/lxtools_installer.py

1265 lines
44 KiB
Python

#!/usr/bin/python3
import gettext
import locale
import tkinter as tk
from tkinter import messagebox, ttk
import shutil
import os
import socket
import subprocess
import tempfile
import urllib.request
import zipfile
import json
from pathlib import Path
# ----------------------------
# LXTools App Configuration
# ----------------------------
class LXToolsAppConfig:
VERSION = "1.0.4"
APP_NAME = "LXTools Installer"
WINDOW_WIDTH = 500
WINDOW_HEIGHT = 600
# Locale settings
LOCALE_DIR = Path("/usr/share/locale/")
# Images and icons paths
IMAGE_PATHS = {
"icon_vpn": "./lx-icons/32/wg_vpn.png",
"icon_vpn2": "./lx-icons/48/wg_vpn.png",
"icon_msg": "./lx-icons/48/wg_msg.png",
"icon_info": "./lx-icons/64/info.png",
"icon_error": "./lx-icons/64/error.png",
"icon_log": "./lx-icons/32/log.png",
"icon_log2": "./lx-icons/48/log.png",
"icon_download": "./lx-icons/32/download.png",
"icon_download_error": "./lx-icons/32/download_error.png",
}
# System-dependent paths
SYSTEM_PATHS = {
"tcl_path": "/usr/share/TK-Themes",
}
# Download URLs
WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip"
SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip"
# API URLs for version checking
WIREPY_API_URL = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases"
SHARED_LIBS_API_URL = (
"https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases"
)
# OS Detection List (order matters - specific first, generic last)
OS_DETECTION = [
("mint", "Linux Mint"),
("pop", "Pop!_OS"),
("manjaro", "Manjaro"),
("garuda", "Garuda Linux"),
("endeavouros", "EndeavourOS"),
("fedora", "Fedora"),
("tumbleweed", "SUSE Tumbleweed"),
("leap", "SUSE Leap"),
("suse", "openSUSE"),
("arch", "Arch Linux"),
("ubuntu", "Ubuntu"),
("debian", "Debian"),
]
@staticmethod
def setup_translations():
"""Initialize translations and set the translation function"""
locale.bindtextdomain(LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR)
gettext.bindtextdomain(LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR)
gettext.textdomain(LXToolsAppConfig.APP_NAME)
return gettext.gettext
# Initialize translations
_ = LXToolsAppConfig.setup_translations()
# ----------------------------
# Image Manager Class
# ----------------------------
class ImageManager:
def __init__(self):
self.images = {}
def load_image(self, image_key, fallback_paths=None):
"""Load PNG image using tk.PhotoImage with fallback options"""
if image_key in self.images:
return self.images[image_key]
# Primary path from config
primary_path = LXToolsAppConfig.IMAGE_PATHS.get(image_key)
paths_to_try = []
if primary_path:
paths_to_try.append(primary_path)
# Add fallback paths
if fallback_paths:
paths_to_try.extend(fallback_paths)
# Try to load image from paths
for path in paths_to_try:
try:
if os.path.exists(path):
photo = tk.PhotoImage(file=path)
self.images[image_key] = photo
return photo
except tk.TclError as e:
print(f"Failed to load image from {path}: {e}")
continue
# Return None if no image found (we'll handle this in GUI)
return None
# ----------------------------
# Gitea API Handler
# ----------------------------
class GiteaUpdate:
@staticmethod
def api_down(url, current_version=""):
"""Get latest version from Gitea API"""
try:
with urllib.request.urlopen(url) as response:
data = json.loads(response.read().decode())
if data and len(data) > 0:
latest_version = data[0].get("tag_name", "Unknown")
return latest_version.lstrip("v") # Remove 'v' prefix if present
return "Unknown"
except Exception as e:
print(f"API Error: {e}")
return "Unknown"
# ----------------------------
# OS Detection Class
# ----------------------------
class OSDetector:
@staticmethod
def detect_os():
"""Detect operating system using ordered list"""
try:
with open("/etc/os-release", "r") as f:
content = f.read().lower()
# Check each OS in order (specific first)
for keyword, os_name in LXToolsAppConfig.OS_DETECTION:
if keyword in content:
return os_name
return "Unknown System"
except FileNotFoundError:
return "File not found"
# ----------------------------
# Network Checker Class
# ----------------------------
class NetworkChecker:
@staticmethod
def check_internet_connection(host="8.8.8.8", port=53, timeout=3):
"""Check if internet connection is available"""
try:
socket.setdefaulttimeout(timeout)
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
return True
except socket.error:
return False
@staticmethod
def check_repository_access(url="https://git.ilunix.de"):
"""Check if repository is accessible"""
try:
urllib.request.urlopen(url, timeout=5)
return True
except:
return False
# ----------------------------
# Application Configuration Class
# ----------------------------
class AppConfig:
def __init__(
self,
key,
name,
files,
config,
desktop,
icon,
symlink,
config_dir,
log_file,
languages,
api_url,
icon_key,
policy_file=None,
):
self.key = key
self.name = name
self.files = files
self.config = config
self.desktop = desktop
self.icon = icon
self.symlink = symlink
self.config_dir = config_dir
self.log_file = log_file
self.languages = languages
self.api_url = api_url
self.icon_key = icon_key # Key for ImageManager
self.policy_file = policy_file
def is_installed(self):
"""Check if application is installed"""
return os.path.exists(f"/usr/local/bin/{self.symlink}")
def get_installed_version(self):
"""Get installed version from config file"""
try:
config_file = f"/usr/lib/python3/dist-packages/shared_libs/{self.config}"
if os.path.exists(config_file):
with open(config_file, "r") as f:
content = f.read()
for line in content.split("\n"):
if "VERSION" in line and "=" in line:
return line.split("=")[1].strip().strip("\"'")
return "Unknown"
except:
return "Unknown"
def get_latest_version(self):
"""Get latest version from API"""
return GiteaUpdate.api_down(self.api_url)
# ----------------------------
# Application Manager Class
# ----------------------------
class AppManager:
def __init__(self):
self.apps = {
"wirepy": AppConfig(
key="wirepy",
name="Wire-Py",
files=[
"wirepy.py",
"start_wg.py",
"ssl_encrypt.py",
"ssl_decrypt.py",
"match_found.py",
"tunnel.py",
],
config="wp_app_config.py",
desktop="Wire-Py.desktop",
icon="wg_vpn.png",
symlink="wirepy",
config_dir="~/.config/wire_py",
log_file="~/.local/share/lxlogs/wirepy.log",
languages=["wirepy.mo"],
api_url=LXToolsAppConfig.WIREPY_API_URL,
icon_key="icon_vpn",
policy_file="org.sslcrypt.policy",
),
"logviewer": AppConfig(
key="logviewer",
name="LogViewer",
files=["logviewer.py"],
config="logview_app_config.py",
desktop="LogViewer.desktop",
icon="log.png",
symlink="logviewer",
config_dir="~/.config/logviewer",
log_file="~/.local/share/lxlogs/logviewer.log",
languages=["logviewer.mo"],
api_url=LXToolsAppConfig.SHARED_LIBS_API_URL,
icon_key="icon_log",
policy_file=None,
),
}
self.shared_files = [
"common_tools.py",
"file_and_dir_ensure.py",
"gitea.py",
"__init__.py",
"logview_app_config.py",
"logviewer.py",
]
def get_app(self, key):
"""Get application configuration by key"""
return self.apps.get(key)
def get_all_apps(self):
"""Get all application configurations"""
return self.apps
def check_other_apps_installed(self, exclude_key):
"""Check if other apps are still installed"""
return any(
app.is_installed() for key, app in self.apps.items() if key != exclude_key
)
# ----------------------------
# Download Manager Class
# ----------------------------
class DownloadManager:
@staticmethod
def download_and_extract(
url, extract_to, progress_callback=None, icon_callback=None
):
"""Download and extract ZIP file with icon status"""
try:
if progress_callback:
progress_callback(f"Downloading from {url}...")
# Set download icon
if icon_callback:
icon_callback("downloading")
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file:
urllib.request.urlretrieve(url, tmp_file.name)
if progress_callback:
progress_callback("Extracting files...")
with zipfile.ZipFile(tmp_file.name, "r") as zip_ref:
zip_ref.extractall(extract_to)
os.unlink(tmp_file.name)
# Set success icon
if icon_callback:
icon_callback("success")
return True
except Exception as e:
if progress_callback:
progress_callback(f"Download failed: {str(e)}")
# Set error icon
if icon_callback:
icon_callback("error")
return False
# ----------------------------
# System Manager Class
# ----------------------------
class SystemManager:
@staticmethod
def create_directories(directories):
"""Create system directories using pkexec"""
for directory in directories:
subprocess.run(["pkexec", "mkdir", "-p", directory], check=True)
@staticmethod
def copy_file(src, dest, make_executable=False):
"""Copy file using pkexec"""
subprocess.run(["pkexec", "cp", src, dest], check=True)
if make_executable:
subprocess.run(["pkexec", "chmod", "755", dest], check=True)
@staticmethod
def copy_directory(src, dest):
"""Copy directory using pkexec"""
subprocess.run(["pkexec", "cp", "-r", src, dest], check=True)
@staticmethod
def remove_file(path):
"""Remove file using pkexec"""
subprocess.run(["pkexec", "rm", "-f", path], check=False)
@staticmethod
def remove_directory(path):
"""Remove directory using pkexec"""
subprocess.run(["pkexec", "rm", "-rf", path], check=False)
@staticmethod
def create_symlink(target, link_name):
"""Create symbolic link using pkexec"""
subprocess.run(["pkexec", "rm", "-f", link_name], check=False)
subprocess.run(["pkexec", "ln", "-sf", target, link_name], check=True)
@staticmethod
def create_ssl_key(pem_file):
"""Create SSL key using pkexec"""
try:
subprocess.run(
["pkexec", "openssl", "genrsa", "-out", pem_file, "4096"], check=True
)
subprocess.run(["pkexec", "chmod", "600", pem_file], check=True)
return True
except subprocess.CalledProcessError:
return False
# ----------------------------
# Installation Manager Class
# ----------------------------
class InstallationManager:
def __init__(self, app_manager, progress_callback=None, icon_callback=None):
self.app_manager = app_manager
self.progress_callback = progress_callback
self.icon_callback = icon_callback
self.system_manager = SystemManager()
self.download_manager = DownloadManager()
def update_progress(self, message):
"""Update progress message"""
if self.progress_callback:
self.progress_callback(message)
def install_app(self, app_key):
"""Install or update application"""
app = self.app_manager.get_app(app_key)
if not app:
raise Exception(f"Unknown application: {app_key}")
self.update_progress(f"Starting installation of {app.name}...")
try:
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
# Download application source
if app_key == "wirepy":
if not self.download_manager.download_and_extract(
LXToolsAppConfig.WIREPY_URL,
temp_dir,
self.update_progress,
self.icon_callback,
):
raise Exception("Failed to download Wire-Py")
source_dir = os.path.join(temp_dir, "Wire-Py")
else:
if not self.download_manager.download_and_extract(
LXToolsAppConfig.SHARED_LIBS_URL,
temp_dir,
self.update_progress,
self.icon_callback,
):
raise Exception("Failed to download LogViewer")
source_dir = os.path.join(temp_dir, "shared_libs")
# Download shared libraries
shared_temp = os.path.join(temp_dir, "shared")
if not self.download_manager.download_and_extract(
LXToolsAppConfig.SHARED_LIBS_URL,
shared_temp,
self.update_progress,
self.icon_callback,
):
raise Exception("Failed to download shared libraries")
shared_source = os.path.join(shared_temp, "shared_libs")
# Create necessary directories
self.update_progress("Creating directories...")
directories = [
"/usr/lib/python3/dist-packages/shared_libs",
"/usr/share/icons/lx-icons/48",
"/usr/share/icons/lx-icons/64",
"/usr/share/locale/de/LC_MESSAGES",
"/usr/share/applications",
"/usr/local/etc/ssl",
"/usr/share/polkit-1/actions",
]
self.system_manager.create_directories(directories)
# Install shared libraries
self.update_progress("Installing shared libraries...")
self._install_shared_libraries(shared_source)
# Install application files
self.update_progress(f"Installing {app.name} files...")
self._install_app_files(app, source_dir)
# Install additional resources
self._install_app_resources(app, source_dir)
# Install policy file if exists
if app.policy_file:
self._install_policy_file(app, source_dir)
# Create symlink
self.update_progress("Creating symlink...")
main_file = app.files[0] # First file is usually the main file
self.system_manager.create_symlink(
f"/usr/local/bin/{main_file}", f"/usr/local/bin/{app.symlink}"
)
# Special handling for Wire-Py SSL key
if app_key == "wirepy":
self._create_ssl_key()
self.update_progress(f"{app.name} installation completed successfully!")
return True
except subprocess.CalledProcessError as e:
self.update_progress("Error: pkexec command failed")
raise Exception(
f"Installation failed (pkexec): {e}\n\nPermission might have been denied."
)
except Exception as e:
self.update_progress(f"Error: {str(e)}")
raise
def _install_policy_file(self, app, source_dir):
"""Install polkit policy file"""
if app.policy_file:
self.update_progress(f"Installing policy file {app.policy_file}...")
policy_src = os.path.join(source_dir, app.policy_file)
if os.path.exists(policy_src):
policy_dest = f"/usr/share/polkit-1/actions/{app.policy_file}"
self.system_manager.copy_file(policy_src, policy_dest)
self.update_progress(
f"Policy file {app.policy_file} installed successfully."
)
else:
self.update_progress(
f"Warning: Policy file {app.policy_file} not found in source."
)
def _install_shared_libraries(self, shared_source):
"""Install shared library files"""
for shared_file in self.app_manager.shared_files:
src = os.path.join(shared_source, shared_file)
if os.path.exists(src):
dest = f"/usr/lib/python3/dist-packages/shared_libs/{shared_file}"
self.system_manager.copy_file(src, dest)
def _install_app_files(self, app, source_dir):
"""Install application executable files"""
for app_file in app.files:
src = os.path.join(source_dir, app_file)
if os.path.exists(src):
dest = f"/usr/local/bin/{app_file}"
self.system_manager.copy_file(src, dest, make_executable=True)
# Install app config
config_src = os.path.join(source_dir, app.config)
if os.path.exists(config_src):
config_dest = f"/usr/lib/python3/dist-packages/shared_libs/{app.config}"
self.system_manager.copy_file(config_src, config_dest)
def _install_app_resources(self, app, source_dir):
"""Install icons, desktop files, and language files"""
# Install icons
self.update_progress("Installing icons...")
icons_src = os.path.join(source_dir, "lx-icons")
if os.path.exists(icons_src):
# Copy all icon subdirectories
for item in os.listdir(icons_src):
item_path = os.path.join(icons_src, item)
if os.path.isdir(item_path):
dest_path = f"/usr/share/icons/lx-icons/{item}"
self.system_manager.copy_directory(item_path, dest_path)
# Install desktop file
desktop_src = os.path.join(source_dir, app.desktop)
if os.path.exists(desktop_src):
self.system_manager.copy_file(
desktop_src, f"/usr/share/applications/{app.desktop}"
)
# Install language files
self.update_progress("Installing language files...")
lang_dir = os.path.join(source_dir, "languages", "de")
if os.path.exists(lang_dir):
for lang_file in app.languages:
lang_src = os.path.join(lang_dir, lang_file)
if os.path.exists(lang_src):
lang_dest = f"/usr/share/locale/de/LC_MESSAGES/{lang_file}"
self.system_manager.copy_file(lang_src, lang_dest)
def _create_ssl_key(self):
"""Create SSL key for Wire-Py"""
pem_file = "/usr/local/etc/ssl/pwgk.pem"
if not os.path.exists(pem_file):
self.update_progress("Creating SSL key...")
if not self.system_manager.create_ssl_key(pem_file):
self.update_progress(
"Warning: SSL key creation failed. OpenSSL might be missing."
)
def uninstall_app(self, app_key):
"""Uninstall application"""
app = self.app_manager.get_app(app_key)
if not app:
raise Exception(f"Unknown application: {app_key}")
if not app.is_installed():
raise Exception(f"{app.name} is not installed.")
try:
self.update_progress(f"Uninstalling {app.name}...")
# Remove policy file if exists
if app.policy_file:
self.system_manager.remove_file(
f"/usr/share/polkit-1/actions/{app.policy_file}"
)
# Remove application files
for app_file in app.files:
self.system_manager.remove_file(f"/usr/local/bin/{app_file}")
# Remove symlink
self.system_manager.remove_file(f"/usr/local/bin/{app.symlink}")
# Remove app config
self.system_manager.remove_file(
f"/usr/lib/python3/dist-packages/shared_libs/{app.config}"
)
# Remove desktop file
self.system_manager.remove_file(f"/usr/share/applications/{app.desktop}")
# Remove language files
for lang_file in app.languages:
self.system_manager.remove_file(
f"/usr/share/locale/de/LC_MESSAGES/{lang_file}"
)
# Remove user config directory
config_dir = os.path.expanduser(app.config_dir)
if os.path.exists(config_dir):
shutil.rmtree(config_dir)
# Remove log file
log_file = os.path.expanduser(app.log_file)
if os.path.exists(log_file):
os.remove(log_file)
# Check if other apps are still installed before removing shared resources
if not self.app_manager.check_other_apps_installed(app_key):
self.update_progress("Removing shared resources...")
self._remove_shared_resources()
self.update_progress(f"{app.name} uninstalled successfully!")
return True
except Exception as e:
self.update_progress(f"Error during uninstallation: {str(e)}")
raise
def _remove_shared_resources(self):
"""Remove shared resources when no apps are installed"""
# Remove shared libraries
for shared_file in self.app_manager.shared_files:
self.system_manager.remove_file(
f"/usr/lib/python3/dist-packages/shared_libs/{shared_file}"
)
# Remove icons and SSL directory
self.system_manager.remove_directory("/usr/share/icons/lx-icons")
self.system_manager.remove_directory("/usr/local/etc/ssl")
# Remove shared_libs directory if empty
subprocess.run(
["pkexec", "rmdir", "/usr/lib/python3/dist-packages/shared_libs"],
check=False,
)
# ----------------------------
# GUI Application Class (Erweiterte Version)
# ----------------------------
class LXToolsGUI:
def __init__(self):
self.root = None
self.progress_label = None
self.download_icon_label = None
self.app_var = None
self.status_labels = {}
self.version_labels = {}
# Initialize managers
self.app_manager = AppManager()
self.installation_manager = InstallationManager(
self.app_manager, self.update_progress, self.update_download_icon
)
self.image_manager = ImageManager()
# Detect OS
self.detected_os = OSDetector.detect_os()
def create_gui(self):
"""Create the main GUI"""
self.root = tk.Tk()
self.root.title(LXToolsAppConfig.APP_NAME)
self.root.geometry(
f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}"
)
self.root.resizable(True, True)
# Apply theme
try:
self.root.tk.call("source", "TK-Themes/water.tcl")
self.root.tk.call("set_theme", "light")
except tk.TclError as e:
print(f"Theme loading failed: {e}")
# Create GUI components
self._create_header()
self._create_system_info()
self._create_app_selection() # Diese wird durch erweiterte Version ersetzt
self._create_progress_section()
self._create_buttons()
self._create_info_section()
self._check_system_requirements()
# Configure responsive layout
self._configure_responsive_layout()
# Initial status refresh
self.refresh_status()
return self.root
def _create_header(self):
"""Create header section"""
header_frame = tk.Frame(self.root, bg="lightblue", height=60)
header_frame.pack(fill="x", padx=10, pady=10)
header_frame.pack_propagate(False)
title_label = tk.Label(
header_frame,
text=LXToolsAppConfig.APP_NAME,
font=("Helvetica", 18, "bold"),
bg="lightblue",
)
title_label.pack(expand=True)
version_label = tk.Label(
header_frame,
text=f"v{LXToolsAppConfig.VERSION}",
font=("Helvetica", 10),
bg="lightblue",
)
version_label.pack(side="bottom")
def _create_system_info(self):
"""Create system information section"""
info_frame = tk.Frame(self.root)
info_frame.pack(pady=5)
os_info = tk.Label(
info_frame,
text=f"{_('Detected System')}: {self.detected_os}",
font=("Helvetica", 11),
)
os_info.pack(pady=2)
def _create_app_selection(self):
"""Create application selection section with improved Grid layout"""
selection_frame = ttk.LabelFrame(
self.root, text=_("Select Application"), padding=15
)
selection_frame.pack(fill="both", expand=True, padx=15, pady=10)
self.app_var = tk.StringVar()
# Haupt-Container mit Scrollbar (falls mehr Apps hinzukommen)
canvas = tk.Canvas(selection_frame, highlightthickness=0)
scrollbar = ttk.Scrollbar(
selection_frame, orient="vertical", command=canvas.yview
)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Grid-Container für Apps
apps_container = tk.Frame(scrollable_frame)
apps_container.pack(fill="x", padx=5, pady=5)
# Grid konfigurieren - 4 Spalten mit besserer Verteilung
apps_container.grid_columnconfigure(0, weight=0, minsize=60) # Icon
apps_container.grid_columnconfigure(1, weight=1, minsize=150) # App Name
apps_container.grid_columnconfigure(2, weight=0, minsize=120) # Status
apps_container.grid_columnconfigure(3, weight=0, minsize=150) # Version
# Header-Zeile
header_font = ("Helvetica", 9, "bold")
tk.Label(apps_container, text="", font=header_font).grid(
row=0, column=0, sticky="w", padx=5, pady=2
)
tk.Label(apps_container, text=_("Application"), font=header_font).grid(
row=0, column=1, sticky="w", padx=5, pady=2
)
tk.Label(apps_container, text=_("Status"), font=header_font).grid(
row=0, column=2, sticky="w", padx=5, pady=2
)
tk.Label(apps_container, text=_("Version"), font=header_font).grid(
row=0, column=3, sticky="w", padx=5, pady=2
)
# Trennlinie
separator = ttk.Separator(apps_container, orient="horizontal")
separator.grid(row=1, column=0, columnspan=4, sticky="ew", pady=5)
row = 2
for app_key, app in self.app_manager.get_all_apps().items():
# Spalte 0: Icon
app_icon = self._load_app_icon(app)
if app_icon:
icon_label = tk.Label(apps_container, image=app_icon)
icon_label.grid(row=row, column=0, padx=5, pady=5, sticky="w")
icon_label.image = app_icon
else:
icon_text = "🔧" if app.icon_key == "icon_log" else "🔒"
icon_label = tk.Label(
apps_container, text=icon_text, font=("Helvetica", 16)
)
icon_label.grid(row=row, column=0, padx=5, pady=5, sticky="w")
# Spalte 1: Radio button mit App-Name
radio = ttk.Radiobutton(
apps_container, text=app.name, variable=self.app_var, value=app_key
)
radio.grid(row=row, column=1, padx=5, pady=5, sticky="w")
# Spalte 2: Status
status_label = tk.Label(apps_container, text="", font=("Helvetica", 9))
status_label.grid(row=row, column=2, padx=5, pady=5, sticky="w")
self.status_labels[app_key] = status_label
# Spalte 3: Version Info
version_label = tk.Label(
apps_container, text="", font=("Helvetica", 8), fg="gray"
)
version_label.grid(row=row, column=3, padx=5, pady=5, sticky="w")
self.version_labels[app_key] = version_label
row += 1
# Pack canvas and scrollbar
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Mouse wheel scrolling
def _on_mousewheel(event):
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
canvas.bind_all("<MouseWheel>", _on_mousewheel)
def _load_app_icon(self, app):
"""Load icon for application using tk.PhotoImage"""
fallback_paths = [
f"lx-icons/48/{app.icon}",
f"icons/{app.icon}",
f"./lx-icons/48/{app.icon}",
f"./icons/48/{app.icon}",
(
f"lx-icons/48/wg_vpn.png"
if app.icon_key == "icon_vpn"
else f"lx-icons/48/log.png"
),
]
return self.image_manager.load_image(
app.icon_key, fallback_paths=fallback_paths
)
def _create_progress_section(self):
"""Create progress section with download icon using Grid"""
progress_frame = ttk.LabelFrame(self.root, text=_("Progress"), padding=10)
progress_frame.pack(fill="x", padx=15, pady=10)
# Container für Icon und Progress mit Grid
progress_container = tk.Frame(progress_frame)
progress_container.pack(fill="x")
# Grid konfigurieren
progress_container.grid_columnconfigure(1, weight=1)
# Download Icon (Spalte 0)
self.download_icon_label = tk.Label(
progress_container,
text="",
width=3,
height=2,
relief="flat",
anchor="center",
)
self.download_icon_label.grid(row=0, column=0, padx=(0, 10), pady=2, sticky="w")
# Progress Text (Spalte 1)
self.progress_label = tk.Label(
progress_container,
text=_("Ready for installation..."),
font=("Helvetica", 10),
fg="blue",
anchor="w",
wraplength=400,
)
self.progress_label.grid(row=0, column=1, pady=2, sticky="ew")
# Initial icon laden (neutral)
self._reset_download_icon()
def _configure_responsive_layout(self):
"""Configure responsive layout for window resizing"""
def on_window_resize(event):
# Adjust wraplength for progress label based on window width
if self.progress_label and event.widget == self.root:
new_width = max(300, event.width - 150)
self.progress_label.config(wraplength=new_width)
self.root.bind("<Configure>", on_window_resize)
def _reset_download_icon(self):
"""Reset download icon to neutral state"""
icon = self.image_manager.load_image(
"icon_download",
fallback_paths=["lx-icons/32/download.png", "./lx-icons/32/download.png"],
)
if icon:
self.download_icon_label.config(image=icon, text="", compound="center")
self.download_icon_label.image = icon
else:
self.download_icon_label.config(text="📥", font=("Helvetica", 14), image="")
def update_download_icon(self, status):
"""Update download icon based on status"""
if not self.download_icon_label:
return
if status == "downloading":
icon = self.image_manager.load_image(
"icon_download",
fallback_paths=[
"lx-icons/32/download.png",
"./lx-icons/32/download.png",
],
)
if icon:
self.download_icon_label.config(image=icon, text="", compound="center")
self.download_icon_label.image = icon
else:
self.download_icon_label.config(
text="⬇️", font=("Helvetica", 14), image=""
)
elif status == "error":
icon = self.image_manager.load_image(
"icon_download_error",
fallback_paths=[
"lx-icons/32/download_error.png",
"./lx-icons/32/download_error.png",
"/home/punix/Pyapps/installer-appimage/lx-icons/32/download_error.png",
],
)
if icon:
self.download_icon_label.config(image=icon, text="", compound="center")
self.download_icon_label.image = icon
else:
self.download_icon_label.config(
text="", font=("Helvetica", 14), image=""
)
elif status == "success":
icon = self.image_manager.load_image(
"icon_download",
fallback_paths=[
"lx-icons/32/download.png",
"./lx-icons/32/download.png",
],
)
if icon:
self.download_icon_label.config(image=icon, text="", compound="center")
self.download_icon_label.image = icon
else:
self.download_icon_label.config(
text="", font=("Helvetica", 14), image=""
)
self.download_icon_label.update()
def _create_buttons(self):
"""Create button section using Grid"""
button_frame = tk.Frame(self.root)
button_frame.pack(pady=15)
# Grid für Buttons - 3 Spalten
button_frame.grid_columnconfigure(0, weight=1)
button_frame.grid_columnconfigure(1, weight=1)
button_frame.grid_columnconfigure(2, weight=1)
# Configure button styles
style = ttk.Style()
style.configure("Install.TButton", foreground="green")
style.configure("Uninstall.TButton", foreground="red")
style.configure("Refresh.TButton", foreground="blue")
install_btn = ttk.Button(
button_frame,
text=_("Install / Update"),
command=self.install_app,
style="Install.TButton",
)
install_btn.grid(row=0, column=0, padx=8, sticky="ew")
uninstall_btn = ttk.Button(
button_frame,
text=_("Uninstall"),
command=self.uninstall_app,
style="Uninstall.TButton",
)
uninstall_btn.grid(row=0, column=1, padx=8, sticky="ew")
refresh_btn = ttk.Button(
button_frame,
text=_("Refresh Status"),
command=self.refresh_status,
style="Refresh.TButton",
)
refresh_btn.grid(row=0, column=2, padx=8, sticky="ew")
def _create_info_section(self):
"""Create information section"""
info_text = tk.Label(
self.root,
text=_(
"Notes:\n"
"• Applications are downloaded automatically from the repository\n"
"• Root privileges are requested via pkexec when needed\n"
"• Shared libraries are managed automatically\n"
"• User configuration files are preserved during updates\n"
"• Policy files for pkexec are installed automatically"
),
font=("Helvetica", 9),
fg="gray",
wraplength=450,
justify="left",
)
info_text.pack(pady=15, padx=20)
def _check_system_requirements(self):
"""Check system requirements"""
try:
subprocess.run(["which", "pkexec"], check=True, capture_output=True)
except subprocess.CalledProcessError:
warning_label = tk.Label(
self.root,
text=_("⚠️ WARNING: pkexec is not available! Installation will fail."),
font=("Helvetica", 10, "bold"),
fg="red",
)
warning_label.pack(pady=5)
def update_progress(self, message):
"""Update progress label"""
if self.progress_label:
self.progress_label.config(text=message)
self.progress_label.update()
def refresh_status(self):
"""Refresh application status and version information"""
self.update_progress(_("Refreshing status and checking versions..."))
self._reset_download_icon()
for app_key, app in self.app_manager.get_all_apps().items():
status_label = self.status_labels[app_key]
version_label = self.version_labels[app_key]
if app.is_installed():
installed_version = app.get_installed_version()
status_label.config(
text=f"{_('Installed')} (v{installed_version})", fg="green"
)
# Get latest version from API
try:
latest_version = app.get_latest_version()
if latest_version != "Unknown":
if installed_version != latest_version:
version_label.config(
text=f"{_('Latest')}: v{latest_version} ({_('Update available')})",
fg="orange",
)
else:
version_label.config(
text=f"{_('Latest')}: v{latest_version} ({_('Up to date')})",
fg="green",
)
else:
version_label.config(
text=f"{_('Latest')}: {_('Unknown')}", fg="gray"
)
except:
version_label.config(
text=f"{_('Latest')}: {_('Check failed')}", fg="gray"
)
else:
status_label.config(text=f"{_('Not installed')}", fg="red")
# Still show latest available version
try:
latest_version = app.get_latest_version()
if latest_version != "Unknown":
version_label.config(
text=f"{_('Available')}: v{latest_version}", fg="blue"
)
else:
version_label.config(
text=f"{_('Available')}: {_('Unknown')}", fg="gray"
)
except:
version_label.config(
text=f"{_('Available')}: {_('Check failed')}", fg="gray"
)
self.update_progress(_("Status refresh completed."))
def install_app(self):
"""Handle install button click"""
selected_app = self.app_var.get()
if not selected_app:
messagebox.showwarning(
_("Warning"), _("Please select an application to install.")
)
return
# Check internet connection
if not NetworkChecker.check_internet_connection():
self.update_download_icon("error")
messagebox.showerror(
_("Network Error"),
_(
"No internet connection available.\nPlease check your network connection."
),
)
return
if not NetworkChecker.check_repository_access():
self.update_download_icon("error")
messagebox.showerror(
_("Repository Error"),
_("Cannot access repository.\nPlease try again later."),
)
return
# Reset download icon
self._reset_download_icon()
app = self.app_manager.get_app(selected_app)
# Check if already installed
if app.is_installed():
installed_version = app.get_installed_version()
latest_version = app.get_latest_version()
dialog_text = (
f"{app.name} {_('is already installed')}.\n\n"
f"{_('Installed version')}: v{installed_version}\n"
f"{_('Latest version')}: v{latest_version}\n\n"
f"{_('YES')} = {_('Update')} ({_('reinstall all files')})\n"
f"{_('NO')} = {_('Uninstall')}\n"
f"{_('Cancel')} = {_('Do nothing')}"
)
result = messagebox.askyesnocancel(
f"{app.name} {_('already installed')}", dialog_text
)
if result is None: # Cancel
self.update_progress(_("Installation cancelled."))
return
elif not result: # Uninstall
self.uninstall_app(selected_app)
return
else: # Update
self.update_progress(_("Updating application..."))
try:
self.installation_manager.install_app(selected_app)
messagebox.showinfo(
_("Success"),
f"{app.name} {_('has been successfully installed/updated')}.",
)
self.refresh_status()
except Exception as e:
# Bei Fehler Error-Icon anzeigen
self.update_download_icon("error")
messagebox.showerror(_("Error"), f"{_('Installation failed')}: {e}")
def uninstall_app(self, app_key=None):
"""Handle uninstall button click"""
if app_key is None:
app_key = self.app_var.get()
if not app_key:
messagebox.showwarning(
_("Warning"), _("Please select an application to uninstall.")
)
return
app = self.app_manager.get_app(app_key)
if not app.is_installed():
messagebox.showinfo(_("Info"), f"{app.name} {_('is not installed')}.")
return
result = messagebox.askyesno(
_("Confirm Uninstall"),
f"{_('Are you sure you want to uninstall')} {app.name}?\n\n"
f"{_('This will remove all application files and user configurations')}.",
)
if not result:
return
try:
self.installation_manager.uninstall_app(app_key)
messagebox.showinfo(
_("Success"), f"{app.name} {_('has been successfully uninstalled')}."
)
self.refresh_status()
except Exception as e:
messagebox.showerror(_("Error"), f"{_('Uninstallation failed')}: {e}")
def run(self):
"""Start the GUI application"""
root = self.create_gui()
root.mainloop()
# ----------------------------
# Main Application Entry Point
# ----------------------------
def main():
"""Main function to start the application"""
try:
# Create and run the GUI
app = LXToolsGUI()
app.run()
except KeyboardInterrupt:
print("\nApplication interrupted by user.")
except Exception as e:
print(f"Fatal error: {e}")
messagebox.showerror(
_("Fatal Error"), f"{_('Application failed to start')}: {e}"
)
if __name__ == "__main__":
main()