From b59768bcffed77fa777a08659e9e03e4fce8823b Mon Sep 17 00:00:00 2001 From: KeksNino Date: Sun, 22 Mar 2026 01:41:16 +0100 Subject: [PATCH 01/11] fix image resizing and general code of image class --- src/interface/dialogs/image.py | 63 +++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/src/interface/dialogs/image.py b/src/interface/dialogs/image.py index f39c327..04474f6 100644 --- a/src/interface/dialogs/image.py +++ b/src/interface/dialogs/image.py @@ -1,29 +1,60 @@ from PySide6.QtGui import QImage, QPixmap from PySide6.QtWidgets import QLabel from utils.data.state import state -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, QSize, QEvent, QObject import os -class Image(): +class Image(QObject): + TARGET_WIDTH = 300 + def __init__(self, parent): + super().__init__(parent) + self.application = parent self.overlay_label = QLabel(parent) self.overlay_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) + self._current_image_path = None - if state.image_path is not None and os.path.exists(state.image_path): - self.image = QImage(state.image_path) - self.image = self.image.scaledToWidth(300, Qt.TransformationMode.SmoothTransformation) + parent.installEventFilter(self) - self.pixmap = QPixmap.fromImage(self.image) - self.overlay_label.setPixmap(self.pixmap) - self.overlay_label.adjustSize() - self.overlay_label.raise_() + if state.image_path and os.path.exists(state.image_path): + self._load_and_display(state.image_path) - x = parent.width() - self.overlay_label.width() - y = parent.height() - self.overlay_label.height() - self.overlay_label.move(x, y) + def eventFilter(self, obj, event): + if obj == self.application and event.type() == QEvent.Type.Resize: + if self._current_image_path: + self._load_and_display(self._current_image_path) + return False + + def _load_and_display(self, image_path): + self._current_image_path = image_path + parent = self.application + image = QImage(image_path) + if image.isNull(): + self.overlay_label.hide() + return + + scaled_image = image.scaledToWidth( + self.TARGET_WIDTH, Qt.TransformationMode.SmoothTransformation + ) + + max_width = parent.width() + max_height = parent.height() + if scaled_image.width() > max_width or scaled_image.height() > max_height: + scaled_image = scaled_image.scaled( + QSize(max_width, max_height), + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + + pixmap = QPixmap.fromImage(scaled_image) + self.overlay_label.setPixmap(pixmap) + self.overlay_label.adjustSize() + self.overlay_label.raise_() + + x = parent.width() - self.overlay_label.width() - 100 + y = parent.height() - self.overlay_label.height() - 100 + self.overlay_label.move(x, y) + self.overlay_label.show() def update_image_overlay(self, new_image_path): - self.image = QImage(new_image_path) - self.pixmap = QPixmap.fromImage(self.image) - self.overlay_label.setPixmap(self.pixmap) - self.overlay_label.adjustSize() + self._load_and_display(new_image_path) From cb7e74a2248dfff88a799259f83ae19e60ee0906 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sat, 4 Apr 2026 02:06:06 +0200 Subject: [PATCH 02/11] Add configurable image overlay settings Introduce configurable image overlay options and persist them. Added new state fields (image_width, image_offset, image_enabled) with defaults and wired them into the image overlay: the image display now respects the enabled flag, uses image_width for scaling and image_offset for placement. Extended the Settings dialog (resized, API field sizing) with a new Image tab containing enable checkbox, width and offset spinboxes, and included these values when saving. Updated config read/write to include an Image section so settings persist across runs and extended save_settings to accept image_width and image_offset. Minor import/order cleanup in image dialog. --- src/interface/dialogs/image.py | 13 ++++---- src/interface/dialogs/settings.py | 51 +++++++++++++++++++++++++++++-- src/utils/config/config.py | 12 ++++++++ src/utils/config/settings.py | 8 +++-- src/utils/data/state.py | 3 ++ 5 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/interface/dialogs/image.py b/src/interface/dialogs/image.py index 04474f6..0fe7c09 100644 --- a/src/interface/dialogs/image.py +++ b/src/interface/dialogs/image.py @@ -1,12 +1,11 @@ +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 -from PySide6.QtCore import Qt, QSize, QEvent, QObject import os -class Image(QObject): - TARGET_WIDTH = 300 +class Image(QObject): def __init__(self, parent): super().__init__(parent) self.application = parent @@ -26,6 +25,8 @@ class Image(QObject): return False def _load_and_display(self, image_path): + if state.image_enabled is not True: + return self._current_image_path = image_path parent = self.application image = QImage(image_path) @@ -34,7 +35,7 @@ class Image(QObject): return scaled_image = image.scaledToWidth( - self.TARGET_WIDTH, Qt.TransformationMode.SmoothTransformation + int(state.image_width), Qt.TransformationMode.SmoothTransformation ) max_width = parent.width() @@ -51,8 +52,8 @@ class Image(QObject): self.overlay_label.adjustSize() self.overlay_label.raise_() - x = parent.width() - self.overlay_label.width() - 100 - y = parent.height() - self.overlay_label.height() - 100 + x = parent.width() - self.overlay_label.width() - int(state.image_offset) + y = parent.height() - self.overlay_label.height() - int(state.image_offset) self.overlay_label.move(x, y) self.overlay_label.show() diff --git a/src/interface/dialogs/settings.py b/src/interface/dialogs/settings.py index 0682323..2c3f58f 100644 --- a/src/interface/dialogs/settings.py +++ b/src/interface/dialogs/settings.py @@ -29,7 +29,7 @@ def settings_dialog(self): consoleLog("Settings dialog opened") dialog = QDialog(self) dialog.setWindowTitle("Settings") - dialog.setFixedSize(700, 450) + dialog.setFixedSize(580, 400) dialog_layout = QVBoxLayout() dialog.setLayout(dialog_layout) @@ -92,6 +92,8 @@ def settings_dialog(self): api_url_container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) api_url_container.setLayout(api_url_layout) api_url.setText(state.api_url) + api_url.setFixedWidth(180) + api_url.setFixedHeight(30) # Download Path @@ -158,6 +160,46 @@ def settings_dialog(self): image_path_layout.addWidget(browse_button) browse_button.clicked.connect(browse_image_path) + # Image Settings + + enable_image_container = QWidget() + enable_image_layout = QHBoxLayout() + + enable_image_checkbox = QCheckBox() + enable_image_container.setLayout(enable_image_layout) + enable_image_layout.addWidget(QLabel("Enable Image: ")) + + enable_image_layout.addStretch() + enable_image_checkbox.setChecked(state.image_enabled) + enable_image_checkbox.toggled.connect(lambda checked: setattr(state, 'image_enabled', checked)) + enable_image_layout.addWidget(enable_image_checkbox) + + image_width_container = QWidget() + image_width_layout = QHBoxLayout() + + image_width_layout.addWidget(QLabel("Image Width: ")) + image_width = QSpinBox() + image_width.setMinimum(0) + image_width.setMaximum(10000000) + image_width.setValue(state.image_width) + image_width_container.setLayout(image_width_layout) + image_width_layout.addWidget(image_width) + image_width.setFixedWidth(180) + image_width.setFixedHeight(30) + + + image_offset_container = QWidget() + image_offset_layout = QHBoxLayout() + + image_offset_layout.addWidget(QLabel("Corner image_offset: ")) + image_offset = QSpinBox() + image_offset.setMinimum(0) + image_offset.setMaximum(10000000) + image_offset.setValue(state.image_offset) + image_offset_container.setLayout(image_offset_layout) + image_offset_layout.addWidget(image_offset) + image_offset.setFixedWidth(180) + image_offset.setFixedHeight(30) # Speed Limiting down_speed_limit_container = QWidget() @@ -255,7 +297,10 @@ def settings_dialog(self): autoresume_checkbox.isChecked(), max_connections.value(), max_downloads.value(), - interface_select.currentText())) + interface_select.currentText(), + image_width.value(), + image_offset.value() + )) cancel_btn.clicked.connect(dialog.reject) layout.addWidget(cancel_btn) @@ -263,9 +308,11 @@ 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("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) + dialog_layout.addWidget(tabs) dialog_layout.addLayout(layout) diff --git a/src/utils/config/config.py b/src/utils/config/config.py index 3036ea8..6e8b484 100644 --- a/src/utils/config/config.py +++ b/src/utils/config/config.py @@ -28,6 +28,12 @@ def create_config(): "image_path": f"{state.image_path}" } + config["Image"] = { + "enable_image": f"{state.image_enabled}", + "image_width": f"{state.image_width}", + "image_offset": f"{state.image_offset}" + } + if platform.system() == "Windows": config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming")) else: @@ -77,4 +83,10 @@ def read_config(): 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) + + create_config() diff --git a/src/utils/config/settings.py b/src/utils/config/settings.py index 1d330fc..9de04e9 100644 --- a/src/utils/config/settings.py +++ b/src/utils/config/settings.py @@ -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): +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): if apiurl is not None: state.api_url = apiurl if download_path is not None: @@ -24,8 +24,12 @@ def save_settings(close=lambda: None, apiurl=None, download_path=None, down_spee state.max_downloads = max_downloads if bound_interface is not None: state.bound_interface = None if bound_interface == "None" else bound_interface + if image_width is not None: + state.image_width = image_width + if image_offset is not None: + state.image_offset = image_offset - update_settings() + update_settings() # Update LibTorrent Session Settings consoleLog("Saved Settings") create_config() close() diff --git a/src/utils/data/state.py b/src/utils/data/state.py index 75c3e79..041cf71 100644 --- a/src/utils/data/state.py +++ b/src/utils/data/state.py @@ -29,6 +29,9 @@ class AppState(QObject): self.interfaces: List = [] self.active_interfaces: List = [] self.bound_interface: Any = None + self.image_width: int = 300 # Default to 300px + self.image_offset: int = 50 + self.image_enabled: bool = False # Trackers / Scraping self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers From 3de2110a3e8d136963b3b89c5edaf2f85045e3cd Mon Sep 17 00:00:00 2001 From: KeksNino Date: Sat, 4 Apr 2026 02:51:45 +0200 Subject: [PATCH 03/11] fix: redraws pixmap after changing image settings so restart is no longer required --- src/interface/dialogs/image.py | 4 +++- src/interface/dialogs/settings.py | 4 ++-- src/utils/data/state.py | 24 ++++++++++++++++++++++-- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/interface/dialogs/image.py b/src/interface/dialogs/image.py index 0fe7c09..6fb69d9 100644 --- a/src/interface/dialogs/image.py +++ b/src/interface/dialogs/image.py @@ -15,6 +15,8 @@ class Image(QObject): parent.installEventFilter(self) + state.image_changed.connect(self.update_image_overlay) + if state.image_path and os.path.exists(state.image_path): self._load_and_display(state.image_path) @@ -35,7 +37,7 @@ class Image(QObject): return scaled_image = image.scaledToWidth( - int(state.image_width), Qt.TransformationMode.SmoothTransformation + state.image_width, Qt.TransformationMode.SmoothTransformation ) max_width = parent.width() diff --git a/src/interface/dialogs/settings.py b/src/interface/dialogs/settings.py index 2c3f58f..bb9077c 100644 --- a/src/interface/dialogs/settings.py +++ b/src/interface/dialogs/settings.py @@ -134,7 +134,7 @@ def settings_dialog(self): image_path_layout = QHBoxLayout() image_path = QLineEdit() - image_path_layout.addWidget(QLabel("Image Path (requires restart, experimental):")) + image_path_layout.addWidget(QLabel("Image Path:")) image_path_layout.addWidget(image_path) image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) image_path_container.setLayout(image_path_layout) @@ -167,7 +167,7 @@ def settings_dialog(self): enable_image_checkbox = QCheckBox() enable_image_container.setLayout(enable_image_layout) - enable_image_layout.addWidget(QLabel("Enable Image: ")) + enable_image_layout.addWidget(QLabel("Enable Image (needs image_path): ")) enable_image_layout.addStretch() enable_image_checkbox.setChecked(state.image_enabled) diff --git a/src/utils/data/state.py b/src/utils/data/state.py index 041cf71..7c63433 100644 --- a/src/utils/data/state.py +++ b/src/utils/data/state.py @@ -29,8 +29,8 @@ class AppState(QObject): self.interfaces: List = [] self.active_interfaces: List = [] self.bound_interface: Any = None - self.image_width: int = 300 # Default to 300px - self.image_offset: int = 50 + self._image_width: int = 300 # Default to 300px + self._image_offset: int = 50 self.image_enabled: bool = False # Trackers / Scraping @@ -61,4 +61,24 @@ class AppState(QObject): self._image_path = new_path self.image_changed.emit(new_path) + @property + def image_offset(self) -> int: + return self._image_offset + + @image_offset.setter + def image_offset(self, new_offset: int): + if new_offset != self._image_offset: + self._image_offset = new_offset + self.image_changed.emit(self._image_path) + + @property + def image_width(self) -> int: + 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 + self.image_changed.emit(self._image_path) + state = AppState() From fda45efcb5f59a0473724d37802d5d3fb0d758ad Mon Sep 17 00:00:00 2001 From: KeksNino Date: Sat, 4 Apr 2026 03:52:20 +0200 Subject: [PATCH 04/11] refactor: create a helper function to create widgets in settings dialog --- src/interface/dialogs/settings.py | 199 +++++++++--------------------- 1 file changed, 58 insertions(+), 141 deletions(-) diff --git a/src/interface/dialogs/settings.py b/src/interface/dialogs/settings.py index bb9077c..b0992a2 100644 --- a/src/interface/dialogs/settings.py +++ b/src/interface/dialogs/settings.py @@ -26,6 +26,37 @@ SVG_FOLDER = ' Date: Sat, 4 Apr 2026 05:36:16 +0200 Subject: [PATCH 05/11] Restore image settings on cancel; live sync Save original image_width/image_offset when opening Settings and restore them if the dialog is cancelled. Add valueChanged handlers to immediately update state.image_width and state.image_offset, enforce an image width maximum of 2500, and refactor Save wiring to a dedicated handler that calls save_settings(dialog.accept, ...). Connect dialog.finished to restore temp values on rejection and clean up dialog layout/logic. --- src/interface/dialogs/settings.py | 396 ++++++++++++++++-------------- 1 file changed, 205 insertions(+), 191 deletions(-) diff --git a/src/interface/dialogs/settings.py b/src/interface/dialogs/settings.py index b0992a2..047fdb2 100644 --- a/src/interface/dialogs/settings.py +++ b/src/interface/dialogs/settings.py @@ -25,190 +25,198 @@ SVG_FOLDER = '= 0: - interface_select.setCurrentIndex(index) - else: - interface_select.setCurrentIndex(0) - - interface_select.setFixedWidth(180) - interface_select.setFixedHeight(30) - interface_layout.addWidget(interface_select) - interface_container.setLayout(interface_layout) - - - # Save / Cancel buttons + def create_widget(widget_type, label_text, **kwargs): + container = QWidget() layout = QHBoxLayout() + container.setLayout(layout) + layout.addWidget(QLabel(label_text)) - save_btn = QPushButton("Save") - cancel_btn = QPushButton("Cancel") - save_btn.clicked.connect(lambda: save_settings( - close_settings, + widget = widget_type() + + if widget_type in (QSpinBox,): + layout.addWidget(widget) + widget.setMinimum(kwargs.get("minimum", 0)) + widget.setMaximum(kwargs.get("maximum", 10000000)) + widget.setFixedWidth(kwargs.get("width", 180)) + widget.setFixedHeight(kwargs.get("height", 30)) + elif widget_type in (QCheckBox,): + layout.addStretch() + layout.addWidget(widget) + elif widget_type in (QLineEdit,): + layout.addWidget(widget) + container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) + if "width" in kwargs: + widget.setFixedWidth(kwargs["width"]) + if "height" in kwargs: + widget.setFixedHeight(kwargs["height"]) + elif widget_type in (QComboBox,): + layout.addWidget(widget) + widget.setFixedWidth(kwargs.get("width", 180)) + widget.setFixedHeight(kwargs.get("height", 30)) + + return container, widget + + consoleLog("Settings dialog opened") + dialog = QDialog(self) + dialog.setWindowTitle("Settings") + dialog.setFixedSize(580, 400) + + dialog_layout = QVBoxLayout() + dialog.setLayout(dialog_layout) + + 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": + update_checkbox.setChecked(state.ignore_updates) + update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked)) + + # Autoresume Container Checkbox + autoresume_container, autoresume_checkbox = create_widget(QCheckBox, "Auto-Resume Downloads: ") + autoresume_checkbox.setChecked(state.autoresume) + autoresume_checkbox.toggled.connect(lambda checked: setattr(state, 'autoresume', checked)) + + # Transparent Window Checkbox + transparent_window_container, transparent_window_checkbox = create_widget(QCheckBox, "Window Transparency (requires restart) (Linux/MacOS only): ") + transparent_window_checkbox.setChecked(state.window_transparency) + transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked)) + + # API URL Widget + api_url_container, api_url = create_widget(QLineEdit, "API Server URL: ", width=180, height=30) + api_url.setText(state.api_url) + + # Download Path Widget + download_path_container, download_path = create_widget(QLineEdit, "Download Path: ") + download_path.setText(state.download_path) + download_path_layout = download_path_container.layout() + + def browse_download_path(): + dir_path = QFileDialog.getExistingDirectory(dialog, "Select Download Directory", state.download_path) + if dir_path: + download_path.setText(dir_path) + + browse_button = create_widget(QPushButton, "", width=36, height=36)[1] + browse_button.setIconSize(QSize(24, 24)) + browse_button.setIcon(svg_icon(SVG_FOLDER, 24)) + browse_button.setCursor(Qt.CursorShape.PointingHandCursor) + browse_button.setStyleSheet(""" + QPushButton { + border: none; + background: transparent; + padding: 0px; + } + """) + + download_path_layout.addWidget(browse_button) + browse_button.clicked.connect(browse_download_path) + + + # Image Path + image_path_container, image_path = create_widget(QLineEdit, "Image Path: ") + image_path.setText(state.image_path) + image_path_layout = image_path_container.layout() + + def browse_image_path(): + file_path = QFileDialog.getOpenFileName(dialog, "Select Image File", state.image_path, "Image Files (*.png *.jpg)")[0] + if file_path: + image_path.setText(file_path) + + browse_button = create_widget(QPushButton, "", width=36, height=36)[1] + browse_button.setIconSize(QSize(24, 24)) + browse_button.setIcon(svg_icon(SVG_FOLDER, 24)) + browse_button.setCursor(Qt.CursorShape.PointingHandCursor) + browse_button.setStyleSheet(""" + QPushButton { + border: none; + background: transparent; + padding: 0px; + } + """) + image_path_layout.addWidget(browse_button) + 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_checkbox.setChecked(state.image_enabled) + enable_image_checkbox.toggled.connect(lambda checked: setattr(state, 'image_enabled', checked)) + + # Image Width SpinBox + image_width_container, image_width = create_widget(QSpinBox, "Image Width: ", width=180, height=30) + image_width.setMinimum(0) + image_width.setMaximum(2500) + image_width.setValue(state.image_width) + 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.setMinimum(0) + image_offset.setValue(state.image_offset) + image_offset.valueChanged.connect(lambda val: setattr(state, 'image_offset', 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) + down_speed_limit.setValue(state.down_speed_limit) + + up_speed_limit_container, up_speed_limit = create_widget(QSpinBox, "Max Upload Speed (KiB, 0 for unlimited): ", width=180, height=30) + up_speed_limit.setMinimum(0) + up_speed_limit.setValue(state.up_speed_limit) + + # Connection Configs + max_connections_container, max_connections = create_widget(QSpinBox, "Max Connections: ", width=180, height=30) + max_connections.setMinimum(0) + max_connections.setValue(state.max_connections) + + + # Download Configs + max_downloads_container, max_downloads = create_widget(QSpinBox, "Max Downloads: ", width=180, height=30) + max_downloads.setMinimum(0) + max_downloads.setValue(state.max_downloads) + + # Interface Binding + interface_container = QWidget() + interface_layout = QHBoxLayout() + + interface_label = QLabel("Network Interface:") + interface_layout.addWidget(interface_label) + interface_select = QComboBox() + interface_select.addItems(["None"] + state.interfaces) + + target = state.bound_interface if state.bound_interface else "None" + + index = interface_select.findText(target) + if index >= 0: + interface_select.setCurrentIndex(index) + else: + interface_select.setCurrentIndex(0) + + interface_select.setFixedWidth(180) + interface_select.setFixedHeight(30) + interface_layout.addWidget(interface_select) + interface_container.setLayout(interface_layout) + + + # Save / Cancel buttons + layout = QHBoxLayout() + + save_btn = QPushButton("Save") + cancel_btn = QPushButton("Cancel") + save_btn.clicked.connect(lambda: handle_save()) + + def handle_save(): + save_settings( + dialog.accept, api_url.text(), download_path.text(), - down_speed_limit.value(), + down_speed_limit.value(), up_speed_limit.value(), image_path.text(), autoresume_checkbox.isChecked(), @@ -217,20 +225,26 @@ def settings_dialog(self): interface_select.currentText(), image_width.value(), image_offset.value() - )) + ) - cancel_btn.clicked.connect(dialog.reject) - layout.addWidget(cancel_btn) - layout.addWidget(save_btn) + cancel_btn.clicked.connect(dialog.reject) + layout.addWidget(cancel_btn) + layout.addWidget(save_btn) - 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("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) + 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("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) - dialog_layout.addWidget(tabs) - dialog_layout.addLayout(layout) + dialog_layout.addWidget(tabs) + dialog_layout.addLayout(layout) - dialog.exec() + def on_dialog_finished(result): + if result == QtWidgets.QDialog.DialogCode.Rejected: + state.image_width = temp_image_width + state.image_offset = temp_image_offset + + dialog.finished.connect(on_dialog_finished) + dialog.exec() From 1a7de70c0b1282aba1beb0ce77086fa4a56fb04d Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:36:03 +0200 Subject: [PATCH 06/11] 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. --- src/interface/dialogs/image.py | 13 +- src/interface/dialogs/notificationpopup.py | 17 +++ src/interface/dialogs/settings.py | 25 ++-- src/main.py | 11 +- src/utils/config/config.py | 138 ++++++++++++++------- src/utils/config/settings.py | 4 +- src/utils/data/state.py | 31 ++++- src/utils/logging/loghandler.py | 1 - 8 files changed, 176 insertions(+), 64 deletions(-) create mode 100644 src/interface/dialogs/notificationpopup.py diff --git a/src/interface/dialogs/image.py b/src/interface/dialogs/image.py index 6fb69d9..90aa155 100644 --- a/src/interface/dialogs/image.py +++ b/src/interface/dialogs/image.py @@ -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) + diff --git a/src/interface/dialogs/notificationpopup.py b/src/interface/dialogs/notificationpopup.py new file mode 100644 index 0000000..9f78ca8 --- /dev/null +++ b/src/interface/dialogs/notificationpopup.py @@ -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) \ No newline at end of file diff --git a/src/interface/dialogs/settings.py b/src/interface/dialogs/settings.py index 047fdb2..e815357 100644 --- a/src/interface/dialogs/settings.py +++ b/src/interface/dialogs/settings.py @@ -27,6 +27,8 @@ SVG_FOLDER = ' 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() diff --git a/src/utils/logging/loghandler.py b/src/utils/logging/loghandler.py index ae52c8b..f9d5b3d 100644 --- a/src/utils/logging/loghandler.py +++ b/src/utils/logging/loghandler.py @@ -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 From f11837217c76c6892cfa13740f63bceae3a5796c Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sun, 5 Apr 2026 04:02:10 +0200 Subject: [PATCH 07/11] feat: add basic "wallpaper" functionality --- src/interface/dialogs/image.py | 59 ++++++++++++++++++++++------------ src/utils/data/state.py | 11 +++++++ 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/interface/dialogs/image.py b/src/interface/dialogs/image.py index 90aa155..a81e514 100644 --- a/src/interface/dialogs/image.py +++ b/src/interface/dialogs/image.py @@ -39,32 +39,49 @@ class Image(QObject): self.overlay_label.hide() return - scaled_image = image.scaledToWidth( - state.image_width, Qt.TransformationMode.SmoothTransformation - ) - - max_width = parent.width() - max_height = parent.height() - if scaled_image.width() > max_width or scaled_image.height() > max_height: - scaled_image = scaled_image.scaled( - QSize(max_width, max_height), - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation, + if getattr(state, "image_as_wallpaper", False): + scaled_image = image.scaled( + parent.size(), + Qt.AspectRatioMode.KeepAspectRatioByExpanding, + Qt.TransformationMode.SmoothTransformation ) - pixmap = QPixmap.fromImage(scaled_image) - self.overlay_label.setPixmap(pixmap) - self.overlay_label.adjustSize() - self.overlay_label.raise_() + crop_x = (scaled_image.width() - parent.width()) // 2 + crop_y = (scaled_image.height() - parent.height()) // 2 + scaled_image = scaled_image.copy(crop_x, crop_y, parent.width(), parent.height()) - x = parent.width() - self.overlay_label.width() - int(state.image_offset) - y = parent.height() - self.overlay_label.height() - int(state.image_offset) + pixmap = QPixmap.fromImage(scaled_image) + self.overlay_label.setPixmap(pixmap) - self.opacity_effect.setOpacity(state.image_opacity / 100) + self.overlay_label.setGeometry(0, 0, parent.width(), parent.height()) + self.overlay_label.lower() - self.overlay_label.move(x, y) + else: + scaled_image = image.scaledToWidth( + state.image_width, Qt.TransformationMode.SmoothTransformation + ) + + max_width = parent.width() + max_height = parent.height() + if scaled_image.width() > max_width or scaled_image.height() > max_height: + scaled_image = scaled_image.scaled( + QSize(max_width, max_height), + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + + pixmap = QPixmap.fromImage(scaled_image) + self.overlay_label.setPixmap(pixmap) + self.overlay_label.adjustSize() + self.overlay_label.raise_() + + 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) - + self._load_and_display(new_image_path) \ No newline at end of file diff --git a/src/utils/data/state.py b/src/utils/data/state.py index 9591b2f..1a918bf 100644 --- a/src/utils/data/state.py +++ b/src/utils/data/state.py @@ -35,6 +35,7 @@ class AppState(QObject): self._image_width: int = 300 # Default to 300px self._image_offset: int = 50 self._image_opacity: int = 100 + self._image_as_wallpaper: bool = True # Trackers / Scraping self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers @@ -104,4 +105,14 @@ class AppState(QObject): self._image_enabled = new_state self.image_changed.emit(self._image_path) + @property + def image_as_wallpaper(self) -> bool: + return self._image_as_wallpaper + + @image_as_wallpaper.setter + def image_as_wallpaper(self, new_state: bool): + if new_state != self._image_as_wallpaper: + self._image_as_wallpaper = new_state + self.image_changed.emit(self._image_path) + state = AppState() From 2ad02435a113a188dce261ab0411fc5f9230f00d Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sun, 5 Apr 2026 05:22:24 +0200 Subject: [PATCH 08/11] feat/fix: "wallpaper" transparency handling Introduce dynamic wallpaper transparency support for the image overlay. Adds QStackedWidget import and a new _set_wallpaper_transparency method that toggles WA_TranslucentBackground on the main window and key child widgets, updates stylesheets (with originals preserved/restored), and uses darkdetect to adapt combo popup background. Adds state fields (_wallpaper_active, _original_stylesheets), sets overlay opacity from state, and calls the transparency toggler when loading/hiding the overlay to avoid redundant updates. --- src/interface/dialogs/image.py | 90 +++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/src/interface/dialogs/image.py b/src/interface/dialogs/image.py index a81e514..3edb46b 100644 --- a/src/interface/dialogs/image.py +++ b/src/interface/dialogs/image.py @@ -1,7 +1,8 @@ -from PySide6.QtWidgets import QLabel, QGraphicsOpacityEffect +from PySide6.QtWidgets import QLabel, QGraphicsOpacityEffect, QStackedWidget from PySide6.QtCore import Qt, QSize, QEvent, QObject from PySide6.QtGui import QImage, QPixmap from utils.data.state import state +import darkdetect import os @@ -16,6 +17,8 @@ class Image(QObject): self.overlay_label.setGraphicsEffect(self.opacity_effect) self._current_image_path = None + self._wallpaper_active = False + self._original_stylesheets = {} parent.installEventFilter(self) state.image_changed.connect(self.update_image_overlay) @@ -28,9 +31,90 @@ class Image(QObject): self._load_and_display(self._current_image_path) return False + def _set_wallpaper_transparency(self, enabled): + if self._wallpaper_active == enabled: + return + self._wallpaper_active = enabled + parent = self.application + + central = parent.centralWidget() + if central: + central.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled) + + for attr in ['tab_wrapper', 'corner_widget', 'titlebar']: + w = getattr(parent, attr, None) + if w: + w.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled) + + if hasattr(parent, 'tabs'): + tabs = parent.tabs + tabs.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled) + + for i in range(tabs.count()): + page = tabs.widget(i) + if page: + page.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled) + + stack = tabs.findChild(QStackedWidget) + if stack: + stack.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled) + + tab_bar = tabs.tabBar() + if tab_bar: + tab_bar.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled) + + if 'tabs' not in self._original_stylesheets: + self._original_stylesheets['tabs'] = tabs.styleSheet() + if enabled: + tabs.setStyleSheet( + self._original_stylesheets['tabs'] + + "\nQTabWidget::pane { background: transparent; }" + + "\nQTabBar { background: transparent; }" + + "\nQTabBar::tab { background: transparent; }" + ) + else: + tabs.setStyleSheet(self._original_stylesheets['tabs']) + + # Searchbar + if hasattr(parent, 'searchbar'): + if 'searchbar' not in self._original_stylesheets: + self._original_stylesheets['searchbar'] = parent.searchbar.styleSheet() + if enabled: + parent.searchbar.setStyleSheet("QLineEdit { background-color: transparent; }") + else: + parent.searchbar.setStyleSheet(self._original_stylesheets['searchbar']) + + # Download button + if hasattr(parent, 'dlbutton'): + if 'dlbutton' not in self._original_stylesheets: + self._original_stylesheets['dlbutton'] = parent.dlbutton.styleSheet() + if enabled: + parent.dlbutton.setStyleSheet("QPushButton { background-color: transparent; }") + else: + parent.dlbutton.setStyleSheet(self._original_stylesheets['dlbutton']) + + # Labels + for attr in ['emptyResults', 'emptyDownload']: + w = getattr(parent, attr, None) + if w: + w.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled) + + if hasattr(parent, 'tracker_list'): + if 'tracker_list' not in self._original_stylesheets: + self._original_stylesheets['tracker_list'] = parent.tracker_list.styleSheet() + if enabled: + popup_bg = '#1e1e1e' if darkdetect.isDark() else '#ffffff' + parent.tracker_list.setStyleSheet( + "QComboBox { background-color: transparent; }" + f" QComboBox QAbstractItemView {{ background-color: {popup_bg}; }}" + ) + else: + parent.tracker_list.setStyleSheet(self._original_stylesheets['tracker_list']) + def _load_and_display(self, image_path): if state.image_enabled is not True: self.overlay_label.hide() + self._set_wallpaper_transparency(False) return self._current_image_path = image_path parent = self.application @@ -56,7 +140,11 @@ class Image(QObject): self.overlay_label.setGeometry(0, 0, parent.width(), parent.height()) self.overlay_label.lower() + self.opacity_effect.setOpacity(state.image_opacity / 100) + self._set_wallpaper_transparency(True) + else: + self._set_wallpaper_transparency(False) scaled_image = image.scaledToWidth( state.image_width, Qt.TransformationMode.SmoothTransformation ) From 9e3d1cf90a0c91ab35675e5450903027c1ce9b0c Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sun, 5 Apr 2026 06:12:54 +0200 Subject: [PATCH 09/11] feat: Add image position presets and sliders Introduce image positioning presets and UI improvements for image overlay control. image.py: compute overlay coordinates for top-left/top-right/bottom-left/bottom-right/center positions and respect corner offset and opacity. settings.py: replace spinboxes with horizontal sliders (with value labels and custom style), add Wallpaper Mode checkbox and Position combo, wire controls to state and include them in the settings tab; ensure dialog rollback restores new temp settings. config/config.py & utils/settings.py: persist and load new image_as_wallpaper and image_position settings and extend save_settings to accept them. data/state.py: add new state fields and properties for image_position, custom position flags and x/y coordinates; change default image_as_wallpaper to false. These changes enable preset positioning, improved UX for sizing/offset/opacity, and persistence of new image options. --- src/interface/dialogs/image.py | 16 +++++- src/interface/dialogs/settings.py | 86 ++++++++++++++++++++++++++----- src/utils/config/config.py | 6 ++- src/utils/config/settings.py | 6 ++- src/utils/data/state.py | 43 +++++++++++++++- 5 files changed, 139 insertions(+), 18 deletions(-) diff --git a/src/interface/dialogs/image.py b/src/interface/dialogs/image.py index 3edb46b..f921958 100644 --- a/src/interface/dialogs/image.py +++ b/src/interface/dialogs/image.py @@ -163,8 +163,20 @@ class Image(QObject): self.overlay_label.adjustSize() self.overlay_label.raise_() - x = parent.width() - self.overlay_label.width() - int(state.image_offset) - y = parent.height() - self.overlay_label.height() - int(state.image_offset) + pos = state.image_position + off = int(state.image_offset) + w = self.overlay_label.width() + h = self.overlay_label.height() + if pos == "top-left": + x, y = off, off + elif pos == "top-right": + x, y = parent.width() - w - off, off + elif pos == "bottom-left": + x, y = off, parent.height() - h - off + elif pos == "center": + x, y = (parent.width() - w) // 2, (parent.height() - h) // 2 + else: # bottom-right (default) + x, y = parent.width() - w - off, parent.height() - h - off self.opacity_effect.setOpacity(state.image_opacity / 100) diff --git a/src/interface/dialogs/settings.py b/src/interface/dialogs/settings.py index e815357..e71d1a4 100644 --- a/src/interface/dialogs/settings.py +++ b/src/interface/dialogs/settings.py @@ -13,8 +13,7 @@ from PySide6.QtWidgets import ( QDialog, QLabel, QHBoxLayout, - QSpinBox, - QCheckBox, + QSpinBox, QSlider, QCheckBox, QFileDialog, QComboBox, ) @@ -29,6 +28,8 @@ def settings_dialog(self): temp_image_offset = state.image_offset temp_image_opacity = state.image_opacity temp_image_enabled = state.image_enabled + temp_image_as_wallpaper = state.image_as_wallpaper + temp_image_position = state.image_position def create_widget(widget_type, label_text, **kwargs): container = QWidget() @@ -147,25 +148,80 @@ def settings_dialog(self): enable_image_checkbox.setChecked(state.image_enabled) enable_image_checkbox.toggled.connect(lambda checked: setattr(state, 'image_enabled', checked)) - # Image Width SpinBox - image_width_container, image_width = create_widget(QSpinBox, "Image Width: ", width=180, height=30) + # Image Mode Checkbox + image_mode_container, image_mode_checkbox = create_widget(QCheckBox, "Wallpaper Mode: ") + image_mode_checkbox.setChecked(state.image_as_wallpaper) + image_mode_checkbox.toggled.connect(lambda checked: setattr(state, 'image_as_wallpaper', checked)) + + # Image Position Preset + positions = ["bottom-right", "bottom-left", "top-right", "top-left", "center"] + image_position_container, image_position_combo = create_widget(QComboBox, "Position: ", width=180, height=30) + image_position_combo.addItems(positions) + idx = image_position_combo.findText(state.image_position) + image_position_combo.setCurrentIndex(idx if idx >= 0 else 0) + image_position_combo.currentTextChanged.connect(lambda val: setattr(state, 'image_position', val)) + + _slider_style = """ + QSlider::groove:horizontal { + height: 3px; + background: palette(mid); + border-radius: 1px; + } + QSlider::handle:horizontal { + width: 10px; + height: 10px; + margin: -4px 0; + border-radius: 5px; + } + """ + + # Image Width Slider + image_width_container = QWidget() + image_width_layout = QHBoxLayout(image_width_container) + image_width_layout.addWidget(QLabel("Image Width: ")) + image_width = QSlider(Qt.Orientation.Horizontal) + image_width.setFixedWidth(180) + image_width.setStyleSheet(_slider_style) image_width.setMinimum(0) image_width.setMaximum(2500) image_width.setValue(state.image_width) - image_width.valueChanged.connect(lambda val: setattr(state, 'image_width', val)) + image_width_val = QLabel(str(state.image_width)) + image_width_val.setFixedWidth(36) + image_width.valueChanged.connect(lambda val: (setattr(state, 'image_width', val), image_width_val.setText(str(val)))) + image_width_layout.addWidget(image_width_val) + image_width_layout.addWidget(image_width) - # Image Offset SpinBox - image_offset_container, image_offset = create_widget(QSpinBox, "Corner Offset: ", width=180, height=30) + # Image Offset Slider + image_offset_container = QWidget() + image_offset_layout = QHBoxLayout(image_offset_container) + image_offset_layout.addWidget(QLabel("Corner Offset: ")) + image_offset = QSlider(Qt.Orientation.Horizontal) + image_offset.setFixedWidth(180) + image_offset.setStyleSheet(_slider_style) image_offset.setMinimum(0) + image_offset.setMaximum(500) image_offset.setValue(state.image_offset) - image_offset.valueChanged.connect(lambda val: setattr(state, 'image_offset', val)) + image_offset_val = QLabel(str(state.image_offset)) + image_offset_val.setFixedWidth(36) + image_offset.valueChanged.connect(lambda val: (setattr(state, 'image_offset', val), image_offset_val.setText(str(val)))) + image_offset_layout.addWidget(image_offset_val) + image_offset_layout.addWidget(image_offset) - # Image Opacity SpinBox - image_opacity_container, image_opacity = create_widget(QSpinBox, "Image Opacity: ", width=180, height=30) + # Image Opacity Slider + image_opacity_container = QWidget() + image_opacity_layout = QHBoxLayout(image_opacity_container) + image_opacity_layout.addWidget(QLabel("Image Opacity: ")) + image_opacity = QSlider(Qt.Orientation.Horizontal) + image_opacity.setFixedWidth(180) + image_opacity.setStyleSheet(_slider_style) 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)) + image_opacity_val = QLabel(str(state.image_opacity)) + image_opacity_val.setFixedWidth(36) + image_opacity.valueChanged.connect(lambda val: (setattr(state, 'image_opacity', val), image_opacity_val.setText(str(val)))) + image_opacity_layout.addWidget(image_opacity_val) + image_opacity_layout.addWidget(image_opacity) # Speed Limiting down_speed_limit_container, down_speed_limit = create_widget(QSpinBox, "Max Download Speed (KiB, 0 for unlimited): ", width=180, height=30) @@ -231,7 +287,9 @@ def settings_dialog(self): interface_select.currentText(), image_width.value(), image_offset.value(), - image_opacity.value() + image_opacity.value(), + image_as_wallpaper=image_mode_checkbox.isChecked(), + image_position=image_position_combo.currentText() ) cancel_btn.clicked.connect(dialog.reject) @@ -240,7 +298,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, image_opacity_container], tabs=tabs, stretch=True) + create_tab("Image", [enable_image_container, image_mode_container, image_position_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) @@ -254,6 +312,8 @@ def settings_dialog(self): state.image_offset = temp_image_offset state.image_opacity = temp_image_opacity state.image_enabled = temp_image_enabled + state.image_as_wallpaper = temp_image_as_wallpaper + state.image_position = temp_image_position dialog.finished.connect(on_dialog_finished) dialog.exec() diff --git a/src/utils/config/config.py b/src/utils/config/config.py index 23d024a..15cf7a4 100644 --- a/src/utils/config/config.py +++ b/src/utils/config/config.py @@ -35,7 +35,9 @@ def create_config(): "enable_image": str(state.image_enabled), "image_width": str(state.image_width), "image_offset": str(state.image_offset), - "image_opacity": str(state.image_opacity) + "image_opacity": str(state.image_opacity), + "image_as_wallpaper": str(state.image_as_wallpaper), + "image_position": str(state.image_position) } if platform.system() == "Windows": @@ -102,6 +104,8 @@ def read_config(): 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) + state.image_as_wallpaper = config.getboolean("Image", "image_as_wallpaper", fallback=state.image_as_wallpaper) + state.image_position = config.get("Image", "image_position", fallback=state.image_position) create_config() diff --git a/src/utils/config/settings.py b/src/utils/config/settings.py index 947881b..2fc94a6 100644 --- a/src/utils/config/settings.py +++ b/src/utils/config/settings.py @@ -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, image_opacity=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, image_as_wallpaper=None, image_position=None): if apiurl is not None: state.api_url = apiurl if download_path is not None: @@ -30,6 +30,10 @@ def save_settings(close=lambda: None, apiurl=None, download_path=None, down_spee state.image_offset = image_offset if image_opacity is not None: state.image_opacity = image_opacity + if image_as_wallpaper is not None: + state.image_as_wallpaper = image_as_wallpaper + if image_position is not None: + state.image_position = image_position update_settings() # Update LibTorrent Session Settings consoleLog("Saved Settings") diff --git a/src/utils/data/state.py b/src/utils/data/state.py index 1a918bf..75ed4eb 100644 --- a/src/utils/data/state.py +++ b/src/utils/data/state.py @@ -35,7 +35,8 @@ class AppState(QObject): self._image_width: int = 300 # Default to 300px self._image_offset: int = 50 self._image_opacity: int = 100 - self._image_as_wallpaper: bool = True + self._image_as_wallpaper: bool = False + self._image_position: str = "bottom-right" # top-left, top-right, bottom-left, bottom-right, center # Trackers / Scraping self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers @@ -115,4 +116,44 @@ class AppState(QObject): self._image_as_wallpaper = new_state self.image_changed.emit(self._image_path) + @property + def image_position(self) -> str: + return self._image_position + + @image_position.setter + def image_position(self, new_pos: str): + if new_pos != self._image_position: + self._image_position = new_pos + self.image_changed.emit(self._image_path) + + @property + def image_custom_position(self) -> bool: + return self._image_custom_position + + @image_custom_position.setter + def image_custom_position(self, new_state: bool): + if new_state != self._image_custom_position: + self._image_custom_position = new_state + self.image_changed.emit(self._image_path) + + @property + def image_x(self) -> int: + return self._image_x + + @image_x.setter + def image_x(self, val: int): + if val != self._image_x: + self._image_x = val + self.image_changed.emit(self._image_path) + + @property + def image_y(self) -> int: + return self._image_y + + @image_y.setter + def image_y(self, val: int): + if val != self._image_y: + self._image_y = val + self.image_changed.emit(self._image_path) + state = AppState() From 12506dd1df0a2970dae04215d799000e34199eb9 Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Sun, 5 Apr 2026 06:39:29 +0200 Subject: [PATCH 10/11] feat: Add customizable accent color setting Introduce an accent color preference across the app: add a settings UI input (with restart note), persist it to AppState and config (read/write), and accept it in save_settings. Integrate the value into theming: compute a semi-transparent selection color, apply it to table selection styles, and pass it to qdarktheme as the primary color (including when window transparency is enabled). Also apply the accent to the tracker combo selection styling as a UI polish. --- src/interface/dialogs/settings.py | 10 ++++++++-- src/interface/dialogs/theme.py | 21 ++++++++++++++++++++- src/interface/gui.py | 5 +++++ src/main.py | 8 ++++++-- src/utils/config/config.py | 4 +++- src/utils/config/settings.py | 4 +++- src/utils/data/state.py | 1 + 7 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/interface/dialogs/settings.py b/src/interface/dialogs/settings.py index e71d1a4..2fb6652 100644 --- a/src/interface/dialogs/settings.py +++ b/src/interface/dialogs/settings.py @@ -88,6 +88,11 @@ def settings_dialog(self): transparent_window_container, transparent_window_checkbox = create_widget(QCheckBox, "Window Transparency (requires restart) (Linux/MacOS only): ") transparent_window_checkbox.setChecked(state.window_transparency) transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked)) + + # Accent Color + accent_color_container, accent_color_input = create_widget(QLineEdit, "Accent Color (requires restart): ", width=180, height=30) + accent_color_input.setPlaceholderText("e.g. #fca7d7") + accent_color_input.setText(state.accent_color) # API URL Widget api_url_container, api_url = create_widget(QLineEdit, "API Server URL: ", width=180, height=30) @@ -289,7 +294,8 @@ def settings_dialog(self): image_offset.value(), image_opacity.value(), image_as_wallpaper=image_mode_checkbox.isChecked(), - image_position=image_position_combo.currentText() + image_position=image_position_combo.currentText(), + accent_color=accent_color_input.text().strip() ) cancel_btn.clicked.connect(dialog.reject) @@ -297,7 +303,7 @@ def settings_dialog(self): layout.addWidget(save_btn) tabs = QtWidgets.QTabWidget() - create_tab("General", [autoresume_container, update_checkbox_container, transparent_window_container], tabs=tabs, stretch=True) + create_tab("General", [autoresume_container, update_checkbox_container, transparent_window_container, accent_color_container], tabs=tabs, stretch=True) create_tab("Image", [enable_image_container, image_mode_container, image_position_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) diff --git a/src/interface/dialogs/theme.py b/src/interface/dialogs/theme.py index 18011f8..2b23212 100644 --- a/src/interface/dialogs/theme.py +++ b/src/interface/dialogs/theme.py @@ -1,4 +1,6 @@ from PySide6.QtGui import QColor +from utils.logging.logs import consoleLog +from utils.data.state import state import darkdetect def _is_dark_mode(): @@ -27,15 +29,32 @@ def _theme_colors(): } +def _accent_selection_color(alpha: float = 0.35) -> str: + if not state.accent_color: + return None + try: + hex_color = state.accent_color.lstrip('#') + if len(hex_color) != 6: + return None + r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16) + return f"rgba({r}, {g}, {b}, {alpha})" + except ValueError: + consoleLog(f"Invalid Color: {state.accent_color}") + return None + + def _table_stylesheet(view_type="QTableWidget"): c = _theme_colors() dark = _is_dark_mode() color_rule = "" if dark else f"color: {c['text']};" + selected_bg = _accent_selection_color() or c["selected"] + selection_color_rule = f"selection-background-color: {selected_bg};" if state.accent_color else "" return f""" {view_type} {{ border: none; outline: 0; font-size: 13px; + {selection_color_rule} {color_rule} }} {view_type}::item {{ @@ -45,7 +64,7 @@ def _table_stylesheet(view_type="QTableWidget"): {color_rule} }} {view_type}::item:selected {{ - background: {c["selected"]}; + background: {selected_bg}; outline: none; border: none; border-bottom: 1px solid {c["border"]}; diff --git a/src/interface/gui.py b/src/interface/gui.py index 1b048ef..cd1765a 100644 --- a/src/interface/gui.py +++ b/src/interface/gui.py @@ -6,6 +6,7 @@ from interface.assets.base64_icons import settings_white_base64 from interface.assets.base64_icons import settings_black_base64 from interface.dialogs.downloadlist import download_list_update from interface.dialogs.update import get_version, UpdateDialog +from interface.dialogs.theme import _accent_selection_color from utils.logging.logs import consoleLog, flush_log_buffer from interface.dialogs.downloadmodel import DownloadModel from interface.dialogs.settings import settings_dialog @@ -177,6 +178,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget): self.tracker_list.addItems(list(state.trackers.keys())) self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor) self.tracker_list.activated.connect(self.set_tracker) + if state.accent_color: + self.tracker_list.setStyleSheet( + f"QComboBox QAbstractItemView::item:selected {{ background: {_accent_selection_color()}; }}" + ) self.corner_layout.addWidget(self.tracker_list) # Settings button diff --git a/src/main.py b/src/main.py index 4b9ed24..5722bca 100644 --- a/src/main.py +++ b/src/main.py @@ -21,13 +21,17 @@ import time import sys def run_gui(app): - qdarktheme.setup_theme("auto") + custom_colors = {} + if state.accent_color: + custom_colors["primary"] = state.accent_color + qdarktheme.setup_theme("auto", custom_colors=custom_colors if custom_colors else None) widget = MainWindow() # Check OS for window transparency compatibility and apply if state.window_transparency and platform.system() != "Windows": widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"}) + transparent_colors = {"background": "#00000000", **custom_colors} + qdarktheme.setup_theme("auto", custom_colors=transparent_colors) set_main_window(widget) widget.show() diff --git a/src/utils/config/config.py b/src/utils/config/config.py index 15cf7a4..fc481fe 100644 --- a/src/utils/config/config.py +++ b/src/utils/config/config.py @@ -14,7 +14,8 @@ def create_config(): "debug": str(state.debug), "ignore_updates": str(state.ignore_updates), "autoresume": str(state.autoresume), - "window_transparency": str(state.window_transparency) + "window_transparency": str(state.window_transparency), + "accent_color": str(state.accent_color) } config["Network"] = { @@ -84,6 +85,7 @@ def read_config(): 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) + state.accent_color = config.get("General", "accent_color", fallback=state.accent_color) # Network state.api_url = config.get("Network", "api_url", fallback=state.api_url) diff --git a/src/utils/config/settings.py b/src/utils/config/settings.py index 2fc94a6..09df0da 100644 --- a/src/utils/config/settings.py +++ b/src/utils/config/settings.py @@ -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, image_opacity=None, image_as_wallpaper=None, image_position=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, image_as_wallpaper=None, image_position=None, accent_color=None): if apiurl is not None: state.api_url = apiurl if download_path is not None: @@ -34,6 +34,8 @@ def save_settings(close=lambda: None, apiurl=None, download_path=None, down_spee state.image_as_wallpaper = image_as_wallpaper if image_position is not None: state.image_position = image_position + if accent_color is not None: + state.accent_color = accent_color update_settings() # Update LibTorrent Session Settings consoleLog("Saved Settings") diff --git a/src/utils/data/state.py b/src/utils/data/state.py index 75ed4eb..1004d0a 100644 --- a/src/utils/data/state.py +++ b/src/utils/data/state.py @@ -25,6 +25,7 @@ class AppState(QObject): # GUI self.window_transparency: bool = False + self.accent_color: str = "" self.trackertable: QTableWidget self.interfaces: List = [] self.active_interfaces: List = [] From 929fdf69e5f4ba539ae7f8fcdfc35b3efcea6f8d Mon Sep 17 00:00:00 2001 From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:07:22 +0200 Subject: [PATCH 11/11] Update and revert image path in settings dialog Capture the original state.image_path at dialog start, connect the Image Path QLineEdit's textChanged signal to update state.image_path immediately, and restore the original image_path if the settings dialog is cancelled. This matches existing undo behavior for width/offset/opacity and prevents partial image path edits from persisting when the dialog is rejected. --- src/interface/dialogs/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/interface/dialogs/settings.py b/src/interface/dialogs/settings.py index 2fb6652..6b6a25e 100644 --- a/src/interface/dialogs/settings.py +++ b/src/interface/dialogs/settings.py @@ -24,6 +24,7 @@ SVG_FOLDER = '