From 48b70c12af394bd7f43257f494259ff868cf51c6 Mon Sep 17 00:00:00 2001 From: Wei Hong Date: Mon, 3 Aug 2026 20:55:37 +0800 Subject: [PATCH] Settings page is "fully" implementation. --- app.py | 14 ++ core_lib/common/exception.py | 21 +- main.py | 7 +- manager/account.py | 4 +- manager/game.py | 19 +- manager/profile.py | 39 +++- manager/settings.py | 271 ++++++++++++++++++++++ pages/account.py | 7 +- pages/launch_profile.py | 14 +- pages/settings.py | 433 +++++++++++++++-------------------- pyproject.toml | 6 + resources/icons/add.png | Bin 0 -> 11931 bytes resources/icons/add_temp.png | Bin 4228 -> 7437 bytes resources/icons/settings.png | Bin 0 -> 15398 bytes resources/styles/profile.qss | 3 +- window.py | 2 +- 16 files changed, 579 insertions(+), 261 deletions(-) create mode 100644 manager/settings.py create mode 100644 resources/icons/add.png create mode 100644 resources/icons/settings.png diff --git a/app.py b/app.py index 3801a12..37c5430 100644 --- a/app.py +++ b/app.py @@ -14,6 +14,8 @@ from core_lib.launcher.object import Launcher as LauncherObject from manager.account import AccountManager from manager.game import GameManager from manager.profile import ProfileManager +from manager.settings import SettingsManager +from manager.settings_models import AccountSettings, AppearanceSettings, GeneralSettings from manager.widgets import WidgetManager from window import LauncherMainWindow @@ -31,6 +33,14 @@ class Launcher(QApplication, LauncherObject): # Widget (Signal, Event) self.widget_manager = WidgetManager() # Manager + self.settings_manager = SettingsManager(self.root_settings_dir) + self.settings_manager.register("general", GeneralSettings) + self.settings_manager.register("appearance", AppearanceSettings) + self.settings_manager.register("account", AccountSettings) + if self.root_settings_dir.is_file(): + self.settings_manager.load() + else: + self.settings_manager.load_defaults().save() self.profile_manager = ProfileManager(self, self.widget_manager) self.account_manager = AccountManager(self, self.widget_manager) self.game_manager = GameManager(self) @@ -123,6 +133,10 @@ class Launcher(QApplication, LauncherObject): def log_dir(self): return self.work_dir / "log" + @property + def root_settings_dir(self): + return self.data_dir / "settings.toml" + @property def default_game_dir(self) -> Path: return self.work_dir / "minecraft" if sys.platform.lower() == "darwin" else self.work_dir / ".minecraft" diff --git a/core_lib/common/exception.py b/core_lib/common/exception.py index 3dbf6e0..11604f2 100644 --- a/core_lib/common/exception.py +++ b/core_lib/common/exception.py @@ -1,4 +1,7 @@ from pathlib import Path +from typing import Mapping + +from pydantic import ValidationError class LauncherException(Exception): @@ -110,4 +113,20 @@ class AuthenticationSessionException(LauncherException): user_message = "Authentication failed. Session may expire." class ThirdPartyAPIException(LauncherException): - user_message = "An error occurred while fetching third party API." \ No newline at end of file + user_message = "An error occurred while fetching third party API." + +class SettingsError(LauncherException): + user_message = "An settings error occurred." + +class ModelRegistrationError(SettingsError): + user_message = "Settings section could not be registered." + +class ConfigValidationError(SettingsError): + user_message = "Settings section could not be validated." + + def __init__(self, errors: Mapping[str, ValidationError]) -> None: + self.errors = dict(errors) + details = "\n".join( + f"[{section}]\n{error}" for section, error in self.errors.items() + ) + super().__init__(f"Invalid settings configuration:\n{details}") \ No newline at end of file diff --git a/main.py b/main.py index 28cb198..45ec42e 100644 --- a/main.py +++ b/main.py @@ -71,9 +71,10 @@ def main(): sys.exit(-1) else: # check workdir - logger.error("Work directory '%s' does not exist." % work_dir) - show_error(f"Specified work directory \"{work_dir}\" does not exist or is not a directory.") - sys.exit(-1) + if not os.path.exists(work_dir) or not os.path.isdir(work_dir): + logger.error("Work directory '%s' does not exist." % work_dir) + show_error(f"Specified work directory \"{work_dir}\" does not exist or is not a directory.") + sys.exit(-1) if os.getcwd() != args.work_dir: os.chdir(work_dir) diff --git a/manager/account.py b/manager/account.py index e09119d..dc5a45d 100644 --- a/manager/account.py +++ b/manager/account.py @@ -910,13 +910,13 @@ class AccountManager(QObject): # # Account Login, Refresh, API Handle # - def start_login(self): + def start_login(self, client_id: str = CLIENT_ID): if self.login_thread is not None: return False # Create login worker and run it in outer thread self.login_thread = QThread(self) - self.login_worker = LoginWorker(CLIENT_ID) + self.login_worker = LoginWorker(client_id) self.login_worker.moveToThread(self.login_thread) self.login_worker.device_code_ready.connect( diff --git a/manager/game.py b/manager/game.py index b7805b3..71fb217 100644 --- a/manager/game.py +++ b/manager/game.py @@ -4,7 +4,6 @@ import subprocess import threading import time import traceback -import types from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path @@ -671,6 +670,7 @@ class LaunchManager: self.java_mgr = java_manager self.game_file_mgr = game_file_manager self.account_mgr = account_manager + self.launching_profiles: set[str] = set() self.launched_profiles: list[str] = [] self.signal = self.LaunchSignal() @@ -879,12 +879,20 @@ class LaunchManager: return result - def start_launch_task(self, profile: LaunchProfile) -> Thread: + def start_launch_task(self, profile: LaunchProfile) -> Thread | None: def worker(): result = self.launch(profile) self.signal.finished.emit(profile, result) return result + profile_id = profile.profile_id + + # Check if profile is in launching process + if profile_id in self.launching_profiles: + return None + + self.launching_profiles.add(profile_id) + thread = threading.Thread( target=worker, name=f"launch-{profile.profile_id}", @@ -893,8 +901,11 @@ class LaunchManager: thread.start() return thread - def is_profile_running(self, profile_id): - return profile_id in self.launched_profiles + def is_profile_running(self, profile_id: str) -> bool: + return ( + profile_id in self.launching_profiles + or profile_id in self.launched_profiles + ) def profile_finished(self, profile_id: str, exit_code: int) -> None: try: diff --git a/manager/profile.py b/manager/profile.py index 1545065..b1df0c3 100644 --- a/manager/profile.py +++ b/manager/profile.py @@ -251,7 +251,10 @@ class ProfileManager(QObject): ) result.append(profile_summary) - return result + settings = self.profiles.get("settings", {}) + sorting = settings.get("profileSorting", "ByLastPlayed") + current_id = get_current_profile_id(self.profiles) + return sort_profile_summaries(result, sorting, current_id) def get_profile(self, profile_id: str) -> ProfileDetail | None: if not self.contains(profile_id, show_error=True): @@ -701,3 +704,37 @@ class ProfileManager(QObject): @property def is_modified(self) -> bool: return self._is_modified + + +def sort_profile_summaries( + profiles: list[ProfileSummary], + sorting: str, + current_profile_id: str | None = None, +) -> list[ProfileSummary]: + """Order profiles according to launcher_profiles.json settings.""" + ordered = list(profiles) + + if sorting == "ByName": + return sorted( + ordered, + key=lambda profile: ( + profile.display_name.casefold(), + profile.version_id.casefold(), + profile.id, + ), + ) + + if sorting == "ByLastPlayed": + def last_used_timestamp(profile: ProfileSummary) -> float: + if profile.last_used is None: + return float("-inf") + try: + return profile.last_used.timestamp() + except (OSError, OverflowError, ValueError): + return float("-inf") + + ordered.sort(key=last_used_timestamp, reverse=True) + if current_profile_id is not None: + ordered.sort(key=lambda profile: profile.id != current_profile_id) + + return ordered diff --git a/manager/settings.py b/manager/settings.py new file mode 100644 index 0000000..6e71a45 --- /dev/null +++ b/manager/settings.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +from collections.abc import Mapping +from enum import Enum +from pathlib import Path +from types import UnionType +from typing import Annotated, Any, Literal, TypeVar, Union, cast, get_args, get_origin + +from pydantic import BaseModel, ValidationError +import tomli_w + +from core_lib.common.exception import ModelRegistrationError, ConfigValidationError, SettingsError + +try: + import tomllib +except ModuleNotFoundError: # Python 3.10 + import tomli as tomllib + +ModelT = TypeVar("ModelT", bound=BaseModel) + + +class SettingsManager: + """ + A settings manager (Based-on pydantic) + + Vibed! + """ + def __init__(self, path: str | Path | None = None) -> None: + self.path = Path(path) if path is not None else None + self._models: dict[str, type[BaseModel]] = {} + self._settings: dict[str, BaseModel] = {} + + @property + def registered_sections(self) -> tuple[str, ...]: + return tuple(self._models) + + def register(self, section: str, model: type[ModelT], *, replace: bool = False) -> type[ModelT]: + """ + Register a new model + :param section: + :param model: + :param replace: + :return: + model: type[ModelT] + """ + if not section: + raise ModelRegistrationError( + "Section must be a non-empty" + ) + elif "." in section: + raise ModelRegistrationError( + "Top level section cannot contain dots" + ) + + if not isinstance(model, type) or not issubclass(model, BaseModel): + raise ModelRegistrationError("Model must be a Pydantic BaseModel class") + + if section in self._models and not replace: + raise ModelRegistrationError(f"Section {section!r} is already registered") + + self._models[section] = model + self._settings.pop(section, None) + return model + + def model(self, section: str): + def decorator(model: type[ModelT]) -> type[ModelT]: + return self.register(section, model) + + return decorator + + def load(self, path: str | Path | None = None, *, reject_unknown: bool = True) -> SettingsManager: + """ + Load a toml settings file + :param reject_unknown: Raise exception if settings file contains unknown section + :param path: If not set, use default path instead + """ + config_path = self._resolve_path(path) + + # Check file exist + if not config_path.exists() or not config_path.is_file(): + raise FileNotFoundError(f"Settings file {config_path} does not exist") + + with config_path.open("rb") as file: + raw = tomllib.load(file) + + unknown = set(raw).difference(self._models) + if reject_unknown and unknown: + names = ", ".join(sorted(unknown)) + raise SettingsError(f"unregistered TOML section(s): {names}") + + loaded: dict[str, BaseModel] = {} + errors: dict[str, ValidationError] = {} + for section, model in self._models.items(): + value: Any = raw.get(section, {}) + try: + # Validate and save section data + loaded[section] = model.model_validate(value) + except ValidationError as error: + errors[section] = error + + if errors: + raise ConfigValidationError(errors) + + self.path = config_path + self._settings = loaded + return self + + def get(self, section: str, model: type[ModelT] | None = None) -> ModelT: + """ + Get target section model + :param section: + :param model: + :return: + model: type[ModelT] + """ + if section not in self._models: + raise KeyError(f"settings section {section!r} is not registered") + if section not in self._settings: + raise SettingsError(f"settings have not been loaded for {section!r}") + + value = self._settings[section] + if model is not None and not isinstance(value, model): + raise TypeError( + f"section {section!r} contains {type(value).__name__}, " + f"not {model.__name__}" + ) + + return cast(ModelT, value) + + def set(self, section: str, value: BaseModel | Mapping[str, Any]) -> BaseModel: + """Validate and replace one loaded settings section.""" + try: + model = self._models[section] + except KeyError as error: + raise KeyError(f"settings section {section!r} is not registered") from error + + validated = model.model_validate(value) + self._settings[section] = validated + return validated + + def load_defaults(self) -> SettingsManager: + """Load every registered model using its declared default values.""" + loaded: dict[str, BaseModel] = {} + errors: dict[str, ValidationError] = {} + for section, model in self._models.items(): + try: + loaded[section] = model.model_validate({}) + except ValidationError as error: + errors[section] = error + if errors: + raise ConfigValidationError(errors) + self._settings = loaded + return self + + def save(self, path: str | Path | None = None) -> Path: + """Validate all loaded sections and save them as UTF-8 TOML.""" + config_path = self._resolve_path(path) + missing = set(self._models).difference(self._settings) + if missing: + names = ", ".join(sorted(missing)) + raise SettingsError(f"settings section(s) have not been loaded: {names}") + + data = { + section: self._models[section] + .model_validate(self._settings[section]) + .model_dump(mode="json") + for section in self._models + } + config_path.parent.mkdir(parents=True, exist_ok=True) + temporary_path = config_path.with_suffix(config_path.suffix + ".tmp") + temporary_path.write_bytes(tomli_w.dumps(data).encode("utf-8")) + temporary_path.replace(config_path) + self.path = config_path + return config_path + + def choices(self, section: str, field: str) -> tuple[Any, ...]: + """ + Return choices for a field, accepting paths such as ``window.mode``. + + This can be used by a settings page to populate a combo box. Pydantic + uses the same annotation to validate values read from TOML. + :param section: + :param field: + """ + try: + model = self._models[section] + except KeyError as error: + raise KeyError(f"settings section {section!r} is not registered") from error + + annotation: Any = model + for part in field.split("."): + nested_model = _model_type(annotation) + if nested_model is None: + raise KeyError( + f"{part!r} cannot be resolved in field path {section}.{field}" + ) + try: + annotation = nested_model.model_fields[part].annotation + except KeyError as error: + raise KeyError(f"field path {section}.{field} does not exist") from error + + values = _choice_values(annotation) + if values is None: + raise SettingsError( + f"field {section}.{field} is not declared with Literal or Enum" + ) + return values + + def validate(self, data: Mapping[str, Any], *, reject_unknown: bool = True) -> None: + """ + Validate an in-memory mapping without changing loaded settings. + :param data: + :param reject_unknown: + :return: + """ + unknown = set(data).difference(self._models) + if reject_unknown and unknown: + names = ", ".join(sorted(unknown)) + raise SettingsError(f"unregistered settings section(s): {names}") + + errors: dict[str, ValidationError] = {} + for section, model in self._models.items(): + try: + model.model_validate(data.get(section, {})) + except ValidationError as error: + errors[section] = error + if errors: + raise ConfigValidationError(errors) + + def _resolve_path(self, path: str | Path | None) -> Path: + resolved = Path(path) if path is not None else self.path + + if resolved is None: + raise SettingsError("no TOML configuration path was provided") + + return resolved + +def _choice_values(annotation: Any) -> tuple[Any, ...] | None: + """Extract choices from a field annotation, including Annotated/Optional.""" + origin = get_origin(annotation) + if origin is Literal: + return get_args(annotation) + if isinstance(annotation, type) and issubclass(annotation, Enum): + return tuple(member.value for member in annotation) + if origin is Annotated: + return _choice_values(get_args(annotation)[0]) + if origin in (Union, UnionType): + choices: list[Any] = [] + found = False + for member in get_args(annotation): + values = _choice_values(member) + if values is not None: + choices.extend(values) + found = True + return tuple(choices) if found else None + return None + + +def _model_type(annotation: Any) -> type[BaseModel] | None: + """Find a nested BaseModel type inside Annotated or Optional.""" + if isinstance(annotation, type) and issubclass(annotation, BaseModel): + return annotation + origin = get_origin(annotation) + if origin is Annotated: + return _model_type(get_args(annotation)[0]) + if origin in (Union, UnionType): + for member in get_args(annotation): + model = _model_type(member) + if model is not None: + return model + return None diff --git a/pages/account.py b/pages/account.py index d613c67..b38469e 100644 --- a/pages/account.py +++ b/pages/account.py @@ -17,10 +17,11 @@ from PySide6.QtWidgets import ( QWidget, QDialog, QLineEdit, QApplication, QToolButton, ) -from constant import CLIENT_ID, MICROSOFT_VERIFY_ENDPOINT +from constant import MICROSOFT_VERIFY_ENDPOINT from core_lib.qt.hack import move_window_to_center from manager.account import AccountManager, AccountSummary +from manager.settings_models import AccountSettings class AccountWidget(QWidget): @@ -162,7 +163,6 @@ class AccountPage(QWidget): self.app = app self.widget_manager = app.widget_manager self.account_manager: AccountManager = app.account_manager - self.client_id = CLIENT_ID self.login_dialog = None @@ -343,7 +343,8 @@ class AccountPage(QWidget): self.login_dialog.rejected.connect(self.account_manager.cancel_login) self.login_dialog.show() - self.account_manager.start_login() + account_settings = self.app.settings_manager.get("account", AccountSettings) + self.account_manager.start_login(str(account_settings.client_id)) def _show_device_code(self, device: dict): self._verification_uri = device.get("verification_uri", MICROSOFT_VERIFY_ENDPOINT) diff --git a/pages/launch_profile.py b/pages/launch_profile.py index 3fdfdbd..d2c4a78 100644 --- a/pages/launch_profile.py +++ b/pages/launch_profile.py @@ -362,8 +362,18 @@ class LaunchProfilePage(QWidget): profiles: list[ProfileSummary] = self.profile_manager.list_profiles() self.profile_popup.load_profiles(profiles) - if len(profiles) > 0: - self.select_profile(profiles[0]) + if not profiles: + self.profile_button.setProperty("profile_id", None) + self.profile_button.name_label.setText("No profiles available") + self.profile_button.version_label.clear() + return + + selected_id = self.profile_button.property("profile_id") + available = {profile.id: profile for profile in profiles} + selected = available.get(selected_id) + if selected is None: + selected = available.get(self.profile_manager.get_current_profile_id()) + self.select_profile(selected or profiles[0]) def toggle_profile_popup(self) -> None: if self.profile_popup.isVisible(): diff --git a/pages/settings.py b/pages/settings.py index c2f72d1..5bb6b30 100644 --- a/pages/settings.py +++ b/pages/settings.py @@ -1,328 +1,275 @@ -from PySide6.QtCore import Qt, Signal -from PySide6.QtGui import QPalette, QFont +from pathlib import Path + +from PySide6.QtCore import Qt from PySide6.QtWidgets import ( - QCheckBox, QComboBox, QHBoxLayout, QLabel, + QLineEdit, + QMessageBox, + QPlainTextEdit, QPushButton, - QSlider, QStackedWidget, QVBoxLayout, - QWidget, QLineEdit, QPlainTextEdit, + QWidget, QScrollArea, ) -from constant import LAUNCHER_NAME, LAUNCHER_DESCRIPTION, LAUNCHER_VERSION +from constant import LAUNCHER_DESCRIPTION, LAUNCHER_NAME, LAUNCHER_VERSION +from manager.settings_models import AccountSettings, AppearanceSettings, GeneralSettings + + +LANGUAGE_LABELS = { + "en": "English", + "zh-TW": "Chinese (Traditional)", +} +THEME_LABELS = { + "system": "Follow system settings", + "light": "Light", + "dark": "Dark", +} class SettingsItem(QPushButton): - """ - A modern look settings button. - """ - clicked_page = Signal() - - def __init__(self, title: str, description: str = ""): + def __init__(self, title: str, description: str = "") -> None: super().__init__() - + self.setObjectName("settingsItem") self.setCursor(Qt.CursorShape.PointingHandCursor) - # Widget size - self.setMinimumHeight(68) - self.setMaximumWidth(800) - # Layout layout = QVBoxLayout(self) layout.setContentsMargins(16, 10, 16, 10) layout.setSpacing(3) - # Widgets - self.title_label = QLabel(title) - self.title_label.setAttribute( - Qt.WidgetAttribute.WA_TransparentForMouseEvents - ) + title_label = QLabel(title) + title_label.setObjectName("settingsItemTitle") + title_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) + layout.addWidget(title_label) - self.description_label = QLabel(description) - self.description_label.setAttribute( - Qt.WidgetAttribute.WA_TransparentForMouseEvents - ) - - layout.addWidget(self.title_label) - - # Show description if existing if description: - layout.addWidget(self.description_label) - - self.update_palette() - - def update_palette(self): - """ - Apply system palette - :return: - """ - palette = self.palette() - - button_text = palette.color(QPalette.ColorRole.ButtonText) - placeholder_text = palette.color(QPalette.ColorRole.PlaceholderText) - - title_palette = self.title_label.palette() - title_palette.setColor( - QPalette.ColorRole.WindowText, - button_text, - ) - self.title_label.setPalette(title_palette) - - description_palette = self.description_label.palette() - description_palette.setColor( - QPalette.ColorRole.WindowText, - placeholder_text, - ) - self.description_label.setPalette(description_palette) - - title_font = self.title_label.font() - title_font.setPointSize(11) - title_font.setWeight(QFont.Weight.Bold) - self.title_label.setFont(title_font) - - description_font = self.description_label.font() - description_font.setPointSize(9) - self.description_label.setFont(description_font) + description_label = QLabel(description) + description_label.setObjectName("settingsItemDescription") + description_label.setAttribute( + Qt.WidgetAttribute.WA_TransparentForMouseEvents + ) + layout.addWidget(description_label) class SettingsSubPage(QWidget): - def __init__(self, title: str, back_callback): + def __init__(self, title: str, back_callback) -> None: super().__init__() + self.setObjectName("settingsSubPage") main_layout = QVBoxLayout(self) main_layout.setContentsMargins(20, 16, 20, 20) main_layout.setSpacing(16) header_layout = QHBoxLayout() + back_button = QPushButton("←") + back_button.setObjectName("settingsBackButton") + back_button.setFixedSize(36, 36) + back_button.setCursor(Qt.CursorShape.PointingHandCursor) + back_button.clicked.connect(back_callback) - self.back_button = QPushButton("←") - self.back_button.setFixedSize(36, 36) - self.back_button.setCursor(Qt.CursorShape.PointingHandCursor) - self.back_button.clicked.connect(back_callback) - - self.title_label = QLabel(title) - - title_font = self.title_label.font() - title_font.setPointSize(15) - title_font.setBold(True) - self.title_label.setFont(title_font) - - header_layout.addWidget(self.back_button) - header_layout.addWidget(self.title_label) + title_label = QLabel(title) + title_label.setObjectName("settingsSubPageTitle") + header_layout.addWidget(back_button) + header_layout.addWidget(title_label) header_layout.addStretch() self.content_layout = QVBoxLayout() self.content_layout.setSpacing(12) - main_layout.addLayout(header_layout) main_layout.addLayout(self.content_layout) main_layout.addStretch() - self.update_palette() - - def update_palette(self): - # Apply system palette - palette = self.palette() - - button_color = palette.color(QPalette.ColorRole.Button) - border_color = palette.color(QPalette.ColorRole.Mid) - - self.back_button.setStyleSheet(""" - QPushButton { - border: none; - border-radius: 8px; - background-color: transparent; - font-size: 22px; - } - - QPushButton:hover { - background-color: palette(button); - } - - QPushButton:pressed { - background-color: palette(midlight); - } - """) - - def changeEvent(self, event): - # Update when system palette change - if event.type() in ( - event.Type.PaletteChange, - event.Type.ApplicationPaletteChange, - ): - self.update_palette() - - super().changeEvent(event) - class SettingsPage(QWidget): - def __init__(self, app, parent=None): + def __init__(self, app, parent=None) -> None: super().__init__(parent) self.app = app - - self.setWindowTitle("Settings") - self.resize(560, 440) + self.settings = app.settings_manager + self._settings_dirty = False + self.setObjectName("settingsPage") root_layout = QVBoxLayout(self) root_layout.setContentsMargins(0, 0, 0, 0) - self.stack = QStackedWidget() - self.main_page = self.create_main_page() - self.general_page = self.create_general_page() - self.appearance_page = self.create_appearance_page() - self.profile_page = self.create_profile_page() - self.about_page = self.create_about_page() - - self.stack.addWidget(self.main_page) - self.stack.addWidget(self.general_page) - self.stack.addWidget(self.appearance_page) - self.stack.addWidget(self.profile_page) - self.stack.addWidget(self.about_page) + self.main_page = self._create_main_page() + self.general_page = self._create_general_page() + self.appearance_page = self._create_appearance_page() + self.account_page = self._create_account_page() + self.about_page = self._create_about_page() + for page in ( + self.main_page, + self.general_page, + self.appearance_page, + self.account_page, + self.about_page, + ): + self.stack.addWidget(page) root_layout.addWidget(self.stack) + self.app.apply_qss(Path("settings.qss"), self.setStyleSheet) + self.language_combo.currentIndexChanged.connect(self._mark_settings_dirty) + self.theme_combo.currentIndexChanged.connect(self._mark_settings_dirty) + self.client_id_edit.textEdited.connect(self._mark_settings_dirty) - def create_main_page(self) -> QWidget: + def _create_main_page(self) -> QWidget: page = QWidget() - + page.setObjectName("settingsMainPage") layout = QVBoxLayout(page) - layout.setContentsMargins(24, 20, 24, 24) - layout.setSpacing(12) + layout.setContentsMargins(0, 0, 0, 0) + + scroll_area = QScrollArea() + scroll_area.setObjectName("settingsScrollArea") + scroll_area.setWidgetResizable(True) + scroll_area.setFrameShape(QScrollArea.Shape.NoFrame) + + scroll_content = QWidget() + scroll_content.setObjectName("settingsScrollContent") + scroll_area.setWidget(scroll_content) + scroll_layout = QVBoxLayout(scroll_content) + scroll_layout.setContentsMargins(24, 20, 24, 24) + scroll_layout.setSpacing(12) title = QLabel("Settings") + title.setObjectName("settingsPageTitle") + scroll_layout.addWidget(title) + scroll_layout.addSpacing(8) - title_font = title.font() - title_font.setPointSize(18) - title_font.setBold(True) - title.setFont(title_font) - - # Setting options - info_button = SettingsItem( - "Wait!", - "Setting page is not full implemented yet.", - ) - - general_button = SettingsItem( - "General", - "Some general settings for launcher ", - ) - general_button.clicked.connect( - lambda: self.open_page(self.general_page) - ) - - appearance_button = SettingsItem( - "Appearance", - "Settings to change the launcher look like", - ) - appearance_button.clicked.connect( - lambda: self.open_page(self.appearance_page) - ) - - profile_button = SettingsItem( - "Profile", - "Store profile settings", - ) - profile_button.clicked.connect( - lambda: self.open_page(self.profile_page) - ) - - about_button = SettingsItem( - "About", - "About the launcher", - ) - about_button.clicked.connect( - lambda: self.open_page(self.about_page) - ) - - layout.addWidget(title) - layout.addSpacing(8) - layout.addWidget(info_button) - layout.addWidget(general_button) - layout.addWidget(appearance_button) - layout.addWidget(profile_button) - layout.addWidget(about_button) - layout.addStretch() + pages = ( + ("General", "Language and launcher directories", "general_page"), + ("Appearance", "Launcher theme", "appearance_page"), + ("Account", "Microsoft authentication settings", "account_page"), + ("About", "Version and project information", "about_page"), + ) + for title_text, description, page_name in pages: + button = SettingsItem(title_text, description) + button.clicked.connect( + lambda checked=False, name=page_name: self._open_page( + getattr(self, name) + ) + ) + scroll_layout.addWidget(button) + scroll_layout.addStretch() + layout.addWidget(scroll_area) return page - def create_general_page(self) -> QWidget: - page = SettingsSubPage("General", self.go_back) + def _create_general_page(self) -> QWidget: + page = SettingsSubPage("General", self._go_back) + current = self.settings.get("general", GeneralSettings) - language_label = QLabel("Language") - - language_combo = QComboBox() - language_combo.addItems([ - "English", - "Chinese (Traditional)", - ]) - - launcher_dir_label = QLabel("Current launcher working directory") + page.content_layout.addWidget(QLabel("Current launcher working directory")) launcher_dir = QLineEdit(self.app.work_dir.as_posix()) launcher_dir.setReadOnly(True) + page.content_layout.addWidget(launcher_dir) - program_dir_label = QLabel("Current program directory") + page.content_layout.addWidget(QLabel("Current program directory")) program_dir = QLineEdit(self.app.program_dir.as_posix()) program_dir.setReadOnly(True) - - page.content_layout.addWidget(launcher_dir_label) - page.content_layout.addWidget(launcher_dir) - page.content_layout.addWidget(program_dir_label) page.content_layout.addWidget(program_dir) + page.content_layout.addSpacing(8) - page.content_layout.addWidget(language_label) - page.content_layout.addWidget(language_combo) - + page.content_layout.addWidget(QLabel("Language")) + self.language_combo = self._choice_combo( + "general", "language", LANGUAGE_LABELS, current.language + ) + page.content_layout.addWidget(self.language_combo) return page - def create_appearance_page(self) -> QWidget: - page = SettingsSubPage("Appearance", self.go_back) - - theme_label = QLabel("Theme") - - theme_combo = QComboBox() - theme_combo.addItems([ - "Follow system settings", - "Light (unfinished)", - "Dark", - ]) - - page.content_layout.addWidget(theme_label) - page.content_layout.addWidget(theme_combo) + def _create_appearance_page(self) -> QWidget: + page = SettingsSubPage("Appearance", self._go_back) + current = self.settings.get("appearance", AppearanceSettings) + page.content_layout.addWidget(QLabel("Theme")) + self.theme_combo = self._choice_combo( + "appearance", "theme", THEME_LABELS, current.theme + ) + page.content_layout.addWidget(self.theme_combo) return page - def create_profile_page(self) -> QWidget: - page = SettingsSubPage("Profile", self.go_back) + def _create_account_page(self) -> QWidget: + page = SettingsSubPage("Account", self._go_back) + current = self.settings.get("account", AccountSettings) - test_label = QLabel("Still in development...") - - page.content_layout.addWidget(test_label) + page.content_layout.addWidget(QLabel("Microsoft OAuth Client ID")) + self.client_id_edit = QLineEdit(str(current.client_id)) + self.client_id_edit.setPlaceholderText( + "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + ) + page.content_layout.addWidget(self.client_id_edit) + help_label = QLabel( + "The Client ID must belong to an Azure application configured " + "for Microsoft device-code authentication." + ) + help_label.setObjectName("settingsHelpText") + help_label.setWordWrap(True) + page.content_layout.addWidget(help_label) return page - def create_about_page(self) -> QWidget: - page = SettingsSubPage("About", self.go_back) - + def _create_about_page(self) -> QWidget: + page = SettingsSubPage("About", self._go_back) brand_label = QLabel(LAUNCHER_NAME) - brand_label.setStyleSheet("QLabel { font-size: 20px; }") - - version_label = QLabel("Version: {}".format(LAUNCHER_VERSION)) - version_label.setStyleSheet("QLabel { font-size: 12px; }") - - text = QPlainTextEdit(LAUNCHER_DESCRIPTION) - text.setStyleSheet("QPlainTextEdit { font-size: 13px; }") - text.setReadOnly(True) + brand_label.setObjectName("settingsBrand") + version_label = QLabel(f"Version: {LAUNCHER_VERSION}") + version_label.setObjectName("settingsVersion") + description = QPlainTextEdit(LAUNCHER_DESCRIPTION) + description.setObjectName("settingsDescription") + description.setReadOnly(True) page.content_layout.addWidget(brand_label) page.content_layout.addWidget(version_label) - page.content_layout.addWidget(text) - + page.content_layout.addWidget(description) return page - def open_page(self, page: QWidget): + def _choice_combo( + self, + section: str, + field: str, + labels: dict[str, str], + current: str, + ) -> QComboBox: + combo = QComboBox() + for value in self.settings.choices(section, field): + combo.addItem(labels.get(value, str(value)), value) + index = combo.findData(current) + combo.setCurrentIndex(max(index, 0)) + return combo + + def _mark_settings_dirty(self, *_args) -> None: + self._settings_dirty = True + + def _save_pending_settings(self, *, show_error: bool = True) -> bool: + if not self._settings_dirty: + return True + + data = { + "general": {"language": self.language_combo.currentData()}, + "appearance": {"theme": self.theme_combo.currentData()}, + "account": {"client_id": self.client_id_edit.text().strip()}, + } + try: + self.settings.validate(data) + for section, value in data.items(): + self.settings.set(section, value) + self.settings.save() + except Exception as error: + if show_error: + QMessageBox.critical(self, "Settings error", str(error)) + return False + + self._settings_dirty = False + return True + + def _open_page(self, page: QWidget) -> None: self.stack.setCurrentWidget(page) - def go_back(self): - self.stack.setCurrentWidget(self.main_page) \ No newline at end of file + def _go_back(self) -> None: + if self._save_pending_settings(): + self.stack.setCurrentWidget(self.main_page) + + def hideEvent(self, event) -> None: + self._save_pending_settings() + super().hideEvent(event) diff --git a/pyproject.toml b/pyproject.toml index d13a710..4565840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,11 @@ authors = [ dependencies = [ "colorlog>=6.9,<7", "keyring>=25.6,<26", + "pydantic>=2.11,<3", "PySide6>=6.8,<7", "requests>=2.32,<3", + "tomli>=2.2,<3; python_version < '3.11'", + "tomli-w>=1.2,<2", ] [dependency-groups] @@ -21,6 +24,9 @@ build = [ "zstandard>=0.23", "imageio>=2.37.0" ] +test = [ + "pytest>=8.4,<10", +] [project.urls] Homepage = "https://repo.weispace.net/wei/Launcher" diff --git a/resources/icons/add.png b/resources/icons/add.png new file mode 100644 index 0000000000000000000000000000000000000000..9e157eed98bfef5243c33d0244860067e6e6d264 GIT binary patch literal 11931 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 U_qFj}*cUo+O#5i=5v!~J2lx|0DF6Tf literal 0 HcmV?d00001 diff --git a/resources/icons/add_temp.png b/resources/icons/add_temp.png index 7fb3fb46747d4a348c3cbc202bb2d171471f0d10..d3d2c572b6692804a4085e73ebbdafd930460f5e 100644 GIT binary patch literal 7437 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 q(;R@_V+Av>nk11Fy|Kuid77PqD@le?S literal 4228 zcmeHKdr(tX8b3*(vAV)ib%>z!;gxiY1cGP?V4_wlR}45}#RrctQIQoSQXm8)qJp@5CHPy&hN21-GKmxlx**#m87c6N69PyhAIJ#)U- z{e5Tdx#yejcYbf!uH{SJmI45lqai!?0YHT!6)brHE|VD#X5m74XWy>vpsHidFjO4k zw+XfZP*b?f7Uu}{7sVlm-T}br-om7`CVzPp8ds<8JeV4hd^}YWmvRj3jY~L{$_m@L z|CLQFHjC}Ig0d?W1~nWzh#p7LH^368I083{BcMQOH=ORj$#6U1bqa0KPJ=R=LIo~x zJq%|;p2MO&5y}hWb2$J%!=T^S|IG>jbY7wZ2N>(?x6zmK$*;HzIUPUw6-xo-g+YbL z)Wyhs+yE&3_7>aMcfm`Syr4(XQv*VYxzZYBJJUh*}uB>MY{5Y4;s1}y1rofKRN%^eTPl=cdklYk+$xi zJD0j8esD7)O$f1LA!WpT-uB&(VU_Fta)XS@u^+Xv4yj+>pvQb zN;7qQdZVu4Z+PK)H51R0mxiRIVH?rg7;ztQUN04vRK_b}ox%0583PyDx@V&N+JT=+ z*NSY9pl*Ij1yy2E{yI^9tedFmR_VCw@I$uF=OX*!1`P*@9o<9*U){`T%4e~ao%!sl z*m4?ZIGfKt}PU(l+ZdgxtFN(N4vX z190kM3@{b?%w4>j!5mc>gz@6&0?LW=$XvrAiyD!H=_hR@P_gn9%e24PM$6xqVTMaY zyyAj(wwsc>N-5iEIf|WBw5I>OA6udKZ*B+%*YnnbgY|_L=?oLUkGvesqK>=o!}IU){CD`1x1rK}mzU3WVoFrD`!Khxa`>0pZGt7Hj+v9}6xgCeScXDHp$zP}1R} z5xb`I5{D#nt(ZYU&<6_}u{DB}hfOpwads^L8I7~EPTCRIQBb;@Oh$QIWTkt^@2Vt@ z>6^u^41F+REF#T+;$A(?3-^rg~?VzQ^@^t)vE;TA;t3cj(iVS{Q4Fhpz6lz6OS5BVl~v6Ew^0N0@?YzVWZ zVX@()1s2Ggwtol`Hvt_Za=kPeiwz^2C+h4sVBy`JZ`EmKA>=Z2{+QB5Rre^^I4Y4~ zvC2IArnXKBVD(%c1TJiMp6J+hpFb^lJ;83I(Pb3 z?4;Z_V+-j@%H{);J3AEk3<(Q7#eg zgUb1fy|eV(G9?zp@9dYboMS!|&`f1$zh?LX0IVMm9Jf?b#BXEC6NzxY%4?XRW_YyE zAKadAw-;2s34rV4@y!b{zTZBjG|W4LId;;mXBFNK+7R*^n-5TBt;4UiwS?9sfYNZX z-4=NP?(cTX?Qe?HP`Go6JKY0oeJQU>>5yRHXtw1=}(OEk^ zkHjAFXxi^i*ZK>pnbHmT%ZT*O7ZC5(M?{NsMiy7z?A{Xqj>jr#rftPkARGx}N?*q< zOsmCwr>&+4T&_$&q;KHW9URXO;Aayre#G02g^QPQd!_( ztNr&B_}bk?{5oEM<{&r|YhvOxNn-&(+uLPDe3(){Jeg^|fONKN(lu`fy_nnNLW#gm zVutdCoA5C)D1H1@bh$e?FgTih!+vd|%Ra!Rh9!@b*uU$@8*g$KCt)j4-erO=H6re(sd-Lq{h6RV?ZXL^W<4(P zRD0o#@+L1lZDLH@AG6}R;oG)zwDHLlN=B$C2Qx04=q&A6Rqi$ZG7MR`U8fD`GccR zi@#vr`AkttB=32V|C2@jdE4~7Yy7`Wvo^E9e7Aho z`oCR1-mcT5{NiQW%!R<#;$>OsT|@NEufTrZBZf8F!}oU0ow^KZLLj6w0@7@rEzd`o zl?j=dQgDzS1XTO<{$+beHM9pO&fL58BIFvd$(SbQ%~TbOMbUm(z^4=9 zGi(;aLyNVUlIvw) z3{10imgwzEn*4M#eQe$1Hj86ZbA}UG344oh@^f?yg?&=Jr`i)AMws$ojQA9h@dWzX jwrctx5mM;OeZfvkDpYkpSh?ZBLMXa3Y)930;o1KHKPeRo diff --git a/resources/icons/settings.png b/resources/icons/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..d029c39390ec07374727fab8f04298a3abfe77bc GIT binary patch literal 15398 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 M49>qkYZLMR0XRPlQ~&?~ literal 0 HcmV?d00001 diff --git a/resources/styles/profile.qss b/resources/styles/profile.qss index ec9fdf5..6f93b13 100644 --- a/resources/styles/profile.qss +++ b/resources/styles/profile.qss @@ -23,7 +23,7 @@ QLabel#profileArrow { font-size: 18px; } QFrame#profilePopup { - background: palette(mid); +/* background: palette(mid);*/ border: 1px solid palette(midlight); border-radius: 5px; } @@ -32,6 +32,7 @@ QPushButton#profileItem { background: transparent; border: none; text-align: left; + border-radius: 5px; } QPushButton#profileItem:hover { diff --git a/window.py b/window.py index 226750b..21bbea5 100644 --- a/window.py +++ b/window.py @@ -204,7 +204,7 @@ class LauncherMainWindow(QMainWindow): # current account's avatar # Profile btn - self.open_create_profile_page = self.toolbar.add_button(icon=self.app.get_icon(Path(self.app.icon_dir, "add_temp.png"))) + self.open_create_profile_page = self.toolbar.add_button(icon=self.app.get_icon(Path(self.app.icon_dir, "add.png"))) # Bind tool buttons self.toolbar.addWidget(self.home_button)