Compare commits

...

7 Commits

9 changed files with 249 additions and 30 deletions
+1 -7
View File
@@ -125,8 +125,6 @@ jobs:
--upx-binary="${{ env.UPX_BINARY }}" \ --upx-binary="${{ env.UPX_BINARY }}" \
--noinclude-qt-translations \ --noinclude-qt-translations \
--include-data-files=build_info.json=build_info.json \ --include-data-files=build_info.json=build_info.json \
--include-data-files=build_info.json=src/build_info.json \
--include-data-files=build_info.json=src/core/interface/build_info.json \
--output-filename=${{ env.APP_NAME }}.exe \ --output-filename=${{ env.APP_NAME }}.exe \
--output-dir=nuitka-build \ --output-dir=nuitka-build \
src/main.py src/main.py
@@ -137,8 +135,6 @@ jobs:
run: | run: |
pyinstaller --onefile --windowed \ pyinstaller --onefile --windowed \
--add-data "build_info.json:." \ --add-data "build_info.json:." \
--add-data "build_info.json:src/core" \
--add-data "build_info.json:src/core/interface" \
--exclude-module PyQt6 \ --exclude-module PyQt6 \
--exclude-module PyQt5 \ --exclude-module PyQt5 \
--hidden-import=backports.tarfile \ --hidden-import=backports.tarfile \
@@ -155,8 +151,6 @@ jobs:
run: | run: |
pyinstaller --onefile \ pyinstaller --onefile \
--add-data "build_info.json:." \ --add-data "build_info.json:." \
--add-data "build_info.json:src/core" \
--add-data "build_info.json:src/core/interface" \
--exclude-module PyQt6 \ --exclude-module PyQt6 \
--exclude-module PyQt5 \ --exclude-module PyQt5 \
--hidden-import=backports.tarfile \ --hidden-import=backports.tarfile \
@@ -287,4 +281,4 @@ jobs:
release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos release/SoftwareManager-dev-${{ needs.build.outputs.short_sha }}-macos
release/SHA256SUMS.txt release/SHA256SUMS.txt
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+185
View File
@@ -0,0 +1,185 @@
name: Build Executables (Test)
on:
workflow_dispatch:
permissions:
contents: read
env:
PYTHON_VERSION: "3.12"
APP_NAME: "SoftwareManager"
jobs:
build:
name: Build ${{ matrix.os }}
strategy:
fail-fast: false # keep other platforms building even if one fails
matrix:
include:
- os: ubuntu-22.04
artifact_name: SoftwareManager-test-${{ github.sha }}-linux
- os: windows-latest
artifact_name: SoftwareManager-test-${{ github.sha }}-windows
- os: macos-latest
artifact_name: SoftwareManager-test-${{ github.sha }}-macos
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.7"
enable-cache: true
- name: Install Python dependencies
run: |
uv pip install -r requirements.txt
uv pip install Pillow
env:
UV_SYSTEM_PYTHON: 1
- name: Install Nuitka (Windows only)
if: runner.os == 'Windows'
run: uv pip install nuitka
env:
UV_SYSTEM_PYTHON: 1
- name: Generate version
id: version
shell: bash
run: |
SHORT_SHA="${{ github.sha }}"
SHORT_SHA="${SHORT_SHA:0:7}"
BRANCH="${{ github.head_ref || github.ref_name }}"
BRANCH_SLUG="${BRANCH//\//-}" # replace slashes with dashes
VERSION="${BRANCH_SLUG}-${SHORT_SHA}-test"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION"
- name: Inject build info
shell: bash
run: |
cat > build_info.json << EOF
{
"version": "${{ env.VERSION }}"
}
EOF
- name: Install UPX (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-win64.zip -o upx.zip
7z x upx.zip -oupx-dir
UPX_DIR="$(pwd)/upx-dir/upx-5.1.0-win64"
echo "$UPX_DIR" >> $GITHUB_PATH
export PATH="$UPX_DIR:$PATH"
echo "UPX_BINARY=$UPX_DIR/upx.exe" >> $GITHUB_ENV
upx --version | head -1
- name: Convert icon for Windows
if: runner.os == 'Windows'
shell: bash
run: |
python -c "
from PIL import Image
img = Image.open('src/core/interface/assets/logo.png')
img.save('app_icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(48,48),(32,32),(16,16)])
print('Icon converted successfully')
"
- name: Build with Nuitka (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
python -m nuitka \
--standalone \
--assume-yes-for-downloads \
--windows-console-mode=disable \
--windows-icon-from-ico=app_icon.ico \
--enable-plugin=pyside6 \
--enable-plugin=upx \
--upx-binary="${{ env.UPX_BINARY }}" \
--noinclude-qt-translations \
--include-data-files=build_info.json=build_info.json \
--output-filename=${{ env.APP_NAME }}.exe \
--output-dir=nuitka-build \
src/main.py
- name: Build with PyInstaller (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json:." \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
src/main.py
- name: Build with PyInstaller (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
pyinstaller --onefile \
--add-data "build_info.json:." \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
src/main.py
- name: Stage artifact
shell: bash
run: |
mkdir -p dist-final
if [ "$RUNNER_OS" == "Windows" ]; then
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
else
chmod +x dist/${{ env.APP_NAME }}
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }}
fi
ls -la dist-final/
- name: Create ZIP (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Compress-Archive -Path "dist-final/${{ env.APP_NAME }}/*" -DestinationPath "dist-final/${{ matrix.artifact_name }}.zip"
- name: Upload artifact (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: dist-final/${{ matrix.artifact_name }}.zip
retention-days: 7
- name: Upload artifact (Linux/macOS)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: dist-final/${{ matrix.artifact_name }}
retention-days: 7
+20 -3
View File
@@ -1,18 +1,35 @@
from core.utils.network.update_checker import get_updates from core.utils.network.update_checker import get_updates
from core.utils.network.updater import download_update from core.utils.network.updater import download_update
from PySide6.QtWidgets import QDialog, QMessageBox from PySide6.QtWidgets import QDialog, QMessageBox
from core.utils.logging.logs import consoleLog
from core.utils.data.state import state from core.utils.data.state import state
from pathlib import Path
import platform import platform
import json import json
import os import os
def check_version(): def _get_build_info_path() -> (Path | None):
build_info_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "build_info.json") current_dir = Path(__file__).resolve().parent
for _ in range(6): # Climb max 6 directories
file = current_dir / "build_info.json"
if file.exists():
return file
# Go up one directory if file isn't found
current_dir = current_dir.parent
return None
def get_version() -> None:
# Get build info filepath
build_info_path = _get_build_info_path()
if os.path.exists(build_info_path): if os.path.exists(build_info_path):
with open(build_info_path, "r") as f: with open(build_info_path, "r") as f:
build_info = json.load(f) build_info = json.load(f)
state.version = build_info.get("version") state.version = build_info.get("version")
else:
consoleLog(f"Could not find build info file (Path: {build_info_path})")
class UpdateDialog(QDialog): class UpdateDialog(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
+19 -8
View File
@@ -5,6 +5,7 @@ from core.interface.dialogs.downloadlist import _create_download_list
from core.interface.assets.base64_icons import settings_white_base64 from core.interface.assets.base64_icons import settings_white_base64
from core.interface.assets.base64_icons import settings_black_base64 from core.interface.assets.base64_icons import settings_black_base64
from core.interface.dialogs.downloadlist import download_list_update from core.interface.dialogs.downloadlist import download_list_update
from core.interface.dialogs.update import get_version, UpdateDialog
from core.utils.logging.logs import consoleLog, flush_log_buffer from core.utils.logging.logs import consoleLog, flush_log_buffer
from core.interface.dialogs.downloadmodel import DownloadModel from core.interface.dialogs.downloadmodel import DownloadModel
from core.interface.utils.searchhelper import return_pressed from core.interface.utils.searchhelper import return_pressed
@@ -18,9 +19,7 @@ from core.utils.general.wrappers import run_thread
from core.interface.dialogs.image import Image from core.interface.dialogs.image import Image
from core.utils.data.state import state from core.utils.data.state import state
import core.interface.dialogs.hoverrowdelegate
import core.interface.dialogs.contextmenu import core.interface.dialogs.contextmenu
import core.interface.dialogs.update
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6.QtCore import ( from PySide6.QtCore import (
@@ -77,7 +76,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
pixmap.loadFromData(image_data) pixmap.loadFromData(image_data)
self.setWindowIcon(QIcon(pixmap)) self.setWindowIcon(QIcon(pixmap))
# set appid on windows # Set AppID on Windows
if platform.system() == "Windows": if platform.system() == "Windows":
try: try:
import ctypes import ctypes
@@ -86,23 +85,30 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
except Exception as e: except Exception as e:
consoleLog(f"Could not set app ID: {e}") consoleLog(f"Could not set app ID: {e}")
# Get current version
# core.interface.dialogs.update # core.interface.dialogs.update
get_version()
UpdateDialog()
# Initialize Window
self.setWindowTitle("Software Manager") self.setWindowTitle("Software Manager")
self.setGeometry(100, 100, 800, 600) self.setGeometry(100, 100, 800, 600)
self.controls = QWidget() self.controls = QWidget()
self.controlsLayout = QVBoxLayout() self.controlsLayout = QVBoxLayout()
# Searchbar
self.searchbar = QLineEdit() self.searchbar = QLineEdit()
self.searchbar.setPlaceholderText("Search for software...") self.searchbar.setPlaceholderText("Search for software...")
self.searchbar.setClearButtonEnabled(True) self.searchbar.setClearButtonEnabled(True)
self.searchbar.setMinimumHeight(30) self.searchbar.setMinimumHeight(30)
self.searchbar.returnPressed.connect(self._start_search) self.searchbar.returnPressed.connect(self._start_search)
# Download Button
self.dlbutton = QtWidgets.QPushButton("Download") self.dlbutton = QtWidgets.QPushButton("Download")
self.dlbutton.setCursor(Qt.CursorShape.PointingHandCursor) self.dlbutton.setCursor(Qt.CursorShape.PointingHandCursor)
# Dialog for empty results
self.emptyResults = QLabel("No Results") self.emptyResults = QLabel("No Results")
self.emptyResults.setAlignment(Qt.AlignmentFlag.AlignCenter) self.emptyResults.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.emptyResults.hide() self.emptyResults.hide()
@@ -157,23 +163,32 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.horizontal_layout.addWidget(self.emptyResults, stretch=3) self.horizontal_layout.addWidget(self.emptyResults, stretch=3)
self.horizontal_layout.addWidget(state.trackertable) self.horizontal_layout.addWidget(state.trackertable)
# Tabs
self.tab1 = create_tab("Search", self.searchbar, state.trackertable, self.tabs, self.dlbutton, self.horizontal_layout) self.tab1 = create_tab("Search", self.searchbar, state.trackertable, self.tabs, self.dlbutton, self.horizontal_layout)
self.tab2 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None) self.tab2 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None)
# Corner Widget (Settings Button, Tracker list, Tab button container)
self.corner_widget = QWidget() self.corner_widget = QWidget()
self.corner_layout = QHBoxLayout(self.corner_widget) self.corner_layout = QHBoxLayout(self.corner_widget)
self.corner_layout.setContentsMargins(0, 0, 0, 0) self.corner_layout.setContentsMargins(0, 0, 0, 0)
# Tracker list
self.tracker_list = QComboBox() self.tracker_list = QComboBox()
self.tracker_list.addItems(list(state.trackers.keys())) self.tracker_list.addItems(list(state.trackers.keys()))
self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor) self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor)
self.tracker_list.activated.connect(self.set_tracker) self.tracker_list.activated.connect(self.set_tracker)
self.corner_layout.addWidget(self.tracker_list) self.corner_layout.addWidget(self.tracker_list)
# Settings button
self.settings_btn = QtWidgets.QToolButton() self.settings_btn = QtWidgets.QToolButton()
self.settings_btn.setIconSize(QSize(21, 21)) self.settings_btn.setIconSize(QSize(21, 21))
self.settings_btn.setStyleSheet("background: transparent;") self.settings_btn.setStyleSheet("background: transparent;")
self.settings_btn.setToolTip("Settings")
self.settings_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.settings_btn.clicked.connect(lambda: settings_dialog(self))
self.corner_layout.addWidget(self.settings_btn)
# Adjust theme based on system preferences
if darkdetect.isDark(): if darkdetect.isDark():
settings_white_pixmap = QPixmap() settings_white_pixmap = QPixmap()
settings_white = base64.b64decode(settings_white_base64) settings_white = base64.b64decode(settings_white_base64)
@@ -185,11 +200,6 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
settings_black_pixmap.loadFromData(settings_black) settings_black_pixmap.loadFromData(settings_black)
self.settings_btn.setIcon(QIcon(settings_black_pixmap)) self.settings_btn.setIcon(QIcon(settings_black_pixmap))
self.settings_btn.setToolTip("Settings")
self.settings_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.settings_btn.clicked.connect(lambda: settings_dialog(self))
self.corner_layout.addWidget(self.settings_btn)
self.tab_wrapper = QWidget() self.tab_wrapper = QWidget()
self.tab_layout = QVBoxLayout() self.tab_layout = QVBoxLayout()
self.tab_layout.setContentsMargins(0, 0, 0, 0) self.tab_layout.setContentsMargins(0, 0, 0, 0)
@@ -221,6 +231,7 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.version = QLabel(f"Version: {state.version}") self.version = QLabel(f"Version: {state.version}")
self.statusbar.addPermanentWidget(self.version, Qt.AlignmentFlag.AlignLeft) self.statusbar.addPermanentWidget(self.version, Qt.AlignmentFlag.AlignLeft)
# (\u2193) Arrow down, (\u2191) Arrow up
self.speed_label = QLabel("\u2193 0.0 kB/s \u2191 0.0 kB/s") self.speed_label = QLabel("\u2193 0.0 kB/s \u2191 0.0 kB/s")
self.speed_label.setStyleSheet("padding-right: 6px;") self.speed_label.setStyleSheet("padding-right: 6px;")
self.statusbar.addPermanentWidget(self.speed_label) self.statusbar.addPermanentWidget(self.speed_label)
+2
View File
@@ -29,6 +29,7 @@ def get_active_interfaces():
def list_interfaces() -> None: def list_interfaces() -> None:
consoleLog("Fetching Network Interfaces...")
addrs = psutil.net_if_addrs() addrs = psutil.net_if_addrs()
stats = psutil.net_if_stats() stats = psutil.net_if_stats()
@@ -45,6 +46,7 @@ def list_interfaces() -> None:
consoleLog(f"Found Interface: {interface} [{status}]") consoleLog(f"Found Interface: {interface} [{status}]")
def init_interfaces(): def init_interfaces():
consoleLog("Initializing Interface variables...")
addrs = psutil.net_if_addrs() addrs = psutil.net_if_addrs()
state.interfaces = list(addrs.keys()) state.interfaces = list(addrs.keys())
state.active_interfaces = get_active_interfaces() state.active_interfaces = get_active_interfaces()
+1 -1
View File
@@ -1,3 +1,4 @@
from core.network.libtorrent_misc import cleanup_session
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.utils.data.state import state from core.utils.data.state import state
import os import os
@@ -5,7 +6,6 @@ import os
def closehelper(): def closehelper():
state.shutdown_event.set() state.shutdown_event.set()
try: try:
from core.network.libtorrent_misc import cleanup_session
cleanup_session() cleanup_session()
except Exception as e: except Exception as e:
consoleLog(f"Exception while cleaning up LibTorrent Session: {e}") consoleLog(f"Exception while cleaning up LibTorrent Session: {e}")
+8 -1
View File
@@ -1,2 +1,9 @@
from core.utils.logging.logs import consoleLog
def run_thread(thread): def run_thread(thread):
thread.start() target_name = thread._target.__name__
try:
thread.start()
consoleLog(f"Started Thread: {target_name}")
except Exception as e:
consoleLog(f"Exception while starting thread {target_name}: {e}")
+13 -10
View File
@@ -2,8 +2,10 @@ from core.network.libtorrent_misc import send_notification, update_log, check_de
from core.utils.logging.loghandler import split_data, check_completed, check_downloads from core.utils.logging.loghandler import split_data, check_completed, check_downloads
from core.network.interface import list_interfaces, init_interfaces from core.network.interface import list_interfaces, init_interfaces
from core.utils.logging.logs import get_download_logs from core.utils.logging.logs import get_download_logs
from core.utils.logging.logs import set_main_window
from core.utils.general.shutdown import closehelper from core.utils.general.shutdown import closehelper
from core.utils.general.wrappers import run_thread from core.utils.general.wrappers import run_thread
from core.utils.general.shutdown import force_exit
from core.utils.config.config import read_config from core.utils.config.config import read_config
from core.utils.logging.logs import consoleLog from core.utils.logging.logs import consoleLog
from core.interface.gui import MainWindow from core.interface.gui import MainWindow
@@ -22,44 +24,45 @@ def run_gui():
qdarktheme.setup_theme("auto") qdarktheme.setup_theme("auto")
widget = MainWindow() widget = MainWindow()
# Check OS for window transparency compatibility and apply
if state.window_transparency and platform.system() != "Windows": if state.window_transparency and platform.system() != "Windows":
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"}) qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"})
from core.utils.logging.logs import set_main_window
set_main_window(widget) set_main_window(widget)
widget.show() widget.show()
sys.exit(app.exec()) sys.exit(app.exec())
def keyboardinterrupthandler(signum, frame): def keyboardinterrupthandler(signum, frame):
closehelper() closehelper()
from core.utils.general.shutdown import force_exit
force_exit() force_exit()
def main(): def main():
# Begin counting startup time
start_time = time.perf_counter() start_time = time.perf_counter()
# Parse saved files
read_config() read_config()
logs = get_download_logs() logs = get_download_logs()
_, downloads = split_data(logs) _, downloads = split_data(logs)
# Initialize Keyboardinterrupt signal
signal.signal(signal.SIGINT, keyboardinterrupthandler) signal.signal(signal.SIGINT, keyboardinterrupthandler)
consoleLog("Starting SoftwareManager...") consoleLog("Starting SoftwareManager...")
consoleLog("Fetching Network Interfaces...") # Get network interface info
list_interfaces() list_interfaces()
consoleLog("Initializing Interface variables...")
init_interfaces() init_interfaces()
consoleLog(f"Current Bound: {state.bound_interface}") consoleLog(f"Current Bound: {state.bound_interface}")
# Start background daemon threads
run_thread(threading.Thread(target=send_notification, args=(state.shutdown_event,), daemon=True)) run_thread(threading.Thread(target=send_notification, args=(state.shutdown_event,), daemon=True))
consoleLog("Started Thread: send_notification")
run_thread(threading.Thread(target=update_log, args=(state.shutdown_event,), daemon=True)) run_thread(threading.Thread(target=update_log, args=(state.shutdown_event,), daemon=True))
consoleLog("Started Thread: update_log")
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
consoleLog("Started Thread: check_completed")
run_thread(threading.Thread(target=check_deleted_files, args=(state.shutdown_event,), daemon=True)) run_thread(threading.Thread(target=check_deleted_files, args=(state.shutdown_event,), daemon=True))
consoleLog("Started Thread: check_deleted_files") # Start background non-daemon threads
run_thread(threading.Thread(target=check_completed, args=(downloads, state.autoresume)))
run_thread(threading.Thread(target=check_downloads, args=(downloads,))) run_thread(threading.Thread(target=check_downloads, args=(downloads,)))
consoleLog("Started Thread: check_downloads") # Finish counting startup time
elapsed = time.perf_counter() - start_time elapsed = time.perf_counter() - start_time
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI") consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
# Launch GUI
run_gui() run_gui()
if __name__ == "__main__": if __name__ == "__main__":