2026-08-04 19:40:50 +08:00
|
|
|
import platform
|
2026-07-29 00:03:25 +08:00
|
|
|
import sys
|
2026-07-11 01:19:36 +08:00
|
|
|
from pathlib import Path
|
2026-07-14 16:41:14 +08:00
|
|
|
from typing import Callable
|
2026-07-10 20:57:31 +08:00
|
|
|
|
2026-07-14 16:41:14 +08:00
|
|
|
from PySide6.QtCore import QSize
|
2026-08-04 17:39:38 +08:00
|
|
|
from PySide6.QtGui import QIcon, QPixmap, QPalette, QColor
|
|
|
|
|
from PySide6.QtWidgets import QApplication, QStyle, QToolButton
|
2026-07-11 01:19:36 +08:00
|
|
|
|
2026-07-28 13:35:04 +08:00
|
|
|
from constant import LAUNCHER_VERSION
|
2026-07-14 22:22:58 +08:00
|
|
|
from core_lib.qt.hack import move_window_to_center, is_light_theme, \
|
|
|
|
|
recolor_icon
|
2026-07-14 16:41:14 +08:00
|
|
|
from core_lib.qt import messagebox
|
2026-08-04 17:39:38 +08:00
|
|
|
from core_lib.i18n import TranslationManager
|
2026-07-23 00:47:44 +08:00
|
|
|
from core_lib.launcher.object import Launcher as LauncherObject
|
2026-08-03 00:16:12 +08:00
|
|
|
from manager.account import AccountManager
|
2026-07-30 21:43:19 +08:00
|
|
|
from manager.game import GameManager
|
2026-07-29 00:03:25 +08:00
|
|
|
from manager.profile import ProfileManager
|
2026-08-03 20:55:37 +08:00
|
|
|
from manager.settings import SettingsManager
|
|
|
|
|
from manager.settings_models import AccountSettings, AppearanceSettings, GeneralSettings
|
2026-07-28 13:35:04 +08:00
|
|
|
from manager.widgets import WidgetManager
|
|
|
|
|
from window import LauncherMainWindow
|
2026-07-14 01:13:36 +08:00
|
|
|
|
2026-07-23 00:47:44 +08:00
|
|
|
class Launcher(QApplication, LauncherObject):
|
2026-08-03 00:16:12 +08:00
|
|
|
def __init__(self, logger, debug: bool = False):
|
2026-07-10 20:57:31 +08:00
|
|
|
super().__init__()
|
2026-08-04 17:39:38 +08:00
|
|
|
self._system_palette = QPalette(self.palette()) # theme
|
|
|
|
|
|
2026-07-12 00:06:11 +08:00
|
|
|
# logger
|
2026-07-28 13:35:04 +08:00
|
|
|
self.logger = logger
|
2026-08-03 00:16:12 +08:00
|
|
|
self.debug = debug
|
2026-07-10 23:27:21 +08:00
|
|
|
|
2026-07-12 00:06:11 +08:00
|
|
|
# dirs
|
|
|
|
|
self.program_dir = Path(__file__).parent
|
|
|
|
|
self.work_dir = Path.cwd()
|
|
|
|
|
|
2026-07-28 13:35:04 +08:00
|
|
|
# Widget (Signal, Event)
|
|
|
|
|
self.widget_manager = WidgetManager()
|
2026-07-29 00:03:25 +08:00
|
|
|
# Manager
|
2026-08-03 20:55:37 +08:00
|
|
|
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()
|
2026-08-04 17:39:38 +08:00
|
|
|
|
|
|
|
|
general = self.settings_manager.get("general", GeneralSettings)
|
|
|
|
|
self.i18n = TranslationManager(
|
|
|
|
|
self.resources_dir / "translations",
|
|
|
|
|
general.language,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.apply_theme()
|
|
|
|
|
|
2026-07-29 00:03:25 +08:00
|
|
|
self.profile_manager = ProfileManager(self, self.widget_manager)
|
2026-08-03 00:16:12 +08:00
|
|
|
self.account_manager = AccountManager(self, self.widget_manager)
|
|
|
|
|
self.game_manager = GameManager(self)
|
2026-07-29 00:03:25 +08:00
|
|
|
|
2026-07-28 13:35:04 +08:00
|
|
|
# Window config
|
2026-08-03 22:15:55 +08:00
|
|
|
self.setApplicationName("TapLauncher")
|
2026-07-28 13:35:04 +08:00
|
|
|
self.main_window = LauncherMainWindow(
|
2026-07-30 21:43:19 +08:00
|
|
|
app=self
|
2026-07-28 13:35:04 +08:00
|
|
|
)
|
|
|
|
|
self.main_window.setGeometry(0, 0, 800, 600)
|
|
|
|
|
move_window_to_center(self.main_window)
|
|
|
|
|
|
2026-07-14 01:13:36 +08:00
|
|
|
# Set icon
|
2026-08-04 19:40:50 +08:00
|
|
|
if self.icon_path.exists() and platform.system().lower() != "darwin":
|
2026-07-14 01:13:36 +08:00
|
|
|
self.setWindowIcon(QIcon(self.icon_path.as_posix()))
|
|
|
|
|
else:
|
|
|
|
|
self.logger.error("No icon found.")
|
|
|
|
|
|
2026-07-10 20:57:31 +08:00
|
|
|
def exec(self):
|
2026-07-29 00:03:25 +08:00
|
|
|
# init managers
|
|
|
|
|
self.profile_manager.profile_load.emit()
|
2026-08-03 00:16:12 +08:00
|
|
|
self.account_manager.account_load.emit()
|
2026-07-29 00:03:25 +08:00
|
|
|
|
2026-08-03 00:16:12 +08:00
|
|
|
self.main_window.show()
|
|
|
|
|
self.main_window.toolbar.update_all()
|
2026-07-10 23:27:21 +08:00
|
|
|
self.exec_()
|
2026-07-12 00:06:11 +08:00
|
|
|
|
2026-08-03 00:16:12 +08:00
|
|
|
def get_icon(self, path: Path, no_color_change: bool = False) -> QIcon:
|
2026-07-14 22:22:58 +08:00
|
|
|
icon = self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning)
|
|
|
|
|
if not path.exists() or not path.is_file():
|
|
|
|
|
messagebox.error(
|
|
|
|
|
self.main_window,
|
2026-08-04 17:39:38 +08:00
|
|
|
self.tr_text("Resource Error"),
|
|
|
|
|
self.tr_text("Missing icon file: {path}", path=path)
|
2026-07-14 22:22:58 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return icon
|
|
|
|
|
|
|
|
|
|
try:
|
2026-08-03 00:16:12 +08:00
|
|
|
if is_light_theme() and path.name.endswith(".png") and not no_color_change:
|
2026-07-14 22:22:58 +08:00
|
|
|
icon = recolor_icon(
|
|
|
|
|
path
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
icon = QIcon(path.as_posix())
|
|
|
|
|
except Exception as e:
|
|
|
|
|
messagebox.error(
|
|
|
|
|
self.main_window,
|
2026-08-04 17:39:38 +08:00
|
|
|
self.tr_text("Resource Error"),
|
|
|
|
|
self.tr_text("Failed to load icon {path}: {error}", path=path, error=e)
|
2026-07-14 22:22:58 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return icon
|
|
|
|
|
|
2026-08-04 17:39:38 +08:00
|
|
|
def tr_text(self, key: str, /, **values) -> str:
|
|
|
|
|
"""Translate a stable English source string for the active language."""
|
|
|
|
|
return self.i18n.translate(key, **values)
|
|
|
|
|
|
|
|
|
|
def apply_language(self, language: str) -> None:
|
|
|
|
|
if language != self.i18n.language:
|
|
|
|
|
self.i18n.set_language(language)
|
|
|
|
|
messagebox.info(None,"Launcher",
|
|
|
|
|
self.tr_text("To make all page apply new language."
|
|
|
|
|
" Is recommend to restart the launcher after change it."))
|
|
|
|
|
|
|
|
|
|
def set_reloadable_toolbutton_icon(
|
|
|
|
|
self,
|
|
|
|
|
button: QToolButton,
|
|
|
|
|
path: Path,
|
|
|
|
|
no_color_change: bool = False,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Set an icon and retain its source so theme changes can reload it."""
|
|
|
|
|
button.setProperty("themeIconPath", str(path))
|
|
|
|
|
button.setProperty("themeIconNoColorChange", no_color_change)
|
|
|
|
|
button.setIcon(self.get_icon(path, no_color_change))
|
|
|
|
|
|
|
|
|
|
def reload_toolbutton_icons(self) -> None:
|
|
|
|
|
for button in self.allWidgets():
|
|
|
|
|
if not isinstance(button, QToolButton):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
path = button.property("themeIconPath")
|
|
|
|
|
if not path:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
no_color_change = bool(
|
|
|
|
|
button.property("themeIconNoColorChange")
|
|
|
|
|
)
|
|
|
|
|
button.setIcon(
|
|
|
|
|
self.get_icon(Path(path), no_color_change)
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-14 22:22:58 +08:00
|
|
|
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,
|
2026-08-04 17:39:38 +08:00
|
|
|
self.tr_text("Resource Error"),
|
|
|
|
|
self.tr_text("Missing icon file: {path}", path=path)
|
|
|
|
|
if not custom_error else custom_error
|
2026-07-14 22:22:58 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return picture
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
picture = QPixmap(path.as_posix())
|
|
|
|
|
except Exception as e:
|
|
|
|
|
messagebox.error(
|
|
|
|
|
self.main_window,
|
2026-08-04 17:39:38 +08:00
|
|
|
self.tr_text("Resource Error"),
|
|
|
|
|
self.tr_text("Failed to load icon {path}: {error}", path=path, error=e)
|
2026-07-14 22:22:58 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return picture
|
|
|
|
|
|
2026-07-12 00:06:11 +08:00
|
|
|
@property
|
|
|
|
|
def temp_dir(self):
|
|
|
|
|
return self.work_dir / "temp"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def config_dir(self):
|
|
|
|
|
return self.work_dir / "config"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def data_dir(self):
|
|
|
|
|
return self.work_dir / "data"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def log_dir(self):
|
2026-07-14 01:13:36 +08:00
|
|
|
return self.work_dir / "log"
|
|
|
|
|
|
2026-08-03 20:55:37 +08:00
|
|
|
@property
|
|
|
|
|
def root_settings_dir(self):
|
|
|
|
|
return self.data_dir / "settings.toml"
|
|
|
|
|
|
2026-07-29 00:03:25 +08:00
|
|
|
@property
|
|
|
|
|
def default_game_dir(self) -> Path:
|
2026-08-03 22:15:55 +08:00
|
|
|
# Completely with official Minecraft launcher
|
|
|
|
|
return self.work_dir
|
2026-07-29 00:03:25 +08:00
|
|
|
|
2026-07-14 01:13:36 +08:00
|
|
|
@property
|
|
|
|
|
def resources_dir(self):
|
|
|
|
|
return self.program_dir / "resources"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def icon_path(self):
|
|
|
|
|
return self.resources_dir / "icons" / "icon.png"
|
|
|
|
|
|
2026-07-14 16:41:14 +08:00
|
|
|
@property
|
|
|
|
|
def icon_dir(self):
|
|
|
|
|
return self.resources_dir / "icons"
|
|
|
|
|
|
2026-07-14 01:13:36 +08:00
|
|
|
@property
|
|
|
|
|
def launcher_version(self):
|
|
|
|
|
return LAUNCHER_VERSION
|
2026-07-14 16:41:14 +08:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def styles_path(self):
|
|
|
|
|
return self.resources_dir / "styles"
|
|
|
|
|
|
|
|
|
|
def apply_qss(self, qss_relative_path: Path, apply_qss_callback: Callable[[str], None]) -> str:
|
|
|
|
|
full_path = self.styles_path / qss_relative_path
|
|
|
|
|
if not full_path.exists():
|
|
|
|
|
messagebox.error(
|
2026-08-03 16:02:45 +08:00
|
|
|
None,
|
2026-08-04 17:39:38 +08:00
|
|
|
self.tr_text("Resource Error"),
|
|
|
|
|
self.tr_text("Missing stylesheet file: {path}", path=full_path),
|
2026-07-14 16:41:14 +08:00
|
|
|
)
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
data = full_path.read_text()
|
|
|
|
|
apply_qss_callback(data)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
messagebox.error(
|
|
|
|
|
self.main_window,
|
2026-08-04 17:39:38 +08:00
|
|
|
self.tr_text("Resource Error"),
|
|
|
|
|
self.tr_text(
|
|
|
|
|
"Unable to apply QSS stylesheet {path}: {error}",
|
|
|
|
|
path=full_path,
|
|
|
|
|
error=e,
|
|
|
|
|
),
|
2026-07-14 16:41:14 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
|
2026-07-28 13:35:04 +08:00
|
|
|
def get_main_window(self):
|
|
|
|
|
return self.main_window
|
2026-08-04 17:39:38 +08:00
|
|
|
|
2026-08-04 19:34:53 +08:00
|
|
|
def apply_theme(self, theme=None):
|
2026-08-04 17:39:38 +08:00
|
|
|
if not theme:
|
|
|
|
|
appearance = self.settings_manager.get("appearance", AppearanceSettings)
|
|
|
|
|
|
|
|
|
|
theme = appearance.theme
|
|
|
|
|
|
|
|
|
|
if theme == "system":
|
|
|
|
|
palette = self._system_palette
|
|
|
|
|
elif theme == "dark":
|
|
|
|
|
palette = self._dark_palette()
|
|
|
|
|
elif theme == "light":
|
|
|
|
|
palette = self._light_palette()
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Unsupported theme: {theme}")
|
|
|
|
|
|
|
|
|
|
self.setPalette(palette)
|
|
|
|
|
|
|
|
|
|
for widget in self.allWidgets():
|
|
|
|
|
widget.style().unpolish(widget)
|
|
|
|
|
widget.style().polish(widget)
|
|
|
|
|
widget.update()
|
|
|
|
|
|
|
|
|
|
if hasattr(self, "main_window"):
|
|
|
|
|
self.reload_toolbutton_icons()
|
|
|
|
|
self.main_window.toolbar.update_all()
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _light_palette() -> QPalette:
|
|
|
|
|
palette = QPalette()
|
|
|
|
|
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Window, QColor("#f5f5f5"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.WindowText, QColor("#202020"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Base, QColor("#ffffff"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#eeeeee"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Text, QColor("#202020"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Button, QColor("#ffffff"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.ButtonText, QColor("#202020"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Mid, QColor("#c5c5c5"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Midlight, QColor("#e2e2e2"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Dark, QColor("#aaaaaa"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Highlight, QColor("#bf265e"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
|
|
|
|
|
palette.setColor(
|
|
|
|
|
QPalette.ColorRole.PlaceholderText,
|
|
|
|
|
QColor("#707070"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return palette
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _dark_palette() -> QPalette:
|
|
|
|
|
palette = QPalette()
|
|
|
|
|
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Window, QColor("#202124"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.WindowText, QColor("#f1f3f4"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Base, QColor("#17181a"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#292a2d"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Text, QColor("#f1f3f4"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Button, QColor("#292a2d"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.ButtonText, QColor("#f1f3f4"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Mid, QColor("#4a4c50"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Midlight, QColor("#383a3e"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Dark, QColor("#121315"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.Highlight, QColor("#bf265e"))
|
|
|
|
|
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
|
|
|
|
|
palette.setColor(
|
|
|
|
|
QPalette.ColorRole.PlaceholderText,
|
|
|
|
|
QColor("#a0a4aa"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return palette
|