mirror of
https://github.com/KeksPirates/SoftwareManager.git
synced 2026-08-03 17:39:42 +02:00
Merge pull request #53 from KeksPirates/fix/image-resizing
Fix/image resizing, add accent color support
This commit is contained in:
+172
-14
@@ -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.QtGui import QImage, QPixmap
|
||||||
from PySide6.QtWidgets import QLabel
|
|
||||||
from utils.data.state import state
|
from utils.data.state import state
|
||||||
from PySide6.QtCore import Qt
|
import darkdetect
|
||||||
import os
|
import os
|
||||||
|
|
||||||
class Image():
|
|
||||||
|
class Image(QObject):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.application = parent
|
||||||
self.overlay_label = QLabel(parent)
|
self.overlay_label = QLabel(parent)
|
||||||
self.overlay_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
self.overlay_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||||||
|
|
||||||
|
self.opacity_effect = QGraphicsOpacityEffect()
|
||||||
|
self.overlay_label.setGraphicsEffect(self.opacity_effect)
|
||||||
|
|
||||||
if state.image_path is not None and os.path.exists(state.image_path):
|
self._current_image_path = None
|
||||||
self.image = QImage(state.image_path)
|
self._wallpaper_active = False
|
||||||
self.image = self.image.scaledToWidth(300, Qt.TransformationMode.SmoothTransformation)
|
self._original_stylesheets = {}
|
||||||
|
parent.installEventFilter(self)
|
||||||
|
state.image_changed.connect(self.update_image_overlay)
|
||||||
|
|
||||||
self.pixmap = QPixmap.fromImage(self.image)
|
if state.image_path and os.path.exists(state.image_path):
|
||||||
self.overlay_label.setPixmap(self.pixmap)
|
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.adjustSize()
|
||||||
self.overlay_label.raise_()
|
self.overlay_label.raise_()
|
||||||
|
|
||||||
x = parent.width() - self.overlay_label.width()
|
pos = state.image_position
|
||||||
y = parent.height() - self.overlay_label.height()
|
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.move(x, y)
|
||||||
|
self.overlay_label.show()
|
||||||
|
|
||||||
def update_image_overlay(self, new_image_path):
|
def update_image_overlay(self, new_image_path):
|
||||||
self.image = QImage(new_image_path)
|
self._load_and_display(new_image_path)
|
||||||
self.pixmap = QPixmap.fromImage(self.image)
|
|
||||||
self.overlay_label.setPixmap(self.pixmap)
|
|
||||||
self.overlay_label.adjustSize()
|
|
||||||
@@ -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)
|
||||||
+289
-233
@@ -13,8 +13,7 @@ from PySide6.QtWidgets import (
|
|||||||
QDialog,
|
QDialog,
|
||||||
QLabel,
|
QLabel,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QSpinBox,
|
QSpinBox, QSlider, QCheckBox,
|
||||||
QCheckBox,
|
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
)
|
)
|
||||||
@@ -25,248 +24,305 @@ SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path
|
|||||||
|
|
||||||
|
|
||||||
def settings_dialog(self):
|
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")
|
def create_widget(widget_type, label_text, **kwargs):
|
||||||
dialog = QDialog(self)
|
container = QWidget()
|
||||||
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
|
|
||||||
layout = QHBoxLayout()
|
layout = QHBoxLayout()
|
||||||
|
container.setLayout(layout)
|
||||||
|
layout.addWidget(QLabel(label_text))
|
||||||
|
|
||||||
save_btn = QPushButton("Save")
|
widget = widget_type()
|
||||||
cancel_btn = QPushButton("Cancel")
|
|
||||||
save_btn.clicked.connect(lambda: save_settings(
|
if widget_type in (QSpinBox,):
|
||||||
close_settings,
|
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(),
|
api_url.text(),
|
||||||
download_path.text(),
|
download_path.text(),
|
||||||
down_speed_limit.value(),
|
down_speed_limit.value(),
|
||||||
up_speed_limit.value(),
|
up_speed_limit.value(),
|
||||||
image_path.text(),
|
image_path.text(),
|
||||||
autoresume_checkbox.isChecked(),
|
autoresume_checkbox.isChecked(),
|
||||||
max_connections.value(),
|
max_connections.value(),
|
||||||
max_downloads.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)
|
cancel_btn.clicked.connect(dialog.reject)
|
||||||
layout.addWidget(cancel_btn)
|
layout.addWidget(cancel_btn)
|
||||||
layout.addWidget(save_btn)
|
layout.addWidget(save_btn)
|
||||||
|
|
||||||
tabs = QtWidgets.QTabWidget()
|
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("Paths", [download_path_container, image_path_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("Network", [interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_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()
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from PySide6.QtGui import QColor
|
from PySide6.QtGui import QColor
|
||||||
|
from utils.logging.logs import consoleLog
|
||||||
|
from utils.data.state import state
|
||||||
import darkdetect
|
import darkdetect
|
||||||
|
|
||||||
def _is_dark_mode():
|
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"):
|
def _table_stylesheet(view_type="QTableWidget"):
|
||||||
c = _theme_colors()
|
c = _theme_colors()
|
||||||
dark = _is_dark_mode()
|
dark = _is_dark_mode()
|
||||||
color_rule = "" if dark else f"color: {c['text']};"
|
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"""
|
return f"""
|
||||||
{view_type} {{
|
{view_type} {{
|
||||||
border: none;
|
border: none;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
{selection_color_rule}
|
||||||
{color_rule}
|
{color_rule}
|
||||||
}}
|
}}
|
||||||
{view_type}::item {{
|
{view_type}::item {{
|
||||||
@@ -45,7 +64,7 @@ def _table_stylesheet(view_type="QTableWidget"):
|
|||||||
{color_rule}
|
{color_rule}
|
||||||
}}
|
}}
|
||||||
{view_type}::item:selected {{
|
{view_type}::item:selected {{
|
||||||
background: {c["selected"]};
|
background: {selected_bg};
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid {c["border"]};
|
border-bottom: 1px solid {c["border"]};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from interface.assets.base64_icons import settings_white_base64
|
|||||||
from interface.assets.base64_icons import settings_black_base64
|
from interface.assets.base64_icons import settings_black_base64
|
||||||
from interface.dialogs.downloadlist import download_list_update
|
from interface.dialogs.downloadlist import download_list_update
|
||||||
from interface.dialogs.update import get_version, UpdateDialog
|
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 utils.logging.logs import consoleLog, flush_log_buffer
|
||||||
from interface.dialogs.downloadmodel import DownloadModel
|
from interface.dialogs.downloadmodel import DownloadModel
|
||||||
from interface.dialogs.settings import settings_dialog
|
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.addItems(list(state.trackers.keys()))
|
||||||
self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor)
|
self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.tracker_list.activated.connect(self.set_tracker)
|
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)
|
self.corner_layout.addWidget(self.tracker_list)
|
||||||
|
|
||||||
# Settings button
|
# Settings button
|
||||||
|
|||||||
+13
-6
@@ -20,15 +20,18 @@ import signal
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
def run_gui():
|
def run_gui(app):
|
||||||
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
custom_colors = {}
|
||||||
qdarktheme.setup_theme("auto")
|
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()
|
widget = MainWindow()
|
||||||
|
|
||||||
# Check OS for window transparency compatibility and apply
|
# Check OS for window transparency compatibility and apply
|
||||||
if state.window_transparency and platform.system() != "Windows":
|
if state.window_transparency and platform.system() != "Windows":
|
||||||
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
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)
|
set_main_window(widget)
|
||||||
widget.show()
|
widget.show()
|
||||||
@@ -42,7 +45,11 @@ def keyboardinterrupthandler(signum, frame):
|
|||||||
def main():
|
def main():
|
||||||
# Begin counting startup time
|
# Begin counting startup time
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
# Parse saved files
|
|
||||||
|
# Initialize UI Engine
|
||||||
|
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
|
||||||
|
|
||||||
|
# Parse saved files
|
||||||
read_config()
|
read_config()
|
||||||
logs = get_download_logs()
|
logs = get_download_logs()
|
||||||
_, downloads = split_data(logs)
|
_, downloads = split_data(logs)
|
||||||
@@ -65,7 +72,7 @@ def main():
|
|||||||
elapsed = time.perf_counter() - start_time
|
elapsed = time.perf_counter() - start_time
|
||||||
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
|
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
|
||||||
# Launch GUI
|
# Launch GUI
|
||||||
run_gui()
|
run_gui(app)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
+106
-36
@@ -1,6 +1,9 @@
|
|||||||
|
from interface.dialogs.notificationpopup import NotificationPopup
|
||||||
|
from utils.logging.logs import consoleLog
|
||||||
from utils.data.state import state
|
from utils.data.state import state
|
||||||
import configparser
|
import configparser
|
||||||
import platform
|
import platform
|
||||||
|
import time
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
@@ -8,24 +11,34 @@ def create_config():
|
|||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
|
|
||||||
config["General"] = {
|
config["General"] = {
|
||||||
"debug": True,
|
"debug": str(state.debug),
|
||||||
"ignore_updates": f"{state.ignore_updates}",
|
"ignore_updates": str(state.ignore_updates),
|
||||||
"autoresume": f"{state.autoresume}",
|
"autoresume": str(state.autoresume),
|
||||||
"window_transparency": f"{state.window_transparency}"
|
"window_transparency": str(state.window_transparency),
|
||||||
|
"accent_color": str(state.accent_color)
|
||||||
}
|
}
|
||||||
|
|
||||||
config["Network"] = {
|
config["Network"] = {
|
||||||
"api_url": f"{state.api_url}",
|
"api_url": str(state.api_url),
|
||||||
"bound_interface": f"{state.bound_interface}" if state.bound_interface is not None else "None",
|
"bound_interface": str(state.bound_interface) if state.bound_interface is not None else "None",
|
||||||
"download_speed_limit": f"{state.down_speed_limit}",
|
"download_speed_limit": str(state.down_speed_limit),
|
||||||
"upload_speed_limit": f"{state.up_speed_limit}",
|
"upload_speed_limit": str(state.up_speed_limit),
|
||||||
"max_connections": f"{state.max_connections}",
|
"max_connections": str(state.max_connections),
|
||||||
"max_downloads": f"{state.max_downloads}"
|
"max_downloads": str(state.max_downloads)
|
||||||
}
|
}
|
||||||
|
|
||||||
config["Paths"] = {
|
config["Paths"] = {
|
||||||
"download_path": f"{state.download_path}",
|
"download_path": str(state.download_path),
|
||||||
"image_path": f"{state.image_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":
|
if platform.system() == "Windows":
|
||||||
@@ -36,45 +49,102 @@ def create_config():
|
|||||||
state.settings_path = os.path.join(config_dir, "SoftwareManager")
|
state.settings_path = os.path.join(config_dir, "SoftwareManager")
|
||||||
os.makedirs(state.settings_path, exist_ok=True)
|
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)
|
config.write(cf)
|
||||||
|
|
||||||
|
|
||||||
def read_config():
|
def read_config():
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
|
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
|
||||||
else:
|
else:
|
||||||
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
||||||
|
|
||||||
state.settings_path = os.path.join(config_dir, "SoftwareManager")
|
state.settings_path = os.path.join(config_dir, "SoftwareManager")
|
||||||
config_file = os.path.join(state.settings_path, "config.yml")
|
|
||||||
|
# Temporary migration logic, keep for a while
|
||||||
|
old_config_file = os.path.join(state.settings_path, "config.yml")
|
||||||
|
new_config_file = os.path.join(state.settings_path, "config.ini")
|
||||||
|
|
||||||
if not os.path.exists(config_file):
|
if os.path.exists(old_config_file):
|
||||||
|
try:
|
||||||
|
os.replace(old_config_file, new_config_file)
|
||||||
|
consoleLog("Successfully migrated config.yml to config.ini")
|
||||||
|
except Exception as e:
|
||||||
|
consoleLog(f"Failed to migrate config file: {e}")
|
||||||
|
|
||||||
|
if not os.path.exists(new_config_file):
|
||||||
create_config()
|
create_config()
|
||||||
return
|
return
|
||||||
|
try:
|
||||||
|
config.read(new_config_file)
|
||||||
|
|
||||||
|
# General
|
||||||
|
state.debug = config.getboolean("General", "debug", fallback=state.debug)
|
||||||
|
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
|
||||||
|
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
|
||||||
|
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
|
||||||
|
state.accent_color = config.get("General", "accent_color", fallback=state.accent_color)
|
||||||
|
|
||||||
config.read(config_file)
|
# Network
|
||||||
|
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
|
||||||
# General
|
state.bound_interface = config.get("Network", "bound_interface", fallback=state.bound_interface)
|
||||||
state.debug = config.getboolean("General", "debug", fallback=state.debug)
|
if state.bound_interface == "None":
|
||||||
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
|
state.bound_interface = None
|
||||||
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
|
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
|
||||||
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
|
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
|
# Paths
|
||||||
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
|
state.download_path = config.get("Paths", "download_path", fallback=state.download_path)
|
||||||
state.bound_interface = config.get("Network", "bound_interface", fallback=state.bound_interface)
|
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
|
||||||
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
|
# Image
|
||||||
state.download_path = config.get("Paths", "download_path", fallback=state.download_path)
|
state.image_enabled = config.getboolean("Image", "enable_image", fallback=state.image_enabled)
|
||||||
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
|
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}")
|
||||||
@@ -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:
|
if apiurl is not None:
|
||||||
state.api_url = apiurl
|
state.api_url = apiurl
|
||||||
if download_path is not None:
|
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
|
state.max_downloads = max_downloads
|
||||||
if bound_interface is not None:
|
if bound_interface is not None:
|
||||||
state.bound_interface = None if bound_interface == "None" else bound_interface
|
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")
|
consoleLog("Saved Settings")
|
||||||
create_config()
|
create_config()
|
||||||
close()
|
close()
|
||||||
|
|||||||
@@ -25,11 +25,20 @@ class AppState(QObject):
|
|||||||
|
|
||||||
# GUI
|
# GUI
|
||||||
self.window_transparency: bool = False
|
self.window_transparency: bool = False
|
||||||
|
self.accent_color: str = ""
|
||||||
self.trackertable: QTableWidget
|
self.trackertable: QTableWidget
|
||||||
self.interfaces: List = []
|
self.interfaces: List = []
|
||||||
self.active_interfaces: List = []
|
self.active_interfaces: List = []
|
||||||
self.bound_interface: Any = None
|
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
|
# Trackers / Scraping
|
||||||
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers
|
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers
|
||||||
self.currenttracker: str = "rutracker"
|
self.currenttracker: str = "rutracker"
|
||||||
@@ -58,4 +67,94 @@ class AppState(QObject):
|
|||||||
self._image_path = new_path
|
self._image_path = new_path
|
||||||
self.image_changed.emit(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()
|
state = AppState()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from utils.logging.logs import consoleLog, remove_download_log
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
def split_data(data):
|
def split_data(data):
|
||||||
|
|
||||||
count = data.count
|
count = data.count
|
||||||
downloads = data.data
|
downloads = data.data
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user