Compare commits

..

24 Commits

Author SHA1 Message Date
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
KeksNino 2bab6c3321 fix old filename for updater 2025-10-20 18:22:16 +02:00
KeksNino 91482760b6 fix updater filename 2025-10-20 18:14:09 +02:00
KeksNino 83fa23df58 fix release file names 2025-10-20 17:58:40 +02:00
KeksNino 41d28095c2 fix workflows 2025-10-20 17:53:35 +02:00
KeksNino de2e49e60b add back commit sha to release file name 2025-10-20 17:40:39 +02:00
KeksNino dd6d16d833 fix workflows (again) 2025-10-20 17:24:04 +02:00
KeksNino 7d0c8d16ec fix workflows (again) 2025-10-20 17:21:47 +02:00
KeksNino 80750f010f fix workflows (again) 2025-10-20 17:00:14 +02:00
KeksNino c93f6345c8 fix workflows (again) 2025-10-20 16:36:10 +02:00
KeksNino a7132ed1cd fix workflows (again) 2025-10-19 19:45:19 +02:00
KeksNino a2cff15d0d fix workflows (hopefully) 2025-10-19 19:39:33 +02:00
KeksNino 2eb384f7cf fix build_info.json 2025-10-19 17:51:21 +02:00
KeksNino 05a5b04507 fix error if build_info doesnt exist in source code 2025-10-19 16:22:49 +02:00
KeksNino 87c974f124 add build_info.json to workflow for win updater 2025-10-19 15:58:16 +02:00
7 changed files with 189 additions and 108 deletions
+55 -39
View File
@@ -25,16 +25,19 @@ jobs:
matrix: matrix:
include: include:
- os: ubuntu-latest - os: ubuntu-latest
artifact_name: SoftwareManager-dev-linux artifact_name: SoftwareManager-dev-${{ github.sha }}-linux
asset_name: SoftwareManager-dev-linux asset_name: SoftwareManager-dev-${{ github.sha }}-linux
- os: windows-latest - os: windows-latest
artifact_name: SoftwareManager-dev-windows.exe artifact_name: SoftwareManager-dev-${{ github.sha }}-windows.exe
asset_name: SoftwareManager-dev-windows.exe asset_name: SoftwareManager-dev-${{ github.sha }}-windows.exe
- os: macos-latest - os: macos-latest
artifact_name: SoftwareManager-dev-macos artifact_name: SoftwareManager-dev-${{ github.sha }}-macos
asset_name: SoftwareManager-dev-macos asset_name: SoftwareManager-dev-${{ github.sha }}-macos
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
outputs:
version: ${{ steps.version.outputs.version }}
short_sha: ${{ steps.version.outputs.short_sha }}
steps: steps:
- name: Checkout code - name: Checkout code
@@ -66,7 +69,40 @@ jobs:
pip install -r requirements.txt pip install -r requirements.txt
pip install pyinstaller pip install pyinstaller
- name: Build executable with PyInstaller - name: Generate version
id: version
shell: bash
run: |
COMMIT_SHA=${{ github.sha }}
SHORT_SHA=${COMMIT_SHA:0:7}
VERSION="${SHORT_SHA}-dev"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION"
- name: Inject commit hash into code (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$json = '{
"version": "' + $env:VERSION + '"
}'
$json | Out-File -Encoding utf8 build_info.json
- 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:." --add-data "deps/aria2c.exe:." --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 run: pyinstaller --onefile --windowed --name ${{ env.APP_NAME }} main.py
- name: Make executable (Unix) - name: Make executable (Unix)
@@ -94,49 +130,29 @@ jobs:
if: success() if: success()
steps: steps:
- name: Checkout code - uses: actions/checkout@v4
uses: actions/checkout@v4 - uses: actions/download-artifact@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with: with:
path: artifacts path: artifacts
- name: Display downloaded artifacts
run: |
echo "Downloaded artifacts:"
find artifacts -type f
ls -la artifacts/
- name: Generate version
id: version
run: |
COMMIT_SHA=${{ github.sha }}
SHORT_SHA=${COMMIT_SHA:0:7}
VERSION="${SHORT_SHA}-dev"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "Generated version: ${VERSION}"
- name: Prepare release files - name: Prepare release files
run: | run: |
mkdir -p release mkdir -p release
cp artifacts/ubuntu-latest-build/SoftwareManager-dev-linux release/SoftwareManager-dev-linux SHORT_SHA="${{ needs.build.outputs.short_sha }}"
cp artifacts/windows-latest-build/SoftwareManager-dev-windows.exe release/SoftwareManager-dev-windows.exe cp artifacts/ubuntu-latest-build/SoftwareManager-dev-*-linux release/SoftwareManager-dev-${SHORT_SHA}-linux
cp artifacts/macos-latest-build/SoftwareManager-dev-macos release/SoftwareManager-dev-macos cp artifacts/windows-latest-build/SoftwareManager-dev-*-windows.exe release/SoftwareManager-dev-${SHORT_SHA}-windows.exe
chmod +x release/SoftwareManager-dev-linux release/SoftwareManager-dev-macos cp artifacts/macos-latest-build/SoftwareManager-dev-*-macos release/SoftwareManager-dev-${SHORT_SHA}-macos
chmod +x release/SoftwareManager-dev-*-linux release/SoftwareManager-dev-*-macos
ls -la release/ ls -la release/
- name: Create Release (Public) - name: Create Release (Public)
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ steps.version.outputs.version }} tag_name: ${{ needs.build.outputs.version }}
name: ${{ steps.version.outputs.version }} name: ${{ needs.build.outputs.version }}
draft: false
prerelease: false
files: | files: |
release/SoftwareManager-dev-linux release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-linux
release/SoftwareManager-dev-windows.exe release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-windows.exe
release/SoftwareManager-dev-macos release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-1
View File
@@ -1,6 +1,5 @@
import requests import requests
import asyncio import asyncio
import aiohttp
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from core.utils.data.state import state from core.utils.data.state import state
+16 -1
View File
@@ -114,6 +114,21 @@ def settings_dialog(self):
speed_limit.setFixedHeight(30) speed_limit.setFixedHeight(30)
dialog.layout().addWidget(speed_limit_container) 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 # # SAVE/CANCEL #
############### ###############
@@ -122,7 +137,7 @@ def settings_dialog(self):
save_btn = QPushButton("Save") save_btn = QPushButton("Save")
cancel_btn = QPushButton("Cancel") 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) cancel_btn.clicked.connect(dialog.reject)
layout.addWidget(cancel_btn) layout.addWidget(cancel_btn)
+48 -18
View File
@@ -1,6 +1,6 @@
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6 import QtCore 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 ( from PySide6.QtWidgets import (
QLineEdit, QLineEdit,
QTableView, QTableView,
@@ -16,10 +16,9 @@ from PySide6.QtWidgets import (
QHeaderView, QHeaderView,
QMessageBox, QMessageBox,
QTableWidget, QTableWidget,
QTableWidgetItem
) )
from PySide6.QtGui import QIcon, QAction, QCloseEvent, QPalette from PySide6.QtGui import QIcon, QAction, QCloseEvent, QImage, QPixmap
import darkdetect import darkdetect
import threading import threading
import platform import platform
@@ -28,7 +27,7 @@ import os
import subprocess import subprocess
import time import time
import sys import sys
from core.utils.config.settings import save_settings import json
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
@@ -39,27 +38,40 @@ from core.interface.utils.searchhelper import return_pressed
from core.interface.dialogs.settings import settings_dialog from core.interface.dialogs.settings import settings_dialog
from core.network.aria2_integration import dlprogress from core.network.aria2_integration import dlprogress
def download_update(latest_version): def download_update(latest_version):
filename = "SoftwareManager-dev-windows.exe" new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
url = "https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-windows.exe" url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe"
print("Downloading update...") print("Downloading update...")
if os.path.exists(filename):
os.remove(filename)
response = r.get(url, allow_redirects=True) response = r.get(url, allow_redirects=True)
with open(filename, "wb") as f: with open(new_filename, "wb") as f:
f.write(response.content) f.write(response.content)
if not os.path.exists(filename): if not os.path.exists(new_filename):
raise FileNotFoundError(f"Executable not found") raise FileNotFoundError("Executable not found")
subprocess.Popen([filename], shell=True) subprocess.Popen([new_filename], shell=True)
time.sleep(0.5) time.sleep(0.5)
save_settings(version=latest_version)
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) sys.exit(0)
class MainWindow(QtWidgets.QMainWindow, QWidget): class MainWindow(QtWidgets.QMainWindow, QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
build_info_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "build_info.json")
if os.path.exists(build_info_path):
with open(build_info_path, "r") as f:
build_info = json.load(f)
state.version = build_info.get("version")
# Check for updates on Windows # Check for updates on Windows
if state.ignore_updates is False: if state.ignore_updates is False:
if platform.system() == "Windows": if platform.system() == "Windows":
@@ -70,8 +82,8 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
msg = QMessageBox() msg = QMessageBox()
msg.setIcon(QMessageBox.Information) msg.setIcon(QMessageBox.Information)
msg.setWindowTitle("Update Available") msg.setWindowTitle("Update Available")
msg.setText(f"A new version is available.") msg.setText("A new version is available.")
msg.setInformativeText("Please visit the GitHub releases page to download the latest version.") msg.setInformativeText("Press Ok to download the update.")
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Ignore) msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Ignore)
response = msg.exec_() response = msg.exec_()
@@ -98,7 +110,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.emptyResults.setAlignment(Qt.AlignCenter) self.emptyResults.setAlignment(Qt.AlignCenter)
self.emptyResults.hide() self.emptyResults.hide()
self.download_model = None self.download_model = None
self.downloadList = QTableView() self.downloadList = QTableView()
self.emptyLibrary = QLabel("No items in library.") self.emptyLibrary = QLabel("No items in library.")
@@ -183,12 +194,25 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None) 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.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.addWidget(self.tabs)
containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom) containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom)
toolbar = QToolBar("Main Toolbar") toolbar = QToolBar("Main Toolbar")
self.addToolBar(toolbar) self.addToolBar(toolbar)
toolbar.setLayoutDirection(Qt.RightToLeft) toolbar.setLayoutDirection(Qt.RightToLeft)
@@ -199,7 +223,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
if darkdetect.isDark(): if darkdetect.isDark():
settings_action = QAction(QIcon("core/interface/assets/settings_white.png"), "Settings", self) 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_black.png"), "Settings", self)
settings_action.triggered.connect(lambda: settings_dialog(self)) settings_action.triggered.connect(lambda: settings_dialog(self))
@@ -219,6 +242,13 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update))) self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update)))
self.download_timer.start(500) 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): def download_list_update(self):
if self.download_model: if self.download_model:
self.download_model.layoutAboutToBeChanged.emit() self.download_model.layoutAboutToBeChanged.emit()
+15 -8
View File
@@ -7,7 +7,7 @@ def create_config():
config = configparser.ConfigParser() 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}", 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}",
"version": f"{state.version}", "ignore_updates": f"{state.ignore_updates}"} "ignore_updates": f"{state.ignore_updates}", "image_path": f"{state.image_path}"}
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"))
@@ -37,10 +37,17 @@ def read_config():
config.read(config_file) config.read(config_file)
state.debug = config.getboolean("General", "debug") if state.debug is not None:
state.api_url = config.get("General", "api_url") state.debug = config.getboolean("General", "debug")
state.aria2_threads = config.getint("General", "aria2_threads") if state.api_url is not None:
state.download_path = config.get("General", "download_path") state.api_url = config.get("General", "api_url")
state.speed_limit = config.getint("General", "speed_limit") if state.aria2_threads is not None:
state.version = config.get("General", "version") state.aria2_threads = config.getint("General", "aria2_threads")
state.ignore_updates = config.getboolean("General", "ignore_updates") 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 -3
View File
@@ -13,7 +13,7 @@ def restart_aria2c():
atexit.unregister(kill_aria2server) atexit.unregister(kill_aria2server)
atexit.register(kill_aria2server) atexit.register(kill_aria2server)
def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None, version=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: if thread_count is not None:
state.aria2_threads = thread_count state.aria2_threads = thread_count
if apiurl is not None: if apiurl is not None:
@@ -22,8 +22,8 @@ def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_p
state.download_path = download_path state.download_path = download_path
if speed_limit is not None: if speed_limit is not None:
state.speed_limit = speed_limit state.speed_limit = speed_limit
if version is not None: if image_path is not None:
state.version = version state.image_path = image_path
create_config() create_config()
restart_aria2c() 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 typing import Optional, Any, List, Dict
from pathlib import Path from pathlib import Path
@dataclass class AppState(QObject):
class AppState: image_changed = Signal(str)
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
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
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()