Installer divided into several modules and added new MessageDialog module

This commit is contained in:
2025-06-14 22:16:15 +02:00
parent f288a2bd7f
commit 58ca160050
31 changed files with 1178 additions and 712 deletions

View File

@ -4,6 +4,19 @@ Changelog for LXTools installer
- -
### Added
14-06-2025
- Installer divided into several modules and added new MessageDialog module
### Added
4-06-2025
- replace modul path /usr/lib/python3/dist-packages/shared_libs
with /usr/local/share/shared_libs for better ensure that the shared libs are found
- add ensure_shared_libs_pth_exists Script to install
### Added ### Added
4-06-2025 4-06-2025

68
ensure_modules.py Normal file
View File

@ -0,0 +1,68 @@
import sys
import os
# ✅ Path to be added in the .pth file
SHARED_LIBS_PATH = "/usr/local/share/shared_libs"
PTH_FILE_NAME = "shared_libs.pth"
def ensure_shared_libs_pth_exists():
"""
Checks if all site-packages directories have a `.pth` file with the correct path.
Creates or updates it if missing or incorrect.
"""
# Search for all site-packages directories (e.g., /usr/lib/python3.x/site-packages/)
for root, dirs, files in os.walk("/usr"):
if "site-packages" in dirs:
site_packages_dir = os.path.join(root, "site-packages")
pth_file_path = os.path.join(site_packages_dir, PTH_FILE_NAME)
# Check if the file exists and is correct
if not os.path.exists(pth_file_path):
print(f"⚠️ .pth file not found: {pth_file_path}. Creating...")
with open(pth_file_path, "w") as f:
f.write(SHARED_LIBS_PATH + "\n")
else:
# Check if the correct path is in the file
with open(pth_file_path, "r") as f:
content = f.read().strip()
if not content == SHARED_LIBS_PATH:
print(f"⚠️ .pth file exists but has incorrect content. Fixing...")
with open(pth_file_path, "w") as f:
f.write(SHARED_LIBS_PATH + "\n")
print("✅ All .pth files checked and corrected.")
def main():
try:
# Try to import the module
from shared_libs.wp_app_config import AppConfig
print("'shared_libs' is correctly loaded. Starting the application...")
# Your main program logic here...
except ModuleNotFoundError as e:
# Only handle errors related to missing .pth file
if "No module named 'shared_libs'" in str(e):
print("⚠️ Error: 'shared_libs' module not found. Checking .pth file...")
ensure_shared_libs_pth_exists()
# Try again after fixing the .pth file
try:
from shared_libs.wp_app_config import AppConfig
print("✅ After correcting the .pth file: Module loaded.")
# Your main program logic here...
except Exception as e2:
print(f"❌ Error after correcting the .pth file: {e2}")
else:
# For other errors, re-raise them
raise
if __name__ == "__main__":
main()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
lx-icons/128/warning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
lx-icons/256/warning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
lx-icons/32/warning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
lx-icons/48/warning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
lx-icons/64/warning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because it is too large Load Diff

493
manager.py Normal file
View File

