Compare commits

...

5 Commits

5 changed files with 145 additions and 77 deletions
+45 -19
View File
@@ -4,8 +4,9 @@ from core.utils.logging.logs import consoleLog
from core.interface.utils.tabhelper import general_tab from core.interface.utils.tabhelper import general_tab
from core.interface.utils.tabhelper import paths_tab from core.interface.utils.tabhelper import paths_tab
from core.interface.utils.tabhelper import network_tab from core.interface.utils.tabhelper import network_tab
from core.interface.utils.svghelper import svg_icon
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6.QtCore import Qt from PySide6.QtCore import Qt, QSize
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QLineEdit, QLineEdit,
QPushButton, QPushButton,
@@ -22,6 +23,8 @@ from PySide6.QtWidgets import (
) )
import platform import platform
SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 6c0-1.1.9-2 2-2h5l2 2h7c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6z" fill="{color}"/></svg>'
def settings_dialog(self): def settings_dialog(self):
@@ -113,7 +116,19 @@ def settings_dialog(self):
if dir_path: if dir_path:
download_path.setText(dir_path) download_path.setText(dir_path)
browse_button = QPushButton("📁") 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) download_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_download_path) browse_button.clicked.connect(browse_download_path)
@@ -121,24 +136,35 @@ def settings_dialog(self):
# IMAGE PATH # # IMAGE PATH #
############### ###############
# image_path_container = QWidget() image_path_container = QWidget()
# image_path_layout = QHBoxLayout() image_path_layout = QHBoxLayout()
# image_path = QLineEdit() image_path = QLineEdit()
# image_path_layout.addWidget(QLabel("Image Path (requires restart):")) image_path_layout.addWidget(QLabel("Image Path (requires restart):"))
# image_path_layout.addWidget(image_path) image_path_layout.addWidget(image_path)
# image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
# image_path_container.setLayout(image_path_layout) image_path_container.setLayout(image_path_layout)
# image_path.setText(state.image_path) image_path.setText(state.image_path)
# def browse_image_path(): def browse_image_path():
# file_path = QFileDialog.getOpenFileName(dialog, "Select Image File", state.image_path, "Image Files (*.png *.jpg)")[0] file_path = QFileDialog.getOpenFileName(dialog, "Select Image File", state.image_path, "Image Files (*.png *.jpg)")[0]
# if file_path: if file_path:
# image_path.setText(file_path) image_path.setText(file_path)
# browse_button = QPushButton("📁") browse_button = QPushButton()
# image_path_layout.addWidget(browse_button) browse_button.setFixedSize(36, 36)
# browse_button.clicked.connect(browse_image_path) 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 # # SPEED LIMITING #
@@ -244,7 +270,7 @@ def settings_dialog(self):
download_path.text(), download_path.text(),
down_speed_limit.value(), down_speed_limit.value(),
up_speed_limit.value(), up_speed_limit.value(),
None, image_path.text(),
autoresume_checkbox.isChecked(), autoresume_checkbox.isChecked(),
max_connections.value(), max_connections.value(),
max_downloads.value(), max_downloads.value(),
@@ -256,7 +282,7 @@ def settings_dialog(self):
self.tabs = QTabWidget() self.tabs = QTabWidget()
self.tab1 = general_tab("General", autoresume_container, update_checkbox_container, transparent_window_container, self.tabs) self.tab1 = general_tab("General", autoresume_container, update_checkbox_container, transparent_window_container, self.tabs)
self.tab2 = paths_tab("Paths", download_path_container, self.tabs) self.tab2 = paths_tab("Paths", download_path_container, image_path_container, self.tabs)
self.tab3 = network_tab("Network", interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container, self.tabs) self.tab3 = network_tab("Network", interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container, self.tabs)
dialog.layout().addWidget(self.tabs) dialog.layout().addWidget(self.tabs)
+54 -47
View File
@@ -19,7 +19,6 @@ from PySide6.QtWidgets import (
) )
from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent, QGuiApplication, QColor, QBrush from PySide6.QtGui import QIcon, QCloseEvent, QImage, QPixmap, QContextMenuEvent, QGuiApplication, QColor, QBrush
from PySide6.QtSvg import QSvgRenderer
import darkdetect import darkdetect
import threading import threading
import platform import platform
@@ -34,9 +33,10 @@ from core.utils.logging.logs import consoleLog, remove_download_log, flush_log_b
from core.utils.general.wrappers import run_thread from core.utils.general.wrappers import run_thread
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.network.download import download_selected from core.utils.network.download import download_selected
from core.utils.network.update_checker import check_for_updates from core.utils.network.update_checker import get_updates
from core.interface.utils.tabhelper import create_tab from core.interface.utils.tabhelper import create_tab
from core.interface.utils.searchhelper import return_pressed from core.interface.utils.searchhelper import return_pressed
from core.interface.utils.svghelper import svg_icon
from core.interface.dialogs.settings import settings_dialog from core.interface.dialogs.settings import settings_dialog
from core.interface.assets.base64_icons import settings_black_base64 from core.interface.assets.base64_icons import settings_black_base64
from core.interface.assets.base64_icons import settings_white_base64 from core.interface.assets.base64_icons import settings_white_base64
@@ -90,10 +90,13 @@ def _table_stylesheet(view_type="QTableWidget"):
{view_type}::item:selected {{ {view_type}::item:selected {{
background: {c["selected"]}; background: {c["selected"]};
outline: none; outline: none;
border: none;
border-bottom: 1px solid {c["border"]};
}} }}
{view_type}::item:focus {{ {view_type}::item:focus {{
outline: none; outline: none;
border: none; border: none;
border-bottom: 1px solid {c["border"]};
}} }}
QHeaderView {{ QHeaderView {{
background: transparent; background: transparent;
@@ -114,38 +117,32 @@ def _table_stylesheet(view_type="QTableWidget"):
""" """
def _svg_icon(svg_str, size=20):
app = QtWidgets.QApplication.instance()
if app:
if _is_dark_mode():
color = "white"
else:
color = "#555555"
else:
color = "white"
svg = svg_str.replace("{color}", color)
renderer = QSvgRenderer(QByteArray(svg.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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21" fill="{color}"/></svg>' SVG_PLAY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><polygon points="6,3 20,12 6,21" fill="{color}"/></svg>'
SVG_PAUSE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="5" y="3" width="4" height="18" rx="1" fill="{color}"/><rect x="15" y="3" width="4" height="18" rx="1" fill="{color}"/></svg>' SVG_PAUSE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="5" y="3" width="4" height="18" rx="1" fill="{color}"/><rect x="15" y="3" width="4" height="18" rx="1" fill="{color}"/></svg>'
SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 6c0-1.1.9-2 2-2h5l2 2h7c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6z" fill="{color}"/></svg>' SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 6c0-1.1.9-2 2-2h5l2 2h7c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6z" fill="{color}"/></svg>'
def _verify_hash(file_path, expected_hash):
import hashlib
sha256 = hashlib.sha256()
def download_update(latest_version): with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
return f"sha256:{sha256.hexdigest()}" == expected_hash
def _download_update(assets):
import tempfile import tempfile
import time import time
new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows-setup.exe"
url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows-setup.exe" for asset in assets:
installer_path = os.path.join(tempfile.gettempdir(), new_filename) if "-windows-setup.exe" in asset["name"]:
filename = asset["name"]
setup_hash = asset["hash"]
url = asset ["url"]
installer_path = os.path.join(tempfile.gettempdir(), filename)
progress = QtWidgets.QProgressDialog("Downloading installer...", None, 0, 0) progress = QtWidgets.QProgressDialog("Downloading installer...", None, 0, 0)
progress.setWindowTitle("Updating") progress.setWindowTitle("Updating")
@@ -174,6 +171,12 @@ def download_update(latest_version):
progress.close() progress.close()
raise FileNotFoundError("Executable not found") raise FileNotFoundError("Executable not found")
if _verify_hash(installer_path, setup_hash):
consoleLog(f"Sucessfully validated installer hash ({setup_hash})")
else:
consoleLog("Error: Invalid Filehash, file may be corrupted")
sys.exit(0)
progress.setLabelText("Installing update...") progress.setLabelText("Installing update...")
progress.setValue(100) progress.setValue(100)
QtWidgets.QApplication.processEvents() QtWidgets.QApplication.processEvents()
@@ -217,21 +220,19 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
# Check for updates on Windows # Check for updates on Windows
if state.ignore_updates is False and platform.system() == "Windows": if state.ignore_updates is False and platform.system() == "Windows":
result = check_for_updates() assets = get_updates()
if result != (None, None): if assets != None:
assets, latest_version = result
if assets: msg = QMessageBox()
msg = QMessageBox() msg.setIcon(QMessageBox.Icon.Information)
msg.setIcon(QMessageBox.Icon.Information) msg.setWindowTitle("Update Available")
msg.setWindowTitle("Update Available") msg.setText("A new version is available.")
msg.setText("A new version is available.") msg.setInformativeText("Press Ok to download the update.")
msg.setInformativeText("Press Ok to download the update.") msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore)
response = msg.exec_() response = msg.exec_()
if response == QMessageBox.StandardButton.Ok: if response == QMessageBox.StandardButton.Ok:
download_update(latest_version) _download_update(assets)
self.setWindowTitle("Software Manager") self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600) self.setGeometry(100, 100, 800, 600)
@@ -455,21 +456,27 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
class HoverRowDelegate(QStyledItemDelegate): class HoverRowDelegate(QStyledItemDelegate):
def paint(self, painter, option, index): def paint(self, painter, option, index):
opt = QtWidgets.QStyleOptionViewItem(option)
opt.state &= ~QtWidgets.QStyle.StateFlag.State_HasFocus
opt.state &= ~QtWidgets.QStyle.StateFlag.State_Selected
if index.row() == MainWindow._instance._hovered_row: if index.row() == MainWindow._instance._hovered_row:
painter.save() painter.save()
painter.fillRect(option.rect, _theme_colors()["hover"]) painter.fillRect(opt.rect, _theme_colors()["hover"])
painter.restore() painter.restore()
super().paint(painter, option, index) super().paint(painter, opt, index)
class PauseResumeDelegate(QStyledItemDelegate): class PauseResumeDelegate(QStyledItemDelegate):
clicked = Signal(int) clicked = Signal(int)
def paint(self, painter, option, index): def paint(self, painter, option, index):
opt = QtWidgets.QStyleOptionViewItem(option)
opt.state &= ~QtWidgets.QStyle.StateFlag.State_HasFocus
opt.state &= ~QtWidgets.QStyle.StateFlag.State_Selected
if index.row() == MainWindow._instance._hovered_row: if index.row() == MainWindow._instance._hovered_row:
painter.save() painter.save()
painter.fillRect(option.rect, _theme_colors()["hover"]) painter.fillRect(opt.rect, _theme_colors()["hover"])
painter.restore() painter.restore()
super().paint(painter, option, index) super().paint(painter, opt, index)
def setEditorData(self, editor, index): def setEditorData(self, editor, index):
button = editor.findChild(QtWidgets.QPushButton) button = editor.findChild(QtWidgets.QPushButton)
@@ -480,11 +487,11 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if button: if button:
if status.state == lt.torrent_status.seeding: if status.state == lt.torrent_status.seeding:
button.setIcon(_svg_icon(SVG_FOLDER, 18)) button.setIcon(svg_icon(SVG_FOLDER, 18))
button.setText("") button.setText("")
else: else:
is_user_paused = status.paused and not bool(magnetdl.flags() & lt.torrent_flags.auto_managed) is_user_paused = status.paused and not bool(magnetdl.flags() & lt.torrent_flags.auto_managed)
button.setIcon(_svg_icon(SVG_PLAY if is_user_paused else SVG_PAUSE, 18)) button.setIcon(svg_icon(SVG_PLAY if is_user_paused else SVG_PAUSE, 18))
button.setText("") button.setText("")
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
@@ -498,11 +505,11 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
if status.state == lt.torrent_status.seeding: if status.state == lt.torrent_status.seeding:
btnPause = QtWidgets.QPushButton() btnPause = QtWidgets.QPushButton()
btnPause.setIcon(_svg_icon(SVG_FOLDER, 18)) btnPause.setIcon(svg_icon(SVG_FOLDER, 18))
else: else:
btnPause = QtWidgets.QPushButton() btnPause = QtWidgets.QPushButton()
is_user_paused = status.paused and not bool(magnetdl.flags() & lt.torrent_flags.auto_managed) is_user_paused = status.paused and not bool(magnetdl.flags() & lt.torrent_flags.auto_managed)
btnPause.setIcon(_svg_icon(SVG_PLAY if is_user_paused else SVG_PAUSE, 18)) btnPause.setIcon(svg_icon(SVG_PLAY if is_user_paused else SVG_PAUSE, 18))
btnPause.setIconSize(QSize(18, 18)) btnPause.setIconSize(QSize(18, 18))
btnPause.setFixedSize(30, 30) btnPause.setFixedSize(30, 30)
btnPause.setCursor(Qt.CursorShape.PointingHandCursor) btnPause.setCursor(Qt.CursorShape.PointingHandCursor)
+30
View File
@@ -0,0 +1,30 @@
from PySide6 import QtWidgets
from PySide6.QtCore import Qt, QByteArray
from PySide6.QtGui import QIcon, QPixmap, QPainter
from PySide6.QtSvg import QSvgRenderer
import darkdetect
def _is_dark_mode():
return darkdetect.isDark()
def svg_icon(svg_str, size=20):
app = QtWidgets.QApplication.instance()
if app:
if _is_dark_mode():
color = "white"
else:
color = "#555555"
else:
color = "white"
svg = svg_str.replace("{color}", color)
renderer = QSvgRenderer(QByteArray(svg.encode()))
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
return QIcon(pixmap)
+2 -2
View File
@@ -30,11 +30,11 @@ def general_tab(title, autoresume, update_checkbox, transparent_window, tabs):
tabs.addTab(tab, title) tabs.addTab(tab, title)
return tab return tab
def paths_tab(title, download_path, tabs): def paths_tab(title, download_path, image_path, tabs):
tab = QWidget() tab = QWidget()
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addWidget(download_path) layout.addWidget(download_path)
# layout.addWidget(image_path) layout.addWidget(image_path)
layout.addStretch() layout.addStretch()
tab.setLayout(layout) tab.setLayout(layout)
tabs.addTab(tab, title) tabs.addTab(tab, title)
+14 -9
View File
@@ -2,28 +2,33 @@ import requests
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
def check_for_updates(): def get_updates():
url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases" url = f"https://api.github.com/repos/KeksPirates/SoftwareManager/releases/latest"
response = requests.get(url, timeout=15) response = requests.get(url, timeout=15)
if response.status_code != 200: if response.status_code != 200:
consoleLog(f"Failed to fetch releases: {response.status_code}") consoleLog(f"Failed to fetch releases: {response.status_code}")
return None, None return None, None
releases = response.json() release = response.json()
releases.sort(key=lambda r: r["published_at"], reverse=True)
latest_release = releases[0] latest_version = release["name"]
latest_version = latest_release["name"] assets = release["assets"]
assets = latest_release["assets"]
release_assets = []
if latest_version != state.version: if latest_version != state.version:
consoleLog(f"New release available: {latest_version}") consoleLog(f"New release available: {latest_version}")
if assets: if assets:
consoleLog("Assets:") consoleLog("Assets:")
for asset in assets: for asset in assets:
consoleLog(f" - {asset['name']}: {asset['browser_download_url']}") consoleLog(f"{asset['name']}")
return assets, latest_version release_assets.append(dict(
name=asset['name'],
url=asset['browser_download_url'],
hash=asset['digest']
))
return release_assets
else: else:
return None, None return None, None
else: else: