1265 lines
44 KiB
Python
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()
|