@ -0,0 +1,493 @@
import locale
import gettext
import tkinter as tk
from pathlib import Path
from tkinter import ttk
import os
import subprocess
import stat
from network import GiteaUpdate
class LXToolsAppConfig:
VERSION = "1.1.4"
APP_NAME = "Lunix Tools Installer"
WINDOW_WIDTH = 450
WINDOW_HEIGHT = 580
# Working directory
WORK_DIR = os.getcwd()
ICONS_DIR = os.path.join(WORK_DIR, "lx-icons")
THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes")
# Locale settings
LOCALE_DIR = Path("/usr/share/locale/")
# Download URLs
WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip"
SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip"
# API URLs for version checking
WIREPY_API_URL = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases"
SHARED_LIBS_API_URL = (
"https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases"
)
# Project configurations
PROJECTS = {
"wirepy": {
"name": "Wire-Py",
"description": "WireGuard VPN Manager with GUI",
"download_url": WIREPY_URL,
"api_url": WIREPY_API_URL,
"icon_key": "icon_vpn",
"main_executable": "wirepy.py",
"symlink_name": "wirepy",
"config_file": "wp_app_config.py",
"desktop_file": "Wire-Py.desktop",
"policy_file": "org.sslcrypt.policy",
"requires_ssl": True,
},
"logviewer": {
"name": "LogViewer",
"description": "System Log Viewer with GUI",
"download_url": SHARED_LIBS_URL,
"api_url": SHARED_LIBS_API_URL,
"icon_key": "icon_log",
"main_executable": "logviewer.py",
"symlink_name": "logviewer",
"config_file": "logview_app_config.py",
"desktop_file": "LogViewer.desktop",
"policy_file": None,
"requires_ssl": False,
},
}
# OS Detection List (order matters - specific first, generic last)
OS_DETECTION = [
("mint", "Linux Mint"),
("pop", "Pop!_OS"),
("manjaro", "Manjaro"),
("garuda", "Garuda Linux"),
("endeavouros", "EndeavourOS"),
("fedora", "Fedora"),
("tumbleweed", "SUSE Tumbleweed"),
("leap", "SUSE Leap"),
("arch", "Arch Linux"),
("ubuntu", "Ubuntu"),
("debian", "Debian"),
]
# Package manager commands for TKinter installation
TKINTER_INSTALL_COMMANDS = {
"Ubuntu": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"],
"Debian": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"],
"Linux Mint": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"],
"Pop!_OS": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"],
"Fedora": ["dnf", "install", "-y", "tkinter"],
"Arch Linux": ["pacman", "-S", "--noconfirm", "tk"],
"Manjaro": ["pacman", "-S", "--noconfirm", "tk"],
"Garuda Linux": ["pacman", "-S", "--noconfirm", "tk"],
"EndeavourOS": ["pacman", "-S", "--noconfirm", "tk"],
"SUSE Tumbleweed": ["zypper", "install", "-y", "python314-tk"],
"SUSE Leap": ["zypper", "install", "-y", "python312-tk"],
}
@staticmethod
def setup_translations():
"""Initialize translations and set the translation function"""
try:
locale.bindtextdomain(
LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR
)
gettext.bindtextdomain(
LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR
)
gettext.textdomain(LXToolsAppConfig.APP_NAME)
except:
pass
return gettext.gettext
# Initialize translations
_ = LXToolsAppConfig.setup_translations()
class OSDetector:
@staticmethod
def detect_os():
"""Detect operating system using ordered list"""
try:
with open("/etc/os-release", "r") as f:
content = f.read().lower()
# Check each OS in order (specific first)
for keyword, os_name in LXToolsAppConfig.OS_DETECTION:
if keyword in content:
return os_name
return "Unknown System"
except FileNotFoundError:
return "File not found"
@staticmethod
def check_tkinter_available():
"""Check if tkinter is available"""
try:
import tkinter
return True
except ImportError:
return False
@staticmethod
def install_tkinter():
"""Install tkinter based on detected OS"""
detected_os = OSDetector.detect_os()
if detected_os in LXToolsAppConfig.TKINTER_INSTALL_COMMANDS:
commands = LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os]
print(f"Installing tkinter for {detected_os}...")
print(_(f"Command: {' '.join(commands)}"))
try:
# Use pkexec for privilege escalation
full_command = ["pkexec", "bash", "-c", " ".join(commands)]
result = subprocess.run(
full_command, capture_output=True, text=True, timeout=300
)
if result.returncode == 0:
print(_("TKinter installation completed successfully!"))
return True
else:
print(_(f"TKinter installation failed: {result.stderr}"))
return False
except subprocess.TimeoutExpired:
print(_("TKinter installation timed out"))
return False
except Exception as e:
print(_(f"Error installing tkinter: {e}"))
return False
else:
print(_(f"No tkinter installation command defined for {detected_os}"))
return False
class Theme:
@staticmethod
def apply_light_theme(root):
"""Apply light theme"""
try:
theme_dir = LXToolsAppConfig.THEMES_DIR
water_theme_path = os.path.join(theme_dir, "water.tcl")
if os.path.exists(water_theme_path):
try:
root.tk.call("source", water_theme_path)
root.tk.call("set_theme", "light")
return True
except tk.TclError:
pass
# System theme fallback
try:
style = ttk.Style()
if "clam" in style.theme_names():
style.theme_use("clam")
return True
except:
pass
except Exception:
pass
return False
class System:
@staticmethod
def create_directories(directories):
"""Create system directories using pkexec"""
for directory in directories:
subprocess.run(["pkexec", "mkdir", "-p", directory], check=True)
@staticmethod
def copy_file(src, dest, make_executable=False):
"""Copy file using pkexec"""
subprocess.run(["pkexec", "cp", src, dest], check=True)
if make_executable:
subprocess.run(["pkexec", "chmod", "755", dest], check=True)
@staticmethod
def copy_directory(src, dest):
"""Copy directory using pkexec"""
subprocess.run(["pkexec", "cp", "-r", src, dest], check=True)
@staticmethod
def remove_file(path):
"""Remove file using pkexec"""
subprocess.run(["pkexec", "rm", "-f", path], check=False)
@staticmethod
def remove_directory(path):
"""Remove directory using pkexec"""
subprocess.run(["pkexec", "rm", "-rf", path], check=False)
@staticmethod
def create_symlink(target, link_name):
"""Create symbolic link using pkexec"""
subprocess.run(["pkexec", "rm", "-f", link_name], check=False)
subprocess.run(["pkexec", "ln", "-sf", target, link_name], check=True)
@staticmethod
def create_ssl_key(pem_file):
"""Create SSL key using pkexec"""
try:
subprocess.run(
["pkexec", "openssl", "genrsa", "-out", pem_file, "4096"], check=True
)
subprocess.run(["pkexec", "chmod", "600", pem_file], check=True)
return True
except subprocess.CalledProcessError:
return False
class Image:
def __init__(self):
self.images = {}
def load_image(self, image_key, fallback_paths=None):
"""Load PNG image using tk.PhotoImage with fallback options"""
if image_key in self.images:
return self.images[image_key]
# Define image paths based on key
image_paths = {
"app_icon": [
"./lx-icons/48/wg_vpn.png",
"/usr/share/icons/lx-icons/48/wg_vpn.png",
],
"download_icon": [
"./lx-icons/32/download.png",
"/usr/share/icons/lx-icons/32/download.png",
],
"download_error_icon": [
"./lx-icons/32/download_error.png",
"/usr/share/icons/lx-icons/32/download_error.png",
],
"success_icon": [
"./lx-icons/32/download.png",
"/usr/share/icons/lx-icons/32/download.png",
],
"icon_vpn": [
"./lx-icons/48/wg_vpn.png",
"/usr/share/icons/lx-icons/48/wg_vpn.png",
],
"icon_log": [
"./lx-icons/48/log.png",
"/usr/share/icons/lx-icons/48/log.png",
],
}
# Get paths to try
paths_to_try = image_paths.get(image_key, [])
# Add fallback paths if provided
if fallback_paths:
paths_to_try.extend(fallback_paths)
# Try to load image from paths
for path in paths_to_try:
try:
if os.path.exists(path):
photo = tk.PhotoImage(file=path)
self.images[image_key] = photo
return photo
except tk.TclError as e:
print(_(f"Failed to load image from {path}: {e}"))
continue
# Return None if no image found
return None
class AppManager:
def __init__(self):
self.projects = LXToolsAppConfig.PROJECTS
def get_project_info(self, project_key):
"""Get project information by key"""
return self.projects.get(project_key)
def get_all_projects(self):
"""Get all project configurations"""
return self.projects
def is_installed(self, project_key):
"""Check if project is installed with better detection"""
if project_key == "wirepy":
# Check for wirepy symlink
return os.path.exists("/usr/local/bin/wirepy") and os.path.islink(
"/usr/local/bin/wirepy"
)
elif project_key == "logviewer":
# Check for logviewer symlink AND executable file
symlink_exists = os.path.exists("/usr/local/bin/logviewer")
executable_exists = os.path.exists(
"/usr/local/share/shared_libs/logviewer.py"
)
executable_is_executable = False
if executable_exists:
try:
# Check if file is executable
file_stat = os.stat("/usr/local/share/shared_libs/logviewer.py")
executable_is_executable = bool(file_stat.st_mode & stat.S_IEXEC)
except:
executable_is_executable = False
# LogViewer is installed if symlink exists AND executable file exists AND is executable
is_installed = (
symlink_exists and executable_exists and executable_is_executable
)
# Debug logging
print(_("LogViewer installation check:"))
print(_(f" Symlink exists: {symlink_exists}"))
print(_(f" Executable exists: {executable_exists}"))
print(_(f" Is executable: {executable_is_executable}"))
print(_(f" Final result: {is_installed}"))
return is_installed
return False
def get_installed_version(self, project_key):
"""Get installed version from config file"""
try:
if project_key == "wirepy":
config_file = "/usr/local/share/shared_libs/wp_app_config.py"
elif project_key == "logviewer":
config_file = "/usr/local/share/shared_libs/logview_app_config.py"
else:
return "Unknown"
if os.path.exists(config_file):
with open(config_file, "r") as f:
content = f.read()
for line in content.split("\n"):
if "VERSION" in line and "=" in line:
version = line.split("=")[1].strip().strip("\"'")
return version
return "Unknown"
except Exception as e:
print(_(f"Error getting version for {project_key}: {e}"))
return "Unknown"
def get_latest_version(self, project_key):
"""Get latest version from API"""
project_info = self.get_project_info(project_key)
if not project_info:
return "Unknown"
return GiteaUpdate.api_down(project_info["api_url"])
def check_other_apps_installed(self, exclude_key):
"""Check if other apps are still installed"""
return any(
self.is_installed(key) for key in self.projects.keys() if key != exclude_key
)
class Center:
@staticmethod
def center_window_cross_platform(window, width, height):
"""
Centers a window on the primary monitor in a way that works on both X11 and Wayland
Args:
window: The tkinter window to center
width: Window width
height: Window height
"""
# Calculate the position before showing the window
# First attempt: Try to use GDK if available (works on both X11 and Wayland)
try:
import gi
gi.require_version("Gdk", "3.0")
from gi.repository import Gdk
display = Gdk.Display.get_default()
monitor = display.get_primary_monitor() or display.get_monitor(0)
geometry = monitor.get_geometry()
scale_factor = monitor.get_scale_factor()
# Calculate center position on the primary monitor
x = geometry.x + (geometry.width - width // scale_factor) // 2
y = geometry.y + (geometry.height - height // scale_factor) // 2
# Set window geometry
window.geometry(f"{width}x{height}+{x}+{y}")
return
except (ImportError, AttributeError):
pass
# Second attempt: Try xrandr for X11
try:
import subprocess
output = subprocess.check_output(
["xrandr", "--query"], universal_newlines=True
)
# Parse the output to find the primary monitor
primary_info = None
for line in output.splitlines():
if "primary" in line:
parts = line.split()
for part in parts:
if "x" in part and "+" in part:
primary_info = part
break
break
if primary_info:
# Parse the geometry: WIDTH x HEIGHT+X+Y
geometry = primary_info.split("+")
dimensions = geometry[0].split("x")
primary_width = int(dimensions[0])
primary_height = int(dimensions[1])
primary_x = int(geometry[1])
primary_y = int(geometry[2])
# Calculate center position on the primary monitor
x = primary_x + (primary_width - width) // 2
y = primary_y + (primary_height - height) // 2
# Set window geometry
window.geometry(f"{width}x{height}+{x}+{y}")
return
except (ImportError, IndexError, ValueError):
pass
# Final fallback: Use standard Tkinter method
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
# Try to make an educated guess for multi-monitor setups
# If screen width is much larger than height, assume multiple monitors side by side
if (
screen_width > screen_height * 1.8
): # Heuristic for detecting multiple monitors
# Assume the primary monitor is on the left half
screen_width = screen_width // 2
x = (screen_width - width) // 2
y = (screen_height - height) // 2
window.geometry(f"{width}x{height}+{x}+{y}")

384
message.py Normal file
View File

@ -0,0 +1,384 @@
import os
from typing import List, Optional, Dict
import tkinter as tk
from tkinter import ttk
from manager import Center
"""
####################################################
Attention! MessageDialog returns different values.
From 3 buttons with Cancel, Cancel and the Close (x)
None returns. otherwise always False.
####################################################
Usage Examples
1. Basic Info Dialog
from tkinter import Tk
root = Tk()
dialog = MessageDialog(
message_type="info",
text="This is an information message.",
buttons=["OK"],
master=root,
)
result = dialog.show()
print("User clicked OK:", result)
-----------------------------------------------------
My Favorite Example,
for simply information message:
MessageDialog(text="This is an information message.")
result = MessageDialog(text="This is an information message.").show()
-----------------------------------------------------
Explanation: if you need the return value e.g. in the vaiable result,
you need to add .show(). otherwise only if no root.mainloop z.b is used to test the window.
#####################################################
2. Error Dialog with Custom Icon and Command
def on_cancel():
print("User canceled the operation.")
root = Tk()
result = MessageDialog(
message_type="error",
text="An error occurred during processing.",
buttons=["Retry", "Cancel"],
commands=[None, on_cancel],
icon="/path/to/custom/error_icon.png",
title="Critical Error"
).show()
print("User clicked Retry:", result)
-----------------------------------------------------
My Favorite Example,
for simply Error message:
MessageDialog(
"error",
text="An error occurred during processing.",
).show()
#####################################################
3. Confirmation Dialog with Yes/No Buttons
def on_confirm():
print("User confirmed the action.")
root = Tk()
dialog = MessageDialog(
message_type="ask",
text="Are you sure you want to proceed?",
buttons=["Yes", "No"],
commands=[on_confirm, None], # Either use comando or work with the values True and False
)
result = dialog.show()
print("User confirmed:", result)
-----------------------------------------------------
My Favorite Example,
for simply Question message:
dialog = MessageDialog(
"ask",
text="Are you sure you want to proceed?",
buttons=["Yes", "No"]
).show()
#####################################################
4. Warning Dialog with Custom Title
root = Tk()
dialog = MessageDialog(
message_type="warning",
text="This action cannot be undone.",
buttons=["Proceed", "Cancel"],
title="Warning: Irreversible Action"
)
result = dialog.show()
print("User proceeded:", result)
-----------------------------------------------------
And a special example for a "open link" button:
Be careful not to forget to import it into the script in which this dialog is used!!!
import webbrowser
from functools import partial
dialog = MessageDialog(
"ask",
text="Are you sure you want to proceed?",
buttons=["Yes", "Go to Exapmle"],
commands=[
None, # Default on "OK"
partial(webbrowser.open, "https://exapmle.com"),
],
icon="/pathh/to/custom/icon.png",
title="Example",
).show()
In all dialogues, a font can also be specified as a tuple. With font=("ubuntu", 11)
and wraplength=300, the text is automatically wrapped.
"""
class MessageDialog:
"""
A customizable message dialog window using tkinter.
This class creates modal dialogs for displaying information, warnings, errors,
or questions to the user. It supports multiple button configurations and custom
icons. The dialog is centered on the screen and handles user interactions.
Attributes:
message_type (str): Type of message ("info", "error", "warning", "ask").
text (str): Main message content.
buttons (List[str]): List of button labels (e.g., ["OK", "Cancel"]).
result (bool): True if the user clicked a positive button (like "Yes" or "OK"), else False.
icons: Dictionary mapping message types to tkinter.PhotoImage objects.
Parameters:
message_type: Type of message dialog (default: "info").
text: Message content to display.
buttons: List of button labels (default: ["OK"]).
master: Parent tkinter window (optional).
commands: List of callables for each button (default: [None]).
icon: Custom icon path (overrides default icons if provided).
title: Window title (default: derived from message_type).
Methods:
_get_title(): Returns the default window title based on message type.
_load_icons(): Loads icons from system paths or fallback locations.
_on_button_click(button_text): Sets result and closes the dialog.
show(): Displays the dialog and waits for user response (returns self.result).
"""
DEFAULT_ICON_PATH = "/usr/share/icons/lx-icons"
def __init__(
self,
message_type: str = "info",
text: str = "",
buttons: List[str] = ["OK"],
master: Optional[tk.Tk] = None,
commands: List[Optional[callable]] = [None],
icon: str = None,
title: str = None,
font: tuple = None,
wraplength: int = None,
):
self.message_type = message_type or "info" # Default is "info"
self.text = text
self.buttons = buttons
self.master = master
self.result: bool = False # Default is False
self.icon_path = self._get_icon_path()
self.icon = icon
self.title = title
# Window creation
self.window = tk.Toplevel(master)
self.window.grab_set()
self.window.resizable(False, False)
ttk.Style().configure("TButton", font=("Helvetica", 11), padding=5)
self.buttons_widgets = []
self.current_button_index = 0
self._load_icons()
# Window title and icon
self.window.title(self._get_title() if not self.title else self.title)
self.window.iconphoto(False, self.icons[self.message_type])
# Layout
frame = ttk.Frame(self.window)
frame.pack(expand=True, fill="both")
# Grid-Configuration
frame.grid_rowconfigure(0, weight=1)
frame.grid_columnconfigure(0, weight=1)
frame.grid_columnconfigure(1, weight=3)
# Icon and Text
icon_label = ttk.Label(frame, image=self.icons[self.message_type])
pady_value = 5 if self.icon is not None else 15
icon_label.grid(
row=0, column=0, padx=(20, 10), pady=(pady_value, 15), sticky="nsew"
)
text_label = tk.Label(
frame,
text=text,
wraplength=wraplength if wraplength else 300,
justify="left",
anchor="center",
font=font if font else ("Helvetica", 12),
pady=20,
)
text_label.grid(
row=0,
column=1,
padx=(10, 20),
pady=(8, 20),
sticky="nsew",
)
# Create button frame
self.button_frame = ttk.Frame(frame)
self.button_frame.grid(row=1, columnspan=2, pady=(15, 10))
for i, btn_text in enumerate(buttons):
if commands and len(commands) > i and commands[i] is not None:
# Button with individual command
btn = ttk.Button(
self.button_frame,
text=btn_text,
command=commands[i],
)
else:
# Default button set self.result and close window
btn = ttk.Button(
self.button_frame,
text=btn_text,
command=lambda t=btn_text: self._on_button_click(t),
)
padx_value = 50 if self.icon is not None and len(buttons) == 2 else 10
btn.pack(side="left" if i == 0 else "right", padx=padx_value, pady=15)
btn.focus_set() if i == 0 else None # Set focus on first button
self.buttons_widgets.append(btn)
self.window.bind("<Return>", lambda event: self._on_enter_pressed())
self.window.bind("<Left>", lambda event: self._navigate_left())
self.window.bind("<Right>", lambda event: self._navigate_right())
self.window.update_idletasks()
self.window.attributes("-alpha", 0.0) # 100% Transparencence
self.window.after(200, lambda: self.window.attributes("-alpha", 100.0))
self.window.update() # Window update before centering!
Center.center_window_cross_platform(
self.window, self.window.winfo_width(), self.window.winfo_height()
)
# Close Window on Cancel
self.window.protocol(
"WM_DELETE_WINDOW", lambda: self._on_button_click("Cancel")
)
def _get_title(self) -> str:
return {
"error": "Error",
"info": "Info",
"ask": "Question",
"warning": "Warning",
}[self.message_type]
def _load_icons(self):
# Try to load the icon from the provided path
self.icons = {}
icon_paths: Dict[str, str] = {
"error": os.path.join(self.icon_path, "64/error.png"),
"info": os.path.join(self.icon_path, "64/info.png"),
"warning": os.path.join(self.icon_path, "64/warning.png"),
"ask": os.path.join(self.icon_path, "64/question_mark.png"),
}
fallback_paths: Dict[str, str] = {
"error": "./lx-icons/64/error.png",
"info": "./lx-icons/64/info.png",
"warning": "./lx-icons/64/warning.png",
"ask": "./lx-icons/64/question_mark.png",
}
for key in icon_paths:
try:
# Check if an individual icon is provided
if (
self.message_type == key
and self.icon is not None
and os.path.exists(self.icon)
):
try:
self.icons[key] = tk.PhotoImage(file=self.icon)
except Exception as e:
print(
f"Erro on loading individual icon '{key}': {e}\n",
"Try to use the default icon",
)
else:
# Check for standard path
if os.path.exists(icon_paths[key]):
self.icons[key] = tk.PhotoImage(file=icon_paths[key])
else:
self.icons[key] = tk.PhotoImage(file=fallback_paths[key])
except Exception as e:
print(f"Error on load Icon '{[key]}': {e}")
self.icons[key] = tk.PhotoImage()
print(f"⚠️ No Icon found for '{key}'. Use standard Tkinter icon.")
return self.icons
def _get_icon_path(self) -> str:
"""Get the path to the default icon."""
if os.path.exists(self.DEFAULT_ICON_PATH):
return self.DEFAULT_ICON_PATH
else:
# Fallback to the directory of the script
return os.path.dirname(os.path.abspath(__file__))
def _navigate_left(self):
if not self.buttons_widgets:
return
self.current_button_index = (self.current_button_index - 1) % len(
self.buttons_widgets
)
self.buttons_widgets[self.current_button_index].focus_set()
def _navigate_right(self):
if not self.buttons_widgets:
return
self.current_button_index = (self.current_button_index + 1) % len(
self.buttons_widgets
)
self.buttons_widgets[self.current_button_index].focus_set()
def _on_enter_pressed(self):
focused = self.window.focus_get()
if isinstance(focused, ttk.Button):
focused.invoke()
def _on_button_click(self, button_text: str) -> None:
"""
Sets `self.result` based on the clicked button.
- Returns `None` if the button is "Cancel", "Abort", or "Exit" **and** there are 3 or more buttons.
- Returns `True` if the button is "Yes", "Ok", "Continue", "Next", or "Start".
- Returns `False` in all other cases (e.g., "No", closing with X, or fewer than 3 buttons).
"""
# Check: If there are 3+ buttons and the button text matches "Cancel", "Abort", or "Exit"
if len(self.buttons) >= 3 and button_text.lower() in [
"cancel",
"abort",
"exit",
]:
self.result = None
# Check: Button text is "Yes", "Ok", "Continue", "Next", or "Start"
elif button_text.lower() in ["yes", "ok", "continue", "next", "start"]:
self.result = True
else:
# Fallback for all other cases (e.g., "No", closing with X, or fewer than 3 buttons)
self.result = False
self.window.destroy()
def show(self) -> Optional[bool]:
"""
Displays the dialog window and waits for user interaction.
Returns:
bool or None:
- `True` if "Yes", "Ok", etc. was clicked.
- `False` if "No" was clicked, or the window was closed with X (when there are 2 buttons).
- `None` if "Cancel", "Abort", or "Exit" was clicked **and** there are 3+ buttons,
or the window was closed with X (when there are 3+ buttons).
"""
self.window.wait_window()
return self.result

40
network.py Normal file
View File

@ -0,0 +1,40 @@
import socket
import urllib.request
import json
class GiteaUpdate:
@staticmethod
def api_down(url, current_version=""):
"""Get latest version from Gitea API"""
try:
with urllib.request.urlopen(url, timeout=10) as response:
data = json.loads(response.read().decode())
if data and len(data) > 0:
latest_version = data[0].get("tag_name", "Unknown")
return latest_version.lstrip("v") # Remove 'v' prefix if present
return "Unknown"
except Exception as e:
print(f"API Error: {e}")
return "Unknown"
class NetworkChecker:
@staticmethod
def check_internet_connection(host="8.8.8.8", port=53, timeout=3):
"""Check if internet connection is available"""
try:
socket.setdefaulttimeout(timeout)
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
return True
except socket.error:
return False
@staticmethod
def check_repository_access(url="https://git.ilunix.de", timeout=5):
"""Check if repository is accessible"""
try:
urllib.request.urlopen(url, timeout=timeout)
return True
except:
return False