Compare commits

...

12 Commits

Author SHA1 Message Date
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
KeksNino 95a687ddd9 fix updater 2025-10-19 04:07:41 +02:00
KeksNino 813d2b31c4 add setting to ignore updates on windows 2025-10-19 04:05:57 +02:00
Vxrtrauter deb5ce5b88 fixed aria2 killing issues on settings close 2025-10-19 03:53:52 +02:00
Vxrtrauter a11c1d7129 added dev release for version check 2025-10-19 03:23:37 +02:00
Vxrtrauter dcc78ade44 fixed qtablewidget colors 2025-10-19 03:13:08 +02:00
KeksNino 10e80ab4b6 fix row count 2025-10-19 02:58:02 +02:00
KeksNino 3290ff3fac switch to spreadsheet view in softwareList
QTableWidget
2025-10-19 02:56:10 +02:00
Vxrtrauter d5d99d6a0e fix debug outputs for update checker 2025-10-18 23:55:25 +02:00
Vxrtrauter 3960612f94 fix settings icon not showing up 2025-10-18 23:47:54 +02:00
8 changed files with 125 additions and 92 deletions
+7 -1
View File
@@ -66,8 +66,14 @@ jobs:
pip install -r requirements.txt pip install -r requirements.txt
pip install pyinstaller pip install pyinstaller
- name: Inject commit hash into code
run: |
echo '{' > build_info.json
echo " \"version\": \"${GITHUB_SHA}\"" >> build_info.json
echo '}' >> build_info.json
- name: Build executable with PyInstaller - name: Build executable with PyInstaller
run: pyinstaller --onefile --windowed --name ${{ env.APP_NAME }} main.py run: pyinstaller --onefile --windowed --add-data "build_info.json:." --name ${{ env.APP_NAME }} main.py
- name: Make executable (Unix) - name: Make executable (Unix)
if: runner.os != 'Windows' if: runner.os != 'Windows'
+16 -5
View File
@@ -10,12 +10,10 @@ from PySide6.QtWidgets import (
QLabel, QLabel,
QHBoxLayout, QHBoxLayout,
QSpinBox, QSpinBox,
QCheckBox,
) )
def settings_dialog(self): def settings_dialog(self):
if state.debug: if state.debug:
@@ -29,7 +27,20 @@ def settings_dialog(self):
def close_settings(): def close_settings():
dialog.reject() dialog.reject()
checkbox_container = QWidget()
checkbox_layout = QHBoxLayout()
# ignore updates checkbox
checkbox = QCheckBox()
checkbox_container.setLayout(checkbox_layout)
checkbox_layout.addWidget(QLabel("Ignore Updates: "))
checkbox_layout.addStretch()
checkbox.setChecked(state.ignore_updates)
checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
checkbox_layout.addWidget(checkbox)
dialog.layout().addWidget(checkbox_container)
################## ##################
# THREAD SETTING # # THREAD SETTING #
################## ##################
@@ -119,4 +130,4 @@ def settings_dialog(self):
dialog.layout().addLayout(layout) dialog.layout().addLayout(layout)
dialog.exec() dialog.exec()
+49 -31
View File
@@ -15,7 +15,9 @@ from PySide6.QtWidgets import (
QProgressBar, QProgressBar,
QHeaderView, QHeaderView,
QMessageBox, QMessageBox,
QTableWidget,
) )
from PySide6.QtGui import QIcon, QAction, QCloseEvent from PySide6.QtGui import QIcon, QAction, QCloseEvent
import darkdetect import darkdetect
import threading import threading
@@ -25,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
@@ -36,7 +38,7 @@ 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():
filename = "SoftwareManager-dev-windows.exe" filename = "SoftwareManager-dev-windows.exe"
url = "https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-windows.exe" url = "https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-windows.exe"
@@ -50,30 +52,35 @@ def download_update(latest_version):
raise FileNotFoundError(f"Executable not found") raise FileNotFoundError(f"Executable not found")
subprocess.Popen([filename], shell=True) subprocess.Popen([filename], shell=True)
time.sleep(0.5) time.sleep(0.5)
save_settings(version=latest_version)
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__()
searchresults = []
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 platform.system() == "Windows": if state.ignore_updates is False:
result = check_for_updates() if platform.system() == "Windows":
assets, latest_version = result result = check_for_updates()
assets, latest_version = result
if assets: if assets:
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(f"A new version is available.")
msg.setInformativeText("Please visit the GitHub releases page to download the latest version.") msg.setInformativeText("Please visit the GitHub releases page to download the latest version.")
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Ignore) msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Ignore)
response = msg.exec_() response = msg.exec_()
if response == QMessageBox.Ok: if response == QMessageBox.Ok:
download_update(latest_version) download_update(latest_version)
self.setWindowTitle("Software Manager") self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600) self.setGeometry(100, 100, 800, 600)
@@ -89,8 +96,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.searchbar.returnPressed.connect(lambda: run_thread(threading.Thread(target=return_pressed, args=(self,)))) # Triggers data function thread on enter 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.dlbutton = QtWidgets.QPushButton("Download")
self.softwareList = QListWidget()
self.post_author_list = QListWidget()
self.libraryList = QListWidget() self.libraryList = QListWidget()
self.emptyResults = QLabel("No Results") self.emptyResults = QLabel("No Results")
@@ -103,14 +108,24 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.emptyLibrary = QLabel("No items in library.") self.emptyLibrary = QLabel("No items in library.")
self.emptyDownload = QLabel("No items in downloads.") self.emptyDownload = QLabel("No items in downloads.")
self.progressbar = QProgressBar() self.progressbar = QProgressBar()
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)
self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground)
self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground)
header = self.qtablewidget.horizontalHeader()
header.setSectionResizeMode(1, QHeaderView.Fixed)
header.resizeSection(1, 500)
container = QWidget() container = QWidget()
containerLayout = QVBoxLayout() containerLayout = QVBoxLayout()
containerLayout.addWidget(self.searchbar) containerLayout.addWidget(self.searchbar)
containerLayout.addWidget(self.qtablewidget)
containerLayout.addWidget(self.softwareList)
containerLayout.addWidget(self.post_author_list)
self.softwareList.addItems(searchresults)
class DownloadModel(QAbstractTableModel): class DownloadModel(QAbstractTableModel):
def __init__(self): def __init__(self):
@@ -152,9 +167,11 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.downloadList.horizontalHeader().setStretchLastSection(False) self.downloadList.horizontalHeader().setStretchLastSection(False)
self.downloadList.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.downloadList.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
self.downloadList.setSelectionBehavior(QTableView.SelectRows) self.downloadList.setSelectionBehavior(QTableView.SelectRows)
self.downloadList.setAttribute(Qt.WA_TranslucentBackground)
self.downloadList.viewport().setAttribute(Qt.WA_TranslucentBackground)
# download button triggers # download button triggers
self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.softwareList.currentItem(), state.posts, state.post_titles)))) self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.qtablewidget.currentItem(), state.posts, state.post_titles))))
container.setLayout(containerLayout) container.setLayout(containerLayout)
self.setCentralWidget(container) self.setCentralWidget(container)
@@ -163,13 +180,14 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.tabs = QTabWidget() self.tabs = QTabWidget()
self.horizontal_layout = QHBoxLayout() self.horizontal_layout = QHBoxLayout()
self.horizontal_layout.addWidget(self.softwareList, stretch=3)
self.horizontal_layout.addWidget(self.emptyResults, stretch=3) self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
self.horizontal_layout.addWidget(self.post_author_list) self.horizontal_layout.addWidget(self.qtablewidget)
self.tab1 = create_tab("Search", self.searchbar, self.softwareList, self.tabs, self.dlbutton, self.horizontal_layout) 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.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)
containerLayout.addWidget(self.tabs) containerLayout.addWidget(self.tabs)
@@ -184,9 +202,9 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.tracker_list.activated.connect(self.set_tracker) self.tracker_list.activated.connect(self.set_tracker)
if darkdetect.isDark(): if darkdetect.isDark():
settings_action = QAction(QIcon("core/interface/assets/settings_dark.png"), "Settings", self) settings_action = QAction(QIcon("core/interface/assets/settings_white.png"), "Settings", self)
else: else:
settings_action = QAction(QIcon("core/interface/assets/settings.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))
toolbar.addAction(settings_action) toolbar.addAction(settings_action)
@@ -223,8 +241,8 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
def show_empty_results(self, show: bool): def show_empty_results(self, show: bool):
if show: if show:
self.softwareList.hide() self.qtablewidget.hide()
self.emptyResults.show() self.emptyResults.show()
else: else:
self.emptyResults.hide() self.emptyResults.hide()
self.softwareList.show() self.qtablewidget.show()
+27 -16
View File
@@ -1,3 +1,4 @@
from PySide6.QtWidgets import QTableWidgetItem
from core.data.scrapers.uztracker import scrape_uztracker from core.data.scrapers.uztracker import scrape_uztracker
from core.data.scrapers.rutracker import scrape_rutracker from core.data.scrapers.rutracker import scrape_rutracker
from core.data.scrapers.monkrus import scrape_monkrus_telegram from core.data.scrapers.monkrus import scrape_monkrus_telegram
@@ -17,20 +18,24 @@ def return_pressed(self):
response = scrape_uztracker(search_text) response = scrape_uztracker(search_text)
if response: if response:
state.post_titles, state.post_urls = response state.post_titles, state.post_urls = response
self.softwareList.clear() self.qtablewidget.clear()
if state.post_titles and len(state.post_titles) > 0: if state.post_titles and len(state.post_titles) > 0:
self.softwareList.addItems(state.post_titles) self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
self.qtablewidget.setRowCount(len(state.post_titles))
for i, author in enumerate(state.post_author):
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
for i, title in enumerate(state.post_titles):
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
self.show_empty_results(False) self.show_empty_results(False)
self.post_author_list.clear()
else: else:
if state.debug: if state.debug:
print(f"No Results for {search_text}") print(f"No Results for {search_text}")
self.softwareList.clear() self.qtablewidget.clear()
self.show_empty_results(True) self.show_empty_results(True)
else: else:
if state.debug: if state.debug:
print(f"No response from uztracker") print(f"No response from uztracker")
self.softwareList.clear() self.qtablewidget.clear()
self.show_empty_results(True) self.show_empty_results(True)
elif state.tracker == "rutracker": elif state.tracker == "rutracker":
@@ -40,21 +45,24 @@ def return_pressed(self):
if state.posts == []: if state.posts == []:
if state.debug: if state.debug:
print(f"No Results for {search_text}") print(f"No Results for {search_text}")
self.softwareList.clear() self.qtablewidget.clear()
self.show_empty_results(True) self.show_empty_results(True)
else: else:
state.post_titles, _, state.post_author = format_data(state.posts) state.post_titles, _, state.post_author = format_data(state.posts)
self.show_empty_results(False) self.show_empty_results(False)
self.post_author_list.clear() self.qtablewidget.clear()
self.post_author_list.addItems(state.post_author) self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
self.softwareList.clear() self.qtablewidget.setRowCount(len(state.post_titles))
self.softwareList.addItems(state.post_titles) for i, author in enumerate(state.post_author):
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
for i, title in enumerate(state.post_titles):
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
if state.debug == True: if state.debug == True:
print(f"Response Cached: {cached}") print(f"Response Cached: {cached}")
else: else:
if state.debug: if state.debug:
print(f"No response from rutracker") print(f"No response from rutracker")
self.softwareList.clear() self.qtablewidget.clear()
self.show_empty_results(True) self.show_empty_results(True)
elif state.tracker == "m0nkrus": elif state.tracker == "m0nkrus":
@@ -63,12 +71,15 @@ def return_pressed(self):
if state.posts == []: if state.posts == []:
if state.debug: if state.debug:
print(f"No Results for {search_text}") print(f"No Results for {search_text}")
self.softwareList.clear() self.qtablewidget.clear()
self.show_empty_results(True) self.show_empty_results(True)
else: else:
state.post_titles, _, state.post_author = format_data(state.posts) state.post_titles, _, state.post_author = format_data(state.posts)
self.show_empty_results(False) self.show_empty_results(False)
self.post_author_list.clear() self.qtablewidget.clear()
self.post_author_list.addItems(state.post_author) self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"])
self.softwareList.clear() self.qtablewidget.setRowCount(len(state.post_titles))
self.softwareList.addItems(state.post_titles) for i, author in enumerate(state.post_author):
self.qtablewidget.setItem(i, 1, QTableWidgetItem(author))
for i, title in enumerate(state.post_titles):
self.qtablewidget.setItem(i, 0, QTableWidgetItem(title))
+3 -3
View File
@@ -6,8 +6,8 @@ from core.utils.data.state import state
def create_config(): 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}"}
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"))
@@ -42,4 +42,4 @@ def read_config():
state.aria2_threads = config.getint("General", "aria2_threads") state.aria2_threads = config.getint("General", "aria2_threads")
state.download_path = config.get("General", "download_path") state.download_path = config.get("General", "download_path")
state.speed_limit = config.getint("General", "speed_limit") state.speed_limit = config.getint("General", "speed_limit")
state.version = config.get("General", "version") state.ignore_updates = config.getboolean("General", "ignore_updates")
+8 -30
View File
@@ -1,39 +1,19 @@
from core.utils.data.state import state from core.utils.data.state import state
from core.utils.general.shutdown import kill_aria2server
from .config import create_config from .config import create_config
def restart_aria2c(): def restart_aria2c():
import main # had to do this because of circle import :( import main # had to do this because of circle import :(
import signal import signal
import atexit import atexit
import time kill_aria2server()
state.aria2process.wait()
state.aria2process = main.run_aria2server()
signal.signal(signal.SIGINT, main.keyboardinterrupthandler)
atexit.unregister(kill_aria2server)
atexit.register(kill_aria2server)
try: def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None):
main.kill_aria2server()
except Exception as e:
print(f"Warning: aria2server kill failed: {e}")
try:
if state.aria2process:
state.aria2process.wait(timeout=3)
except Exception as e:
print(f"Warning: aria2process wait failed: {e}")
time.sleep(0.5)
try:
state.aria2process = main.run_aria2server()
except Exception as e:
print(f"Error: failed to restart aria2server: {e}")
return
try:
signal.signal(signal.SIGINT, main.keyboardinterrupthandler)
atexit.unregister(main.kill_aria2server)
atexit.register(main.kill_aria2server)
except Exception as e:
print(f"Warning: signal/atexit setup failed: {e}")
def save_settings(thread_count=None, close=lambda: None, apiurl=None, download_path=None, speed_limit=None, version=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:
@@ -42,8 +22,6 @@ 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:
state.version = version
create_config() create_config()
restart_aria2c() restart_aria2c()
+1
View File
@@ -10,6 +10,7 @@ class AppState:
post_author: List[str] post_author: List[str]
downloads: List[str] downloads: List[str]
version: str version: str
ignore_updates: bool = False
debug: bool = False debug: bool = False
tracker: str = "rutracker" tracker: str = "rutracker"
api_url: str = "https://api.michijackson.xyz" api_url: str = "https://api.michijackson.xyz"
+14 -6
View File
@@ -16,13 +16,21 @@ def check_for_updates():
latest_version = latest_release["name"] latest_version = latest_release["name"]
assets = latest_release["assets"] assets = latest_release["assets"]
if latest_version != state.version:
print(f"New release available: {latest_version}") if state.version == "dev":
if state.debug:
print("Dev release detected, skipping version check")
return None, None
elif latest_version != state.version:
if state.debug:
print(f"New release available: {latest_version}")
if assets: if assets:
print("Assets:") if state.debug:
for asset in assets: print("Assets:")
print(f" - {asset['name']}: {asset['browser_download_url']}") for asset in assets:
print(f" - {asset['name']}: {asset['browser_download_url']}")
return assets, latest_version return assets, latest_version
else: else:
print("Already up-to-date.") if state.debug:
print("Already up-to-date.")
return None, None return None, None