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
+10 -3
View File
@@ -1,6 +1,6 @@
from PySide6.QtWidgets import QLabel, QGraphicsOpacityEffect
from PySide6.QtCore import Qt, QSize, QEvent, QObject
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtWidgets import QLabel
from utils.data.state import state
import os
@@ -11,10 +11,12 @@ class Image(QObject):
self.application = parent
self.overlay_label = QLabel(parent)
self.overlay_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
self.opacity_effect = QGraphicsOpacityEffect()
self.overlay_label.setGraphicsEffect(self.opacity_effect)
self._current_image_path = None
parent.installEventFilter(self)
state.image_changed.connect(self.update_image_overlay)
if state.image_path and os.path.exists(state.image_path):
@@ -28,6 +30,7 @@ class Image(QObject):
def _load_and_display(self, image_path):
if state.image_enabled is not True:
self.overlay_label.hide()
return
self._current_image_path = image_path
parent = self.application
@@ -56,8 +59,12 @@ class Image(QObject):
x = parent.width() - self.overlay_label.width() - int(state.image_offset)
y = parent.height() - self.overlay_label.height() - int(state.image_offset)
self.opacity_effect.setOpacity(state.image_opacity / 100)
self.overlay_label.move(x, y)
self.overlay_label.show()
def update_image_overlay(self, new_image_path):
self._load_and_display(new_image_path)
@@ -0,0 +1,17 @@
from PySide6.QtWidgets import QMessageBox
import qdarktheme
class NotificationPopup(QMessageBox):
def __init__(self, title, text, infotext=None, parent=None):
super().__init__(parent)
qdarktheme.setup_theme("auto")
self.setIcon(QMessageBox.Icon.Information)
self.setWindowTitle(title)
self.setText(text)
if infotext is not None:
self.setInformativeText(infotext)
self.setStandardButtons(QMessageBox.StandardButton.Ok)
+17 -8
View File
@@ -27,6 +27,8 @@ SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path
def settings_dialog(self):
temp_image_width = state.image_width
temp_image_offset = state.image_offset
temp_image_opacity = state.image_opacity
temp_image_enabled = state.image_enabled
def create_widget(widget_type, label_text, **kwargs):
container = QWidget()
@@ -70,9 +72,6 @@ def settings_dialog(self):
if state.window_transparency and platform.system() != "Windows" and dialog:
dialog.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
def close_settings():
dialog.reject()
# Ignore Updates checkbox
update_checkbox_container, update_checkbox = create_widget(QCheckBox, "Ignore Updates: ")
if platform.system() == "Windows":
@@ -144,7 +143,7 @@ def settings_dialog(self):
browse_button.clicked.connect(browse_image_path)
# Enable Image Checkbox
enable_image_container, enable_image_checkbox = create_widget(QCheckBox, "Enable Image (needs Image Path): ", width=180, height=30)
enable_image_container, enable_image_checkbox = create_widget(QCheckBox, "Enable Image (requires image path): ", width=180, height=30)
enable_image_checkbox.setChecked(state.image_enabled)
enable_image_checkbox.toggled.connect(lambda checked: setattr(state, 'image_enabled', checked))
@@ -156,11 +155,18 @@ def settings_dialog(self):
image_width.valueChanged.connect(lambda val: setattr(state, 'image_width', val))
# Image Offset SpinBox
image_offset_container, image_offset = create_widget(QSpinBox, "Corner Image Offset: ", width=180, height=30)
image_offset_container, image_offset = create_widget(QSpinBox, "Corner Offset: ", width=180, height=30)
image_offset.setMinimum(0)
image_offset.setValue(state.image_offset)
image_offset.valueChanged.connect(lambda val: setattr(state, 'image_offset', val))
# Image Opacity SpinBox
image_opacity_container, image_opacity = create_widget(QSpinBox, "Image Opacity: ", width=180, height=30)
image_opacity.setMinimum(0)
image_opacity.setMaximum(100)
image_opacity.setValue(state.image_opacity)
image_opacity.valueChanged.connect(lambda val: setattr(state, 'image_opacity', val))
# Speed Limiting
down_speed_limit_container, down_speed_limit = create_widget(QSpinBox, "Max Download Speed (KiB, 0 for unlimited): ", width=180, height=30)
down_speed_limit.setMinimum(0)
@@ -224,7 +230,8 @@ def settings_dialog(self):
max_downloads.value(),
interface_select.currentText(),
image_width.value(),
image_offset.value()
image_offset.value(),
image_opacity.value()
)
cancel_btn.clicked.connect(dialog.reject)
@@ -233,7 +240,7 @@ def settings_dialog(self):
tabs = QtWidgets.QTabWidget()
create_tab("General", [autoresume_container, update_checkbox_container, transparent_window_container], tabs=tabs, stretch=True)
create_tab("Image", [enable_image_container, image_width_container, image_offset_container], tabs=tabs, stretch=True)
create_tab("Image", [enable_image_container, image_width_container, image_offset_container, image_opacity_container], tabs=tabs, stretch=True)
create_tab("Paths", [download_path_container, image_path_container], tabs=tabs, stretch=True)
create_tab("Network", [interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container], tabs=tabs, stretch=True)
@@ -241,10 +248,12 @@ def settings_dialog(self):
dialog_layout.addWidget(tabs)
dialog_layout.addLayout(layout)
def on_dialog_finished(result):
def on_dialog_finished(result): # Undo Image changes if "Save" button is not pressed
if result == QtWidgets.QDialog.DialogCode.Rejected:
state.image_width = temp_image_width
state.image_offset = temp_image_offset
state.image_opacity = temp_image_opacity
state.image_enabled = temp_image_enabled
dialog.finished.connect(on_dialog_finished)
dialog.exec()