feat: add image opacity, config migration, settings

Add image overlay opacity support and related UI/settings, plus robust config migration and error handling.

Key changes:
- UI: image overlay now uses QGraphicsOpacityEffect, hides when disabled, and updates opacity on state changes (src/interface/dialogs/image.py).
- New NotificationPopup dialog for user alerts (src/interface/dialogs/notificationpopup.py).
- Settings dialog: added Image Opacity control, adjusted labels, persist/rollback behavior on cancel, and include opacity when saving (src/interface/dialogs/settings.py).
- State: added properties for image_opacity and image_enabled with change notifications; renamed image_width setter param for clarity (src/utils/data/state.py).
- Config: switch from config.yml to config.ini, implement one-time migration, typed read/write of values, validation/error handling, automatic backups of corrupted files and user notification (src/utils/config/config.py).
- Settings save: accept and persist image_opacity (src/utils/config/settings.py).
- Startup: initialize QApplication earlier and pass app to run_gui (src/main.py).
- Minor: removed an extra blank line in log handler and small import cleanups.

These changes enable adjustable overlay transparency, improve config reliability, and surface errors to users while preserving/backuping old configs.
This commit is contained in:
Vxrtrauter
2026-04-04 07:36:03 +02:00
parent a3ff23147f
commit 1a7de70c0b
8 changed files with 176 additions and 64 deletions
+95 -43
View File
@@ -1,6 +1,9 @@
from interface.dialogs.notificationpopup import NotificationPopup
from utils.logging.logs import consoleLog
from utils.data.state import state
import configparser
import platform
import time
import os
@@ -8,30 +11,31 @@ def create_config():
config = configparser.ConfigParser()
config["General"] = {
"debug": True,
"ignore_updates": f"{state.ignore_updates}",
"autoresume": f"{state.autoresume}",
"window_transparency": f"{state.window_transparency}"
"debug": str(state.debug),
"ignore_updates": str(state.ignore_updates),
"autoresume": str(state.autoresume),
"window_transparency": str(state.window_transparency)
}
config["Network"] = {
"api_url": f"{state.api_url}",
"bound_interface": f"{state.bound_interface}" if state.bound_interface is not None else "None",
"download_speed_limit": f"{state.down_speed_limit}",
"upload_speed_limit": f"{state.up_speed_limit}",
"max_connections": f"{state.max_connections}",
"max_downloads": f"{state.max_downloads}"
"api_url": str(state.api_url),
"bound_interface": str(state.bound_interface) if state.bound_interface is not None else "None",
"download_speed_limit": str(state.down_speed_limit),
"upload_speed_limit": str(state.up_speed_limit),
"max_connections": str(state.max_connections),
"max_downloads": str(state.max_downloads)
}
config["Paths"] = {
"download_path": f"{state.download_path}",
"image_path": f"{state.image_path}"
"download_path": str(state.download_path),
"image_path": str(state.image_path)
}
config["Image"] = {
"enable_image": f"{state.image_enabled}",
"image_width": f"{state.image_width}",
"image_offset": f"{state.image_offset}"
"enable_image": str(state.image_enabled),
"image_width": str(state.image_width),
"image_offset": str(state.image_offset),
"image_opacity": str(state.image_opacity)
}
if platform.system() == "Windows":
@@ -42,51 +46,99 @@ def create_config():
state.settings_path = os.path.join(config_dir, "SoftwareManager")
os.makedirs(state.settings_path, exist_ok=True)
with open(os.path.join(state.settings_path, "config.yml"), 'w') as cf:
with open(os.path.join(state.settings_path, "config.ini"), 'w') as cf:
config.write(cf)
def read_config():
config = configparser.ConfigParser()
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
state.settings_path = os.path.join(config_dir, "SoftwareManager")
config_file = os.path.join(state.settings_path, "config.yml")
# Temporary migration logic, keep for a while
old_config_file = os.path.join(state.settings_path, "config.yml")
new_config_file = os.path.join(state.settings_path, "config.ini")
if not os.path.exists(config_file):
if os.path.exists(old_config_file):
try:
os.replace(old_config_file, new_config_file)
consoleLog("Successfully migrated config.yml to config.ini")
except Exception as e:
consoleLog(f"Failed to migrate config file: {e}")
if not os.path.exists(new_config_file):
create_config()
return
try:
config.read(new_config_file)
# General
state.debug = config.getboolean("General", "debug", fallback=state.debug)
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
config.read(config_file)
# General
state.debug = config.getboolean("General", "debug", fallback=state.debug)
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
# Network
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
state.bound_interface = config.get("Network", "bound_interface", fallback=state.bound_interface)
if state.bound_interface == "None":
state.bound_interface = None
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit)
state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections)
state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
# Network
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
state.bound_interface = config.get("Network", "bound_interface", fallback=state.bound_interface)
if state.bound_interface == "None":
state.bound_interface = None
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit)
state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections)
state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
# Paths
state.download_path = config.get("Paths", "download_path", fallback=state.download_path)
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
# Paths
state.download_path = config.get("Paths", "download_path", fallback=state.download_path)
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
# Image
state.image_enabled = config.getboolean("Image", "enable_image", fallback=state.image_enabled)
state.image_width = config.getint("Image", "image_width", fallback=state.image_width)
state.image_offset = config.getint("Image", "image_offset", fallback=state.image_offset)
state.image_opacity = config.getint("Image", "image_opacity", fallback=state.image_opacity)
# Image
state.image_enabled = config.getboolean("Image", "enable_image", fallback=state.image_enabled)
state.image_width = config.getint("Image", "image_width", fallback=state.image_width)
state.image_offset = config.getint("Image", "image_offset", fallback=state.image_offset)
create_config()
except configparser.Error as e:
consoleLog(f"Error: Configuration file corrupted ({e}). Resetting to defaults.")
backup_config(new_config_file) # Back up corrupted config
create_config() # Create new config file
NotificationPopup(
title="Config Reset",
text="Your configuration file has been reset due to a structural error.",
infotext="A backup of your old configuration file is available in the settings folder."
).exec()
create_config()
except ValueError as e:
consoleLog(f"Error: Invalid data types in configuration file ({e}). Resetting to defaults.")
backup_config(new_config_file) # Back up corrupted config
create_config() # Create new config file
NotificationPopup(
title="Config Reset",
text="Your configuration file has been reset due to invalid data types.",
infotext="A backup of your old configuration file is available in the settings folder."
).exec()
except Exception as e:
consoleLog(f"Unexpected error occurred while loading settings: {e}")
NotificationPopup(
title="Unexpected Error",
text="An unknown error occurred while loading your settings.",
infotext=f"System returned: {e}\n\nThe application may not function as expected."
).exec()
def backup_config(config_path):
if os.path.exists(config_path):
timestamp = time.strftime("%Y%m%d-%H%M%S")
backup_path = f"{config_path}.{timestamp}.bak"
try:
os.replace(config_path, backup_path)
consoleLog(f"Successfully created backup of corrupted config: {backup_path}")
except Exception as e:
consoleLog(f"Failed to create backup of corrupted config: {e}")