Add profile module.
This commit is contained in:
@@ -5,25 +5,39 @@ from typing import Callable
|
|||||||
from PySide6 import QtWidgets
|
from PySide6 import QtWidgets
|
||||||
from PySide6.QtCore import QSize
|
from PySide6.QtCore import QSize
|
||||||
from PySide6.QtGui import QIcon, Qt, QPixmap
|
from PySide6.QtGui import QIcon, Qt, QPixmap
|
||||||
from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton, QStyle, QToolButton, \
|
from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QStyle, QToolButton, \
|
||||||
QStackedWidget
|
QStackedWidget
|
||||||
|
|
||||||
from core_lib.qt.hack import move_window_to_center, recolor_standard_icon
|
from core_lib.qt.hack import move_window_to_center, is_light_theme, \
|
||||||
|
recolor_icon
|
||||||
from core_lib.qt import messagebox
|
from core_lib.qt import messagebox
|
||||||
|
from pages.account import AccountPage
|
||||||
from pages.test import TestPage
|
from pages.test import TestPage
|
||||||
|
|
||||||
LAUNCHER_VERSION = "alpha_0.0.1"
|
LAUNCHER_VERSION = "alpha_0.0.2"
|
||||||
MAIN_REPO_URL = "https://repo.weispace.net/wei/Launcher/"
|
MAIN_REPO_URL = "https://repo.weispace.net/wei/Launcher/"
|
||||||
|
|
||||||
class ToolBar(QtWidgets.QToolBar):
|
class ToolBar(QtWidgets.QToolBar):
|
||||||
def __init__(self, app: QApplication, parent=None):
|
def __init__(self, app: QApplication, parent=None):
|
||||||
super(ToolBar, self).__init__(parent)
|
super(ToolBar, self).__init__(parent)
|
||||||
self.app = app
|
self.app = app
|
||||||
|
self.spacer = QWidget()
|
||||||
self.orientationChanged.connect(self._update_widget_size)
|
self.orientationChanged.connect(self._update_widget_size)
|
||||||
|
|
||||||
def _update_widget_size(self, orientation):
|
def _update_widget_size(self, orientation):
|
||||||
vertical = orientation == Qt.Orientation.Vertical
|
vertical = orientation == Qt.Orientation.Vertical
|
||||||
|
|
||||||
|
if vertical:
|
||||||
|
self.spacer.setSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Expanding,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.spacer.setSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Expanding,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
)
|
||||||
|
|
||||||
for button in self.findChildren(QToolButton):
|
for button in self.findChildren(QToolButton):
|
||||||
if vertical:
|
if vertical:
|
||||||
button.setToolButtonStyle(
|
button.setToolButtonStyle(
|
||||||
@@ -140,9 +154,13 @@ class Launcher(QApplication):
|
|||||||
# test page
|
# test page
|
||||||
self.test_page = TestPage(self, self.main_window)
|
self.test_page = TestPage(self, self.main_window)
|
||||||
|
|
||||||
|
# Account page
|
||||||
|
self.account_page = AccountPage(self, self.main_window)
|
||||||
|
|
||||||
# Add page
|
# Add page
|
||||||
self.central.addWidget(self.home_page)
|
self.central.addWidget(self.home_page)
|
||||||
self.central.addWidget(self.test_page)
|
self.central.addWidget(self.test_page)
|
||||||
|
self.central.addWidget(self.account_page)
|
||||||
|
|
||||||
# Bind toolbar and center widget
|
# Bind toolbar and center widget
|
||||||
self.main_window.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar)
|
self.main_window.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar)
|
||||||
@@ -155,23 +173,29 @@ class Launcher(QApplication):
|
|||||||
# Toolbar
|
# Toolbar
|
||||||
|
|
||||||
# Test Window btn
|
# Test Window btn
|
||||||
self.open_test_page = self.toolbar.add_button(" Tests", icon=QIcon(Path(self.icon_dir, "debug.png").as_posix()))
|
self.open_test_page = self.toolbar.add_button(" Tests", icon=self.get_icon(Path(self.icon_dir, "debug.png")))
|
||||||
self.open_test_page.setObjectName("open_test_page")
|
self.open_test_page.setObjectName("open_test_page")
|
||||||
|
|
||||||
# Home btn
|
# Home btn
|
||||||
self.home_button = self.toolbar.add_button(icon=QIcon(Path(self.icon_dir, "home.png").as_posix()))
|
self.home_button = self.toolbar.add_button(icon=self.get_icon(Path(self.icon_dir, "home.png")))
|
||||||
self.home_button.clicked.connect(
|
|
||||||
lambda: self.central.setCurrentWidget(self.home_page)
|
# Account btn (will be moved to the bottom (or last one item) of the toolbar
|
||||||
)
|
self.open_account_page = self.toolbar.add_button(icon=self.get_icon(Path(self.icon_dir, "head.png")))
|
||||||
|
|
||||||
|
# Profile page
|
||||||
|
|
||||||
# Bind tool buttons
|
# Bind tool buttons
|
||||||
self.toolbar.addWidget(self.home_button)
|
self.toolbar.addWidget(self.home_button)
|
||||||
self.toolbar.addSeparator()
|
|
||||||
self.toolbar.addWidget(self.open_test_page)
|
self.toolbar.addWidget(self.open_test_page)
|
||||||
self.toolbar.setMovable(True)
|
self.toolbar.setMovable(True)
|
||||||
self.toolbar.setFloatable(False)
|
self.toolbar.setFloatable(False)
|
||||||
self.toolbar.setAllowedAreas(Qt.ToolBarArea.AllToolBarAreas)
|
self.toolbar.setAllowedAreas(Qt.ToolBarArea.AllToolBarAreas)
|
||||||
|
|
||||||
|
self.toolbar.addWidget(self.toolbar.spacer)
|
||||||
|
self.toolbar.addSeparator()
|
||||||
|
self.toolbar.addWidget(self.open_account_page)
|
||||||
|
|
||||||
|
# Bind page-related button click event
|
||||||
self.home_button.clicked.connect(
|
self.home_button.clicked.connect(
|
||||||
lambda: self.set_current_page(self.home_page, self.home_button)
|
lambda: self.set_current_page(self.home_page, self.home_button)
|
||||||
)
|
)
|
||||||
@@ -180,6 +204,10 @@ class Launcher(QApplication):
|
|||||||
lambda: self.set_current_page(self.test_page, self.open_test_page)
|
lambda: self.set_current_page(self.test_page, self.open_test_page)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.open_account_page.clicked.connect(
|
||||||
|
lambda: self.set_current_page(self.account_page, self.open_account_page)
|
||||||
|
)
|
||||||
|
|
||||||
self.set_current_page(self.home_page, self.home_button)
|
self.set_current_page(self.home_page, self.home_button)
|
||||||
|
|
||||||
def exec(self):
|
def exec(self):
|
||||||
@@ -191,6 +219,55 @@ class Launcher(QApplication):
|
|||||||
self.central.setCurrentWidget(page)
|
self.central.setCurrentWidget(page)
|
||||||
related_button.setFocus()
|
related_button.setFocus()
|
||||||
|
|
||||||
|
def get_icon(self, path: Path):
|
||||||
|
icon = self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning)
|
||||||
|
if not path.exists() or not path.is_file():
|
||||||
|
messagebox.error(
|
||||||
|
self.main_window,
|
||||||
|
"Resource Error",
|
||||||
|
"Missing icon file: {}".format(path)
|
||||||
|
)
|
||||||
|
|
||||||
|
return icon
|
||||||
|
|
||||||
|
try:
|
||||||
|
if is_light_theme() and path.name.endswith(".png"):
|
||||||
|
icon = recolor_icon(
|
||||||
|
path
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
icon = QIcon(path.as_posix())
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.error(
|
||||||
|
self.main_window,
|
||||||
|
"Resource Error",
|
||||||
|
"Failed to load icon {}: {}".format(path, e)
|
||||||
|
)
|
||||||
|
|
||||||
|
return icon
|
||||||
|
|
||||||
|
def get_picture(self, path: Path, size: QSize, custom_error: str=None) -> QPixmap:
|
||||||
|
picture = self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning).pixmap(size)
|
||||||
|
if not path.exists() or not path.is_file():
|
||||||
|
messagebox.error(
|
||||||
|
self.main_window,
|
||||||
|
"Resource Error",
|
||||||
|
"Missing icon file: {}".format(path) if not custom_error else custom_error
|
||||||
|
)
|
||||||
|
|
||||||
|
return picture
|
||||||
|
|
||||||
|
try:
|
||||||
|
picture = QPixmap(path.as_posix())
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.error(
|
||||||
|
self.main_window,
|
||||||
|
"Resource Error",
|
||||||
|
"Failed to load icon {}: {}".format(path, e)
|
||||||
|
)
|
||||||
|
|
||||||
|
return picture
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temp_dir(self):
|
def temp_dir(self):
|
||||||
return self.work_dir / "temp"
|
return self.work_dir / "temp"
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ def download_file(file: FileObject, overwrite=True, progress_callback: Callable[
|
|||||||
|
|
||||||
return file
|
return file
|
||||||
|
|
||||||
def download_multiple_files(files: List[FileObject], download_callback: Callable[str]=None,
|
def download_multiple_files(files: List[FileObject], download_callback: Callable[[str], None]=None,
|
||||||
max_workers=THREADED_DOWNLOAD_MAX_WORKERS, progress_callback: Callable[str]=None) \
|
max_workers=THREADED_DOWNLOAD_MAX_WORKERS, progress_callback: Callable[str]=None) \
|
||||||
-> tuple[list[FileObject], list[FileObject]]:
|
-> tuple[list[FileObject], list[FileObject]]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -75,3 +75,15 @@ class RuntimeManifestFetchException(LauncherException):
|
|||||||
|
|
||||||
class RuntimeManifestDataException(LauncherException):
|
class RuntimeManifestDataException(LauncherException):
|
||||||
user_message = "Runtime manifest is missing or corrupted."
|
user_message = "Runtime manifest is missing or corrupted."
|
||||||
|
|
||||||
|
class ProfileException(LauncherException):
|
||||||
|
user_message = "An error occurred while processing profile."
|
||||||
|
|
||||||
|
class ProfileSaveException(LauncherException):
|
||||||
|
user_message = "Unable to save profile. Try again later."
|
||||||
|
|
||||||
|
class ProfileLoadException(LauncherException):
|
||||||
|
user_message = "Unable to load profile. Profile may corrupted"
|
||||||
|
|
||||||
|
class ProfileKeyNotFoundException(LauncherException):
|
||||||
|
user_message = "Specified profile key not found. Profile may corrupted."
|
||||||
@@ -223,7 +223,7 @@ def collect_assets_from_manifest(manifest: dict, asset_id: str, assets_dir: Path
|
|||||||
|
|
||||||
def download_assets_and_copy(assets: list[FileObject], legacy_assets: list[dict],
|
def download_assets_and_copy(assets: list[FileObject], legacy_assets: list[dict],
|
||||||
no_hash_check: bool=False, allow_missing=False,
|
no_hash_check: bool=False, allow_missing=False,
|
||||||
progress_callback: Callable[str]=None,
|
progress_callback: Callable[[str], None]=None,
|
||||||
redownload_callback: Callable[[list[FileObject]], tuple[bool, list[FileObject]]]=None) -> list[FileObject]:
|
redownload_callback: Callable[[list[FileObject]], tuple[bool, list[FileObject]]]=None) -> list[FileObject]:
|
||||||
"""
|
"""
|
||||||
Download assets from target version's asset manifest. (Not sure why this function exist, may for compatibly)
|
Download assets from target version's asset manifest. (Not sure why this function exist, may for compatibly)
|
||||||
|
|||||||
@@ -0,0 +1,430 @@
|
|||||||
|
"""
|
||||||
|
This profile module also compatible with official launcher profile format
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core_lib.common.exception import ProfileKeyNotFoundException, ProfileException, ProfileSaveException, \
|
||||||
|
ProfileLoadException
|
||||||
|
|
||||||
|
logger = logging.getLogger("Launcher.CoreLib")
|
||||||
|
|
||||||
|
def get_profile_sample() -> dict:
|
||||||
|
return {
|
||||||
|
"lastPlayedProfileID": None,
|
||||||
|
"profiles": {},
|
||||||
|
"settings": {
|
||||||
|
# Not all settings are included
|
||||||
|
"profileSorting": "ByLastPlayed",
|
||||||
|
"showGameLog": True,
|
||||||
|
},
|
||||||
|
"version": 6
|
||||||
|
}
|
||||||
|
|
||||||
|
class Profile:
|
||||||
|
def __init__(self, version_id: str, created: datetime.datetime,
|
||||||
|
last_used: datetime.datetime | None, name: str="", icon: str | Path="",
|
||||||
|
game_dir: str | Path="", java_args: str="", resolution_raw: dict | None = None, type_raw="custom"):
|
||||||
|
self.version_id = version_id
|
||||||
|
self.created = created
|
||||||
|
self.last_used = last_used
|
||||||
|
self.name = name
|
||||||
|
self.icon: str | Path = icon
|
||||||
|
self.game_dir: Path | str = game_dir
|
||||||
|
self.java_args: str = java_args
|
||||||
|
self.resolution: dict | None = resolution_raw
|
||||||
|
self.type: str = type_raw
|
||||||
|
|
||||||
|
def _get_current_datetime_string() -> str:
|
||||||
|
return datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
|
||||||
|
|
||||||
|
def _create_profile_data(version_id: str, version_type: str, java_args: str= "", game_dir: str= "", icon: str= "", name: str= "", resolution: dict=None) -> dict:
|
||||||
|
content = {
|
||||||
|
"name": name,
|
||||||
|
"created": _get_current_datetime_string(),
|
||||||
|
"gameDir": game_dir,
|
||||||
|
"lastVersionId": version_id,
|
||||||
|
"lastUsed": "",
|
||||||
|
"type": version_type,
|
||||||
|
"javaArgs": java_args,
|
||||||
|
"icon": icon,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(resolution, dict):
|
||||||
|
content["resolution"] = resolution
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def create_profile_data_from_object(p: Profile):
|
||||||
|
created: datetime.datetime | str = p.created
|
||||||
|
last_used: datetime.datetime | str | None = p.last_used
|
||||||
|
|
||||||
|
if isinstance(created, datetime.datetime):
|
||||||
|
created = created.isoformat().replace("+00:00", "Z")
|
||||||
|
elif not isinstance(created, str):
|
||||||
|
raise TypeError("\"created\" key must be a datetime.datetime or str")
|
||||||
|
|
||||||
|
game_dir: str | Path = p.game_dir
|
||||||
|
|
||||||
|
if isinstance(game_dir, Path):
|
||||||
|
game_dir = game_dir.as_posix()
|
||||||
|
|
||||||
|
icon: str | Path = p.icon
|
||||||
|
|
||||||
|
if isinstance(icon, Path):
|
||||||
|
icon = icon.as_posix()
|
||||||
|
|
||||||
|
content = {
|
||||||
|
"name": p.name,
|
||||||
|
"created": created,
|
||||||
|
"gameDir": game_dir,
|
||||||
|
"lastVersionId": p.version_id,
|
||||||
|
"type": p.type,
|
||||||
|
"javaArgs": p.java_args,
|
||||||
|
"icon": icon,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(last_used, datetime.datetime):
|
||||||
|
last_used = last_used.isoformat().replace("+00:00", "Z")
|
||||||
|
elif isinstance(last_used, str):
|
||||||
|
pass
|
||||||
|
elif last_used is not None:
|
||||||
|
raise TypeError("\"last_used\" key must be a datetime.datetime or str")
|
||||||
|
|
||||||
|
if last_used:
|
||||||
|
content["lastUsed"] = last_used
|
||||||
|
|
||||||
|
if isinstance(p.resolution, dict):
|
||||||
|
content["resolution"] = p.resolution
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def create_profile(version_id: str, version_type: str, java_args: str="", game_dir: str="",
|
||||||
|
icon: str="", name: str="", resolution: dict=None, profiles: dict | None=None,
|
||||||
|
max_generate_retry=10) -> tuple[dict, str]:
|
||||||
|
"""
|
||||||
|
Create a new profile with the given arguments
|
||||||
|
:param version_id:
|
||||||
|
:param version_type:
|
||||||
|
:param java_args:
|
||||||
|
:param game_dir:
|
||||||
|
:param icon:
|
||||||
|
:param name:
|
||||||
|
:param resolution:
|
||||||
|
:param profiles: If not given, this function will create a new profiles dict instead
|
||||||
|
:param max_generate_retry:
|
||||||
|
:return:
|
||||||
|
profile: dict
|
||||||
|
new_profile_id: str
|
||||||
|
"""
|
||||||
|
if profiles is None:
|
||||||
|
profiles = get_profile_sample()
|
||||||
|
|
||||||
|
profile = _create_profile_data(version_id, version_type, java_args, game_dir, icon, name, resolution)
|
||||||
|
|
||||||
|
for _ in range(max_generate_retry):
|
||||||
|
profile_id = str(uuid.uuid4().hex)
|
||||||
|
|
||||||
|
if profile_id not in profiles["profiles"]:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ProfileException(
|
||||||
|
"Unable to generate a unique profile ID."
|
||||||
|
)
|
||||||
|
|
||||||
|
profiles["profiles"][profile_id] = profile
|
||||||
|
|
||||||
|
return profile, profile_id
|
||||||
|
|
||||||
|
def create_profile_file(profiles: dict, profile_filepath: Path, profile_file_bak_filepath: Path | None=None,
|
||||||
|
overwrite=True, create_backup=True) -> None:
|
||||||
|
"""
|
||||||
|
Create a new profile
|
||||||
|
:param profiles:
|
||||||
|
:param profile_filepath:
|
||||||
|
:param profile_file_bak_filepath:
|
||||||
|
:param overwrite:
|
||||||
|
:param create_backup:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
profile_filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if profile_filepath.exists() and not overwrite:
|
||||||
|
raise ProfileSaveException(
|
||||||
|
f"Profile file already exists: {profile_filepath}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create backup path
|
||||||
|
if create_backup and profile_file_bak_filepath is None:
|
||||||
|
profile_file_bak_filepath = profile_filepath.with_suffix(
|
||||||
|
profile_filepath.suffix + ".bak"
|
||||||
|
)
|
||||||
|
|
||||||
|
temporary_path = profile_filepath.with_suffix(
|
||||||
|
profile_filepath.suffix + ".tmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Write temp profile file first
|
||||||
|
temporary_path.write_text(
|
||||||
|
json.dumps(profiles, indent=4, ensure_ascii=False),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ProfileSaveException(
|
||||||
|
"Unable to create profile file: {}".format(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if create_backup and profile_filepath.exists():
|
||||||
|
if profile_file_bak_filepath.exists():
|
||||||
|
profile_file_bak_filepath.unlink()
|
||||||
|
|
||||||
|
# Rename old profile with an ".bak" extension
|
||||||
|
os.replace(
|
||||||
|
profile_filepath,
|
||||||
|
profile_file_bak_filepath,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rename new profile file
|
||||||
|
os.replace(temporary_path, profile_filepath)
|
||||||
|
except OSError as exc:
|
||||||
|
if (create_backup and profile_file_bak_filepath is not None and profile_file_bak_filepath.exists()
|
||||||
|
and not profile_filepath.exists()):
|
||||||
|
os.replace(
|
||||||
|
profile_file_bak_filepath,
|
||||||
|
profile_filepath,
|
||||||
|
)
|
||||||
|
raise ProfileSaveException(
|
||||||
|
f"Unable to save profile file: {exc}"
|
||||||
|
) from exc
|
||||||
|
except Exception as e:
|
||||||
|
raise ProfileSaveException(
|
||||||
|
"Unable to replace path from old to new profile: {}".format(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_profile_file(profile_filepath: Path) -> dict:
|
||||||
|
"""
|
||||||
|
Read profile from existing file
|
||||||
|
:param profile_filepath:
|
||||||
|
:return:
|
||||||
|
profiles: dict
|
||||||
|
"""
|
||||||
|
if not profile_filepath.exists():
|
||||||
|
raise ProfileException(
|
||||||
|
"Profile file does not exist: {}".format(profile_filepath)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(str(profile_filepath), "r", encoding="utf-8") as f:
|
||||||
|
return json.loads(f.read())
|
||||||
|
except Exception as e:
|
||||||
|
raise ProfileLoadException(
|
||||||
|
"Unable to read profile file: {}".format(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_profile(profile: dict, fix_wrong=False) -> bool:
|
||||||
|
if not isinstance(profile, dict):
|
||||||
|
return False
|
||||||
|
|
||||||
|
profile_name = profile.get("name")
|
||||||
|
last_version_id = profile.get("lastVersionId", None)
|
||||||
|
|
||||||
|
if not last_version_id or not isinstance(last_version_id, str):
|
||||||
|
return False
|
||||||
|
elif profile_name is None or not isinstance(profile_name, str):
|
||||||
|
return False
|
||||||
|
|
||||||
|
resolution = profile.get("resolution", None)
|
||||||
|
|
||||||
|
if resolution is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not isinstance(resolution, dict):
|
||||||
|
if fix_wrong:
|
||||||
|
profile.pop("resolution", None)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
width = resolution.get("width")
|
||||||
|
height = resolution.get("height")
|
||||||
|
|
||||||
|
if not isinstance(width, int) or isinstance(width, bool) or width <= 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not isinstance(height, int) or isinstance(height, bool) or height <= 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_profiles_valid(profiles: dict, fix_wrong=False, ignore_invalid_profile=False) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a profiles dictionary is valid
|
||||||
|
:param profiles:
|
||||||
|
:param fix_wrong:
|
||||||
|
:param ignore_invalid_profile:
|
||||||
|
:return:
|
||||||
|
filtered_profiles: dict
|
||||||
|
"""
|
||||||
|
if not isinstance(profiles, dict):
|
||||||
|
return False
|
||||||
|
|
||||||
|
profiles_map = profiles.get("profiles", None)
|
||||||
|
settings = profiles.get("settings", None)
|
||||||
|
|
||||||
|
if not isinstance(profiles_map, dict):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not isinstance(settings, dict):
|
||||||
|
return False
|
||||||
|
|
||||||
|
invalid_ids = []
|
||||||
|
|
||||||
|
for profile_id, profile in profiles_map.items():
|
||||||
|
if not _check_profile(profile, fix_wrong=fix_wrong):
|
||||||
|
invalid_ids.append(profile_id)
|
||||||
|
|
||||||
|
if ignore_invalid_profile:
|
||||||
|
for profile_id in invalid_ids:
|
||||||
|
del profiles_map[profile_id]
|
||||||
|
return True
|
||||||
|
|
||||||
|
return not invalid_ids
|
||||||
|
|
||||||
|
def list_profiles(profiles: dict) -> dict:
|
||||||
|
"""
|
||||||
|
List all profiles that inside profiles data
|
||||||
|
:param profiles:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return profiles.get("profiles", {})
|
||||||
|
|
||||||
|
def get_profile(profiles: dict, profile_id: str) -> dict:
|
||||||
|
profile_map = list_profiles(profiles)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return profile_map[profile_id]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise ProfileKeyNotFoundException(
|
||||||
|
f"Specified profile id {profile_id} not found."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
def add_profile(profiles: dict, profile: dict, max_generate_retry=10) -> tuple[dict, str]:
|
||||||
|
"""
|
||||||
|
Add a new profile
|
||||||
|
:param profiles:
|
||||||
|
:param profile:
|
||||||
|
:param max_generate_retry:
|
||||||
|
:return:
|
||||||
|
profiles: dict
|
||||||
|
new_profile_id: str
|
||||||
|
"""
|
||||||
|
for _ in range(max_generate_retry):
|
||||||
|
profile_id = str(uuid.uuid4().hex)
|
||||||
|
|
||||||
|
if profile_id not in profiles["profiles"]:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ProfileException(
|
||||||
|
"Unable to generate a unique profile ID."
|
||||||
|
)
|
||||||
|
|
||||||
|
profiles["profiles"][profile_id] = profile
|
||||||
|
|
||||||
|
return profiles, profile_id
|
||||||
|
|
||||||
|
def remove_profile(profiles: dict, profile_id: str):
|
||||||
|
"""
|
||||||
|
Remove a profile
|
||||||
|
:param profiles:
|
||||||
|
:param profile_id:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
profiles_map = list_profiles(profiles)
|
||||||
|
|
||||||
|
if not profiles_map:
|
||||||
|
raise ProfileException(
|
||||||
|
"No profiles exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
del profiles_map[profile_id]
|
||||||
|
except KeyError:
|
||||||
|
raise ProfileKeyNotFoundException("Specified profile id {} not found.".format(profile_id))
|
||||||
|
|
||||||
|
def duplicate_profile(profiles: dict, profile_id: str, max_generate_retry=10) -> str:
|
||||||
|
"""
|
||||||
|
Duplicate a profile
|
||||||
|
:param profiles:
|
||||||
|
:param profile_id:
|
||||||
|
:param max_generate_retry:
|
||||||
|
:return:
|
||||||
|
new_profile_id: str
|
||||||
|
"""
|
||||||
|
profiles_map = list_profiles(profiles)
|
||||||
|
|
||||||
|
if not profiles_map:
|
||||||
|
raise ProfileException(
|
||||||
|
"No profiles exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
for _ in range(max_generate_retry):
|
||||||
|
new_profile_id = str(uuid.uuid4().hex)
|
||||||
|
|
||||||
|
if new_profile_id not in profiles_map:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ProfileException(
|
||||||
|
"Unable to generate a unique profile ID."
|
||||||
|
)
|
||||||
|
|
||||||
|
for p_id in profiles_map:
|
||||||
|
if p_id == profile_id:
|
||||||
|
profiles["profiles"][new_profile_id] = deepcopy(profiles_map[profile_id])
|
||||||
|
return new_profile_id
|
||||||
|
|
||||||
|
raise ProfileKeyNotFoundException(
|
||||||
|
"Specified profile id {} not found.".format(profile_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def convert_profile_data_to_object(profiles: dict, profile_id: str) -> Profile:
|
||||||
|
profile = get_profile(profiles, profile_id)
|
||||||
|
|
||||||
|
created: str | datetime.datetime = profile.get("created", datetime.datetime.now())
|
||||||
|
last_used: None | str | datetime.datetime = profile.get("lastUsed", None)
|
||||||
|
|
||||||
|
if not isinstance(created, datetime.datetime):
|
||||||
|
try:
|
||||||
|
created = datetime.datetime.fromisoformat(created)
|
||||||
|
except ValueError:
|
||||||
|
created = datetime.datetime.now()
|
||||||
|
|
||||||
|
if isinstance(last_used, str):
|
||||||
|
try:
|
||||||
|
last_used = datetime.datetime.fromisoformat(last_used)
|
||||||
|
except ValueError:
|
||||||
|
last_used = None
|
||||||
|
|
||||||
|
item = Profile(
|
||||||
|
version_id=profile.get("lastVersionId", ""),
|
||||||
|
created=created,
|
||||||
|
last_used=last_used,
|
||||||
|
name=profile.get("name", ""),
|
||||||
|
icon=profile.get("icon", ""),
|
||||||
|
game_dir=profile.get("gameDir", ""),
|
||||||
|
java_args=profile.get("javaArgs", ""),
|
||||||
|
resolution_raw=profile.get("resolution", {}),
|
||||||
|
type_raw=profile.get("type", "custom"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+59
-2
@@ -1,6 +1,8 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QCoreApplication, QThread, QSize, Qt
|
from PySide6.QtCore import QCoreApplication, QThread, QSize, Qt
|
||||||
from PySide6.QtGui import QScreen, QPixmap, QPainter, QPalette, QIcon
|
from PySide6.QtGui import QScreen, QPixmap, QPainter, QPalette, QIcon, QColor
|
||||||
from PySide6.QtWidgets import QMainWindow, QApplication
|
from PySide6.QtWidgets import QMainWindow, QApplication, QWidget
|
||||||
|
|
||||||
|
|
||||||
def move_window_to_center(window: QMainWindow):
|
def move_window_to_center(window: QMainWindow):
|
||||||
@@ -14,6 +16,7 @@ def is_main_thread() -> bool:
|
|||||||
app = QCoreApplication.instance()
|
app = QCoreApplication.instance()
|
||||||
return app is not None and QThread.currentThread() == app.thread()
|
return app is not None and QThread.currentThread() == app.thread()
|
||||||
|
|
||||||
|
# ======================== AI generated ========================
|
||||||
def recolor_standard_icon(widget, standard_pixmap, size=24):
|
def recolor_standard_icon(widget, standard_pixmap, size=24):
|
||||||
source_icon = widget.style().standardIcon(standard_pixmap)
|
source_icon = widget.style().standardIcon(standard_pixmap)
|
||||||
source = source_icon.pixmap(QSize(size, size))
|
source = source_icon.pixmap(QSize(size, size))
|
||||||
@@ -33,3 +36,57 @@ def recolor_standard_icon(widget, standard_pixmap, size=24):
|
|||||||
painter.end()
|
painter.end()
|
||||||
|
|
||||||
return QIcon(result)
|
return QIcon(result)
|
||||||
|
|
||||||
|
def recolor_pixmap(
|
||||||
|
path: str | Path,
|
||||||
|
color: QColor | None = None,
|
||||||
|
) -> QPixmap:
|
||||||
|
source = QPixmap(str(path))
|
||||||
|
|
||||||
|
if source.isNull():
|
||||||
|
raise FileNotFoundError(f"Unable to load image: {path}")
|
||||||
|
|
||||||
|
if color is None:
|
||||||
|
color = QApplication.palette().color(
|
||||||
|
QPalette.ColorRole.ButtonText
|
||||||
|
)
|
||||||
|
|
||||||
|
result = QPixmap(source.size())
|
||||||
|
result.setDevicePixelRatio(source.devicePixelRatio())
|
||||||
|
result.fill(Qt.GlobalColor.transparent)
|
||||||
|
|
||||||
|
painter = QPainter(result)
|
||||||
|
painter.drawPixmap(0, 0, source)
|
||||||
|
painter.setCompositionMode(
|
||||||
|
QPainter.CompositionMode.CompositionMode_SourceIn
|
||||||
|
)
|
||||||
|
painter.fillRect(result.rect(), color)
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def recolor_icon(
|
||||||
|
path: str | Path,
|
||||||
|
color: QColor | None = None,
|
||||||
|
) -> QIcon:
|
||||||
|
return QIcon(recolor_pixmap(path, color))
|
||||||
|
|
||||||
|
def is_light_theme(widget=None) -> bool:
|
||||||
|
palette = (
|
||||||
|
widget.palette()
|
||||||
|
if widget is not None
|
||||||
|
else QApplication.palette()
|
||||||
|
)
|
||||||
|
|
||||||
|
color = palette.color(QPalette.ColorRole.Window)
|
||||||
|
|
||||||
|
luminance = (
|
||||||
|
0.2126 * color.redF()
|
||||||
|
+ 0.7152 * color.greenF()
|
||||||
|
+ 0.0722 * color.blueF()
|
||||||
|
)
|
||||||
|
|
||||||
|
return luminance >= 0.5
|
||||||
|
|
||||||
|
# ======================== AI generated END ========================
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtGui import QPixmap
|
||||||
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
||||||
|
|
||||||
|
|
||||||
|
class AccountPage(QWidget):
|
||||||
|
def __init__(self, app, parent=None):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# center message (will be deleted if launcher finished development)
|
||||||
|
content = """Still digging!"""
|
||||||
|
message = "If you found that something might be a issue. You can report it to the main repo! Any suggestion are welcome."
|
||||||
|
self.icon_label = QLabel()
|
||||||
|
self.icon_label.setObjectName("icon_label")
|
||||||
|
self.dev_info_box = QLabel(content)
|
||||||
|
self.dev_info_box.setObjectName("dev_info_box")
|
||||||
|
self.dev_info_2 = QLabel(message)
|
||||||
|
self.dev_info_2.setObjectName("dev_info_message")
|
||||||
|
|
||||||
|
self.layout.addStretch()
|
||||||
|
self.layout.addWidget(self.icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.layout.addWidget(self.dev_info_box, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
self.layout.addWidget(self.dev_info_2, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
self.layout.addStretch()
|
||||||
|
|
||||||
|
if Path(self.app.resources_dir, "pictures", "in_progress.png").exists():
|
||||||
|
image = QPixmap(Path(self.app.resources_dir, "pictures", "in_progress.png"))
|
||||||
|
self.icon_label.setPixmap(image.scaled(300, 300))
|
||||||
|
else:
|
||||||
|
self.icon_label.setText("Ouch. The icon is missing.")
|
||||||
|
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.app.apply_qss(Path("main.qss"), self.setStyleSheet)
|
||||||
Reference in New Issue
Block a user