mirror of
https://github.com/blw1138/Zordon.git
synced 2025-12-17 16:58:12 +00:00
Compare commits
11 Commits
feature/10
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
193d74919f | ||
|
|
9951915bbe | ||
|
|
eaa796221d | ||
|
|
caecf07c06 | ||
|
|
2617c0a357 | ||
|
|
5534f0b1b2 | ||
|
|
06e7bb15d8 | ||
|
|
400a9e997b | ||
|
|
429d6bf0e9 | ||
|
|
c917cd665c | ||
|
|
90478f256d |
38
.github/workflows/create-executables.yml
vendored
38
.github/workflows/create-executables.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Create Executables
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
- types: [created]
|
||||
|
||||
jobs:
|
||||
pyinstaller-build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Create Executables (Windows)
|
||||
uses: sayyid5416/pyinstaller@v1
|
||||
with:
|
||||
python_ver: '3.11'
|
||||
spec: 'main.spec'
|
||||
requirements: 'requirements.txt'
|
||||
upload_exe_with_name: 'Zordon'
|
||||
pyinstaller-build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create Executables (Linux)
|
||||
uses: sayyid5416/pyinstaller@v1
|
||||
with:
|
||||
python_ver: '3.11'
|
||||
spec: 'main.spec'
|
||||
requirements: 'requirements.txt'
|
||||
upload_exe_with_name: 'Zordon'
|
||||
pyinstaller-build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Create Executables (macOS)
|
||||
uses: sayyid5416/pyinstaller@v1
|
||||
with:
|
||||
python_ver: '3.11'
|
||||
spec: 'main.spec'
|
||||
requirements: 'requirements.txt'
|
||||
upload_exe_with_name: 'Zordon'
|
||||
23
.github/workflows/pylint.yml
vendored
Normal file
23
.github/workflows/pylint.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Pylint
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pylint
|
||||
- name: Analysing the code with pylint
|
||||
run: |
|
||||
pylint $(git ls-files '*.py')
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
A tool designed for small render farms, such as those used in home studios or small businesses, to efficiently manage and run render jobs for Blender, FFMPEG, and other video renderers. It simplifies the process of distributing rendering tasks across multiple available machines, optimizing the rendering workflow for artists, animators, and video professionals.
|
||||
|
||||
Notice: This should be considered a beta and is meant for casual / hobbiest use. Do not use in mission critical environments!
|
||||
|
||||
## Supported Renderers
|
||||
|
||||
Zordon supports or plans to support the following renderers:
|
||||
|
||||
@@ -26,7 +26,7 @@ a = Analysis(
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=1, # fyi: optim level 2 breaks on windows
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
@@ -40,7 +40,7 @@ if platform.system() == 'Darwin': # macOS
|
||||
name=APP_NAME,
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=True,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
@@ -87,7 +87,7 @@ elif platform.system() == 'Windows':
|
||||
name=APP_NAME,
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=True,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
@@ -108,7 +108,7 @@ else: # linux
|
||||
name=APP_NAME,
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=True,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
|
||||
@@ -35,4 +35,3 @@ attrs>=23.2.0
|
||||
lxml>=5.1.0
|
||||
click>=8.1.7
|
||||
requests_toolbelt>=1.0.0
|
||||
pyinstaller_versionfile>=2.1.1
|
||||
|
||||
@@ -50,11 +50,8 @@ def start_server(hostname=None):
|
||||
flask_log.setLevel(Config.flask_log_level.upper())
|
||||
|
||||
logger.debug('Starting API server')
|
||||
try:
|
||||
server.run(host=hostname, port=server.config['PORT'], debug=Config.flask_debug_enable, use_reloader=False,
|
||||
threaded=True)
|
||||
finally:
|
||||
logger.debug('Stopping API server')
|
||||
server.run(host='0.0.0.0', port=server.config['PORT'], debug=Config.flask_debug_enable, use_reloader=False,
|
||||
threaded=True)
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
|
||||
@@ -8,7 +8,8 @@ from src.engines.blender.blender_engine import Blender
|
||||
from src.engines.core.base_downloader import EngineDownloader
|
||||
from src.utilities.misc_helper import current_system_os, current_system_cpu
|
||||
|
||||
url = "https://download.blender.org/release/"
|
||||
# url = "https://download.blender.org/release/"
|
||||
url = "https://ftp.nluug.nl/pub/graphics/blender/release/" # much faster mirror for testing
|
||||
|
||||
logger = logging.getLogger()
|
||||
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
||||
|
||||
@@ -23,10 +23,6 @@ class EngineManager:
|
||||
def supported_engines():
|
||||
return [Blender, FFMPEG]
|
||||
|
||||
@classmethod
|
||||
def downloadable_engines(cls):
|
||||
return [engine for engine in cls.supported_engines() if hasattr(engine, "downloader") and engine.downloader()]
|
||||
|
||||
@classmethod
|
||||
def engine_with_name(cls, engine_name):
|
||||
for obj in cls.supported_engines():
|
||||
@@ -34,7 +30,7 @@ class EngineManager:
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def get_engines(cls, filter_name=None, include_corrupt=False, ignore_system=False):
|
||||
def get_engines(cls, filter_name=None, include_corrupt=False):
|
||||
|
||||
if not cls.engines_path:
|
||||
raise FileNotFoundError("Engine path is not set")
|
||||
@@ -96,47 +92,46 @@ class EngineManager:
|
||||
'type': 'system'
|
||||
}
|
||||
|
||||
if not ignore_system:
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures = {
|
||||
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
||||
for eng in cls.supported_engines()
|
||||
if eng.default_renderer_path() and (not filter_name or filter_name == eng.name())
|
||||
}
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
futures = {
|
||||
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
||||
for eng in cls.supported_engines()
|
||||
if eng.default_renderer_path() and (not filter_name or filter_name == eng.name())
|
||||
}
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
if result:
|
||||
results.append(result)
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
if result:
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def all_versions_for_engine(cls, engine_name, include_corrupt=False, ignore_system=False):
|
||||
versions = cls.get_engines(filter_name=engine_name, include_corrupt=include_corrupt, ignore_system=ignore_system)
|
||||
def all_versions_for_engine(cls, engine_name, include_corrupt=False):
|
||||
versions = cls.get_engines(filter_name=engine_name, include_corrupt=include_corrupt)
|
||||
sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True)
|
||||
return sorted_versions
|
||||
|
||||
@classmethod
|
||||
def newest_engine_version(cls, engine, system_os=None, cpu=None, ignore_system=None):
|
||||
def newest_engine_version(cls, engine, system_os=None, cpu=None):
|
||||
system_os = system_os or current_system_os()
|
||||
cpu = cpu or current_system_cpu()
|
||||
|
||||
try:
|
||||
filtered = [x for x in cls.all_versions_for_engine(engine, ignore_system=ignore_system)
|
||||
if x['system_os'] == system_os and x['cpu'] == cpu]
|
||||
filtered = [x for x in cls.all_versions_for_engine(engine) if x['system_os'] == system_os and
|
||||
x['cpu'] == cpu]
|
||||
return filtered[0]
|
||||
except IndexError:
|
||||
logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None, ignore_system=False):
|
||||
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None):
|
||||
system_os = system_os or current_system_os()
|
||||
cpu = cpu or current_system_cpu()
|
||||
|
||||
filtered = [x for x in cls.get_engines(filter_name=engine, ignore_system=ignore_system) if
|
||||
x['system_os'] == system_os and x['cpu'] == cpu and x['version'] == version]
|
||||
filtered = [x for x in cls.get_engines(filter_name=engine) if x['system_os'] == system_os and
|
||||
x['cpu'] == cpu and x['version'] == version]
|
||||
return filtered[0] if filtered else False
|
||||
|
||||
@classmethod
|
||||
@@ -169,7 +164,7 @@ class EngineManager:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def download_engine(cls, engine, version, system_os=None, cpu=None, background=False, ignore_system=False):
|
||||
def download_engine(cls, engine, version, system_os=None, cpu=None, background=False):
|
||||
|
||||
engine_to_download = cls.engine_with_name(engine)
|
||||
existing_task = cls.get_existing_download_task(engine, version, system_os, cpu)
|
||||
@@ -192,7 +187,7 @@ class EngineManager:
|
||||
return thread
|
||||
|
||||
thread.join()
|
||||
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu, ignore_system) # Check that engine downloaded
|
||||
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu) # Check that engine downloaded
|
||||
if not found_engine:
|
||||
logger.error(f"Error downloading {engine}")
|
||||
return found_engine
|
||||
@@ -218,21 +213,31 @@ class EngineManager:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def is_engine_update_available(cls, engine_class, ignore_system_installs=False):
|
||||
logger.debug(f"Checking for updates to {engine_class.name()}")
|
||||
latest_version = engine_class.downloader().find_most_recent_version()
|
||||
def update_all_engines(cls):
|
||||
def engine_update_task(engine_class):
|
||||
logger.debug(f"Checking for updates to {engine_class.name()}")
|
||||
latest_version = engine_class.downloader().find_most_recent_version()
|
||||
|
||||
if not latest_version:
|
||||
logger.warning(f"Could not find most recent version of {engine_class.name()} to download")
|
||||
return
|
||||
if not latest_version:
|
||||
logger.warning(f"Could not find most recent version of {engine.name()} to download")
|
||||
return
|
||||
|
||||
version_num = latest_version.get('version')
|
||||
if cls.is_version_downloaded(engine_class.name(), version_num, ignore_system=ignore_system_installs):
|
||||
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
|
||||
return
|
||||
version_num = latest_version.get('version')
|
||||
if cls.is_version_downloaded(engine_class.name(), version_num):
|
||||
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
|
||||
return
|
||||
|
||||
return latest_version
|
||||
# download the engine
|
||||
logger.info(f"Downloading latest version of {engine_class.name()} ({version_num})...")
|
||||
cls.download_engine(engine=engine_class.name(), version=version_num, background=True)
|
||||
|
||||
logger.info(f"Checking for updates for render engines...")
|
||||
threads = []
|
||||
for engine in cls.supported_engines():
|
||||
if engine.downloader():
|
||||
thread = threading.Thread(target=engine_update_task, args=(engine,))
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
|
||||
@classmethod
|
||||
def create_worker(cls, renderer, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
|
||||
@@ -300,8 +305,7 @@ class EngineDownloadWorker(threading.Thread):
|
||||
self.cpu = cpu
|
||||
|
||||
def run(self):
|
||||
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu,
|
||||
ignore_system=True)
|
||||
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu)
|
||||
if existing_download:
|
||||
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
|
||||
return existing_download
|
||||
|
||||
59
src/init.py
59
src/init.py
@@ -5,9 +5,6 @@ import socket
|
||||
import sys
|
||||
import threading
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt6.QtCore import QSettings
|
||||
|
||||
from src.api.api_server import start_server
|
||||
from src.api.preview_manager import PreviewManager
|
||||
@@ -16,10 +13,8 @@ from src.distributed_job_manager import DistributedJobManager
|
||||
from src.engines.engine_manager import EngineManager
|
||||
from src.render_queue import RenderQueue
|
||||
from src.utilities.config import Config
|
||||
from src.utilities.misc_helper import (system_safe_path, current_system_cpu, current_system_os,
|
||||
current_system_os_version, check_for_updates)
|
||||
from src.utilities.misc_helper import system_safe_path, current_system_cpu, current_system_os, current_system_os_version
|
||||
from src.utilities.zeroconf_server import ZeroconfServer
|
||||
from version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER, APP_AUTHOR
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
@@ -34,45 +29,16 @@ def run(server_only=False) -> int:
|
||||
int: The exit status code.
|
||||
"""
|
||||
|
||||
def existing_process(process_name):
|
||||
import psutil
|
||||
current_pid = os.getpid()
|
||||
current_process = psutil.Process(current_pid)
|
||||
for proc in psutil.process_iter(['pid', 'name', 'ppid']):
|
||||
proc_name = proc.info['name'].lower().rstrip('.exe')
|
||||
if proc_name == process_name.lower() and proc.info['pid'] != current_pid:
|
||||
if proc.info['pid'] == current_process.ppid():
|
||||
continue # parent process
|
||||
elif proc.info['ppid'] == current_pid:
|
||||
continue # child process
|
||||
else:
|
||||
return proc # unrelated process
|
||||
return None
|
||||
|
||||
# setup logging
|
||||
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(module)s: %(message)s', datefmt='%d-%b-%y %H:%M:%S',
|
||||
level=Config.server_log_level.upper())
|
||||
logging.getLogger("requests").setLevel(logging.WARNING) # suppress noisy requests/urllib3 logging
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
|
||||
# check for existing instance
|
||||
existing_proc = existing_process(APP_NAME)
|
||||
if existing_proc:
|
||||
logger.fatal(f"Another instance of {APP_NAME} is already running (pid: {existing_proc.pid})")
|
||||
sys.exit(1)
|
||||
|
||||
# Setup logging for console ui
|
||||
buffer_handler = __setup_buffer_handler() if not server_only else None
|
||||
|
||||
# check for updates
|
||||
update_thread = threading.Thread(target=check_for_updates, args=(APP_REPO_NAME, APP_REPO_OWNER, APP_NAME,
|
||||
APP_VERSION))
|
||||
update_thread.start()
|
||||
|
||||
settings = QSettings(APP_AUTHOR, APP_NAME)
|
||||
|
||||
# main start
|
||||
logger.info(f"Starting {APP_NAME} Render Server")
|
||||
logger.info(f"Starting Zordon Render Server")
|
||||
return_code = 0
|
||||
try:
|
||||
# Load Config YAML
|
||||
@@ -97,16 +63,9 @@ def run(server_only=False) -> int:
|
||||
ServerProxyManager.subscribe_to_listener()
|
||||
DistributedJobManager.subscribe_to_listener()
|
||||
|
||||
# check for updates for render engines if configured
|
||||
ignore_system = settings.value("engines_ignore_system_installs", False)
|
||||
if settings.value('check_for_engine_updates_on_launch', False):
|
||||
for engine in EngineManager.downloadable_engines():
|
||||
if settings.value(f'engine_download-{engine.name()}', False):
|
||||
update_result = EngineManager.is_engine_update_available(engine, ignore_system_installs=ignore_system)
|
||||
EngineManager.download_engine(engine=engine.name(), version=update_result['version'],
|
||||
background=True,
|
||||
ignore_system=ignore_system)
|
||||
settings.setValue("engines_last_update_time", datetime.now().isoformat())
|
||||
# check for updates for render engines if configured or on first launch
|
||||
if Config.update_engines_on_launch or not EngineManager.get_engines():
|
||||
EngineManager.update_all_engines()
|
||||
|
||||
# get hostname
|
||||
local_hostname = socket.gethostname()
|
||||
@@ -118,13 +77,13 @@ def run(server_only=False) -> int:
|
||||
api_server.start()
|
||||
|
||||
# start zeroconf server
|
||||
ZeroconfServer.configure(f"_{APP_NAME.lower()}._tcp.local.", local_hostname, Config.port_number)
|
||||
ZeroconfServer.configure("_zordon._tcp.local.", local_hostname, Config.port_number)
|
||||
ZeroconfServer.properties = {'system_cpu': current_system_cpu(),
|
||||
'system_cpu_cores': multiprocessing.cpu_count(),
|
||||
'system_os': current_system_os(),
|
||||
'system_os_version': current_system_os_version()}
|
||||
ZeroconfServer.start()
|
||||
logger.info(f"{APP_NAME} Render Server started - Hostname: {local_hostname}")
|
||||
logger.info(f"Zordon Render Server started - Hostname: {local_hostname}")
|
||||
RenderQueue.start() # Start evaluating the render queue
|
||||
|
||||
# start in gui or server only (cli) mode
|
||||
@@ -141,13 +100,13 @@ def run(server_only=False) -> int:
|
||||
return_code = 1
|
||||
finally:
|
||||
# shut down gracefully
|
||||
logger.info(f"{APP_NAME} Render Server is preparing to shut down")
|
||||
logger.info(f"Zordon Render Server is preparing to shut down")
|
||||
try:
|
||||
RenderQueue.prepare_for_shutdown()
|
||||
except Exception as e:
|
||||
logger.exception(f"Exception during prepare for shutdown: {e}")
|
||||
ZeroconfServer.stop()
|
||||
logger.info(f"{APP_NAME} Render Server has shut down")
|
||||
logger.info(f"Zordon Render Server has shut down")
|
||||
return sys.exit(return_code)
|
||||
|
||||
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import humanize
|
||||
import socket
|
||||
|
||||
from datetime import datetime
|
||||
from PyQt6 import QtCore
|
||||
from PyQt6.QtCore import Qt, QSettings, pyqtSignal as Signal
|
||||
from PyQt6.QtGui import QIcon
|
||||
from PyQt6.QtWidgets import QApplication, QMainWindow, QListWidget, QListWidgetItem, QStackedWidget, QVBoxLayout, \
|
||||
QWidget, QLabel, QCheckBox, QLineEdit, \
|
||||
QComboBox, QPushButton, QHBoxLayout, QGroupBox, QTableWidget, QAbstractItemView, QTableWidgetItem, QHeaderView, \
|
||||
QMessageBox
|
||||
|
||||
from api.server_proxy import RenderServerProxy
|
||||
from engines.engine_manager import EngineManager
|
||||
from utilities.config import Config
|
||||
from utilities.misc_helper import launch_url, system_safe_path
|
||||
from version import APP_AUTHOR, APP_NAME
|
||||
|
||||
settings = QSettings(APP_AUTHOR, APP_NAME)
|
||||
|
||||
|
||||
class SettingsWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
if not EngineManager.engines_path: # fix issue where sometimes path was not set
|
||||
EngineManager.engines_path = system_safe_path(
|
||||
os.path.join(os.path.join(os.path.expanduser(Config.upload_folder),
|
||||
'engines')))
|
||||
|
||||
self.installed_engines_table = None
|
||||
|
||||
self.setWindowTitle("Settings")
|
||||
|
||||
# Create the main layout
|
||||
main_layout = QVBoxLayout()
|
||||
|
||||
# Create the sidebar (QListWidget) for navigation
|
||||
self.sidebar = QListWidget()
|
||||
self.sidebar.setFixedWidth(150)
|
||||
|
||||
# Set the icon size
|
||||
self.sidebar.setIconSize(QtCore.QSize(32, 32)) # Increase the icon size to 32x32 pixels
|
||||
|
||||
# Adjust the font size for the sidebar items
|
||||
font = self.sidebar.font()
|
||||
font.setPointSize(12) # Increase the font size
|
||||
self.sidebar.setFont(font)
|
||||
|
||||
# Add items with icons to the sidebar
|
||||
resources_dir = os.path.join(Path(__file__).resolve().parent.parent.parent, 'resources')
|
||||
self.add_sidebar_item("General", os.path.join(resources_dir, "Gear.png"))
|
||||
self.add_sidebar_item("Server", os.path.join(resources_dir, "Server.png"))
|
||||
self.add_sidebar_item("Engines", os.path.join(resources_dir, "Blender.png"))
|
||||
self.sidebar.setCurrentRow(0)
|
||||
|
||||
# Create the stacked widget to hold different settings pages
|
||||
self.stacked_widget = QStackedWidget()
|
||||
|
||||
# Create pages for each section
|
||||
general_page = self.create_general_page()
|
||||
network_page = self.create_network_page()
|
||||
engines_page = self.create_engines_page()
|
||||
|
||||
# Add pages to the stacked widget
|
||||
self.stacked_widget.addWidget(general_page)
|
||||
self.stacked_widget.addWidget(network_page)
|
||||
self.stacked_widget.addWidget(engines_page)
|
||||
|
||||
# Connect the sidebar to the stacked widget
|
||||
self.sidebar.currentRowChanged.connect(self.stacked_widget.setCurrentIndex)
|
||||
|
||||
# Create a horizontal layout to hold the sidebar and stacked widget
|
||||
content_layout = QHBoxLayout()
|
||||
content_layout.addWidget(self.sidebar)
|
||||
content_layout.addWidget(self.stacked_widget)
|
||||
|
||||
# Add the content layout to the main layout
|
||||
main_layout.addLayout(content_layout)
|
||||
|
||||
# Add the "OK" button at the bottom
|
||||
ok_button = QPushButton("OK")
|
||||
ok_button.clicked.connect(self.close)
|
||||
ok_button.setFixedWidth(80)
|
||||
ok_button.setDefault(True)
|
||||
main_layout.addWidget(ok_button, alignment=Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
# Create a central widget and set the layout
|
||||
central_widget = QWidget()
|
||||
central_widget.setLayout(main_layout)
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
self.setMinimumSize(700, 400)
|
||||
|
||||
def add_sidebar_item(self, name, icon_path):
|
||||
"""Add an item with an icon to the sidebar."""
|
||||
item = QListWidgetItem(QIcon(icon_path), name)
|
||||
self.sidebar.addItem(item)
|
||||
|
||||
def create_general_page(self):
|
||||
"""Create the General settings page."""
|
||||
page = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Startup Settings Group
|
||||
startup_group = QGroupBox("Startup Settings")
|
||||
startup_layout = QVBoxLayout()
|
||||
# startup_layout.addWidget(QCheckBox("Start application on system startup"))
|
||||
check_for_updates_checkbox = QCheckBox("Check for updates automatically")
|
||||
check_for_updates_checkbox.setChecked(settings.value("auto_check_for_updates", True))
|
||||
check_for_updates_checkbox.stateChanged.connect(lambda state: settings.setValue("auto_check_for_updates", bool(state)))
|
||||
startup_layout.addWidget(check_for_updates_checkbox)
|
||||
startup_group.setLayout(startup_layout)
|
||||
|
||||
# Local Files Group
|
||||
data_path = os.path.expanduser(Config.upload_folder)
|
||||
path_size = sum(f.stat().st_size for f in Path(data_path).rglob('*') if f.is_file())
|
||||
database_group = QGroupBox("Local Files")
|
||||
database_layout = QVBoxLayout()
|
||||
database_layout.addWidget(QLabel(f"Local Directory: {data_path}"))
|
||||
database_layout.addWidget(QLabel(f"Size: {humanize.naturalsize(path_size, binary=True)}"))
|
||||
open_database_path_button = QPushButton("Open Directory")
|
||||
open_database_path_button.clicked.connect(lambda: launch_url(data_path))
|
||||
open_database_path_button.setFixedWidth(200)
|
||||
database_layout.addWidget(open_database_path_button)
|
||||
database_group.setLayout(database_layout)
|
||||
|
||||
|
||||
# Render Settings Group
|
||||
render_settings_group = QGroupBox("Render Settings")
|
||||
render_settings_layout = QVBoxLayout()
|
||||
render_settings_layout.addWidget(QLabel("Restrict to render nodes with same:"))
|
||||
require_same_engine_checkbox = QCheckBox("Renderer Version")
|
||||
require_same_engine_checkbox.setChecked(settings.value("render_require_same_engine_version"))
|
||||
require_same_engine_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_engine_version", bool(state)))
|
||||
render_settings_layout.addWidget(require_same_engine_checkbox)
|
||||
require_same_cpu_checkbox = QCheckBox("CPU Architecture")
|
||||
require_same_cpu_checkbox.setChecked(settings.value("render_require_same_cpu_type"))
|
||||
require_same_cpu_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_cpu_type", bool(state)))
|
||||
render_settings_layout.addWidget(require_same_cpu_checkbox)
|
||||
require_same_os_checkbox = QCheckBox("Operating System")
|
||||
require_same_os_checkbox.setChecked(settings.value("render_require_same_os"))
|
||||
require_same_os_checkbox.stateChanged.connect(lambda state: settings.setValue("render_require_same_os", bool(state)))
|
||||
render_settings_layout.addWidget(require_same_os_checkbox)
|
||||
render_settings_group.setLayout(render_settings_layout)
|
||||
|
||||
layout.addWidget(startup_group)
|
||||
layout.addWidget(database_group)
|
||||
layout.addWidget(render_settings_group)
|
||||
|
||||
layout.addStretch() # Add a stretch to push content to the top
|
||||
page.setLayout(layout)
|
||||
return page
|
||||
|
||||
def create_network_page(self):
|
||||
"""Create the Network settings page."""
|
||||
page = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Sharing Settings Group
|
||||
sharing_group = QGroupBox("Sharing Settings")
|
||||
sharing_layout = QVBoxLayout()
|
||||
|
||||
enable_sharing_checkbox = QCheckBox("Enable other computers on the network to render to this machine")
|
||||
enable_sharing_checkbox.setChecked(settings.value("enable_network_sharing", False))
|
||||
enable_sharing_checkbox.stateChanged.connect(self.toggle_render_sharing)
|
||||
sharing_layout.addWidget(enable_sharing_checkbox)
|
||||
|
||||
password_layout = QHBoxLayout()
|
||||
password_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.enable_network_password_checkbox = QCheckBox("Enable network password:")
|
||||
self.enable_network_password_checkbox.setChecked(settings.value("enable_network_password", False))
|
||||
self.enable_network_password_checkbox.stateChanged.connect(self.enable_network_password_changed)
|
||||
sharing_layout.addWidget(self.enable_network_password_checkbox)
|
||||
self.network_password_line = QLineEdit()
|
||||
self.network_password_line.setPlaceholderText("Enter a password")
|
||||
self.network_password_line.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
self.network_password_line.setEnabled(settings.value("enable_network_password", False))
|
||||
password_layout.addWidget(self.network_password_line)
|
||||
self.show_password_button = QPushButton("Show")
|
||||
self.show_password_button.setEnabled(settings.value("enable_network_password", False))
|
||||
self.show_password_button.clicked.connect(self.show_password_button_pressed)
|
||||
password_layout.addWidget(self.show_password_button)
|
||||
sharing_layout.addLayout(password_layout)
|
||||
|
||||
sharing_group.setLayout(sharing_layout)
|
||||
|
||||
layout.addWidget(sharing_group)
|
||||
|
||||
layout.addStretch() # Add a stretch to push content to the top
|
||||
page.setLayout(layout)
|
||||
return page
|
||||
|
||||
def toggle_render_sharing(self, enable_sharing):
|
||||
settings.setValue("enable_network_sharing", enable_sharing)
|
||||
self.enable_network_password_checkbox.setEnabled(enable_sharing)
|
||||
enable_password = enable_sharing and settings.value("enable_network_password", False)
|
||||
self.network_password_line.setEnabled(enable_password)
|
||||
self.show_password_button.setEnabled(enable_password)
|
||||
|
||||
def enable_network_password_changed(self, new_value):
|
||||
settings.setValue("enable_network_password", new_value)
|
||||
self.network_password_line.setEnabled(new_value)
|
||||
self.show_password_button.setEnabled(new_value)
|
||||
|
||||
def show_password_button_pressed(self):
|
||||
# toggle showing / hiding the password
|
||||
show_pass = self.show_password_button.text() == "Show"
|
||||
self.show_password_button.setText("Hide" if show_pass else "Show")
|
||||
self.network_password_line.setEchoMode(QLineEdit.EchoMode.Normal if show_pass else QLineEdit.EchoMode.Normal)
|
||||
|
||||
def create_engines_page(self):
|
||||
"""Create the Engines settings page."""
|
||||
page = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Installed Engines Group
|
||||
installed_group = QGroupBox("Installed Engines")
|
||||
installed_layout = QVBoxLayout()
|
||||
|
||||
# Setup table
|
||||
self.installed_engines_table = EngineTableWidget()
|
||||
self.installed_engines_table.row_selected.connect(self.engine_table_selected)
|
||||
installed_layout.addWidget(self.installed_engines_table)
|
||||
|
||||
# Ignore system installs
|
||||
engine_ignore_system_installs_checkbox = QCheckBox("Ignore system installs")
|
||||
engine_ignore_system_installs_checkbox.setChecked(settings.value("engines_ignore_system_installs", False))
|
||||
engine_ignore_system_installs_checkbox.stateChanged.connect(self.change_ignore_system_installs)
|
||||
installed_layout.addWidget(engine_ignore_system_installs_checkbox)
|
||||
|
||||
# Engine Launch / Delete buttons
|
||||
installed_buttons_layout = QHBoxLayout()
|
||||
self.launch_engine_button = QPushButton("Launch")
|
||||
self.launch_engine_button.setEnabled(False)
|
||||
self.launch_engine_button.clicked.connect(self.launch_selected_engine)
|
||||
self.delete_engine_button = QPushButton("Delete")
|
||||
self.delete_engine_button.setEnabled(False)
|
||||
self.delete_engine_button.clicked.connect(self.delete_selected_engine)
|
||||
|
||||
installed_buttons_layout.addWidget(self.launch_engine_button)
|
||||
installed_buttons_layout.addWidget(self.delete_engine_button)
|
||||
installed_layout.addLayout(installed_buttons_layout)
|
||||
installed_group.setLayout(installed_layout)
|
||||
|
||||
# Engine Updates Group
|
||||
engine_updates_group = QGroupBox("Auto-Install")
|
||||
engine_updates_layout = QVBoxLayout()
|
||||
|
||||
engine_download_layout = QHBoxLayout()
|
||||
engine_download_layout.addWidget(QLabel("Enable Downloads for:"))
|
||||
|
||||
at_least_one_downloadable = False
|
||||
for engine in EngineManager.downloadable_engines():
|
||||
engine_download_check = QCheckBox(engine.name())
|
||||
is_checked = settings.value(f"engine_download-{engine.name()}", False)
|
||||
at_least_one_downloadable |= is_checked
|
||||
engine_download_check.setChecked(is_checked)
|
||||
# Capture the checkbox correctly using a default argument in lambda
|
||||
engine_download_check.clicked.connect(
|
||||
lambda state, checkbox=engine_download_check: self.engine_download_settings_changed(state, checkbox.text())
|
||||
)
|
||||
engine_download_layout.addWidget(engine_download_check)
|
||||
|
||||
engine_updates_layout.addLayout(engine_download_layout)
|
||||
|
||||
check_for_engine_updates_checkbox = QCheckBox("Check for new versions on launch")
|
||||
check_for_engine_updates_checkbox.setChecked(settings.value('check_for_engine_updates_on_launch', True))
|
||||
check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
|
||||
check_for_engine_updates_checkbox.stateChanged.connect(
|
||||
lambda state: settings.setValue("check_for_engine_updates_on_launch", bool(state)))
|
||||
engine_updates_layout.addWidget(check_for_engine_updates_checkbox)
|
||||
self.engines_last_update_label = QLabel()
|
||||
self.update_last_checked_label()
|
||||
self.engines_last_update_label.setEnabled(at_least_one_downloadable)
|
||||
engine_updates_layout.addWidget(self.engines_last_update_label)
|
||||
self.check_for_new_engines_button = QPushButton("Check for New Versions...")
|
||||
self.check_for_new_engines_button.setEnabled(at_least_one_downloadable)
|
||||
self.check_for_new_engines_button.clicked.connect(self.check_for_new_engines)
|
||||
engine_updates_layout.addWidget(self.check_for_new_engines_button)
|
||||
engine_updates_group.setLayout(engine_updates_layout)
|
||||
|
||||
layout.addWidget(installed_group)
|
||||
layout.addWidget(engine_updates_group)
|
||||
|
||||
layout.addStretch() # Add a stretch to push content to the top
|
||||
page.setLayout(layout)
|
||||
return page
|
||||
|
||||
def change_ignore_system_installs(self, value):
|
||||
settings.setValue("engines_ignore_system_installs", bool(value))
|
||||
self.installed_engines_table.update_table()
|
||||
|
||||
def update_last_checked_label(self):
|
||||
"""Retrieve the last check timestamp and return a human-friendly string."""
|
||||
last_checked_str = settings.value("engines_last_update_time", None)
|
||||
if not last_checked_str:
|
||||
time_string = "Never"
|
||||
else:
|
||||
last_checked_dt = datetime.fromisoformat(last_checked_str)
|
||||
now = datetime.now()
|
||||
time_string = humanize.naturaltime(now - last_checked_dt)
|
||||
self.engines_last_update_label.setText(f"Last Updated: {time_string}")
|
||||
|
||||
def engine_download_settings_changed(self, state, engine_name):
|
||||
settings.setValue(f"engine_download-{engine_name}", state)
|
||||
|
||||
at_least_one_downloadable = False
|
||||
for engine in EngineManager.downloadable_engines():
|
||||
at_least_one_downloadable |= settings.value(f"engine_download-{engine.name()}", False)
|
||||
self.check_for_new_engines_button.setEnabled(at_least_one_downloadable)
|
||||
self.check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
|
||||
self.engines_last_update_label.setEnabled(at_least_one_downloadable)
|
||||
|
||||
def delete_selected_engine(self):
|
||||
engine_info = self.installed_engines_table.selected_engine_data()
|
||||
reply = QMessageBox.question(self, f"Delete {engine_info['engine']} {engine_info['version']}?",
|
||||
f"Do you want to delete {engine_info['engine']} {engine_info['version']}?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||
|
||||
if reply is not QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
delete_result = EngineManager.delete_engine_download(engine_info.get('engine'),
|
||||
engine_info.get('version'),
|
||||
engine_info.get('system_os'),
|
||||
engine_info.get('cpu'))
|
||||
|
||||
if delete_result:
|
||||
QMessageBox.information(self, f"{engine_info['engine']} {engine_info['version']} Deleted",
|
||||
f"{engine_info['engine']} {engine_info['version']} deleted successfully",
|
||||
QMessageBox.StandardButton.Ok)
|
||||
else:
|
||||
QMessageBox.warning(self, f"Unknown Error",
|
||||
f"Unknown error while deleting {engine_info['engine']} {engine_info['version']}.",
|
||||
QMessageBox.StandardButton.Ok)
|
||||
|
||||
self.installed_engines_table.update_table(use_cached=False)
|
||||
|
||||
def launch_selected_engine(self):
|
||||
engine_info = self.installed_engines_table.selected_engine_data()
|
||||
if engine_info:
|
||||
launch_url(engine_info['path'])
|
||||
|
||||
def engine_table_selected(self):
|
||||
engine_data = self.installed_engines_table.selected_engine_data()
|
||||
if engine_data:
|
||||
self.launch_engine_button.setEnabled(bool(engine_data.get('path') or True))
|
||||
self.delete_engine_button.setEnabled(engine_data.get('type') == 'managed')
|
||||
else:
|
||||
self.launch_engine_button.setEnabled(False)
|
||||
self.delete_engine_button.setEnabled(False)
|
||||
|
||||
def check_for_new_engines(self):
|
||||
|
||||
ignore_system = settings.value("engines_ignore_system_installs", False)
|
||||
messagebox_shown = False
|
||||
for engine in EngineManager.downloadable_engines():
|
||||
if settings.value(f'engine_download-{engine.name()}', False):
|
||||
result = EngineManager.is_engine_update_available(engine, ignore_system_installs=ignore_system)
|
||||
if result:
|
||||
result['name'] = engine.name()
|
||||
msg_box = QMessageBox()
|
||||
msg_box.setWindowTitle(f"{result['name']} ({result['version']}) Available")
|
||||
msg_box.setText(f"A new version of {result['name']} is available ({result['version']}).\n\n"
|
||||
f"Would you like to download it now?")
|
||||
msg_box.setIcon(QMessageBox.Icon.Question)
|
||||
msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
||||
msg_result = msg_box.exec()
|
||||
messagebox_shown = True
|
||||
if msg_result == QMessageBox.StandardButton.Yes:
|
||||
EngineManager.download_engine(engine=engine.name(), version=result['version'], background=True,
|
||||
ignore_system=ignore_system)
|
||||
self.update_engine_download_status()
|
||||
|
||||
if not messagebox_shown:
|
||||
msg_box = QMessageBox()
|
||||
msg_box.setWindowTitle("No Updates Available")
|
||||
msg_box.setText("All your render engines are up-to-date.")
|
||||
msg_box.setIcon(QMessageBox.Icon.Information)
|
||||
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
msg_box.exec()
|
||||
|
||||
settings.setValue("engines_last_update_time", datetime.now().isoformat())
|
||||
self.update_engine_download_status()
|
||||
|
||||
def update_engine_download_status(self):
|
||||
running_tasks = [x for x in EngineManager.download_tasks if x.is_alive()]
|
||||
if not running_tasks:
|
||||
self.update_last_checked_label()
|
||||
return
|
||||
|
||||
self.engines_last_update_label.setText(f"Downloading {running_tasks[0].engine} ({running_tasks[0].version})...")
|
||||
|
||||
|
||||
class EngineTableWidget(QWidget):
|
||||
row_selected = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.table = QTableWidget(0, 4)
|
||||
self.table.setHorizontalHeaderLabels(["Engine", "Version", "Type", "Path"])
|
||||
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.table.verticalHeader().setVisible(False)
|
||||
# self.table_widget.itemSelectionChanged.connect(self.engine_picked)
|
||||
self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.table.selectionModel().selectionChanged.connect(self.on_selection_changed)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(self.table)
|
||||
|
||||
self.raw_server_data = None
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Runs when the widget is about to be shown."""
|
||||
self.update_table()
|
||||
super().showEvent(event) # Ensure normal event processing
|
||||
|
||||
def update_table(self, use_cached=True):
|
||||
if not self.raw_server_data or not use_cached:
|
||||
self.raw_server_data = RenderServerProxy(socket.gethostname()).get_renderer_info()
|
||||
if not self.raw_server_data:
|
||||
return
|
||||
|
||||
table_data = [] # convert the data into a flat list
|
||||
for _, engine_data in self.raw_server_data.items():
|
||||
table_data.extend(engine_data['versions'])
|
||||
|
||||
if settings.value("engines_ignore_system_installs", False):
|
||||
table_data = [x for x in table_data if x['type'] != 'system']
|
||||
|
||||
self.table.clear()
|
||||
self.table.setRowCount(len(table_data))
|
||||
self.table.setColumnCount(4)
|
||||
|
||||
self.table.setHorizontalHeaderLabels(['Engine', 'Version', 'Type', 'Path'])
|
||||
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||
self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||||
self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||
|
||||
for row, engine in enumerate(table_data):
|
||||
self.table.setItem(row, 0, QTableWidgetItem(engine['engine']))
|
||||
self.table.setItem(row, 1, QTableWidgetItem(engine['version']))
|
||||
self.table.setItem(row, 2, QTableWidgetItem(engine['type']))
|
||||
self.table.setItem(row, 3, QTableWidgetItem(engine['path']))
|
||||
|
||||
self.table.selectRow(0)
|
||||
|
||||
def selected_engine_data(self):
|
||||
"""Returns the data from the selected row as a dictionary."""
|
||||
row = self.table.currentRow() # Get the selected row index
|
||||
|
||||
if row < 0 or not len(self.table.selectedItems()): # No row selected
|
||||
return None
|
||||
|
||||
data = {
|
||||
"engine": self.table.item(row, 0).text(),
|
||||
"version": self.table.item(row, 1).text(),
|
||||
"type": self.table.item(row, 2).text(),
|
||||
"path": self.table.item(row, 3).text(),
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
def on_selection_changed(self):
|
||||
self.row_selected.emit()
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
window = SettingsWindow()
|
||||
window.show()
|
||||
app.exec()
|
||||
@@ -1,6 +1,8 @@
|
||||
''' app/ui/widgets/menubar.py '''
|
||||
import sys
|
||||
|
||||
from PyQt6.QtGui import QAction
|
||||
from PyQt6.QtWidgets import QMenuBar, QApplication, QMessageBox, QDialog, QVBoxLayout, QLabel, QPushButton
|
||||
from PyQt6.QtWidgets import QMenuBar, QApplication
|
||||
|
||||
|
||||
class MenuBar(QMenuBar):
|
||||
@@ -14,8 +16,6 @@ class MenuBar(QMenuBar):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.settings_window = None
|
||||
|
||||
# setup menus
|
||||
file_menu = self.addMenu("File")
|
||||
# edit_menu = self.addMenu("Edit")
|
||||
@@ -32,7 +32,7 @@ class MenuBar(QMenuBar):
|
||||
settings_action = QAction("Settings...", self)
|
||||
settings_action.triggered.connect(self.show_settings)
|
||||
settings_action.setShortcut(f'Ctrl+,')
|
||||
file_menu.addAction(settings_action)
|
||||
# file_menu.addAction(settings_action) # todo: enable once we have a setting screen
|
||||
# exit
|
||||
exit_action = QAction('&Exit', self)
|
||||
exit_action.setShortcut('Ctrl+Q')
|
||||
@@ -43,63 +43,15 @@ class MenuBar(QMenuBar):
|
||||
about_action = QAction("About", self)
|
||||
about_action.triggered.connect(self.show_about)
|
||||
help_menu.addAction(about_action)
|
||||
update_action = QAction("Check for Updates...", self)
|
||||
update_action.triggered.connect(self.check_for_updates)
|
||||
help_menu.addAction(update_action)
|
||||
|
||||
def new_job(self):
|
||||
self.parent().new_job()
|
||||
|
||||
def show_settings(self):
|
||||
from src.ui.settings_window import SettingsWindow
|
||||
self.settings_window = SettingsWindow()
|
||||
self.settings_window.show()
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def show_about():
|
||||
from src.ui.about_window import AboutDialog
|
||||
dialog = AboutDialog()
|
||||
dialog.exec()
|
||||
|
||||
@staticmethod
|
||||
def check_for_updates():
|
||||
from src.utilities.misc_helper import check_for_updates
|
||||
from version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
|
||||
found_update = check_for_updates(APP_REPO_NAME, APP_REPO_OWNER, APP_NAME, APP_VERSION)
|
||||
if found_update:
|
||||
dialog = UpdateDialog(found_update, APP_VERSION)
|
||||
dialog.exec()
|
||||
else:
|
||||
QMessageBox.information(None, "No Update", "No updates available.")
|
||||
|
||||
|
||||
class UpdateDialog(QDialog):
|
||||
def __init__(self, release_info, current_version, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f"Update Available ({current_version} -> {release_info['tag_name']})")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
label = QLabel(f"A new version ({release_info['tag_name']}) is available! Current version: {current_version}")
|
||||
layout.addWidget(label)
|
||||
|
||||
# Label to show the release notes
|
||||
description = QLabel(release_info["body"])
|
||||
layout.addWidget(description)
|
||||
|
||||
# Button to download the latest version
|
||||
download_button = QPushButton(f"Download Latest Version ({release_info['tag_name']})")
|
||||
download_button.clicked.connect(lambda: self.open_url(release_info["html_url"]))
|
||||
layout.addWidget(download_button)
|
||||
|
||||
# OK button to dismiss the dialog
|
||||
ok_button = QPushButton("Dismiss")
|
||||
ok_button.clicked.connect(self.accept) # Close the dialog when clicked
|
||||
layout.addWidget(ok_button)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def open_url(self, url):
|
||||
from PyQt6.QtCore import QUrl
|
||||
from PyQt6.QtGui import QDesktopServices
|
||||
QDesktopServices.openUrl(QUrl(url))
|
||||
self.accept()
|
||||
|
||||
@@ -159,33 +159,6 @@ def copy_directory_contents(src_dir, dst_dir):
|
||||
shutil.copy2(src_path, dst_path)
|
||||
|
||||
|
||||
def check_for_updates(repo_name, repo_owner, app_name, current_version):
|
||||
def get_github_releases(owner, repo):
|
||||
import requests
|
||||
url = f"https://api.github.com/repos/{owner}/{repo}/releases"
|
||||
try:
|
||||
response = requests.get(url, timeout=3)
|
||||
response.raise_for_status()
|
||||
releases = response.json()
|
||||
return releases
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking for updates: {e}")
|
||||
return []
|
||||
|
||||
releases = get_github_releases(repo_owner, repo_name)
|
||||
if not releases:
|
||||
return
|
||||
|
||||
latest_version = releases[0]
|
||||
latest_version_tag = latest_version['tag_name']
|
||||
|
||||
from packaging import version
|
||||
if version.parse(latest_version_tag) > version.parse(current_version):
|
||||
logger.info(f"Newer version of {app_name} available. "
|
||||
f"Latest: {latest_version_tag}, Current: {current_version}")
|
||||
return latest_version
|
||||
|
||||
|
||||
def is_localhost(comparison_hostname):
|
||||
# this is necessary because socket.gethostname() does not always include '.local' - This is a sanitized comparison
|
||||
try:
|
||||
|
||||
@@ -4,5 +4,3 @@ APP_AUTHOR = "Brett Williams"
|
||||
APP_DESCRIPTION = "Distributed Render Farm Tools"
|
||||
APP_COPYRIGHT_YEAR = "2024"
|
||||
APP_LICENSE = "MIT License"
|
||||
APP_REPO_NAME = APP_NAME
|
||||
APP_REPO_OWNER = "blw1138"
|
||||
|
||||
Reference in New Issue
Block a user