diff --git a/app.py b/app.py index 9f38566..d048db0 100644 --- a/app.py +++ b/app.py @@ -11,7 +11,10 @@ from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QW from core_lib.qt.hack import move_window_to_center, is_light_theme, \ recolor_icon from core_lib.qt import messagebox +from core_lib.launcher.object import Launcher as LauncherObject + from pages.account import AccountPage +from pages.create_profile import CreateProfile from pages.test import TestPage LAUNCHER_VERSION = "alpha_0.0.2" @@ -63,7 +66,7 @@ class ToolBar(QtWidgets.QToolBar): button.setToolButtonStyle( Qt.ToolButtonStyle.ToolButtonTextBesideIcon ) - button.setFixedWidth(150) + button.setMinimumWidth(60) def add_button(self, text=None, icon: QIcon=None) -> QToolButton: button = QtWidgets.QToolButton(self) @@ -71,6 +74,8 @@ class ToolBar(QtWidgets.QToolBar): if text: text = " "+text button.setText(text) + # else: + # button.setText("\u00a0") if icon: button.setIcon(icon) @@ -86,6 +91,7 @@ class ToolBar(QtWidgets.QToolBar): ) button.setAutoRaise(True) + button.setFixedHeight(60) return button @@ -93,7 +99,7 @@ class ToolBar(QtWidgets.QToolBar): self._update_widget_size(self.orientation()) self.update() -class Launcher(QApplication): +class Launcher(QApplication, LauncherObject): def __init__(self): super().__init__() # Window config @@ -157,10 +163,14 @@ class Launcher(QApplication): # Account page self.account_page = AccountPage(self, self.main_window) + # Profile page + self.create_profile_page = CreateProfile(self, self.main_window) + # Add page self.central.addWidget(self.home_page) self.central.addWidget(self.test_page) self.central.addWidget(self.account_page) + self.central.addWidget(self.create_profile_page) # Bind toolbar and center widget self.main_window.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar) @@ -182,11 +192,13 @@ class Launcher(QApplication): # 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 + # Profile btn + self.open_create_profile_page = self.toolbar.add_button(icon=self.get_icon(Path(self.icon_dir, "add_temp.png"))) # Bind tool buttons self.toolbar.addWidget(self.home_button) self.toolbar.addWidget(self.open_test_page) + self.toolbar.addWidget(self.open_create_profile_page) self.toolbar.setMovable(True) self.toolbar.setFloatable(False) self.toolbar.setAllowedAreas(Qt.ToolBarArea.AllToolBarAreas) @@ -208,6 +220,10 @@ class Launcher(QApplication): lambda: self.set_current_page(self.account_page, self.open_account_page) ) + self.open_create_profile_page.clicked.connect( + lambda: self.set_current_page(self.create_profile_page, self.open_create_profile_page) + ) + self.set_current_page(self.home_page, self.home_button) def exec(self): diff --git a/core_lib/common/common.py b/core_lib/common/common.py index a65d6ad..9f28578 100644 --- a/core_lib/common/common.py +++ b/core_lib/common/common.py @@ -19,16 +19,18 @@ THREADED_DOWNLOAD_MAX_WORKERS = 10 logger = logging.getLogger("Launcher.CoreLib") class FileObject: - def __init__(self, path: Path, url=None, sha1=None, sha256=None, md5=None, size=None): + def __init__(self, path: Path, url=None, sha1=None, sha256=None, sha512=None, md5=None, size=None): self.__path = path self.expected_sha1 = sha1 self.expected_sha256 = sha256 + self.expected_sha512 = sha512 self.expected_md5 = md5 self.expected_size = size self.__sha1 = None self.__sha256 = None + self.__sha512 = None self.__md5 = None self.url = url @@ -73,6 +75,13 @@ class FileObject: return self.__sha256 + @property + def sha512(self): + if self.__sha512 is None: + self.__sha512 = self._calculate_hash("sha512") + + return self.__sha512 + @property def md5(self): if self.__md5 is None: @@ -86,6 +95,9 @@ class FileObject: def verify_sha256(self, expected_hash=None): return self.sha256 == expected_hash if expected_hash else self.expected_sha256 == self.sha256 + def verify_sha512(self, expected_hash=None): + return self.sha512 == expected_hash if expected_hash else self.expected_sha512 == self.sha512 + def verify_md5(self, expected_hash=None): return self.sha1 == expected_hash if expected_hash else self.expected_sha1 == self.sha1 @@ -96,6 +108,9 @@ class FileObject: if algorithm == "sha256": return self.verify_sha256(expected_hash if expected_hash else self.expected_sha256) + if algorithm == "sha512": + return self.verify_sha512(expected_hash if expected_hash else self.expected_sha512) + if algorithm == "md5": return self.verify_md5(expected_hash if expected_hash else self.expected_md5) diff --git a/core_lib/game/argument.py b/core_lib/game/argument.py index 2c7cb5b..f46e48e 100644 --- a/core_lib/game/argument.py +++ b/core_lib/game/argument.py @@ -1,4 +1,5 @@ import logging +import re import shlex from core_lib.game.library import get_natives_platform_info @@ -229,4 +230,54 @@ def generate_launch_command(arg_maps: ArgumentMappings, main_class: str, java_pa cmd.append(main_class) cmd.extend(parsed_game_args) - return cmd \ No newline at end of file + return cmd + +def is_a_argument(arg: str, startswith="--"): + pattern = re.compile(rf"{startswith}(.+)") + return pattern.match(arg) is not None + +def generate_argument_details(arguments: list[str | dict], startswith="--") -> list[dict]: + items = [] + + for index, arg in enumerate(arguments): + if isinstance(arg, str) and is_a_argument(arg, startswith): + items.append({ + "value": arg, + "type": "argument", + "dataType": "string", + }) + + if index > 0: + previous = items[index - 1] + if previous["type"] == "argument": + previous["isFlag"] = True + elif isinstance(arg, str): + items.append({ + "value": arg, + "type": "value", + }) + elif isinstance(arg, dict): + value = arg.get("value", None) + + if not value: + raise ValueError("Dictionary argument must contain a value") + + items.append({ + "value": value, + "type": "argument", + "dataType": "dictionary", + "dictValue": arg, + }) + + return items + +def is_same_argument(item: dict, another: dict) -> bool: + pass + + + + + + + + diff --git a/core_lib/game/library.py b/core_lib/game/library.py index 351d97b..ee72a7e 100644 --- a/core_lib/game/library.py +++ b/core_lib/game/library.py @@ -4,6 +4,7 @@ import re import zipfile from pathlib import Path from typing import Callable +from urllib.parse import urlsplit, unquote from ..common.common import get_platform_info, FileObject, download_multiple_files, unzip from .version_info import ( @@ -19,6 +20,87 @@ from ..common.exception import UnsupportedPlatformException, NativeResolveExcept logger = logging.getLogger("Launcher.CoreLib") +def parse_maven_coordinate(value: str, default_extension="jar"): + value = value.strip() + + coordinate, separator, extension = value.partition("@") + parts = coordinate.split(":") + + if len(parts) not in (3, 4): + raise ValueError(f"Invalid maven coordinate {value}") + + group, artifact, version = parts[:3] + classifier = parts[3] if len(parts) == 4 else None + + if version.count("-") > 0: + version, extra = version.split("-", 1) + + if not group or not artifact or not version: + raise ValueError(f"Coordinate key can not be empty: {value!r}") + + if not extension: + extension = default_extension + + return group, artifact, version, classifier, extension + +def convert_maven_version(version: str, failback=None) -> tuple[int, int, int]: + try: + print(version) + items = version.split(".") + if len(items) >= 3: + major, minor, patch = items[0], items[1], items[2] + if len(items) > 3: + logger.debug(f"Found version tag: {".".join(items)}") + elif len(items) == 2: + major, minor, patch = 0, items[0], items[1] + else: + raise ValueError(f"Unable to parse version: {version!r}") + + return int(major), int(minor), int(patch) + except ValueError: + return failback + +def generate_filename_from_maven_coordinate(artifact: str, version, classifier="", extension="jar") -> str: + classifier = f"-{classifier}" if classifier else "" + + return ( + f"{artifact}-" + f"{version}" + f"{classifier}." + f"{extension}" + ) + +def generate_repo_path(group: str, artifact: str, version: str, filename) -> str: + group_path = group.replace(".", "/") + + return ( + f"{group_path}/" + f"{artifact}/" + f"{version}/" + f"{filename}" + ) + +def is_complete_maven_url(url: str, group: str, artifact: str, version: str, classifier: str="", extension: str=".jar") -> bool: + filename = generate_filename_from_maven_coordinate(artifact, version, classifier=classifier, extension=extension) + repository_path = generate_repo_path(group, artifact, version, filename) + parsed = urlsplit(url) + + if parsed.scheme not in ("http", "https"): + return False + + path = unquote(parsed.path).replace("\\", "/") + expected_path = "/" + repository_path + + return path.endswith(expected_path) + +def generate_repo_url(maven_url: str, group: str, artifact: str, version: str, classifier="", extension=".jar") -> str: + filename = generate_filename_from_maven_coordinate(artifact, version, classifier=classifier, extension=extension) + path = generate_repo_path(group, artifact, version, filename) + + if not maven_url.endswith("/"): + maven_url += "/" + + return f"{maven_url}{path}" def is_library_allowed(rules, platform_rule_name, platform_arch_type, platform_version): """ diff --git a/core_lib/game/mod/fabric.py b/core_lib/game/mod/fabric.py new file mode 100644 index 0000000..eb54e23 --- /dev/null +++ b/core_lib/game/mod/fabric.py @@ -0,0 +1,155 @@ +import json +import logging + +import requests + +from core_lib.common.exception import VersionManifestFetchException, VersionDataKeyNotFoundException, \ + VersionManifestException + +logger = logging.getLogger("Launcher.CoreLib") + +FABRIC_META_VERSION_ENDPOINT_V2 = "https://meta.fabricmc.net/v2/versions" +FABRIC_META_LOADER_VERSION_ENDPOINT_V2 = "https://meta.fabricmc.net/v1/versions/loader/{}/" +FABRIC_META_LOADER_MANIFEST_ENDPOINT_V2 = "https://meta.fabricmc.net//v2/versions/loader/{}/{}/profile/json" +FABRIC_MAVEN_URL = "https://maven.fabricmc.net/" + +def fetch_fabric_version_list(version_manifest_url: str=FABRIC_META_VERSION_ENDPOINT_V2, timeout: int=5) -> list[dict]: + try: + r = requests.get(version_manifest_url, timeout=timeout) + except requests.exceptions.Timeout: + raise VersionManifestFetchException( + "Timeout while fetch from {}".format(version_manifest_url) + ) + except requests.exceptions.ConnectionError as e: + raise VersionManifestFetchException( + "Connection error while fetch from {}: {}".format(version_manifest_url, e) + ) + except requests.exceptions.HTTPError as e: + raise VersionManifestFetchException( + "HTTP error while fetch from {}: HTTP Status: {}".format(version_manifest_url, e.response.status_code) + ) + except Exception as e: + raise VersionManifestFetchException( + "Unknown error while fetch from {}: {}".format(version_manifest_url, e) + ) + + try: + data = r.json() + except json.decoder.JSONDecodeError as e: + raise VersionManifestFetchException( + "JSON decode error while fetch from {}: {}".format(version_manifest_url, e) + ) + + versions = data.get("versions") + + if not versions: + raise VersionManifestFetchException( + "Version this is empty or not set yet in mod version manifest" + ) + + return versions + +def is_version_supported_and_stable(fabric_version_list: list[dict], target_version: str) -> tuple[bool, bool | None]: + if not fabric_version_list: + raise VersionManifestException( + "Provided fabric version list is empty." + ) + + for version in fabric_version_list: + if version["version"] == target_version: + is_stable = version.get("stable", None) + return True, is_stable + + return False, None + +def get_version_support_loader_list(target_version: str, loader_version_url: str=FABRIC_META_LOADER_VERSION_ENDPOINT_V2, timeout: int=5)\ + -> list[dict]: + try: + r = requests.get(loader_version_url.format(target_version), timeout=timeout) + except requests.exceptions.Timeout: + raise VersionManifestFetchException( + "Timeout while fetch from {}".format(loader_version_url) + ) + except requests.exceptions.ConnectionError as e: + raise VersionManifestFetchException( + "Connection error while fetch from {}: {}".format(loader_version_url, e) + ) + except requests.exceptions.HTTPError as e: + raise VersionManifestFetchException( + "HTTP error while fetch from {}: HTTP Status: {}".format(loader_version_url, e.response.status_code) + ) + except Exception as e: + raise VersionManifestFetchException( + "Unknown error while fetch from {}: {}".format(loader_version_url, e) + ) + + try: + versions = r.json() + except json.decoder.JSONDecodeError as e: + raise VersionManifestFetchException( + "JSON decode error while fetch from {}: {}".format(loader_version_url, e) + ) + + loaders = [] + + for version in versions: + loader = version.get("loader", None) + if not loader: + logger.warning("Found corrupted or unsupported loader manifest") + continue + + loaders.append(loader) + + if not loaders: + raise VersionManifestException( + "No available loader found in {}".format(loader_version_url) + ) + + return loaders + +def get_loader_manifest_from_loader_data(target_version: str, loader_data: dict, + loader_manifest_url: str=FABRIC_META_LOADER_MANIFEST_ENDPOINT_V2) -> dict: + loader_ver = loader_data.get("version", None) + if not loader_ver: + raise VersionDataKeyNotFoundException( + "Version key not found in loader manifest {}".format(loader_manifest_url) + ) + + # Create url + url = loader_manifest_url.format(target_version, loader_ver) + logger.debug("Full manifest url: {}".format(url)) + + try: + r = requests.get(url, timeout=5) + except requests.exceptions.Timeout: + raise VersionManifestFetchException( + "Timeout while fetch from {}".format(url) + ) + except requests.exceptions.ConnectionError as e: + raise VersionManifestFetchException( + "Connection error while fetch from {}: {}".format(url, e) + ) + except requests.exceptions.HTTPError as e: + raise VersionManifestFetchException( + "HTTP error while fetch from {}: {}".format(url, e.response.status_code) + ) + except Exception as e: + raise VersionManifestFetchException( + "Unknown error while fetch from {}: {}".format(url, e) + ) + + # We don't need verify data, fabric didn't provide manifest hash. + try: + return r.json() + except json.decoder.JSONDecodeError as e: + raise VersionManifestFetchException( + "JSON decode error while fetch from {}: {}".format(url, e) + ) + except Exception as e: + raise VersionManifestFetchException( + "Unknown error while fetch from {}: {}".format(url, e) + ) + + + + diff --git a/core_lib/game/mod/mod_core.py b/core_lib/game/mod/mod_core.py new file mode 100644 index 0000000..7cfc0a5 --- /dev/null +++ b/core_lib/game/mod/mod_core.py @@ -0,0 +1,243 @@ +import logging +from copy import deepcopy +import shlex + +from core_lib.common.exception import VersionDataKeyNotFoundException, DependencyException +from core_lib.game.library import parse_maven_coordinate, convert_maven_version, generate_repo_url, \ + is_complete_maven_url +from core_lib.game.version_info import MOJANG_LIBRARIES_ENDPOINT + +logger = logging.getLogger("Launcher.CoreLib") + +def find_useful_part_in_mod_version_manifest(mod_version_manifest: dict): + ver_id = mod_version_manifest.get("id", None) + inherits_from = mod_version_manifest.get("inheritsFrom", None) + ver_type = mod_version_manifest.get("type", None) + main_class = mod_version_manifest.get("mainClass", None) + arguments = mod_version_manifest.get("arguments", {}) + old_arguments = mod_version_manifest.get("minecraftArguments", None) + libraries = mod_version_manifest.get("libraries", []) + + if not ver_id: + raise VersionDataKeyNotFoundException( + "Provided mod loader version manifest does not contain key \"id\"." + ) + + if not ver_type: + raise VersionDataKeyNotFoundException( + "Provided mod loader version manifest does not contain key \"type\"." + ) + + if not inherits_from: + raise VersionDataKeyNotFoundException( + "Provided mod loader version manifest does not contain key \"inheritsFrom\"." + ) + + if not main_class: + raise VersionDataKeyNotFoundException( + "Provided mod loader version manifest does not contain key \"mainClass\"." + ) + + if not arguments and not old_arguments: + raise VersionDataKeyNotFoundException( + "Provided mod loader version manifest does not contain key \"arguments\" or \"minecraftArguments\"." + ) + + if not libraries: + raise VersionDataKeyNotFoundException( + "Provided mod loader version manifest does not contain key \"libraries\"." + ) + + return { + "id": ver_id, + "type": ver_type, + "inheritsFrom": inherits_from, + "minecraftArguments": old_arguments, + "libraries": libraries, + "arguments": arguments, + "mainClass": main_class, + } + +def merge_necessary_dependencies(inherits_libraries: list, mod_libraries: list, mod_maven_url=None, + inherits_maven_url=MOJANG_LIBRARIES_ENDPOINT, default_extension="jar") -> list[dict]: + + inherits = {} + result = [] + + # ensure below step won't modify original data + inherits_libraries = deepcopy(inherits_libraries) + mod_libraries = deepcopy(mod_libraries) + + def check_lib(lib: dict): + url: str = lib.get("url", "") + source_from = lib["sourceFrom"] + lib_group=lib["parsed"]["group"] + lib_artifact=lib["parsed"]["artifact"] + lib_version=lib["parsed"]["version"] + lib_classifier=lib["parsed"]["classifier"] + lib_extension=lib["parsed"]["extension"] + + recreated = False + + if not url: + if source_from == "official": + new_url = generate_repo_url(inherits_maven_url, lib_group, lib_artifact, lib_version, lib_classifier, + lib_extension) + else: + new_url = generate_repo_url(mod_maven_url, lib_group, lib_artifact, lib_version, lib_classifier, + lib_extension) + + lib["url"] = new_url + + recreated = True + + if not recreated: + url_result = is_complete_maven_url(url, lib_group, lib_artifact, lib_version, lib_classifier, lib_extension) + + if not url_result: + # deal with some weird maven url that don't contain repo path + # Some mod loader (such as fabric), did not contain a full path inside the library. We will need to + # generate it from maven coordinate (aka lib_name) + target_maven = mod_maven_url or url + repo_url = generate_repo_url(target_maven, lib_group, lib_artifact, lib_version, lib_classifier, + lib_extension) + lib["url"] = repo_url + + for library in inherits_libraries: + name = library.get("name", None) + + if not name: + raise DependencyException( + "Provided inherited library does not contain key \"name\"." + ) + + group, artifact, version, classifier, extension = parse_maven_coordinate(name, + default_extension=default_extension) + + # Save into dict for next step to check if library is existing + if not f"{group}:{artifact}" in inherits: + inherits[f"{group}:{artifact}"] = library + inherits[f"{group}:{artifact}"]["parsed"] = { + "group": group, + "artifact": artifact, + "version": version, + "classifier": classifier, + "extension": extension, + } + inherits[f"{group}:{artifact}"]["sourceFrom"] = "official" + else: + logger.warning("Duplicate library: {}".format(name)) + + for mod_library in mod_libraries: + name = mod_library.get("name", None) + group, artifact, version, classifier, extension = parse_maven_coordinate(name, default_extension=default_extension) + + ver_converted = convert_maven_version(version) + + if not name: + raise DependencyException( + "Provided mod library does not contain key \"name\"." + ) + + if ver_converted is None: + raise DependencyException( + "Unable to convert inherits library version {} to tuple".format(version), + ) + + group, artifact, version, classifier, extension = parse_maven_coordinate(name, default_extension=default_extension) + + mod_library["sourceFrom"] = "mod" + mod_library["parsed"] = { + "group": group, + "artifact": artifact, + "version": version, + "classifier": classifier, + "extension": extension, + } + + if f"{group}:{artifact}" in inherits: + found_lib = inherits.pop(f"{group}:{artifact}") + found_lib_ver = convert_maven_version(found_lib["parsed"]["version"], None) + + if found_lib_ver is None: + raise DependencyException( + "Unable to convert mod library version {} to tuple".format(found_lib["version"]) + ) + + if found_lib_ver > ver_converted or found_lib_ver == ver_converted: + result.append(found_lib) + elif found_lib_ver < ver_converted: + result.append(mod_library) + else: + result.append(mod_library) + + # Add the remaining dependencies back to result + for key in inherits: + value = inherits[key] + result.append(value) + + for library in result: + check_lib(library) + + return result + +def merge_arguments(inherits_arguments: str | list, mod_arguments: str | list) -> list[str | dict]: + def str_to_list(args): + p = shlex.split(args) + return p + + if isinstance(inherits_arguments, str): + inherits_arguments = str_to_list(inherits_arguments) + + if isinstance(mod_arguments, str): + mod_arguments = str_to_list(mod_arguments) + + result = [] + result.extend(inherits_arguments) + result.extend(mod_arguments) + + return result + +def merge_game_and_jvm_args(inherits_args: dict[str, list[str]] | str, mod_args: dict[str, list[str]] | str) -> dict: + inherits_game = inherits_args.get("game", []) if isinstance(inherits_args, dict) else inherits_args + inherits_jvm = inherits_args.get("jvm", []) if isinstance(inherits_args, dict) else [] + + mod_game = mod_args.get("game", []) if isinstance(mod_args, dict) else mod_args + mod_jvm = mod_args.get("jvm", []) if isinstance(mod_args, dict) else [] + + merged = { + "game": merge_arguments(inherits_game, mod_game), + "jvm": merge_arguments(inherits_jvm, mod_jvm), + } + + return merged + +def merge_manifest(inherits_version_manifest: dict, mod_version_manifest: dict, mod_maven_url=None) -> dict: + merged_manifest = deepcopy(inherits_version_manifest) + + # main class + main_class = mod_version_manifest.get("mainClass", None) + + # Apply main class change + if main_class: + merged_manifest["mainClass"] = main_class + + # Libraries + inherits_libraries = inherits_version_manifest.get("libraries", []) + mod_libraries = mod_version_manifest.get("libraries", []) + + if not inherits_libraries or not mod_libraries: + raise VersionDataKeyNotFoundException( + "Inherited (or mod) version manifest does not contain key \"libraries\"." + ) + + merged_manifest["libraries"] = merge_necessary_dependencies(inherits_libraries, mod_libraries, + mod_maven_url=mod_maven_url) + + # arguments + inherits_arguments = inherits_version_manifest.get("arguments", []) or inherits_version_manifest.get("minecraftArguments", "") + mod_arguments = mod_version_manifest.get("arguments", {}) or mod_version_manifest.get("minecraftArguments", {}) + + merged_manifest["arguments"] = merge_game_and_jvm_args(inherits_arguments, mod_arguments) + + return merged_manifest \ No newline at end of file diff --git a/core_lib/game/version_info.py b/core_lib/game/version_info.py index 3ec54d9..7694f86 100644 --- a/core_lib/game/version_info.py +++ b/core_lib/game/version_info.py @@ -1,6 +1,5 @@ import json import logging -import re import warnings from collections.abc import Callable from pathlib import Path diff --git a/core_lib/launcher/object.py b/core_lib/launcher/object.py new file mode 100644 index 0000000..3fc185e --- /dev/null +++ b/core_lib/launcher/object.py @@ -0,0 +1,5 @@ + + +class Launcher: + def __init__(self): + pass diff --git a/pages/account.py b/pages/account.py index e43fb1b..94a0d4a 100644 --- a/pages/account.py +++ b/pages/account.py @@ -1,38 +1,124 @@ from pathlib import Path -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, QSize from PySide6.QtGui import QPixmap -from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel +from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QListWidget, QPushButton, QHBoxLayout, QListWidgetItem, \ + QMenu +class AccountWidget(QWidget): + def __init__(self, account_name, account_head: QPixmap, login_status, parent=None, + /, delete_account_callback=None, check_account_callback=None, switch_account_callback=None): + QWidget.__init__(self, parent) + + self.layout = QHBoxLayout(self) + + self.head_icon_label = QLabel() + self.head_icon_label.setObjectName("HeadIcon") + self.head_icon_label.setPixmap(account_head) + + self.account_name = QLabel(account_name) + + self.status_label = QLabel(login_status) + + self.layout.addWidget(self.head_icon_label) + self.layout.addWidget(self.account_name) + self.layout.addStretch() + self.layout.addWidget(self.status_label) + + self.delete_account_callback = delete_account_callback + self.check_account_callback = check_account_callback + self.switch_account_callback = switch_account_callback + + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_right_click_menu) + + def show_right_click_menu(self, position): + context_menu = QMenu(self) + + switch_acc_action = context_menu.addAction("Switch to this account") + check_log_stat_action = context_menu.addAction("Check login status") + + context_menu.addSeparator() + + delete_account_action = context_menu.addAction("Delete account") + + global_position = self.mapToGlobal(position) + + selected_action = context_menu.exec(global_position) + + if selected_action == delete_account_action: + # handle callback here + if callable(self.delete_account_callback): self.delete_account_callback() + elif selected_action == check_log_stat_action and callable(self.check_account_callback): self.check_account_callback() + elif selected_action == switch_acc_action and callable(self.switch_account_callback): self.switch_account_callback() + class AccountPage(QWidget): def __init__(self, app, parent=None): QWidget.__init__(self, parent) self.app = app - self.layout = QVBoxLayout() + self.layout = QHBoxLayout() # 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") + # 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) - 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() + self.main_layout = QVBoxLayout() + self.right_layout = QVBoxLayout() - 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.account_list = QListWidget(self) + + self.add_account_button = QPushButton("Add Account") + self.refresh_button = QPushButton("Refresh all") + self.delete_all_button = QPushButton("Delete All") + + self.main_layout.addWidget(self.account_list) + self.right_layout.addWidget(self.add_account_button) + self.right_layout.addWidget(self.refresh_button) + self.right_layout.addWidget(self.delete_all_button) + self.right_layout.addStretch() + + self.layout.addLayout(self.main_layout) + self.layout.addLayout(self.right_layout) self.setLayout(self.layout) - self.app.apply_qss(Path("main.qss"), self.setStyleSheet) \ No newline at end of file + + self.test() + + def test(self): + head_icon = self.app.get_icon(Path(self.app.icon_dir, "head.png")) + for i in range(1, 11): + name = f"Player_{i}" + head_pixmap = head_icon.pixmap(QSize(50, 50)) + list_item = QListWidgetItem() + + def delete(): + self.account_list.removeItemWidget(list_item) + item = AccountWidget(name, head_pixmap, self, delete_account_callback=delete) + list_item.setSizeHint(item.sizeHint()) + self.account_list.addItem(list_item) + self.account_list.setItemWidget(list_item, item) + + diff --git a/pages/create_profile.py b/pages/create_profile.py index e69de29..3588e58 100644 --- a/pages/create_profile.py +++ b/pages/create_profile.py @@ -0,0 +1,64 @@ +from pathlib import Path + +from PySide6 import QtGui +from PySide6.QtCore import QSize +from PySide6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QListWidget, QPushButton, QLabel, QHBoxLayout + +from core_lib.launcher.object import Launcher as LauncherObject + +class CreateProfile(QWidget): + def __init__(self, launcher: "LauncherObject",parent=None): + super(CreateProfile, self).__init__(parent) + self.app = launcher + + self.layout = QHBoxLayout() + + self.main_layout = QVBoxLayout() + self.right_layout = QVBoxLayout() + self.operate_layout = QHBoxLayout() + + # Some items + self.version_type_label = QLabel("Version Type") + self.version_type_dropdown = QComboBox() + self.version_type_dropdown.setObjectName("Dropdown") + + self.mod_loader_icon_label = QLabel() + icon = self.app.get_icon(Path(self.app.icon_dir, "mod_loader_temp.png")) + pixmap = icon.pixmap(QSize(100, 100)) + self.mod_loader_icon_label.setPixmap(pixmap) + self.mod_loader_label = QLabel("Mod Loader") + self.mod_loader_dropdown = QComboBox() + self.mod_loader_dropdown.setObjectName("Dropdown") + + self.version_list_type_label = QLabel("Version: Vanilla") + self.version_list_label = QLabel("Select a version to continue:") + self.version_list = QListWidget() + self.version_list.setObjectName("ListWidget") + + self.create_profile = QPushButton("Create Profile") + self.create_profile.setObjectName("Button") + + self.refresh_button = QPushButton("Refresh") + self.refresh_button.setObjectName("Button") + + self.main_layout.addWidget(self.version_list_label) + self.main_layout.addWidget(self.version_list) + self.operate_layout.addWidget(self.version_list_type_label) + self.operate_layout.addStretch() + self.operate_layout.addWidget(self.refresh_button) + self.operate_layout.addWidget(self.create_profile) + + self.main_layout.addLayout(self.operate_layout) + + self.right_layout.addWidget(self.mod_loader_icon_label) + self.right_layout.addWidget(self.mod_loader_label) + self.right_layout.addWidget(self.mod_loader_dropdown) + self.right_layout.addWidget(self.version_type_label) + self.right_layout.addWidget(self.version_type_dropdown) + self.right_layout.addStretch() + self.right_layout.setContentsMargins(0, 20, 0, 0) + + self.setLayout(self.layout) + self.layout.addLayout(self.main_layout) + self.layout.addLayout(self.right_layout) + diff --git a/resources/styles/toolbar.qss b/resources/styles/toolbar.qss index e70cb5a..4a2f977 100644 --- a/resources/styles/toolbar.qss +++ b/resources/styles/toolbar.qss @@ -47,6 +47,7 @@ QToolBar:left QToolButton, QToolBar:right QToolButton { padding: 14px 6px; min-width: 0; + border-bottom: 2px solid palette(midlight); } QToolBar QPushButton:hover,