From fbc2225878fa9637706a220348e2610066cb9e0a Mon Sep 17 00:00:00 2001
From: Vxrtrauter <101264710+Vxrtrauter@users.noreply.github.com>
Date: Sat, 7 Mar 2026 20:41:03 +0100
Subject: [PATCH] refactor: standardize version key in build_info.json and
enhance download selection logic in download.py feat: add SVG icons for play,
pause, and folder actions in gui.py; implement status bar for download speed
display fix: remove unnecessary newline in libtorrent_misc.py and streamline
update checker logic in update_checker.py
---
src/build_info.json | 2 +-
src/core/interface/gui.py | 256 +++++++++++++++++++++--
src/core/network/libtorrent_misc.py | 1 -
src/core/utils/network/download.py | 15 +-
src/core/utils/network/update_checker.py | 6 +-
5 files changed, 254 insertions(+), 26 deletions(-)
diff --git a/src/build_info.json b/src/build_info.json
index d405e53..fe645a3 100644
--- a/src/build_info.json
+++ b/src/build_info.json
@@ -1,3 +1,3 @@
{
- "Version": "dev"
+ "version": "dev"
}
diff --git a/src/core/interface/gui.py b/src/core/interface/gui.py
index 8d7a648..e1b8261 100644
--- a/src/core/interface/gui.py
+++ b/src/core/interface/gui.py
@@ -1,5 +1,5 @@
from PySide6 import QtWidgets
-from PySide6.QtCore import QPersistentModelIndex, Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize
+from PySide6.QtCore import QPersistentModelIndex, Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent, QSize, QByteArray
from PySide6.QtWidgets import (
QLineEdit,
QTableView,
@@ -15,9 +15,11 @@ from PySide6.QtWidgets import (
QTableWidget,
QTextEdit,
QStyledItemDelegate,
+ QStatusBar
)
-from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent, QGuiApplication
+from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent, QGuiApplication, QColor, QBrush
+from PySide6.QtSvg import QSvgRenderer
import darkdetect
import threading
import platform
@@ -43,6 +45,22 @@ from core.interface.assets.base64_icons import logo_base64
from core.utils.general.shutdown import closehelper
+def _svg_icon(svg_str, size=20):
+ renderer = QSvgRenderer(QByteArray(svg_str.encode()))
+ pixmap = QPixmap(size, size)
+ pixmap.fill(Qt.GlobalColor.transparent)
+ from PySide6.QtGui import QPainter
+ painter = QPainter(pixmap)
+ renderer.render(painter)
+ painter.end()
+ return QIcon(pixmap)
+
+
+SVG_PLAY = ''
+SVG_PAUSE = ''
+SVG_FOLDER = ''
+
+
def download_update(latest_version):
new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
@@ -137,26 +155,82 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.download_model = None
self.downloadList = QTableView()
+ self.downloadList.setMouseTracking(True)
+ self._hovered_row = -1
+ self._tracker_hovered_row = -1
+ self._tracker_hovered_table = None
+ self.downloadList.viewport().installEventFilter(self)
self.emptyLibrary = QLabel("No items in library.")
self.emptyDownload = QLabel("No items in downloads.")
self.emptyDownload.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.consoleLog = QTextEdit()
+ self.statusbar = QStatusBar()
flush_log_buffer()
+ class TrackerHoverDelegate(QStyledItemDelegate):
+ def paint(self, painter, option, index):
+ if index.row() == MainWindow._instance._tracker_hovered_row:
+ painter.save()
+ painter.fillRect(option.rect, QColor(255, 255, 255, 15))
+ painter.restore()
+ super().paint(painter, option, index)
+
+ self._tracker_hover_delegate = TrackerHoverDelegate(self)
+
def create_tracker_table(headers):
table = QTableWidget()
table.setColumnCount(len(headers))
table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
table.verticalHeader().setVisible(False)
table.setHorizontalHeaderLabels(headers)
+ table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
+ table.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
header = table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
header.resizeSection(1, 500)
header.setStretchLastSection(False)
+ header.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
+ header.setHighlightSections(False)
+
+ table.setShowGrid(False)
+ table.setStyleSheet("""
+ QTableWidget {
+ border: none;
+ outline: 0;
+ font-size: 13px;
+ }
+ QTableWidget::item {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+ padding: 6px 14px;
+ }
+ QTableWidget::item:selected {
+ background: rgba(255, 255, 255, 0.04);
+ }
+ QHeaderView {
+ background: transparent;
+ }
+ QHeaderView::section {
+ background: transparent;
+ color: rgba(255, 255, 255, 0.5);
+ font-weight: normal;
+ border: none;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ padding: 6px 14px;
+ }
+ QHeaderView::section:checked {
+ background: transparent;
+ color: rgba(255, 255, 255, 0.5);
+ font-weight: normal;
+ }
+ """)
+
+ table.setMouseTracking(True)
+ table.viewport().setMouseTracking(True)
+ table.setItemDelegate(self._tracker_hover_delegate)
return table
@@ -173,6 +247,12 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.uztrackerlist.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.uztrackerlist.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
+ self.monkruslist.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
+ self.monkruslist.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
+
+ for tbl in (self.rutrackerlist, self.uztrackerlist, self.monkruslist):
+ tbl.viewport().installEventFilter(self)
+
container = QWidget()
containerLayout = QVBoxLayout()
containerLayout.addWidget(self.searchbar)
@@ -222,11 +302,11 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
elif col == 3:
return f"{status.progress * 100:.1f}%"
elif col == 4:
- speed_kbs = status.download_rate / 1024
- if speed_kbs > 1024:
- return f"{speed_kbs / 1024:.1f} MB/s"
- else:
- return f"{speed_kbs:.1f} kB/s"
+ down_kb = status.download_rate / 1024
+ up_kb = status.upload_rate / 1024
+ down_text = f"{down_kb / 1024:.1f} MB/s" if down_kb > 1024 else f"{down_kb:.1f} kB/s"
+ up_text = f"{up_kb / 1024:.1f} MB/s" if up_kb > 1024 else f"{up_kb:.1f} kB/s"
+ return f"β {down_text} β {up_text}"
elif col == 5:
downloaded_mb = status.total_wanted_done / (1024 * 1024)
if downloaded_mb > 1024:
@@ -297,9 +377,24 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
idx = self.index(row, 0)
self.dataChanged.emit(idx, idx, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.UserRole])
+ class HoverRowDelegate(QStyledItemDelegate):
+ def paint(self, painter, option, index):
+ if index.row() == MainWindow._instance._hovered_row:
+ painter.save()
+ painter.fillRect(option.rect, QColor(255, 255, 255, 15))
+ painter.restore()
+ super().paint(painter, option, index)
+
class PauseResumeDelegate(QStyledItemDelegate):
clicked = Signal(int)
+ def paint(self, painter, option, index):
+ if index.row() == MainWindow._instance._hovered_row:
+ painter.save()
+ painter.fillRect(option.rect, QColor(255, 255, 255, 15))
+ painter.restore()
+ super().paint(painter, option, index)
+
def setEditorData(self, editor, index):
button = editor.findChild(QtWidgets.QPushButton)
@@ -309,9 +404,11 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if button:
if status.state == lt.torrent_status.seeding:
- button.setText("π")
+ button.setIcon(_svg_icon(SVG_FOLDER, 18))
+ button.setText("")
else:
- button.setText("βΆοΈ" if status.paused else "βΈοΈ")
+ button.setIcon(_svg_icon(SVG_PLAY if status.paused else SVG_PAUSE, 18))
+ button.setText("")
def createEditor(self, parent, option, index):
magnet_link = list(state.active_downloads.keys())[index.row()]
@@ -319,14 +416,25 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
status = magnetdl.status()
widget = QWidget(parent)
- widget.setStyleSheet("border: none;")
+ widget.setStyleSheet("border: none; background: transparent;")
layout = QHBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0)
if status.state == lt.torrent_status.seeding:
- btnPause = QtWidgets.QPushButton("π")
+ btnPause = QtWidgets.QPushButton()
+ btnPause.setIcon(_svg_icon(SVG_FOLDER, 18))
else:
- btnPause = QtWidgets.QPushButton("βΆοΈ" if status.paused else "βΈοΈ")
- btnPause.setFixedSize(40, 30)
+ btnPause = QtWidgets.QPushButton()
+ btnPause.setIcon(_svg_icon(SVG_PLAY if status.paused else SVG_PAUSE, 18))
+ btnPause.setIconSize(QSize(18, 18))
+ btnPause.setFixedSize(30, 30)
+ btnPause.setCursor(Qt.CursorShape.PointingHandCursor)
+ btnPause.setStyleSheet("""
+ QPushButton {
+ border: none;
+ background: transparent;
+ padding: 0px;
+ }
+ """)
btnPause.clicked.connect(lambda: self.clicked.emit(index.row()))
layout.addStretch()
layout.addWidget(btnPause)
@@ -342,8 +450,51 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.download_model = DownloadModel()
self.downloadList.setModel(self.download_model)
self.downloadList.horizontalHeader().setStretchLastSection(False)
+ self.downloadList.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
+ self.downloadList.horizontalHeader().resizeSection(0, 70)
self.downloadList.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
+ self.downloadList.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
+ self.downloadList.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
+ self.downloadList.horizontalHeader().setSectionResizeMode(6, QHeaderView.ResizeMode.ResizeToContents)
+ self.downloadList.horizontalHeader().setMinimumSectionSize(60)
+ self.downloadList.verticalHeader().setDefaultSectionSize(40)
self.downloadList.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
+ self.downloadList.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
+ self.downloadList.verticalHeader().setVisible(False)
+ self.downloadList.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
+ self.downloadList.horizontalHeader().setHighlightSections(False)
+ self.downloadList.setMouseTracking(True)
+ self.downloadList.setShowGrid(False)
+ self.downloadList.setStyleSheet("""
+ QTableView {
+ border: none;
+ outline: 0;
+ font-size: 13px;
+ }
+ QTableView::item {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+ padding: 6px 14px;
+ }
+ QTableView::item:selected {
+ background: rgba(255, 255, 255, 0.04);
+ }
+ QHeaderView {
+ background: transparent;
+ }
+ QHeaderView::section {
+ background: transparent;
+ color: rgba(255, 255, 255, 0.5);
+ font-weight: normal;
+ border: none;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ padding: 6px 14px;
+ }
+ QHeaderView::section:checked {
+ background: transparent;
+ color: rgba(255, 255, 255, 0.5);
+ font-weight: normal;
+ }
+ """)
self.downloadList.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.downloadList.viewport().setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
@@ -352,12 +503,16 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def on_pause_resume_clicked(row):
download_model.toggle_pause_resume(row)
+ self.downloadList.viewport().setMouseTracking(True)
+ hover_delegate = HoverRowDelegate(self.downloadList)
+ self.downloadList.setItemDelegate(hover_delegate)
+
delegate = PauseResumeDelegate(self.downloadList)
self.downloadList.setItemDelegateForColumn(0, delegate)
delegate.clicked.connect(on_pause_resume_clicked)
# download button triggers
- self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(state.tracker_list[state.tracker].currentItem(), state.posts, state.post_titles))))
+ self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(state.tracker_list[state.tracker].selectedItems(), state.posts, state.post_titles))))
container.setLayout(containerLayout)
self.setCentralWidget(container)
@@ -461,8 +616,21 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.consoleLog.setReadOnly(True)
self.consoleLog.setFixedHeight(150)
+ self.consoleLog.setStyleSheet("margin-bottom: 0px; background: transparent;")
containerLayout.addWidget(self.consoleLog)
+ self.statusbar.show()
+ self.statusbar.setStyleSheet("background: transparent; border: none; padding-right: 1px; padding-left: 6px; padding-bottom: 1px; margin-top: -8px")
+
+ self.version = QLabel(f"Version: {state.version}")
+ self.statusbar.addPermanentWidget(self.version, Qt.AlignmentFlag.AlignLeft)
+
+ self.speed_label = QLabel("β 0.0 kB/s β 0.0 kB/s")
+ self.speed_label.setStyleSheet("padding-right: 6px;")
+ self.statusbar.addPermanentWidget(self.speed_label)
+
+ self.setStatusBar(self.statusbar)
+
self.download_timer = QTimer()
self.download_timer.timeout.connect(self.download_list_update)
self.download_timer.start(500)
@@ -504,12 +672,72 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
idx = self.download_model.index(row, 0)
self.downloadList.closePersistentEditor(idx)
self.downloadList.openPersistentEditor(idx)
+ self._update_speed_label()
+
+ def _update_speed_label(self):
+ total_down = 0
+ total_up = 0
+ for handle in state.active_downloads.values():
+ try:
+ s = handle.status()
+ total_down += s.download_rate
+ total_up += s.upload_rate
+ except Exception:
+ pass
+ down_kb = total_down / 1024
+ up_kb = total_up / 1024
+ down_text = f"{down_kb / 1024:.1f} MB/s" if down_kb > 1024 else f"{down_kb:.1f} kB/s"
+ up_text = f"{up_kb / 1024:.1f} MB/s" if up_kb > 1024 else f"{up_kb:.1f} kB/s"
+ self.speed_label.setText(f"β {down_text} β {up_text}")
+ def mousePressEvent(self, event):
+ self.rutrackerlist.clearSelection()
+ self.uztrackerlist.clearSelection()
+ self.monkruslist.clearSelection()
+ self.downloadList.clearSelection()
+ super().mousePressEvent(event)
+
def closeEvent(self, event: QCloseEvent):
closehelper()
event.accept()
+ def eventFilter(self, obj, event):
+ for tbl in (self.rutrackerlist, self.uztrackerlist, self.monkruslist):
+ if obj == tbl.viewport():
+ if event.type() == QEvent.Type.MouseMove:
+ pos = event.position().toPoint() if hasattr(event, 'position') else event.pos()
+ idx = tbl.indexAt(pos)
+ new_row = idx.row() if idx.isValid() else -1
+ if new_row != self._tracker_hovered_row or tbl is not self._tracker_hovered_table:
+ self._tracker_hovered_row = new_row
+ self._tracker_hovered_table = tbl
+ tbl.viewport().update()
+ elif event.type() == QEvent.Type.Leave:
+ self._tracker_hovered_row = -1
+ self._tracker_hovered_table = None
+ tbl.viewport().update()
+ break
+ if obj == self.downloadList.viewport():
+ if event.type() == QEvent.Type.MouseMove:
+ pos = event.position().toPoint() if hasattr(event, 'position') else event.pos()
+ idx = self.downloadList.indexAt(pos)
+ new_row = idx.row() if idx.isValid() else -1
+ if new_row != self._hovered_row:
+ old_row = self._hovered_row
+ self._hovered_row = new_row
+ self._invalidate_hover_row(old_row)
+ self._invalidate_hover_row(new_row)
+ elif event.type() == QEvent.Type.Leave:
+ old_row = self._hovered_row
+ self._hovered_row = -1
+ self._invalidate_hover_row(old_row)
+ return super().eventFilter(obj, event)
+
+ def _invalidate_hover_row(self, row):
+ if row >= 0 and self.download_model:
+ self.downloadList.viewport().update()
+
def set_tracker(self, _):
old_tracker = state.tracker
state.tracker = self.tracker_list.currentText()
diff --git a/src/core/network/libtorrent_misc.py b/src/core/network/libtorrent_misc.py
index 54a06b4..6a2fa6d 100644
--- a/src/core/network/libtorrent_misc.py
+++ b/src/core/network/libtorrent_misc.py
@@ -76,7 +76,6 @@ def check_deleted_files(shutdown_event):
consoleLog(f"Registered File Deletion: {status.name}")
state.dl_session.remove_torrent(magnetdl)
del state.active_downloads[magnet_uri]
-
except Exception:
pass
time.sleep(5)
\ No newline at end of file
diff --git a/src/core/utils/network/download.py b/src/core/utils/network/download.py
index 35f94d0..c9d6578 100644
--- a/src/core/utils/network/download.py
+++ b/src/core/utils/network/download.py
@@ -8,12 +8,17 @@ from core.network.libtorrent_int import add_seed
import threading
-def download_selected(item, posts, post_titles):
- if item is not None:
- consoleLog(f"Downloading {item.text()}")
- run_thread(threading.Thread(target=run_download, args=(item.text(), posts, post_titles)))
- else:
+def download_selected(items, posts, post_titles):
+ if not items:
consoleLog("No item selected for download.")
+ return
+ seen = set()
+ for item in items:
+ text = item.text()
+ if text and text not in seen:
+ seen.add(text)
+ consoleLog(f"Downloading {text}")
+ run_thread(threading.Thread(target=run_download, args=(text, posts, post_titles)))
def run_download(item, posts, post_titles):
post_url = get_item_url(item, posts, post_titles)
diff --git a/src/core/utils/network/update_checker.py b/src/core/utils/network/update_checker.py
index f0acb5d..31955a9 100644
--- a/src/core/utils/network/update_checker.py
+++ b/src/core/utils/network/update_checker.py
@@ -17,11 +17,7 @@ def check_for_updates():
latest_version = latest_release["name"]
assets = latest_release["assets"]
-
- if state.version == "dev":
- consoleLog("Dev release detected, skipping version check")
- return None, None
- elif latest_version != state.version:
+ if latest_version != state.version:
consoleLog(f"New release available: {latest_version}")
if assets:
consoleLog("Assets:")