mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
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:
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
+7
-4
@@ -20,8 +20,7 @@ import signal
|
||||
import time
|
||||
import sys
|
||||
|
||||
def run_gui():
|
||||
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
||||
def run_gui(app):
|
||||
qdarktheme.setup_theme("auto")
|
||||
widget = MainWindow()
|
||||
|
||||
@@ -42,7 +41,11 @@ def keyboardinterrupthandler(signum, frame):
|
||||
def main():
|
||||
# Begin counting startup time
|
||||
start_time = time.perf_counter()
|
||||
# Parse saved files
|
||||
|
||||
# Initialize UI Engine
|
||||
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
||||
|
||||
# Parse saved files
|
||||
read_config()
|
||||
logs = get_download_logs()
|
||||
_, downloads = split_data(logs)
|
||||
@@ -65,7 +68,7 @@ def main():
|
||||
elapsed = time.perf_counter() - start_time
|
||||
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
|
||||
# Launch GUI
|
||||
run_gui()
|
||||
run_gui(app)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+95
-43
@@ -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}")
|
||||
@@ -5,7 +5,7 @@ from utils.data.state import state
|
||||
|
||||
|
||||
|
||||
def save_settings(close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None, max_connections=None, max_downloads=None, bound_interface=None, image_width=None, image_offset=None):
|
||||
def save_settings(close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None, max_connections=None, max_downloads=None, bound_interface=None, image_width=None, image_offset=None, image_opacity=None):
|
||||
if apiurl is not None:
|
||||
state.api_url = apiurl
|
||||
if download_path is not None:
|
||||
@@ -28,6 +28,8 @@ def save_settings(close=lambda: None, apiurl=None, download_path=None, down_spee
|
||||
state.image_width = image_width
|
||||
if image_offset is not None:
|
||||
state.image_offset = image_offset
|
||||
if image_opacity is not None:
|
||||
state.image_opacity = image_opacity
|
||||
|
||||
update_settings() # Update LibTorrent Session Settings
|
||||
consoleLog("Saved Settings")
|
||||
|
||||
+27
-4
@@ -29,9 +29,12 @@ class AppState(QObject):
|
||||
self.interfaces: List = []
|
||||
self.active_interfaces: List = []
|
||||
self.bound_interface: Any = None
|
||||
|
||||
# Image
|
||||
self._image_enabled: bool = False
|
||||
self._image_width: int = 300 # Default to 300px
|
||||
self._image_offset: int = 50
|
||||
self.image_enabled: bool = False
|
||||
self._image_opacity: int = 100
|
||||
|
||||
# Trackers / Scraping
|
||||
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers
|
||||
@@ -76,9 +79,29 @@ class AppState(QObject):
|
||||
return self._image_width
|
||||
|
||||
@image_width.setter
|
||||
def image_width(self, new_offset: int):
|
||||
if new_offset != self._image_width:
|
||||
self._image_width = new_offset
|
||||
def image_width(self, new_width: int):
|
||||
if new_width != self._image_width:
|
||||
self._image_width = new_width
|
||||
self.image_changed.emit(self._image_path)
|
||||
|
||||
@property
|
||||
def image_opacity(self) -> int:
|
||||
return self._image_opacity
|
||||
|
||||
@image_opacity.setter
|
||||
def image_opacity(self, new_opacity: int):
|
||||
if new_opacity != self._image_opacity:
|
||||
self._image_opacity = new_opacity
|
||||
self.image_changed.emit(self._image_path)
|
||||
|
||||
@property
|
||||
def image_enabled(self) -> bool:
|
||||
return self._image_enabled
|
||||
|
||||
@image_enabled.setter
|
||||
def image_enabled(self, new_state: bool):
|
||||
if new_state != self._image_enabled:
|
||||
self._image_enabled = new_state
|
||||
self.image_changed.emit(self._image_path)
|
||||
|
||||
state = AppState()
|
||||
|
||||
@@ -3,7 +3,6 @@ from utils.logging.logs import consoleLog, remove_download_log
|
||||
import os
|
||||
|
||||
def split_data(data):
|
||||
|
||||
count = data.count
|
||||
downloads = data.data
|
||||
|
||||
|
||||
Reference in New Issue
Block a user