Add profile module.
This commit is contained in:
@@ -5,25 +5,39 @@ from typing import Callable
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import QSize
|
||||
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
|
||||
|
||||
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 pages.account import AccountPage
|
||||
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/"
|
||||
|
||||
class ToolBar(QtWidgets.QToolBar):
|
||||
def __init__(self, app: QApplication, parent=None):
|
||||
super(ToolBar, self).__init__(parent)
|
||||
self.app = app
|
||||
self.spacer = QWidget()
|
||||
self.orientationChanged.connect(self._update_widget_size)
|
||||
|
||||
def _update_widget_size(self, orientation):
|
||||
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):
|
||||
if vertical:
|
||||
button.setToolButtonStyle(
|
||||
@@ -140,9 +154,13 @@ class Launcher(QApplication):
|
||||
# test page
|
||||
self.test_page = TestPage(self, self.main_window)
|
||||
|
||||
# Account page
|
||||
self.account_page = AccountPage(self, self.main_window)
|
||||
|
||||
# Add page
|
||||
self.central.addWidget(self.home_page)
|
||||
self.central.addWidget(self.test_page)
|
||||
self.central.addWidget(self.account_page)
|
||||
|
||||
# Bind toolbar and center widget
|
||||
self.main_window.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar)
|
||||
@@ -155,23 +173,29 @@ class Launcher(QApplication):
|
||||
# Toolbar
|
||||
|
||||
# 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")
|
||||
|
||||
# Home btn
|
||||
self.home_button = self.toolbar.add_button(icon=QIcon(Path(self.icon_dir, "home.png").as_posix()))
|
||||
self.home_button.clicked.connect(
|
||||
lambda: self.central.setCurrentWidget(self.home_page)
|
||||
)
|
||||
self.home_button = self.toolbar.add_button(icon=self.get_icon(Path(self.icon_dir, "home.png")))
|
||||
|
||||
# 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
|
||||
self.toolbar.addWidget(self.home_button)
|
||||
self.toolbar.addSeparator()
|
||||
self.toolbar.addWidget(self.open_test_page)
|
||||
self.toolbar.setMovable(True)
|
||||
self.toolbar.setFloatable(False)
|
||||
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(
|
||||
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)
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
def exec(self):
|
||||
@@ -191,6 +219,55 @@ class Launcher(QApplication):
|
||||
self.central.setCurrentWidget(page)
|
||||
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
|
||||
def temp_dir(self):
|
||||
return self.work_dir / "temp"
|
||||
|
||||
@@ -200,7 +200,7 @@ def download_file(file: FileObject, overwrite=True, progress_callback: Callable[
|
||||
|
||||
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) \
|
||||
-> tuple[list[FileObject], list[FileObject]]:
|
||||
"""
|
||||
|
||||
@@ -75,3 +75,15 @@ class RuntimeManifestFetchException(LauncherException):
|
||||
|
||||
class RuntimeManifestDataException(LauncherException):
|
||||
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],
|
||||
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]:
|
||||
"""
|
||||
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.QtGui import QScreen, QPixmap, QPainter, QPalette, QIcon
|
||||
from PySide6.QtWidgets import QMainWindow, QApplication
|
||||
from PySide6.QtGui import QScreen, QPixmap, QPainter, QPalette, QIcon, QColor
|
||||
from PySide6.QtWidgets import QMainWindow, QApplication, QWidget
|
||||
|
||||
|
||||
def move_window_to_center(window: QMainWindow):
|
||||
@@ -14,6 +16,7 @@ def is_main_thread() -> bool:
|
||||
app = QCoreApplication.instance()
|
||||
return app is not None and QThread.currentThread() == app.thread()
|
||||
|
||||
# ======================== AI generated ========================
|
||||
def recolor_standard_icon(widget, standard_pixmap, size=24):
|
||||
source_icon = widget.style().standardIcon(standard_pixmap)
|
||||
source = source_icon.pixmap(QSize(size, size))
|
||||
@@ -33,3 +36,57 @@ def recolor_standard_icon(widget, standard_pixmap, size=24):
|
||||
painter.end()
|
||||
|
||||
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