Compare commits

..

16 Commits

Author SHA1 Message Date
shayaa e950d2f90d Refactor dev.yml to use bash for build steps
Updated the workflow to use bash for injecting commit hash and building executables on Windows and Unix. (done with claude)
2025-12-20 04:29:22 +01:00
Vxrtrauter 1a59c45ba4 fix qtablewidget section width 2025-12-16 11:25:40 +01:00
Vxrtrauter 293c1c3ad1 fix settings icon again? probably os issue 2025-12-16 11:00:11 +01:00
KeksNino f30cc5da5e fix typo 2025-11-15 00:20:41 +01:00
KeksNino 004c7ad416 add build_info.json
to fix updater when running from source
2025-11-15 00:19:50 +01:00
Vxrtrauter 65f7c04936 refactor: clean up unused imports and improve code organization 2025-11-15 00:16:47 +01:00
KeksNino 9d0d5332bf fixed updater 2025-10-31 13:45:30 +01:00
KeksNino 6d9991188a fixed config parser error handling 2025-10-31 03:22:47 +01:00
KeksNino f397bc18ce added option to display image in bottom right 2025-10-31 03:18:59 +01:00
KeksNino 59e45efbb6 fix aria2c dependency in pyinstaller 2025-10-25 21:59:29 +02:00
KeksNino 27f0a3f71d fix wget path on windows in workflows 2025-10-25 21:57:31 +02:00
KeksNino 1d500305f7 add aria2c dependency to exe binary on windows 2025-10-25 21:52:39 +02:00
Vxrtrauter 20a310264f Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2025-10-24 10:51:51 +02:00
Vxrtrauter aee8f16353 rm unused import 2025-10-24 10:50:52 +02:00
KeksNino 2354adb349 update updater download url 2025-10-20 19:10:49 +02:00
KeksNino 4c69a17f0e change back to releases for github workflow 2025-10-20 18:44:42 +02:00
9 changed files with 181 additions and 81 deletions
+29 -10
View File
@@ -81,22 +81,42 @@ jobs:
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION"
- name: Inject commit hash into code (Windows)
if: runner.os == 'Windows'
shell: pwsh
- name: Inject commit hash into code
shell: bash
run: |
$json = '{
"version": "' + $env:VERSION + '"
}'
$json | Out-File -Encoding utf8 build_info.json
cat > build_info.json << EOF
{
"version": "${{ env.VERSION }}"
}
EOF
- name: Download aria2c (Windows)
if: runner.os == 'Windows'
run: |
mkdir -p deps
C:\msys64\usr\bin\wget.exe https://github.com/aria2/aria2/releases/download/release-1.37.0/aria2-1.37.0-win-64bit-build1.zip
unzip -j aria2-1.37.0-win-64bit-build1.zip aria2-1.37.0-win-64bit-build1/aria2c.exe -d deps/
- name: Build executable with PyInstaller (Windows)
if: runner.os == 'Windows'
run: pyinstaller --onefile --windowed --add-data "build_info.json:." --name ${{ env.APP_NAME }} main.py
shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json;." \
--add-data "deps/aria2c.exe;." \
--add-data "core/interface/assets;core/interface/assets" \
--name ${{ env.APP_NAME }} \
main.py
- name: Build executable with PyInstaller (Unix)
if: runner.os != 'Windows'
run: pyinstaller --onefile --windowed --name ${{ env.APP_NAME }} main.py
shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json:." \
--add-data "core/interface/assets:core/interface/assets" \
--name ${{ env.APP_NAME }} \
main.py
- name: Make executable (Unix)
if: runner.os != 'Windows'
@@ -143,7 +163,6 @@ jobs:
with:
tag_name: ${{ needs.build.outputs.version }}
name: ${{ needs.build.outputs.version }}
prerelease: true
files: |
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-linux
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-windows.exe
+3
View File
@@ -0,0 +1,3 @@
{
"Version": "dev"
}
-1
View File
@@ -1,6 +1,5 @@
import requests
import asyncio
import aiohttp
from bs4 import BeautifulSoup
from core.utils.data.state import state
+17 -2
View File
@@ -114,6 +114,21 @@ def settings_dialog(self):
speed_limit.setFixedHeight(30)
dialog.layout().addWidget(speed_limit_container)
###############
# IMAGE PATH #
###############
image_path_container = QWidget()
image_path_layout = QHBoxLayout()
image_path = QLineEdit()
image_path_layout.addWidget(QLabel("Image Path (requires restart):"))
image_path_layout.addWidget(image_path)
image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
image_path_container.setLayout(image_path_layout)
image_path.setText(state.image_path)
dialog.layout().addWidget(image_path_container)
###############
# SAVE/CANCEL #
###############
@@ -122,8 +137,8 @@ def settings_dialog(self):
save_btn = QPushButton("Save")
cancel_btn = QPushButton("Cancel")
save_btn.clicked.connect(lambda: save_settings(thread_box.value(), close_settings, api_url.text(), download_path.text(), speed_limit.value()))
save_btn.clicked.connect(lambda: save_settings(thread_box.value(), close_settings, api_url.text(), download_path.text(), speed_limit.value(), image_path.text()))
cancel_btn.clicked.connect(dialog.reject)
layout.addWidget(cancel_btn)
layout.addWidget(save_btn)
+78 -40
View File
@@ -1,16 +1,16 @@
from PySide6 import QtWidgets
from PySide6 import QtCore
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel
from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal
from PySide6.QtWidgets import (
QLineEdit,
QTableView,
QWidget,
QVBoxLayout,
QListWidget,
QToolBar,
QLabel,
QTableView,
QWidget,
QVBoxLayout,
QListWidget,
QToolBar,
QLabel,
QHBoxLayout,
QComboBox,
QComboBox,
QTabWidget,
QProgressBar,
QHeaderView,
@@ -18,7 +18,7 @@ from PySide6.QtWidgets import (
QTableWidget,
)
from PySide6.QtGui import QIcon, QAction, QCloseEvent
from PySide6.QtGui import QIcon, QAction, QCloseEvent, QImage, QPixmap
import darkdetect
import threading
import platform
@@ -38,25 +38,30 @@ from core.interface.utils.searchhelper import return_pressed
from core.interface.dialogs.settings import settings_dialog
from core.network.aria2_integration import dlprogress
print(os.path.abspath(__file__))
def download_update(latest_version):
old_filename = f"SoftwareManager-dev-{state.version.replace('-dev', '')}-windows.exe"
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}-windows.exe"
url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
print("Downloading update...")
if os.path.exists(old_filename):
os.remove(old_filename)
response = r.get(url, allow_redirects=True)
with open(new_filename, "wb") as f:
f.write(response.content)
if not os.path.exists(new_filename):
raise FileNotFoundError(f"Executable not found")
raise FileNotFoundError("Executable not found")
subprocess.Popen([new_filename], shell=True)
time.sleep(0.5)
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setWindowTitle("New Version")
msg.setText("New version installed.")
msg.setInformativeText("Please remove the old exe.")
msg.setStandardButtons(QMessageBox.Ok)
sys.exit(0)
class MainWindow(QtWidgets.QMainWindow, QWidget):
def __init__(self):
super().__init__()
@@ -71,19 +76,20 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if state.ignore_updates is False:
if platform.system() == "Windows":
result = check_for_updates()
assets, latest_version = result
if result != (None, None):
assets, latest_version = result
if assets:
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setWindowTitle("Update Available")
msg.setText(f"A new version is available.")
msg.setInformativeText("Please visit the GitHub releases page to download the latest version.")
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Ignore)
if assets:
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setWindowTitle("Update Available")
msg.setText("A new version is available.")
msg.setInformativeText("Press Ok to download the update.")
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Ignore)
response = msg.exec_()
if response == QMessageBox.Ok:
download_update(latest_version)
response = msg.exec_()
if response == QMessageBox.Ok:
download_update(latest_version)
self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600)
@@ -97,7 +103,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.searchbar.setClearButtonEnabled(True)
self.searchbar.setMinimumHeight(30)
self.searchbar.returnPressed.connect(lambda: run_thread(threading.Thread(target=return_pressed, args=(self,)))) # Triggers data function thread on enter
self.dlbutton = QtWidgets.QPushButton("Download")
self.libraryList = QListWidget()
@@ -105,19 +111,25 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.emptyResults.setAlignment(Qt.AlignCenter)
self.emptyResults.hide()
self.download_model = None
self.downloadList = QTableView()
self.emptyLibrary = QLabel("No items in library.")
self.emptyDownload = QLabel("No items in downloads.")
self.progressbar = QProgressBar()
# Table Widget for Item List
self.qtablewidget = QTableWidget()
self.qtablewidget.setColumnCount(2)
self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.qtablewidget.verticalHeader().setVisible(False)
self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
self.qtablewidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
header = self.qtablewidget.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch)
header.setSectionResizeMode(1, QHeaderView.Fixed)
header.setStretchLastSection(False)
self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground)
self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground)
@@ -134,13 +146,13 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def __init__(self):
super().__init__()
self.headers = ["Name", "Status", "Progress", "Speed", "Size", "Total Size"]
def rowCount(self, parent=QModelIndex()):
return len(state.downloads)
def columnCount(self, parent=QModelIndex()):
return len(self.headers)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.headers[section]
@@ -150,7 +162,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if role == Qt.DisplayRole:
download = state.downloads[index.row()]
col = index.column()
if col == 0:
return download.name
elif col == 1:
@@ -185,29 +197,41 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.horizontal_layout = QHBoxLayout()
self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
self.horizontal_layout.addWidget(self.qtablewidget)
self.tab1 = create_tab("Search", self.searchbar, self.qtablewidget, self.tabs, self.dlbutton, self.horizontal_layout)
self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None)
self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
self.image = QImage(state.image_path)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label = QLabel(self)
self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize()
self.overlay_label.raise_()
offset_x = -1350
offset_y = -550
x = self.width() - self.overlay_label.width() - offset_x
y = self.height() - self.overlay_label.height() - offset_y
self.overlay_label.move(x, y)
# temporarily disabled
# state.image_changed.connect(self.update_image_overlay)
containerLayout.addWidget(self.tabs)
containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom)
toolbar = QToolBar("Main Toolbar")
self.addToolBar(toolbar)
toolbar.setLayoutDirection(Qt.RightToLeft)
self.tracker_list = QComboBox()
self.tracker_list.addItems(["rutracker", "uztracker", "m0nkrus"])
self.tracker_list.activated.connect(self.set_tracker)
if darkdetect.isDark():
settings_action = QAction(QIcon("core/interface/assets/settings_white.png"), "Settings", self)
else:
settings_action = QAction(QIcon("core/interface/assets/settings_black.png"), "Settings", self)
settings_action = QAction(QIcon("core/interface/assets/settings_white.png"), "Settings", self)
settings_action.triggered.connect(lambda: settings_dialog(self))
toolbar.addAction(settings_action)
@@ -217,7 +241,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.progressbar.setValue(0)
containerLayout.addWidget(self.progressbar)
self.progress_timer = QTimer()
self.progress_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.update_progress)))
self.progress_timer.start(1000)
@@ -226,6 +250,13 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update)))
self.download_timer.start(500)
def update_image_overlay(self, new_image_path):
self.image = QImage(new_image_path)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize()
def download_list_update(self):
if self.download_model:
self.download_model.layoutAboutToBeChanged.emit()
@@ -249,3 +280,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
else:
self.emptyResults.hide()
self.qtablewidget.show()
# thank you claude
def resizeEvent(self, event):
super().resizeEvent(event)
table_width = self.qtablewidget.viewport().width()
self.qtablewidget.setColumnWidth(1, int(table_width * 0.3))
+15 -7
View File
@@ -7,7 +7,7 @@ def create_config():
config = configparser.ConfigParser()
config["General"] = {"debug": True, "api_url": f"{state.api_url}", "aria2_threads": f"{state.aria2_threads}", "download_path": f"{state.download_path}", f"speed_limit": f"{state.speed_limit}",
"ignore_updates": f"{state.ignore_updates}"}
"ignore_updates": f"{state.ignore_updates}", "image_path": f"{state.image_path}"}
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
@@ -37,9 +37,17 @@ def read_config():
config.read(config_file)
state.debug = config.getboolean("General", "debug")
state.api_url = config.get("General", "api_url")
state.aria2_threads = config.getint("General", "aria2_threads")
state.download_path = config.get("General", "download_path")
state.speed_limit = config.getint("General", "speed_limit")
state.ignore_updates = config.getboolean("General", "ignore_updates")
if state.debug is not None:
state.debug = config.getboolean("General", "debug")
if state.api_url is not None:
state.api_url = config.get("General", "api_url")
if state.aria2_threads is not None:
state.aria2_threads = config.getint("General", "aria2_threads")
if state.download_path is not None:
state.download_path = config.get("General", "download_path")
if state.speed_limit is not None:
state.speed_limit = config.getint("General", "speed_limit")
if state.ignore_updates is not None:
state.ignore_updates = config.getboolean("General", "ignore_updates")
if state.image_path is not None:
state.image_path = config.get("General", "image_path")
+3 -1
View File
@@ -13,7 +13,7 @@ def restart_aria2c():
atexit.unregister(kill_aria2server)
atexit.register(kill_aria2server)
def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None):
def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None, image_path=None):
if thread_count is not None:
state.aria2_threads = thread_count
if apiurl is not None:
@@ -22,6 +22,8 @@ def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_p
state.download_path = download_path
if speed_limit is not None:
state.speed_limit = speed_limit
if image_path is not None:
state.image_path = image_path
create_config()
restart_aria2c()
+34 -20
View File
@@ -1,24 +1,38 @@
from dataclasses import dataclass
from PySide6.QtCore import QObject, Signal
from typing import Optional, Any, List, Dict
from pathlib import Path
@dataclass
class AppState:
posts: List[Dict[str, Any]]
post_titles: List[str]
post_urls: List[str]
post_author: List[str]
downloads: List[str]
version: str
ignore_updates: bool = False
debug: bool = False
tracker: str = "rutracker"
api_url: str = "https://api.michijackson.xyz"
download_path: str = str(Path.home() / "Downloads")
speed_limit: int = 0
aria2process: Optional[Any] = None
aria2: Any = None
aria2p: Any = None
aria2_threads: int = 4
class AppState(QObject):
image_changed = Signal(str)
state = AppState(posts=[], post_titles=[], post_urls=[], post_author=[], downloads=[], download_path="", version="")
def __init__(self):
super().__init__()
self.posts: List[Dict[str, Any]] = []
self.post_titles: List[str] = []
self.post_urls: List[str] = []
self.post_author: List[str] = []
self.downloads: List[str] = []
self.version: str = "dev"
self._image_path: str = ""
self.ignore_updates: bool = False
self.debug: bool = False
self.tracker: str = "rutracker"
self.api_url: str = "https://api.michijackson.xyz"
self.download_path: str = str(Path.home() / "Downloads")
self.speed_limit: int = 0
self.aria2process: Optional[Any] = None
self.aria2: Any = None
self.aria2p: Any = None
self.aria2_threads: int = 4
@property
def image_path(self) -> str:
return self._image_path
@image_path.setter
def image_path(self, new_path: str):
if new_path != self._image_path:
self._image_path = new_path
self.image_changed.emit(new_path)
state = AppState()
+2
View File
@@ -30,6 +30,8 @@ def check_for_updates():
for asset in assets:
print(f" - {asset['name']}: {asset['browser_download_url']}")
return assets, latest_version
else:
return None, None
else:
if state.debug:
print("Already up-to-date.")