mirror of
https://github.com/blw1138/Zordon.git
synced 2026-02-05 05:36:09 +00:00
Settings Window (#124)
* Initial commit for settings window * More WIP for the Settings panel * Added Local Files section to Settings * More WIP on Settings * Add ability to ignore system builds * Improvements to Launch and Delete buttons * Fix issue where icons were not loading * Network password settings WIP * Update label * Import and naming fixes * Speed improvements to launch * Update requirements.txt * Update Windows CPU name lookup * Add missing default values to a few settings * More settings fixes * Fix Windows Path issue * Added hard types for getting settings values * More UI cleanup * Correctly refresh Engines list after downloading new engine * Improve downloader with UI progress * More download improvements * Add Settings Button to Toolbar
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,3 +11,6 @@
|
|||||||
/venv/
|
/venv/
|
||||||
.env
|
.env
|
||||||
venv/
|
venv/
|
||||||
|
/.eggs/
|
||||||
|
/.ai/
|
||||||
|
/.github/
|
||||||
|
|||||||
@@ -1,41 +1,20 @@
|
|||||||
PyQt6>=6.6.1
|
PyQt6>=6.7.0
|
||||||
psutil>=5.9.8
|
psutil>=5.9.8
|
||||||
requests>=2.31.0
|
requests>=2.32.2
|
||||||
Pillow>=10.2.0
|
Pillow>=10.3.0
|
||||||
PyYAML>=6.0.1
|
PyYAML>=6.0.1
|
||||||
flask>=3.0.2
|
flask>=3.0.3
|
||||||
tqdm>=4.66.2
|
tqdm>=4.66.4
|
||||||
werkzeug>=3.0.1
|
werkzeug>=3.0.3
|
||||||
Pypubsub>=4.0.3
|
Pypubsub>=4.0.3
|
||||||
zeroconf>=0.131.0
|
zeroconf>=0.132.2
|
||||||
SQLAlchemy>=2.0.25
|
SQLAlchemy>=2.0.30
|
||||||
plyer>=2.1.0
|
plyer>=2.1.0
|
||||||
pytz>=2023.3.post1
|
rich>=13.7.1
|
||||||
future>=0.18.3
|
setuptools>=70.0.0
|
||||||
rich>=13.7.0
|
py-cpuinfo>=9.0.0
|
||||||
pytest>=8.0.0
|
requests-toolbelt>=1.0.0
|
||||||
numpy>=1.26.3
|
PyQt6-sip>=13.6.0
|
||||||
setuptools>=69.0.3
|
humanize>=4.12.1
|
||||||
pandas>=2.2.0
|
macholib>=1.16.3
|
||||||
matplotlib>=3.8.2
|
altgraph>=0.17.4
|
||||||
MarkupSafe>=2.1.4
|
|
||||||
dmglib>=0.9.5; sys_platform == 'darwin'
|
|
||||||
python-dateutil>=2.8.2
|
|
||||||
certifi>=2023.11.17
|
|
||||||
shiboken6>=6.6.1
|
|
||||||
Pygments>=2.17.2
|
|
||||||
cycler>=0.12.1
|
|
||||||
contourpy>=1.2.0
|
|
||||||
packaging>=23.2
|
|
||||||
fonttools>=4.47.2
|
|
||||||
Jinja2>=3.1.3
|
|
||||||
pyparsing>=3.1.1
|
|
||||||
kiwisolver>=1.4.5
|
|
||||||
attrs>=23.2.0
|
|
||||||
lxml>=5.1.0
|
|
||||||
click>=8.1.7
|
|
||||||
requests_toolbelt>=1.0.0
|
|
||||||
pyinstaller_versionfile>=2.1.1
|
|
||||||
py-cpuinfo~=9.0.0
|
|
||||||
requests-toolbelt~=1.0.0
|
|
||||||
ifaddr~=0.2.0
|
|
||||||
13
server.py
13
server.py
@@ -17,7 +17,7 @@ from src.engines.engine_manager import EngineManager
|
|||||||
from src.render_queue import RenderQueue
|
from src.render_queue import RenderQueue
|
||||||
from src.utilities.config import Config
|
from src.utilities.config import Config
|
||||||
from src.utilities.misc_helper import (get_gpu_info, system_safe_path, current_system_cpu, current_system_os,
|
from src.utilities.misc_helper import (get_gpu_info, system_safe_path, current_system_cpu, current_system_os,
|
||||||
current_system_os_version, check_for_updates)
|
current_system_os_version, current_system_cpu_brand, check_for_updates)
|
||||||
from src.utilities.zeroconf_server import ZeroconfServer
|
from src.utilities.zeroconf_server import ZeroconfServer
|
||||||
from src.version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
|
from src.version import APP_NAME, APP_VERSION, APP_REPO_NAME, APP_REPO_OWNER
|
||||||
|
|
||||||
@@ -93,10 +93,6 @@ def start_server(skip_updates=False) -> int:
|
|||||||
ServerProxyManager.subscribe_to_listener()
|
ServerProxyManager.subscribe_to_listener()
|
||||||
DistributedJobManager.subscribe_to_listener()
|
DistributedJobManager.subscribe_to_listener()
|
||||||
|
|
||||||
# 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
|
# get hostname
|
||||||
local_hostname = socket.gethostname()
|
local_hostname = socket.gethostname()
|
||||||
|
|
||||||
@@ -108,7 +104,7 @@ def start_server(skip_updates=False) -> int:
|
|||||||
# start zeroconf server
|
# start zeroconf server
|
||||||
ZeroconfServer.configure(f"_{APP_NAME.lower()}._tcp.local.", local_hostname, Config.port_number)
|
ZeroconfServer.configure(f"_{APP_NAME.lower()}._tcp.local.", local_hostname, Config.port_number)
|
||||||
ZeroconfServer.properties = {'system_cpu': current_system_cpu(),
|
ZeroconfServer.properties = {'system_cpu': current_system_cpu(),
|
||||||
'system_cpu_brand': cpuinfo.get_cpu_info()['brand_raw'],
|
'system_cpu_brand': current_system_cpu_brand(),
|
||||||
'system_cpu_cores': multiprocessing.cpu_count(),
|
'system_cpu_cores': multiprocessing.cpu_count(),
|
||||||
'system_os': current_system_os(),
|
'system_os': current_system_os(),
|
||||||
'system_os_version': current_system_os_version(),
|
'system_os_version': current_system_os_version(),
|
||||||
@@ -118,6 +114,11 @@ def start_server(skip_updates=False) -> int:
|
|||||||
ZeroconfServer.start()
|
ZeroconfServer.start()
|
||||||
logger.info(f"{APP_NAME} Render Server started - Hostname: {local_hostname}")
|
logger.info(f"{APP_NAME} Render Server started - Hostname: {local_hostname}")
|
||||||
RenderQueue.start() # Start evaluating the render queue
|
RenderQueue.start() # Start evaluating the render queue
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
api_server.join()
|
api_server.join()
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ from src.engines.blender.blender_engine import Blender
|
|||||||
from src.engines.core.base_downloader import EngineDownloader
|
from src.engines.core.base_downloader import EngineDownloader
|
||||||
from src.utilities.misc_helper import current_system_os, current_system_cpu
|
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()
|
logger = logging.getLogger()
|
||||||
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
supported_formats = ['.zip', '.tar.xz', '.dmg']
|
||||||
@@ -88,8 +87,8 @@ class BlenderDownloader(EngineDownloader):
|
|||||||
threads = []
|
threads = []
|
||||||
results = [[] for _ in majors]
|
results = [[] for _ in majors]
|
||||||
|
|
||||||
def thread_function(major_version, index, system_os, cpu):
|
def thread_function(major_version, index, system_os_t, cpu_t):
|
||||||
results[index] = cls.__get_minor_versions(major_version, system_os, cpu)
|
results[index] = cls.__get_minor_versions(major_version, system_os_t, cpu_t)
|
||||||
|
|
||||||
for i, m in enumerate(majors):
|
for i, m in enumerate(majors):
|
||||||
thread = threading.Thread(target=thread_function, args=(m, i, system_os, cpu))
|
thread = threading.Thread(target=thread_function, args=(m, i, system_os, cpu))
|
||||||
@@ -126,7 +125,7 @@ class BlenderDownloader(EngineDownloader):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120):
|
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120, progress_callback=None):
|
||||||
system_os = system_os or current_system_os()
|
system_os = system_os or current_system_os()
|
||||||
cpu = cpu or current_system_cpu()
|
cpu = cpu or current_system_cpu()
|
||||||
|
|
||||||
@@ -136,7 +135,7 @@ class BlenderDownloader(EngineDownloader):
|
|||||||
minor_versions = [x for x in cls.__get_minor_versions(major_version, system_os, cpu) if
|
minor_versions = [x for x in cls.__get_minor_versions(major_version, system_os, cpu) if
|
||||||
x['version'] == version]
|
x['version'] == version]
|
||||||
cls.download_and_extract_app(remote_url=minor_versions[0]['url'], download_location=download_location,
|
cls.download_and_extract_app(remote_url=minor_versions[0]['url'], download_location=download_location,
|
||||||
timeout=timeout)
|
timeout=timeout, progress_callback=progress_callback)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.error("Cannot find requested engine")
|
logger.error("Cannot find requested engine")
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class EngineDownloader:
|
|||||||
raise NotImplementedError(f"version_is_available_to_download not implemented for {cls.__class__.__name__}")
|
raise NotImplementedError(f"version_is_available_to_download not implemented for {cls.__class__.__name__}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120):
|
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120, progress_callback=None):
|
||||||
"""Downloads the requested version of the rendering engine to the given download location.
|
"""Downloads the requested version of the rendering engine to the given download location.
|
||||||
|
|
||||||
This method should be overridden in a subclass to implement the logic for downloading
|
This method should be overridden in a subclass to implement the logic for downloading
|
||||||
@@ -88,6 +88,7 @@ class EngineDownloader:
|
|||||||
system_os (str, optional): Desired OS ('linux', 'macos', 'windows'). Defaults to system os.
|
system_os (str, optional): Desired OS ('linux', 'macos', 'windows'). Defaults to system os.
|
||||||
cpu (str, optional): The CPU architecture for which to download the engine. Default is system cpu.
|
cpu (str, optional): The CPU architecture for which to download the engine. Default is system cpu.
|
||||||
timeout (int, optional): The maximum time in seconds to wait for the download. Default is 120 seconds.
|
timeout (int, optional): The maximum time in seconds to wait for the download. Default is 120 seconds.
|
||||||
|
progress_callback (callable, optional): A callback function that is called periodically with the current download progress.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: If the method is not overridden in a subclass.
|
NotImplementedError: If the method is not overridden in a subclass.
|
||||||
@@ -125,7 +126,7 @@ class EngineDownloader:
|
|||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download_and_extract_app(cls, remote_url, download_location, timeout=120):
|
def download_and_extract_app(cls, remote_url, download_location, timeout=120, progress_callback=None):
|
||||||
"""Downloads an application from the given remote URL and extracts it to the specified location.
|
"""Downloads an application from the given remote URL and extracts it to the specified location.
|
||||||
|
|
||||||
This method handles the downloading of the application, supports multiple archive formats,
|
This method handles the downloading of the application, supports multiple archive formats,
|
||||||
@@ -136,6 +137,7 @@ class EngineDownloader:
|
|||||||
remote_url (str): The URL of the application to download.
|
remote_url (str): The URL of the application to download.
|
||||||
download_location (str): The directory where the application should be extracted.
|
download_location (str): The directory where the application should be extracted.
|
||||||
timeout (int, optional): The maximum time in seconds to wait for the download. Default is 120 seconds.
|
timeout (int, optional): The maximum time in seconds to wait for the download. Default is 120 seconds.
|
||||||
|
progress_callback (callable, optional): A callback function that is called periodically with the current download progress.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The path to the directory where the application was extracted.
|
str: The path to the directory where the application was extracted.
|
||||||
@@ -154,6 +156,8 @@ class EngineDownloader:
|
|||||||
and return without downloading or extracting.
|
and return without downloading or extracting.
|
||||||
- Temporary files created during the download process are cleaned up after completion.
|
- Temporary files created during the download process are cleaned up after completion.
|
||||||
"""
|
"""
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(0)
|
||||||
|
|
||||||
# Create a temp download directory
|
# Create a temp download directory
|
||||||
temp_download_dir = tempfile.mkdtemp()
|
temp_download_dir = tempfile.mkdtemp()
|
||||||
@@ -166,7 +170,7 @@ class EngineDownloader:
|
|||||||
|
|
||||||
if os.path.exists(os.path.join(download_location, output_dir_name)):
|
if os.path.exists(os.path.join(download_location, output_dir_name)):
|
||||||
logger.error(f"Engine download for {output_dir_name} already exists")
|
logger.error(f"Engine download for {output_dir_name} already exists")
|
||||||
return
|
return None
|
||||||
|
|
||||||
if not os.path.exists(temp_downloaded_file_path):
|
if not os.path.exists(temp_downloaded_file_path):
|
||||||
# Make a GET request to the URL with stream=True to enable streaming
|
# Make a GET request to the URL with stream=True to enable streaming
|
||||||
@@ -182,20 +186,26 @@ class EngineDownloader:
|
|||||||
progress_bar = tqdm(total=file_size, unit="B", unit_scale=True)
|
progress_bar = tqdm(total=file_size, unit="B", unit_scale=True)
|
||||||
|
|
||||||
# Open a file for writing in binary mode
|
# Open a file for writing in binary mode
|
||||||
|
total_saved = 0
|
||||||
with open(temp_downloaded_file_path, "wb") as file:
|
with open(temp_downloaded_file_path, "wb") as file:
|
||||||
for chunk in response.iter_content(chunk_size=1024):
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
if chunk:
|
if chunk:
|
||||||
# Write the chunk to the file
|
# Write the chunk to the file
|
||||||
file.write(chunk)
|
file.write(chunk)
|
||||||
|
total_saved += len(chunk)
|
||||||
# Update the progress bar
|
# Update the progress bar
|
||||||
progress_bar.update(len(chunk))
|
progress_bar.update(len(chunk))
|
||||||
|
if progress_callback:
|
||||||
|
percent = float(total_saved) / float(file_size)
|
||||||
|
progress_callback(percent)
|
||||||
|
|
||||||
# Close the progress bar
|
# Close the progress bar
|
||||||
|
progress_callback(1.0)
|
||||||
progress_bar.close()
|
progress_bar.close()
|
||||||
logger.info(f"Successfully downloaded {os.path.basename(temp_downloaded_file_path)}")
|
logger.info(f"Successfully downloaded {os.path.basename(temp_downloaded_file_path)}")
|
||||||
else:
|
else:
|
||||||
logger.error(f"Failed to download the file. Status code: {response.status_code}")
|
logger.error(f"Failed to download the file. Status code: {response.status_code}")
|
||||||
return
|
return None
|
||||||
|
|
||||||
os.makedirs(download_location, exist_ok=True)
|
os.makedirs(download_location, exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ class EngineManager:
|
|||||||
def supported_engines():
|
def supported_engines():
|
||||||
return [Blender, FFMPEG]
|
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 active_downloads(cls) -> list:
|
||||||
|
return [x for x in cls.download_tasks if x.is_alive()]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def engine_with_name(cls, engine_name):
|
def engine_with_name(cls, engine_name):
|
||||||
for obj in cls.supported_engines():
|
for obj in cls.supported_engines():
|
||||||
@@ -30,7 +38,15 @@ class EngineManager:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_engines(cls, filter_name=None, include_corrupt=False):
|
def update_all_engines(cls):
|
||||||
|
for engine in cls.downloadable_engines():
|
||||||
|
update_available = cls.is_engine_update_available(engine)
|
||||||
|
if update_available:
|
||||||
|
update_available['name'] = engine.name()
|
||||||
|
cls.download_engine(engine.name(), update_available['version'], background=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_engines(cls, filter_name=None, include_corrupt=False, ignore_system=False):
|
||||||
|
|
||||||
if not cls.engines_path:
|
if not cls.engines_path:
|
||||||
raise FileNotFoundError("Engine path is not set")
|
raise FileNotFoundError("Engine path is not set")
|
||||||
@@ -63,13 +79,13 @@ class EngineManager:
|
|||||||
)
|
)
|
||||||
result_dict['path'] = path
|
result_dict['path'] = path
|
||||||
|
|
||||||
# fetch version number from binary - helps detect corrupted downloads
|
# fetch version number from binary - helps detect corrupted downloads - disabled due to perf issues
|
||||||
binary_version = eng(path).version()
|
# binary_version = eng(path).version()
|
||||||
if not binary_version:
|
# if not binary_version:
|
||||||
logger.warning(f"Possible corrupt {eng.name()} {result_dict['version']} install detected: {path}")
|
# logger.warning(f"Possible corrupt {eng.name()} {result_dict['version']} install detected: {path}")
|
||||||
if not include_corrupt:
|
# if not include_corrupt:
|
||||||
continue
|
# continue
|
||||||
result_dict['version'] = binary_version or 'error'
|
# result_dict['version'] = binary_version or 'error'
|
||||||
|
|
||||||
# Add the result dictionary to results if it matches the filter_name or if no filter is applied
|
# Add the result dictionary to results if it matches the filter_name or if no filter is applied
|
||||||
if not filter_name or filter_name == result_dict['engine']:
|
if not filter_name or filter_name == result_dict['engine']:
|
||||||
@@ -92,6 +108,7 @@ class EngineManager:
|
|||||||
'type': 'system'
|
'type': 'system'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if not ignore_system:
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
futures = {
|
futures = {
|
||||||
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
executor.submit(fetch_engine_details, eng, include_corrupt): eng.name()
|
||||||
@@ -107,31 +124,31 @@ class EngineManager:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all_versions_for_engine(cls, engine_name, include_corrupt=False):
|
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)
|
versions = cls.get_engines(filter_name=engine_name, include_corrupt=include_corrupt, ignore_system=ignore_system)
|
||||||
sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True)
|
sorted_versions = sorted(versions, key=lambda x: x['version'], reverse=True)
|
||||||
return sorted_versions
|
return sorted_versions
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def newest_engine_version(cls, engine, system_os=None, cpu=None):
|
def newest_engine_version(cls, engine, system_os=None, cpu=None, ignore_system=None):
|
||||||
system_os = system_os or current_system_os()
|
system_os = system_os or current_system_os()
|
||||||
cpu = cpu or current_system_cpu()
|
cpu = cpu or current_system_cpu()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filtered = [x for x in cls.all_versions_for_engine(engine) if x['system_os'] == system_os and
|
filtered = [x for x in cls.all_versions_for_engine(engine, ignore_system=ignore_system)
|
||||||
x['cpu'] == cpu]
|
if x['system_os'] == system_os and x['cpu'] == cpu]
|
||||||
return filtered[0]
|
return filtered[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}")
|
logger.error(f"Cannot find newest engine version for {engine}-{system_os}-{cpu}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None):
|
def is_version_downloaded(cls, engine, version, system_os=None, cpu=None, ignore_system=False):
|
||||||
system_os = system_os or current_system_os()
|
system_os = system_os or current_system_os()
|
||||||
cpu = cpu or current_system_cpu()
|
cpu = cpu or current_system_cpu()
|
||||||
|
|
||||||
filtered = [x for x in cls.get_engines(filter_name=engine) if x['system_os'] == system_os and
|
filtered = [x for x in cls.get_engines(filter_name=engine, ignore_system=ignore_system) if
|
||||||
x['cpu'] == cpu and x['version'] == version]
|
x['system_os'] == system_os and x['cpu'] == cpu and x['version'] == version]
|
||||||
return filtered[0] if filtered else False
|
return filtered[0] if filtered else False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -164,7 +181,7 @@ class EngineManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download_engine(cls, engine, version, system_os=None, cpu=None, background=False):
|
def download_engine(cls, engine, version, system_os=None, cpu=None, background=False, ignore_system=False):
|
||||||
|
|
||||||
engine_to_download = cls.engine_with_name(engine)
|
engine_to_download = cls.engine_with_name(engine)
|
||||||
existing_task = cls.get_existing_download_task(engine, version, system_os, cpu)
|
existing_task = cls.get_existing_download_task(engine, version, system_os, cpu)
|
||||||
@@ -172,10 +189,10 @@ class EngineManager:
|
|||||||
logger.debug(f"Already downloading {engine} {version}")
|
logger.debug(f"Already downloading {engine} {version}")
|
||||||
if not background:
|
if not background:
|
||||||
existing_task.join() # If download task exists, wait until it's done downloading
|
existing_task.join() # If download task exists, wait until it's done downloading
|
||||||
return
|
return None
|
||||||
elif not engine_to_download.downloader():
|
elif not engine_to_download.downloader():
|
||||||
logger.warning("No valid downloader for this engine. Please update this software manually.")
|
logger.warning("No valid downloader for this engine. Please update this software manually.")
|
||||||
return
|
return None
|
||||||
elif not cls.engines_path:
|
elif not cls.engines_path:
|
||||||
raise FileNotFoundError("Engines path must be set before requesting downloads")
|
raise FileNotFoundError("Engines path must be set before requesting downloads")
|
||||||
|
|
||||||
@@ -187,7 +204,7 @@ class EngineManager:
|
|||||||
return thread
|
return thread
|
||||||
|
|
||||||
thread.join()
|
thread.join()
|
||||||
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu) # Check that engine downloaded
|
found_engine = cls.is_version_downloaded(engine, version, system_os, cpu, ignore_system) # Check that engine downloaded
|
||||||
if not found_engine:
|
if not found_engine:
|
||||||
logger.error(f"Error downloading {engine}")
|
logger.error(f"Error downloading {engine}")
|
||||||
return found_engine
|
return found_engine
|
||||||
@@ -213,31 +230,21 @@ class EngineManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_all_engines(cls):
|
def is_engine_update_available(cls, engine_class, ignore_system_installs=False):
|
||||||
def engine_update_task(engine_class):
|
|
||||||
logger.debug(f"Checking for updates to {engine_class.name()}")
|
logger.debug(f"Checking for updates to {engine_class.name()}")
|
||||||
latest_version = engine_class.downloader().find_most_recent_version()
|
latest_version = engine_class.downloader().find_most_recent_version()
|
||||||
|
|
||||||
if not latest_version:
|
if not latest_version:
|
||||||
logger.warning(f"Could not find most recent version of {engine.name()} to download")
|
logger.warning(f"Could not find most recent version of {engine_class.name()} to download")
|
||||||
return
|
return None
|
||||||
|
|
||||||
version_num = latest_version.get('version')
|
version_num = latest_version.get('version')
|
||||||
if cls.is_version_downloaded(engine_class.name(), version_num):
|
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")
|
logger.debug(f"Latest version of {engine_class.name()} ({version_num}) already downloaded")
|
||||||
return
|
return None
|
||||||
|
|
||||||
# download the engine
|
return latest_version
|
||||||
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
|
@classmethod
|
||||||
def create_worker(cls, engine_name, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
|
def create_worker(cls, engine_name, input_path, output_path, engine_version=None, args=None, parent=None, name=None):
|
||||||
@@ -303,18 +310,23 @@ class EngineDownloadWorker(threading.Thread):
|
|||||||
self.version = version
|
self.version = version
|
||||||
self.system_os = system_os
|
self.system_os = system_os
|
||||||
self.cpu = cpu
|
self.cpu = cpu
|
||||||
|
self.percent_complete = 0
|
||||||
|
|
||||||
|
def _update_progress(self, current_progress):
|
||||||
|
self.percent_complete = current_progress
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu)
|
existing_download = EngineManager.is_version_downloaded(self.engine, self.version, self.system_os, self.cpu,
|
||||||
|
ignore_system=True)
|
||||||
if existing_download:
|
if existing_download:
|
||||||
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
|
logger.info(f"Requested download of {self.engine} {self.version}, but local copy already exists")
|
||||||
return existing_download
|
return existing_download
|
||||||
|
|
||||||
# Get the appropriate downloader class based on the engine type
|
# Get the appropriate downloader class based on the engine type
|
||||||
EngineManager.engine_with_name(self.engine).downloader().download_engine(
|
downloader = EngineManager.engine_with_name(self.engine).downloader()
|
||||||
self.version, download_location=EngineManager.engines_path, system_os=self.system_os, cpu=self.cpu,
|
downloader.download_engine( self.version, download_location=EngineManager.engines_path,
|
||||||
timeout=300)
|
system_os=self.system_os, cpu=self.cpu, timeout=300, progress_callback=self._update_progress)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in download worker: {e}")
|
logger.error(f"Error in download worker: {e}")
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class FFMPEGDownloader(EngineDownloader):
|
|||||||
'windows': cls.__get_windows_versions}
|
'windows': cls.__get_windows_versions}
|
||||||
if not versions_per_os.get(system_os):
|
if not versions_per_os.get(system_os):
|
||||||
logger.error(f"Cannot find version list for {system_os}")
|
logger.error(f"Cannot find version list for {system_os}")
|
||||||
return
|
return None
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
all_versions = versions_per_os[system_os]()
|
all_versions = versions_per_os[system_os]()
|
||||||
@@ -144,7 +144,7 @@ class FFMPEGDownloader(EngineDownloader):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120):
|
def download_engine(cls, version, download_location, system_os=None, cpu=None, timeout=120, progress_callback=None):
|
||||||
system_os = system_os or current_system_os()
|
system_os = system_os or current_system_os()
|
||||||
cpu = cpu or current_system_cpu()
|
cpu = cpu or current_system_cpu()
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ class FFMPEGDownloader(EngineDownloader):
|
|||||||
found_version = [item for item in cls.all_versions(system_os, cpu) if item['version'] == version]
|
found_version = [item for item in cls.all_versions(system_os, cpu) if item['version'] == version]
|
||||||
if not found_version:
|
if not found_version:
|
||||||
logger.error(f"Cannot find FFMPEG version {version} for {system_os} and {cpu}")
|
logger.error(f"Cannot find FFMPEG version {version} for {system_os} and {cpu}")
|
||||||
return
|
return None
|
||||||
|
|
||||||
# Platform specific naming cleanup
|
# Platform specific naming cleanup
|
||||||
remote_url = cls.__get_remote_url_for_version(version=version, system_os=system_os, cpu=cpu)
|
remote_url = cls.__get_remote_url_for_version(version=version, system_os=system_os, cpu=cpu)
|
||||||
@@ -162,7 +162,8 @@ class FFMPEGDownloader(EngineDownloader):
|
|||||||
# Download and extract
|
# Download and extract
|
||||||
try:
|
try:
|
||||||
logger.info(f"Requesting download of ffmpeg-{version}-{system_os}-{cpu}")
|
logger.info(f"Requesting download of ffmpeg-{version}-{system_os}-{cpu}")
|
||||||
cls.download_and_extract_app(remote_url=remote_url, download_location=download_location, timeout=timeout)
|
cls.download_and_extract_app(remote_url=remote_url, download_location=download_location, timeout=timeout,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
|
||||||
# naming cleanup to match existing naming convention
|
# naming cleanup to match existing naming convention
|
||||||
output_path = os.path.join(download_location, f'ffmpeg-{version}-{system_os}-{cpu}')
|
output_path = os.path.join(download_location, f'ffmpeg-{version}-{system_os}-{cpu}')
|
||||||
|
|||||||
@@ -128,18 +128,18 @@ class EngineBrowserWindow(QMainWindow):
|
|||||||
self.launch_button.setEnabled(is_localhost(self.hostname))
|
self.launch_button.setEnabled(is_localhost(self.hostname))
|
||||||
|
|
||||||
def update_download_status(self):
|
def update_download_status(self):
|
||||||
running_tasks = [x for x in EngineManager.download_tasks if x.is_alive()]
|
running_tasks = EngineManager.active_downloads()
|
||||||
hide_progress = not bool(running_tasks)
|
hide_progress = not bool(running_tasks)
|
||||||
self.progress_bar.setHidden(hide_progress)
|
self.progress_bar.setHidden(hide_progress)
|
||||||
self.progress_label.setHidden(hide_progress)
|
self.progress_label.setHidden(hide_progress)
|
||||||
# Update the status labels
|
# Update the status labels
|
||||||
if len(EngineManager.download_tasks) == 0:
|
if len(running_tasks) == 0:
|
||||||
new_status = ""
|
new_status = ""
|
||||||
elif len(EngineManager.download_tasks) == 1:
|
elif len(running_tasks) == 1:
|
||||||
task = EngineManager.download_tasks[0]
|
task = running_tasks[0]
|
||||||
new_status = f"Downloading {task.engine.capitalize()} {task.version}..."
|
new_status = f"Downloading {task.engine.capitalize()} {task.version}..."
|
||||||
else:
|
else:
|
||||||
new_status = f"Downloading {len(EngineManager.download_tasks)} engines..."
|
new_status = f"Downloading {len(running_tasks)} engines..."
|
||||||
self.progress_label.setText(new_status)
|
self.progress_label.setText(new_status)
|
||||||
|
|
||||||
def launch_button_click(self):
|
def launch_button_click(self):
|
||||||
|
|||||||
@@ -91,12 +91,13 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.setup_ui(main_layout)
|
self.setup_ui(main_layout)
|
||||||
|
|
||||||
self.create_toolbars()
|
|
||||||
|
|
||||||
# Add Widgets to Window
|
# Add Widgets to Window
|
||||||
|
# self.custom_menu_bar =
|
||||||
self.setMenuBar(MenuBar(self))
|
self.setMenuBar(MenuBar(self))
|
||||||
self.setStatusBar(StatusBar(self))
|
self.setStatusBar(StatusBar(self))
|
||||||
|
|
||||||
|
self.create_toolbars()
|
||||||
|
|
||||||
# start background update
|
# start background update
|
||||||
self.bg_update_thread = QThread()
|
self.bg_update_thread = QThread()
|
||||||
self.bg_update_thread.run = self.__background_update
|
self.bg_update_thread.run = self.__background_update
|
||||||
@@ -504,9 +505,9 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Top Toolbar Buttons
|
# Top Toolbar Buttons
|
||||||
self.topbar.add_button(
|
self.topbar.add_button(
|
||||||
"Console", f"{resources_directory}/Console.png", self.open_console_window)
|
"Settings", f"{resources_directory}/Gear.png", self.menuBar().show_settings)
|
||||||
self.topbar.add_button(
|
self.topbar.add_button(
|
||||||
"Engines", f"{resources_directory}/SoftwareInstaller.png", self.engine_browser)
|
"Console", f"{resources_directory}/Console.png", self.open_console_window)
|
||||||
self.topbar.add_separator()
|
self.topbar.add_separator()
|
||||||
self.topbar.add_button(
|
self.topbar.add_button(
|
||||||
"Stop Job", f"{resources_directory}/StopSign.png", self.stop_job)
|
"Stop Job", f"{resources_directory}/StopSign.png", self.stop_job)
|
||||||
|
|||||||
554
src/ui/settings_window.py
Normal file
554
src/ui/settings_window.py
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
import os
|
||||||
|
import socket
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import humanize
|
||||||
|
from PyQt6 import QtCore
|
||||||
|
from PyQt6.QtCore import Qt, QSettings, pyqtSignal as Signal, QThread, pyqtSignal, QTimer
|
||||||
|
from PyQt6.QtGui import QIcon
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QListWidget, QListWidgetItem, QStackedWidget, QVBoxLayout, \
|
||||||
|
QWidget, QLabel, QCheckBox, QLineEdit, \
|
||||||
|
QPushButton, QHBoxLayout, QGroupBox, QTableWidget, QAbstractItemView, QTableWidgetItem, QHeaderView, \
|
||||||
|
QMessageBox, QProgressBar
|
||||||
|
|
||||||
|
from src.api.server_proxy import RenderServerProxy
|
||||||
|
from src.engines.engine_manager import EngineManager
|
||||||
|
from src.utilities.config import Config
|
||||||
|
from src.utilities.misc_helper import launch_url, system_safe_path
|
||||||
|
from src.version import APP_AUTHOR, APP_NAME
|
||||||
|
|
||||||
|
settings = QSettings(APP_AUTHOR, APP_NAME)
|
||||||
|
|
||||||
|
class GetEngineInfoWorker(QThread):
|
||||||
|
"""
|
||||||
|
The GetEngineInfoWorker class fetches engine information from a server in a background thread.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
done: A signal emitted when the engine information is retrieved.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
run(self): Fetches engine information from the server.
|
||||||
|
"""
|
||||||
|
done = pyqtSignal(object) # emits the result when finished
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
data = RenderServerProxy(socket.gethostname()).get_engine_info()
|
||||||
|
self.done.emit(data)
|
||||||
|
|
||||||
|
class SettingsWindow(QMainWindow):
|
||||||
|
"""
|
||||||
|
The SettingsWindow class provides a user interface for managing engine settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.engine_download_progress_bar = None
|
||||||
|
self.engines_last_update_label = None
|
||||||
|
self.check_for_engine_updates_checkbox = None
|
||||||
|
self.delete_engine_button = None
|
||||||
|
self.launch_engine_button = None
|
||||||
|
self.show_password_button = None
|
||||||
|
self.network_password_line = None
|
||||||
|
self.enable_network_password_checkbox = None
|
||||||
|
self.check_for_new_engines_button = None
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# timers for background download UI updates
|
||||||
|
self.timer = QTimer(self)
|
||||||
|
self.timer.timeout.connect(self.update_engine_download_status)
|
||||||
|
|
||||||
|
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, type=bool))
|
||||||
|
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 = Path(Config.upload_folder).expanduser()
|
||||||
|
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 Engine 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", False, type=bool))
|
||||||
|
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", False, type=bool))
|
||||||
|
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", False, type=bool))
|
||||||
|
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, type=bool))
|
||||||
|
enable_sharing_checkbox.stateChanged.connect(self.toggle_render_sharing)
|
||||||
|
sharing_layout.addWidget(enable_sharing_checkbox)
|
||||||
|
|
||||||
|
password_enabled = (settings.value("enable_network_sharing", False, type=bool) and
|
||||||
|
settings.value("enable_network_password", False, type=bool))
|
||||||
|
|
||||||
|
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, type=bool))
|
||||||
|
self.enable_network_password_checkbox.stateChanged.connect(self.enable_network_password_changed)
|
||||||
|
self.enable_network_password_checkbox.setEnabled(settings.value("enable_network_sharing", False, type=bool))
|
||||||
|
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(password_enabled)
|
||||||
|
password_layout.addWidget(self.network_password_line)
|
||||||
|
self.show_password_button = QPushButton("Show")
|
||||||
|
self.show_password_button.setEnabled(password_enabled)
|
||||||
|
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, type=bool)
|
||||||
|
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.Password)
|
||||||
|
|
||||||
|
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, type=bool))
|
||||||
|
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, type=bool)
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.check_for_engine_updates_checkbox = QCheckBox("Check for new versions on launch")
|
||||||
|
self.check_for_engine_updates_checkbox.setChecked(settings.value('check_for_engine_updates_on_launch', True, type=bool))
|
||||||
|
self.check_for_engine_updates_checkbox.setEnabled(at_least_one_downloadable)
|
||||||
|
self.check_for_engine_updates_checkbox.stateChanged.connect(
|
||||||
|
lambda state: settings.setValue("check_for_engine_updates_on_launch", bool(state)))
|
||||||
|
engine_updates_layout.addWidget(self.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.engine_download_progress_bar = QProgressBar()
|
||||||
|
engine_updates_layout.addWidget(self.engine_download_progress_bar)
|
||||||
|
self.engine_download_progress_bar.setHidden(True)
|
||||||
|
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_engines_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, type=bool)
|
||||||
|
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'))
|
||||||
|
self.installed_engines_table.update_engines_table(use_cached=False)
|
||||||
|
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)
|
||||||
|
|
||||||
|
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, type=bool)
|
||||||
|
messagebox_shown = False
|
||||||
|
for engine in EngineManager.downloadable_engines():
|
||||||
|
if settings.value(f'engine_download-{engine.name()}', False, type=bool):
|
||||||
|
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.engine_download_progress_bar.setHidden(False)
|
||||||
|
self.engine_download_progress_bar.setValue(0)
|
||||||
|
self.engine_download_progress_bar.setMaximum(100)
|
||||||
|
self.check_for_new_engines_button.setEnabled(False)
|
||||||
|
self.timer.start(1000)
|
||||||
|
|
||||||
|
if not messagebox_shown:
|
||||||
|
msg_box = QMessageBox()
|
||||||
|
msg_box.setWindowTitle("No Updates Available")
|
||||||
|
msg_box.setText("No Updates Available.")
|
||||||
|
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 = EngineManager.active_downloads()
|
||||||
|
if not running_tasks:
|
||||||
|
self.timer.stop()
|
||||||
|
self.engine_download_progress_bar.setHidden(True)
|
||||||
|
self.installed_engines_table.update_engines_table(use_cached=False)
|
||||||
|
self.update_last_checked_label()
|
||||||
|
self.check_for_new_engines_button.setEnabled(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
percent_complete = int(running_tasks[0].percent_complete * 100)
|
||||||
|
self.engine_download_progress_bar.setValue(percent_complete)
|
||||||
|
if percent_complete == 100:
|
||||||
|
status_update = f"Installing {running_tasks[0].engine.capitalize()} {running_tasks[0].version}..."
|
||||||
|
else:
|
||||||
|
status_update = f"Downloading {running_tasks[0].engine.capitalize()} {running_tasks[0].version}..."
|
||||||
|
self.engines_last_update_label.setText(status_update)
|
||||||
|
|
||||||
|
|
||||||
|
class EngineTableWidget(QWidget):
|
||||||
|
"""
|
||||||
|
The EngineTableWidget class displays a table of installed engines.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
table: A table widget displaying engine information.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
on_selection_changed(self): Emits a signal when the user selects a different row in the table.
|
||||||
|
"""
|
||||||
|
|
||||||
|
row_selected = Signal()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.__get_engine_info_worker = None
|
||||||
|
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_engines_table()
|
||||||
|
super().showEvent(event) # Ensure normal event processing
|
||||||
|
|
||||||
|
def engine_data_ready(self, raw_server_data):
|
||||||
|
self.raw_server_data = raw_server_data
|
||||||
|
self.update_engines_table()
|
||||||
|
|
||||||
|
def update_engines_table(self, use_cached=True):
|
||||||
|
if not self.raw_server_data or not use_cached:
|
||||||
|
self.__get_engine_info_worker = GetEngineInfoWorker(self)
|
||||||
|
self.__get_engine_info_worker.done.connect(self.engine_data_ready)
|
||||||
|
self.__get_engine_info_worker.start()
|
||||||
|
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, type=bool):
|
||||||
|
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()
|
||||||
@@ -14,6 +14,8 @@ class MenuBar(QMenuBar):
|
|||||||
def __init__(self, parent=None) -> None:
|
def __init__(self, parent=None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.settings_window = None
|
||||||
|
|
||||||
# setup menus
|
# setup menus
|
||||||
file_menu = self.addMenu("File")
|
file_menu = self.addMenu("File")
|
||||||
# edit_menu = self.addMenu("Edit")
|
# edit_menu = self.addMenu("Edit")
|
||||||
@@ -30,7 +32,7 @@ class MenuBar(QMenuBar):
|
|||||||
settings_action = QAction("Settings...", self)
|
settings_action = QAction("Settings...", self)
|
||||||
settings_action.triggered.connect(self.show_settings)
|
settings_action.triggered.connect(self.show_settings)
|
||||||
settings_action.setShortcut(f'Ctrl+,')
|
settings_action.setShortcut(f'Ctrl+,')
|
||||||
# file_menu.addAction(settings_action) # todo: enable once we have a setting screen
|
file_menu.addAction(settings_action)
|
||||||
# exit
|
# exit
|
||||||
exit_action = QAction('&Exit', self)
|
exit_action = QAction('&Exit', self)
|
||||||
exit_action.setShortcut('Ctrl+Q')
|
exit_action.setShortcut('Ctrl+Q')
|
||||||
@@ -49,7 +51,9 @@ class MenuBar(QMenuBar):
|
|||||||
self.parent().new_job()
|
self.parent().new_job()
|
||||||
|
|
||||||
def show_settings(self):
|
def show_settings(self):
|
||||||
pass
|
from src.ui.settings_window import SettingsWindow
|
||||||
|
self.settings_window = SettingsWindow()
|
||||||
|
self.settings_window.show()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_about():
|
def show_about():
|
||||||
|
|||||||
@@ -35,12 +35,13 @@ class StatusBar(QStatusBar):
|
|||||||
try:
|
try:
|
||||||
# update status label - get download status
|
# update status label - get download status
|
||||||
new_status = proxy.status()
|
new_status = proxy.status()
|
||||||
if EngineManager.download_tasks:
|
active_downloads = EngineManager.active_downloads()
|
||||||
if len(EngineManager.download_tasks) == 1:
|
if active_downloads:
|
||||||
task = EngineManager.download_tasks[0]
|
if len(active_downloads) == 1:
|
||||||
|
task = active_downloads[0]
|
||||||
new_status = f"{new_status} | Downloading {task.engine.capitalize()} {task.version}..."
|
new_status = f"{new_status} | Downloading {task.engine.capitalize()} {task.version}..."
|
||||||
else:
|
else:
|
||||||
new_status = f"{new_status} | Downloading {len(EngineManager.download_tasks)} engines"
|
new_status = f"{new_status} | Downloading {len(active_downloads)} engines"
|
||||||
self.messageLabel.setText(new_status)
|
self.messageLabel.setText(new_status)
|
||||||
|
|
||||||
# update status image
|
# update status image
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import shutil
|
|||||||
import socket
|
import socket
|
||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
@@ -139,6 +140,40 @@ def current_system_cpu():
|
|||||||
# convert all x86 64 to "x64"
|
# convert all x86 64 to "x64"
|
||||||
return platform.machine().lower().replace('amd64', 'x64').replace('x86_64', 'x64')
|
return platform.machine().lower().replace('amd64', 'x64').replace('x86_64', 'x64')
|
||||||
|
|
||||||
|
def current_system_cpu_brand():
|
||||||
|
"""Fast cross-platform CPU brand string"""
|
||||||
|
if sys.platform.startswith('darwin'): # macOS
|
||||||
|
try:
|
||||||
|
return subprocess.check_output(['sysctl', '-n', 'machdep.cpu.brand_string']).decode().strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif sys.platform.startswith('win'): # Windows
|
||||||
|
from winreg import HKEY_LOCAL_MACHINE, OpenKey, QueryValueEx
|
||||||
|
try:
|
||||||
|
# Open the registry key where Windows stores the CPU name
|
||||||
|
key = OpenKey(HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0")
|
||||||
|
# The value name is "ProcessorNameString"
|
||||||
|
value, _ = QueryValueEx(key, "ProcessorNameString")
|
||||||
|
return value.strip() # Usually perfect, with full marketing name
|
||||||
|
except Exception:
|
||||||
|
# Fallback: sometimes the key is under a different index, try 1
|
||||||
|
try:
|
||||||
|
key = OpenKey(HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\1")
|
||||||
|
value, _ = QueryValueEx(key, "ProcessorNameString")
|
||||||
|
return value.strip()
|
||||||
|
except Exception:
|
||||||
|
return "Unknown CPU"
|
||||||
|
elif sys.platform.startswith('linux'):
|
||||||
|
try:
|
||||||
|
with open('/proc/cpuinfo') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('model name'):
|
||||||
|
return line.split(':', 1)[1].strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Ultimate fallback
|
||||||
|
return platform.processor() or 'Unknown CPU'
|
||||||
|
|
||||||
def resources_dir():
|
def resources_dir():
|
||||||
resource_environment_path = os.environ.get('RESOURCEPATH', None)
|
resource_environment_path = os.environ.get('RESOURCEPATH', None)
|
||||||
@@ -284,7 +319,11 @@ def get_gpu_info():
|
|||||||
def get_macos_gpu_info():
|
def get_macos_gpu_info():
|
||||||
"""Get GPU info on macOS (works with Apple Silicon)"""
|
"""Get GPU info on macOS (works with Apple Silicon)"""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(['system_profiler', 'SPDisplaysDataType', '-json'],
|
if current_system_cpu() == "arm64":
|
||||||
|
# don't bother with system_profiler with Apple ARM - we know its integrated
|
||||||
|
return [{'name': current_system_cpu_brand(), 'memory': 'Integrated'}]
|
||||||
|
|
||||||
|
result = subprocess.run(['system_profiler', 'SPDisplaysDataType', '-detailLevel', 'mini', '-json'],
|
||||||
capture_output=True, text=True, timeout=5)
|
capture_output=True, text=True, timeout=5)
|
||||||
data = json.loads(result.stdout)
|
data = json.loads(result.stdout)
|
||||||
|
|
||||||
@@ -296,7 +335,7 @@ def get_gpu_info():
|
|||||||
'name': display.get('sppci_model', 'Unknown GPU'),
|
'name': display.get('sppci_model', 'Unknown GPU'),
|
||||||
'memory': display.get('sppci_vram', 'Integrated'),
|
'memory': display.get('sppci_vram', 'Integrated'),
|
||||||
})
|
})
|
||||||
return gpus if gpus else [{'name': 'Apple Silicon GPU', 'memory': 'Integrated'}]
|
return gpus if gpus else [{'name': 'Apple GPU', 'memory': 'Integrated'}]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to get macOS GPU info: {e}")
|
print(f"Failed to get macOS GPU info: {e}")
|
||||||
return [{'name': 'Unknown GPU', 'memory': 'Unknown'}]
|
return [{'name': 'Unknown GPU', 'memory': 'Unknown'}]
|
||||||
|
|||||||
Reference in New Issue
Block a user