diff --git a/assets/background.png b/assets/background.png index a01e03c..5d7a852 100644 Binary files a/assets/background.png and b/assets/background.png differ diff --git a/assets/styles/minecraft.qss b/assets/styles/minecraft.qss index 97e0ddc..34f2731 100644 --- a/assets/styles/minecraft.qss +++ b/assets/styles/minecraft.qss @@ -1,5 +1,6 @@ * { font-family : "Monocraft"; + background-image : ":/assets/background.png" } QPushButton { @@ -17,7 +18,7 @@ QPushButton::pressed { QPushButton::hover { border-image : url(":/assets/button-pressed.png"); - color : white; + color : yellow; text-decoration : underline; border : none; } @@ -32,11 +33,20 @@ QMainWindow QLabel { color : white; } +QMenu::item:selected { + color: yellow; + text-decoration: underline; +} + QStatusBar { color : white; background-color : black; } -QWidget { +QMenu { + background-color : black; +} + +QMenuBar { background-color : black; } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 27f550a..f0cdc1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "This is a custom Free/Libre Minecraft LCE Launcher written in Pyt requires-python = "==3.12.*" license = "GPL-3.0-or-later" readme = "readme.md" -lisence-files = "license.md" +license-files = "license.md" dependencies = [ "pyside6", @@ -17,7 +17,8 @@ dependencies = [ "rich", "term-image", "pillow", - "pip" + "pip", + "hatch" ] [project.urls] @@ -64,6 +65,8 @@ files = [ "src/json_trans.py", "src/main.py", "src/cli.py", + "src/term_service.py", + "src/features.py", "src/browser_dialog.py", "pkg/LCE-Qt-Launcher.desktop", "license.md", diff --git a/readme.md b/readme.md index 4cce825..563715f 100644 --- a/readme.md +++ b/readme.md @@ -8,13 +8,25 @@ ## About -This is a custom MCLCE Launcher written in python and Qt with Freedom and GNU/Linux support in mind. +This is a custom Minecraft LCE Launcher written in Python and Qt with Freedom and GNU/Linux support in mind. ## Features -- cli.y : simple way to install and launch the game -- launcher.py - +- Command Line interface +- Qt 6 GUI (Native like Interface) +- Written in Python (No Electron or Rust) +- Customisable +- Free Software (GPLv3) +- Multiple Instance (Work in progress) + +- GNU/Linux compatibility first (with secondary Windows and FreeBSD support) +- Focus on being the main hub for Minecraft LCE on GNU/Linux +- Minecraft Theme pre-configured + ## Software Requirement - [Python 3.12.x](https://www.python.org/downloads/latest/python3.12/) diff --git a/res.qrc b/res.qrc index bc57bcb..85be65c 100644 --- a/res.qrc +++ b/res.qrc @@ -5,7 +5,7 @@ assets/Launcher_medium.png assets/Launcher.png assets/minecraft.png - assets/minecraft-lce-title.png + assets/minecraft-lce-title.png assets/button.png assets/button-pressed.png assets/app.ico diff --git a/src/build_info.py b/src/build_info.py index 9cce50e..2c34cf2 100644 --- a/src/build_info.py +++ b/src/build_info.py @@ -2,6 +2,8 @@ from PySide6.QtCore import qVersion from system_manager import SystemManager +import term_service + from pathlib import Path import os @@ -71,12 +73,10 @@ try: _LICENSE_LINK = license_url_metadata if license_metadata is not "" or None: _LICENSE = license_metadata - except importlib.metadata.PackageNotFoundError: pass - except RuntimeError: - print(f"Error : Metadata not found! More info : {RuntimeError.with_traceback}") + term_service.print_error(f"Metadata not found! More info : {RuntimeError.with_traceback}") class BuildInfo: def __init__(self): diff --git a/src/cli.py b/src/cli.py old mode 100755 new mode 100644 index dfcde46..15679a2 --- a/src/cli.py +++ b/src/cli.py @@ -1,10 +1,6 @@ -#!/usr/bin/env python3 - -from term_image.image import from_file -from rich import print - from instance_manager import InstanceManager, Instance +import term_service import build_info import os @@ -16,14 +12,19 @@ MENU_STR = """ """ def launch_cli(): - - ascii_art = os.path.join(build_info.get_assets_dir(), "ascii-art-text.png") - - image = from_file(ascii_art) - - image.draw() - - print(MENU_STR) + try: + ascii_art = os.path.join(build_info.get_assets_dir(), "ascii-art-text.png") + term_service.show_image(ascii_art) + except FileNotFoundError: + term_service.print_error("Image cannot be found.") + print("LCE Qt Launcher") + except PermissionError: + term_service.print_error("File Permission error on the file") + print("LCE Qt Launcher") + except: + term_service.print_error("An unkown error occurred") + print("LCE Qt Launcher") + term_service.print_pretty(MENU_STR) user_output = input() @@ -35,4 +36,4 @@ def launch_cli(): if user_output == "2": instanceManager.install_instance() else: - print("not implemented yet") + term_service.print_information("Not implemented Yet!") diff --git a/src/downloader.py b/src/downloader.py index 9ad5c42..00f7fd6 100644 --- a/src/downloader.py +++ b/src/downloader.py @@ -1,6 +1,8 @@ from __future__ import annotations from typing import TYPE_CHECKING +import term_service + if TYPE_CHECKING: from instance_manager import Instance from build_info import BuildInfo @@ -18,15 +20,17 @@ class Downloader: self._build_info = build_info def download_instance(self, instance : Instance): + print("Go to installation") response = requests.get(instance.get_download_url()) + response.raise_for_status() if response.status_code == SUCCESS_STATUS_CODE: - print(f"Download of {instance.name} from {instance.get_download_url} was a success") + term_service.print_success(f"Download of {instance.name} from {instance.get_download_url} was a success") try: archive : ZipFile = self.extract_instance(response, instance) except BadZipFile as err: - print(f"Error : {err} while extracting {archive.filename}") + term_service.print_error(f"{err} while extracting {archive.filename}") except LargeZipFile as err: - print(f"Error : {archive.filename} was too big.") + term_service.print_error(f"{archive.filename} was too big.") else: if os.name == "posix": exe_abs_path = os.path.join(instance.installation_path, instance.exe_name) diff --git a/src/features.py b/src/features.py new file mode 100644 index 0000000..c340f75 --- /dev/null +++ b/src/features.py @@ -0,0 +1,67 @@ +from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QDialog, QMessageBox, QFileDialog, QInputDialog +from PySide6.QtGui import QPalette, QPixmap, QBrush +from PySide6.QtCore import qVersion +from user_pref import UserPref +from build_info import BuildInfo +from instance_manager import InstanceManager, Instance +from cmd_arg import CmdArgAction, parse_args, argsDetected + +import term_service + +from browser_dialog import BrowserDialog +from setting_dialog import SettingDialog +from cli import launch_cli + +import sys +import os +import platform + +def confirm_changes(parent): + pass + +def launch_game(parent): + pass + +def launch_game(parent): + pass + +def launch_setting(parent): + pass + +def load_instance(parent): + pass + +def new_instance_from_form(mainWindow : QMainWindow): + form = mainWindow.ui + + username_str = form.usernameInputBox.text() + path_str = form.pathInputBox.text() + server_ip_str = form.serverIPInputBox.text() + server_name_str = form.serverNameInputBox.text() + repo_url_str = form.repoURLInputBox.text() + + instance_name = QInputDialog.getText(mainWindow, "Name your instance", "Set the name of the instance") + + newInstance = Instance() + + if instance_name: + newInstance.name = instance_name + if username_str: + newInstance.username = username_str + if path_str: + newInstance.installation_path = path_str + if server_ip_str and server_name_str: + newInstance.servers = {f"ip : {server_ip_str}, name : {server_name_str}" } + if repo_url_str: + newInstance.repo_url = repo_url_str + + return newInstance; + +def setup_update(parent): + pass + +def hide_options(parent): + pass + +def show_webpage(parent): + pass \ No newline at end of file diff --git a/src/form.ui b/src/form.ui index 762a3e5..cbb5e10 100644 --- a/src/form.ui +++ b/src/form.ui @@ -10,11 +10,6 @@ 600 - - - Adwaita Sans - - Minecraft LCE QT Launcher @@ -26,11 +21,6 @@ false - - - Adwaita Sans - - false @@ -148,17 +138,11 @@ 50 - - - Adwaita Sans - 31 - - - :/assets/minecraft_lce_titl;e.png + :/assets/minecraft_lce_title.png @@ -186,7 +170,7 @@ 0 0 800 - 27 + 29 @@ -413,6 +397,9 @@ true + + false + See Instance Options @@ -426,7 +413,6 @@ - diff --git a/src/instance_manager.py b/src/instance_manager.py index 3176fbf..fc5d5ae 100644 --- a/src/instance_manager.py +++ b/src/instance_manager.py @@ -3,6 +3,8 @@ from enum import Enum from downloader import Downloader from build_info import BuildInfo +import term_service + from subprocess import TimeoutExpired import subprocess @@ -63,18 +65,17 @@ class InstanceManager: try: game_process = subprocess.run(self.instance.installation_path + self.instance.exe_name) except TimeoutExpired as err: - print(f"Error : process of lauching instance {self.instance.name} Failed. Reason : Timeout Expired.\n traceback : {err.with_traceback}") - return f"Error : process of lauching instance {self.instance.name} Failed. Reason : Timeout Expired.\n traceback : {err.with_traceback}" + term_service.print_error(f"process of lauching instance {self.instance.name} Failed. Reason : Timeout Expired.\n traceback : {err.with_traceback}") + return f"process of lauching instance {self.instance.name} Failed. Reason : Timeout Expired.\n traceback : {err.with_traceback}" except PermissionError as err: - print(f"Error : Cannot launch {self.instance.name}. Reason : Permission Denied.\n traceback : {err.with_traceback}") - return f"Error : Cannot launch {self.instance.name}. Reason : Permission Denied.\n traceback : {err.with_traceback}" - + term_service.print_error(f"Cannot launch {self.instance.name}. Reason : Permission Denied.\n traceback : {err.with_traceback}") + return f"Cannot launch {self.instance.name}. Reason : Permission Denied.\n traceback : {err.with_traceback}" else: return f"Client closed with code {game_process.returncode}" - def install_instance(self) -> str: - if self.instance in [InstanceType.GITHUB_RELEASE, InstanceType.REMOTE_GIT_SOURCE]: - self._downloader.download_instance(self.instance) + def install_instance(self): + if self.instance.instance_type in [InstanceType.GITHUB_RELEASE, InstanceType.REMOTE_GIT_SOURCE]: + return self._downloader.download_instance(self.instance) else: return "Already Installed, skip installation." @@ -93,11 +94,11 @@ class InstanceManager: f.write(json_string) def load_instance(self, save_file : str): - print("Not implemented Yet!") - #if not save_file.endswith(self._build_info.instance_extension): - # save_file = os.path.join(save_file, ".lce_inst") + term_service.print_information("Not Implemented Yet!") + if not save_file.endswith(self._build_info.instance_extension): + save_file = os.path.join(save_file, ".lce_inst") - #with open(save_file, 'r') as f: - #json_file = f.read(save_file) + with open(save_file, 'r') as f: + json_file = f.read(save_file) - #self.instance = json.load(json_file) + self.instance = json.load(json_file) diff --git a/src/json_trans.py b/src/json_trans.py index 314ce3d..0e63b9d 100644 --- a/src/json_trans.py +++ b/src/json_trans.py @@ -4,6 +4,7 @@ import json import os import build_info +import term_service class JsonTrans(QObject): @@ -21,7 +22,7 @@ class JsonTrans(QObject): file_path: str = os.path.join(build_info.os, f"{lang_code}.json") if not os.path.exists(path=file_path): - print(f"Error: Language file {file_path} not found. Defaulting to English Fallback.") + term_service.print_error(f"Language file {file_path} not found. Defaulting to English Fallback") return try: @@ -30,7 +31,7 @@ class JsonTrans(QObject): self._current_lang = lang_code self.languageChanged.emit() # Notify the UI except Exception as e: - print(f"Error loading JSON: {e}. Defaulting to English Fallback.") + term_service.print_error(f"loading JSON: {e}. Defaulting to English Fallback.") def translate(self, key: str, default: str = "") -> str: return self.json_data.get(key, default or key) \ No newline at end of file diff --git a/src/main.py b/src/main.py index 50189a0..f3dc8f9 100755 --- a/src/main.py +++ b/src/main.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 -from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QDialog, QMessageBox, QFileDialog, QInputDialog -from PySide6.QtGui import QPalette, QPixmap, QBrush, Qt +from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QDialog, QMessageBox, QFileDialog +from PySide6.QtGui import QPalette, QPixmap, QBrush from PySide6.QtCore import qVersion from user_pref import UserPref from build_info import BuildInfo from instance_manager import InstanceManager, Instance from cmd_arg import CmdArgAction, parse_args, argsDetected +import term_service +import features + from browser_dialog import BrowserDialog from setting_dialog import SettingDialog from cli import launch_cli @@ -37,7 +40,7 @@ HELP_STR = """ -v or --version to get the app version -L or --license to get the license information -a or --about to get information about the app --c or --cli to launch the cli version +-cl or --cli to launch the cli version -g or --gen-config to generate or update the app config """ @@ -45,34 +48,13 @@ from ui_form import Ui_launcher from ui_system_info import Ui_sys_info_dialog def gen_inst_from_form(parent): - username_str = parent.ui.usernameInputBox.text() - path_str = parent.ui.pathInputBox.text() - server_ip_str = parent.ui.serverIPInputBox.text() - server_name_str = parent.ui.serverNameInputBox.text() - repo_url_str = parent.ui.repoURLInputBox.text() - - instance_name = QInputDialog.getText(parent, "Name your instance", "Set the name of the instance") - - newInstance = Instance() - - if instance_name: - newInstance.name = instance_name - if username_str: - newInstance.username = username_str - if path_str: - newInstance.installation_path = path_str - if server_ip_str and server_name_str: - newInstance.servers = { f"ip : {server_ip_str}, name : {server_name_str}" } - if repo_url_str: - newInstance.repo_url = repo_url_str - - instanceManager.instance = newInstance + instanceManager.instance = features.new_instance_from_form(parent) def about_to_quit_event(): - anwser = QMessageBox.question("Instance Manager", "Do you want to save the instance to a file ?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + anwser = QMessageBox.question(None, "Instance Manager", "Do you want to save the instance to a file ?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) if anwser == QMessageBox.StandardButton.Yes: - file_name = QFileDialog.getSaveFileName("Set the instance save file path", f"{buildInfo.system_manager.found_default_save_path }(\"LCE Instance Save File\" (*{buildInfo.instance_extension}))") - instanceManager.save_instance(file_name) + file_name = QFileDialog.getSaveFileName(None, "Set the instance save file path", f"{buildInfo.system_manager.found_default_save_path }(\"LCE Instance Save File\" (*{buildInfo.instance_extension}))") + instanceManager.save_instance(file_name[0]) class launcher(QMainWindow): def confirm_changes_button(self): @@ -81,7 +63,7 @@ class launcher(QMainWindow): def update_page(self): BrowserDialog(self, buildInfo.git_repo_url) def launch(self): - print("Starting the game!") + term_service.print_information("Starting the game!") instanceManager.play() def install(self): @@ -91,7 +73,7 @@ class launcher(QMainWindow): " so a backup is recommended.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) if button_reply == QMessageBox.StandardButton.Yes: - print("Starting Installation") + term_service.print_information("Starting Installation") self.ui.progressLabel.setVisible(True) self.ui.progressBar.setVisible(True) self.ui.progressBar.setEnabled(True) @@ -100,7 +82,7 @@ class launcher(QMainWindow): instanceManager.install_instance() self.ui.progressBar.setValue(100) else: - QMessageBox.information(self, "Minecraft LCE Qt Launcher" "Installation Cancelled") + QMessageBox.critical(self, "Minecraft LCE Qt Launcher" "Installation Cancelled") def show_aboutQt(self): print("Show About Qt popup.") @@ -157,6 +139,8 @@ class launcher(QMainWindow): palette.setBrush(QPalette.ColorRole.Window, QBrush(background_pixmap)) self.setPalette(palette) self.setAutoFillBackground(True) + else: + term_service.print_error("Cannot set the background") self.ui = Ui_launcher() @@ -203,30 +187,30 @@ if __name__ == "__main__": if action == CmdArgAction.GEN_CONFIG: userPref.generate_default_config() if action == CmdArgAction.PRINT_LICENSE: - print(f"{buildInfo.app_name} is licensed via the {buildInfo.license} License.") - print(f"See {buildInfo.license_link} for more info") + term_service.print_information(f"{buildInfo.app_name} is licensed via the {buildInfo.license} License.") + term_service.print_information(f"See {buildInfo.license_link} for more info") if action == CmdArgAction.PRINT_HELP: print(HELP_STR) if action == CmdArgAction.PRINT_ABOUT_INFO: - print("This is a custom MCLCE Launcher written in python and Qt with Freedom and GNU/Linux support in mind.") + term_service.print_information("This is a custom Minecraft LCE Launcher written in Python and Qt with Freedom and GNU/Linux support in mind.") if action == CmdArgAction.PRINT_VERSION: - print(f"{buildInfo.app_name} Version {buildInfo.version}") - print(f"Qt Version {buildInfo.qt_version}") + term_service.print_information(f"{buildInfo.app_name} Version {buildInfo.version}") + term_service.print_information(f"Qt Version {buildInfo.qt_version}") if action == CmdArgAction.CLI_VERSION: launch_cli() else: - print("Not Implemented Yet!") + term_service.print_information("Not Implemented Yet!") else: app = QApplication(sys.argv) try: with open("assets/styles/minecraft.qss", "r") as file: app.setStyleSheet(file.read()) except FileNotFoundError: - print(f"Error : :/styles/minecraft.qss file not found. Reverting to default theme") + QMessageBox.warning(None,"Error", ":/styles/minecraft.qss file not found. Reverting to default theme") widget = launcher() widget.show() - app.aboutToQuit.connect(instanceManager.save_instance) + app.aboutToQuit.connect(gen_inst_from_form) sys.exit(app.exec()) diff --git a/src/term_service.py b/src/term_service.py new file mode 100644 index 0000000..951056b --- /dev/null +++ b/src/term_service.py @@ -0,0 +1,18 @@ +from rich import print + +def print_error(msg : str): + print(f"[bold red]Error![/bold red] {msg}") + +def print_information(msg : str): + print(f"[yellow]{msg}[/yellow]") + +def print_pretty(msg : str): + print(msg) + +def print_success(msg : str): + print(f"[green] {msg}") + +def show_image(path : str): + from term_image.image import from_file + image = from_file(path) + image.draw() \ No newline at end of file diff --git a/src/theme.py b/src/theme.py index 2c566ab..c6379d1 100644 --- a/src/theme.py +++ b/src/theme.py @@ -2,8 +2,6 @@ from enum import StrEnum class Theme(StrEnum): MINECRAFT = ":/styles/minecraft.qss" - SYSTEM = "" DARK = ":/styles/minecraft.qss" LIGHT = ":/sytles/light.qss" - CUSTOM = "" - + SYSTEM = "" \ No newline at end of file