Merge pull request #53 from KeksPirates/fix/image-resizing

Fix/image resizing, add accent color support
This commit is contained in:
shayaa
2026-04-07 01:09:02 +02:00
committed by GitHub
10 changed files with 735 additions and 293 deletions
+172 -14
View File
@@ -1,29 +1,187 @@
from PySide6.QtWidgets import QLabel, QGraphicsOpacityEffect, QStackedWidget
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
import darkdetect
import os
class Image():
class Image(QObject):
def __init__(self, parent):
super().__init__(parent)
self.application = parent
self.overlay_label = QLabel(parent)
self.overlay_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
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)
self.opacity_effect = QGraphicsOpacityEffect()
self.overlay_label.setGraphicsEffect(self.opacity_effect)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label.setPixmap(self.pixmap)
self._current_image_path = None
self._wallpaper_active = False
self._original_stylesheets = {}
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)
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 _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
image = QImage(image_path)
if image.isNull():
self.overlay_label.hide()
return
if getattr(state, "image_as_wallpaper", False):
scaled_image = image.scaled(
parent.size(),
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
Qt.TransformationMode.SmoothTransformation
)
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())
pixmap = QPixmap.fromImage(scaled_image)
self.overlay_label.setPixmap(pixmap)
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
)
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()
y = parent.height() - self.overlay_label.height()
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)
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)
@@ -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)
+288 -232
View File
@@ -13,8 +13,7 @@ from PySide6.QtWidgets import (
QDialog,
QLabel,
QHBoxLayout,
QSpinBox,
QCheckBox,
QSpinBox, QSlider, QCheckBox,
QFileDialog,
QComboBox,
)
@@ -25,228 +24,265 @@ SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path
def settings_dialog(self):
temp_image_path = state.image_path
temp_image_width = state.image_width
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
consoleLog("Settings dialog opened")
dialog = QDialog(self)
dialog.setWindowTitle("Settings")
dialog.setFixedSize(700, 450)
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()
update_checkbox_container = QWidget()
update_checkbox_layout = QHBoxLayout()
# Ignore updates checkbox
if platform.system() == "Windows":
update_checkbox = QCheckBox()
update_checkbox_container.setLayout(update_checkbox_layout)
update_checkbox_layout.addWidget(QLabel("Ignore Updates: "))
update_checkbox_layout.addStretch()
update_checkbox.setChecked(state.ignore_updates)
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
update_checkbox_layout.addWidget(update_checkbox)
# Auto-resume downloads checkbox
autoresume_container = QWidget()
autoresume_layout = QHBoxLayout()
autoresume_checkbox = QCheckBox()
autoresume_container.setLayout(autoresume_layout)
autoresume_layout.addWidget(QLabel("Auto-Resume Downloads: "))
autoresume_layout.addStretch()
autoresume_checkbox.setChecked(state.autoresume)
autoresume_layout.addWidget(autoresume_checkbox)
# Transparent window checkbox
transparent_window_container = QWidget()
transparent_window_layout = QHBoxLayout()
transparent_window_checkbox = QCheckBox()
transparent_window_container.setLayout(transparent_window_layout)
transparent_window_layout.addWidget(QLabel("Window Transparency (requires restart) (Linux/MacOS only): "))
transparent_window_layout.addStretch()
transparent_window_checkbox.setChecked(state.window_transparency)
transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
transparent_window_layout.addWidget(transparent_window_checkbox)
# API Server Setting
api_url_container = QWidget()
api_url_layout = QHBoxLayout()
api_url = QLineEdit()
api_url_layout.addWidget(QLabel("API Server URL:"))
api_url_layout.addWidget(api_url)
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)
# Download Path
download_path_container = QWidget()
download_path_layout = QHBoxLayout()
download_path = QLineEdit()
download_path_layout.addWidget(QLabel("Download Path:"))
download_path_layout.addWidget(download_path)
download_path_container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
download_path_container.setLayout(download_path_layout)
download_path.setText(state.download_path)
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 = QPushButton()
browse_button.setFixedSize(36, 36)
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 = QWidget()
image_path_layout = QHBoxLayout()
image_path = QLineEdit()
image_path_layout.addWidget(QLabel("Image Path (requires restart, experimental):"))
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)
image_path.setText(state.image_path)
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 = QPushButton()
browse_button.setFixedSize(36, 36)
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)
# Speed Limiting
down_speed_limit_container = QWidget()
down_speed_limit_layout = QHBoxLayout()
down_speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): "))
down_speed_limit = QSpinBox()
down_speed_limit.setMinimum(0)
down_speed_limit.setMaximum(10000000)
down_speed_limit.setValue(state.down_speed_limit)
down_speed_limit_container.setLayout(down_speed_limit_layout)
down_speed_limit_layout.addWidget(down_speed_limit)
down_speed_limit.setFixedWidth(180)
down_speed_limit.setFixedHeight(30)
up_speed_limit_container = QWidget()
up_speed_limit_layout = QHBoxLayout()
up_speed_limit_layout.addWidget(QLabel("Max Upload Speed (KiB, 0 for unlimited): "))
up_speed_limit = QSpinBox()
up_speed_limit.setMinimum(0)
up_speed_limit.setMaximum(10000000)
up_speed_limit.setValue(state.up_speed_limit)
up_speed_limit_container.setLayout(up_speed_limit_layout)
up_speed_limit_layout.addWidget(up_speed_limit)
up_speed_limit.setFixedWidth(180)
up_speed_limit.setFixedHeight(30)
# Connection Configs
max_connections_container = QWidget()
max_connections_layout = QHBoxLayout()
max_connections_layout.addWidget(QLabel("Max Connections: "))
max_connections = QSpinBox()
max_connections.setMinimum(0)
max_connections.setMaximum(10000000)
max_connections.setValue(state.max_connections)
max_connections_container.setLayout(max_connections_layout)
max_connections_layout.addWidget(max_connections)
max_connections.setFixedWidth(180)
max_connections.setFixedHeight(30)
# Download Configs
max_downloads_container = QWidget()
max_downloads_layout = QHBoxLayout()
max_downloads_layout.addWidget(QLabel("Max Downloads: "))
max_downloads = QSpinBox()
max_downloads.setMinimum(0)
max_downloads.setMaximum(10000000)
max_downloads.setValue(state.max_downloads)
max_downloads_container.setLayout(max_downloads_layout)
max_downloads_layout.addWidget(max_downloads)
max_downloads.setFixedWidth(180)
max_downloads.setFixedHeight(30)
# 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
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)
# 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))
# 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)
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()
image_path.textChanged.connect(lambda text: setattr(state, 'image_path', text))
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 (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))
# 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_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 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_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 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_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)
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(),
@@ -255,18 +291,38 @@ 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(),
image_opacity.value(),
image_as_wallpaper=image_mode_checkbox.isChecked(),
image_position=image_position_combo.currentText(),
accent_color=accent_color_input.text().strip()
)
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("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, 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)
dialog_layout.addWidget(tabs)
dialog_layout.addLayout(layout)
dialog.exec()
dialog_layout.addWidget(tabs)
dialog_layout.addLayout(layout)
def on_dialog_finished(result): # Undo Image changes if "Save" button is not pressed
if result == QtWidgets.QDialog.DialogCode.Rejected:
state.image_path = temp_image_path
state.image_width = temp_image_width
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()
+20 -1
View File
@@ -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"]};
+5
View File
@@ -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
+12 -5
View File
@@ -20,15 +20,18 @@ import signal
import time
import sys
def run_gui():
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
qdarktheme.setup_theme("auto")
def run_gui(app):
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()
@@ -42,6 +45,10 @@ def keyboardinterrupthandler(signum, frame):
def main():
# Begin counting startup time
start_time = time.perf_counter()
# Initialize UI Engine
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
# Parse saved files
read_config()
logs = get_download_logs()
@@ -65,7 +72,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()
+106 -36
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,24 +11,34 @@ 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),
"accent_color": str(state.accent_color)
}
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": str(state.image_enabled),
"image_width": str(state.image_width),
"image_offset": str(state.image_offset),
"image_opacity": str(state.image_opacity),
"image_as_wallpaper": str(state.image_as_wallpaper),
"image_position": str(state.image_position)
}
if platform.system() == "Windows":
@@ -36,45 +49,102 @@ 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")
if not os.path.exists(config_file):
state.settings_path = os.path.join(config_dir, "SoftwareManager")
# 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 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)
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)
state.accent_color = config.get("General", "accent_color", fallback=state.accent_color)
# 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)
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()
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()
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}")
+14 -2
View File
@@ -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, 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:
@@ -24,8 +24,20 @@ 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
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
if accent_color is not None:
state.accent_color = accent_color
update_settings()
update_settings() # Update LibTorrent Session Settings
consoleLog("Saved Settings")
create_config()
close()
+99
View File
@@ -25,11 +25,20 @@ class AppState(QObject):
# GUI
self.window_transparency: bool = False
self.accent_color: str = ""
self.trackertable: QTableWidget
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_opacity: int = 100
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
self.currenttracker: str = "rutracker"
@@ -58,4 +67,94 @@ 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_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)
@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)
@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()
-1
View File
@@ -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