Compare commits

..

No commits in common. "main" and "0.12.1" have entirely different histories.
main ... 0.12.1

8 changed files with 380 additions and 690 deletions

36
.github/workflows/Bleeding-Job.yaml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Bleeding Update version
on:
push:
branches:
- main
jobs:
update-version:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Update version.json
run: |
git fetch --prune --unshallow
commit_count=$(git rev-list --count HEAD)
version=$(jq -r '.version' version.json)
jq --arg versionBleeding "$version-$commit_count" '. + {versionBleeding: $versionBleeding}' version.json > version.tmp && mv version.tmp version.json
- name: Commit and push changes
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add version.json
git commit -m "Update version.json with commit count"
git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -27,7 +27,7 @@
</p> </p>
Picodulce is a feature-rich launcher for Minecraft, developed using Qt5. It serves as a graphical user interface (GUI) for the [zucaro backend](https://github.com/nixietab/zucaro), providing users with a seamless experience in managing and launching game versions. Picodulce is a feature-rich launcher for Minecraft, developed using Qt5. It serves as a graphical user interface (GUI) for the picomc project, providing users with a seamless experience in managing and launching game versions.
![imagen](https://github.com/user-attachments/assets/115b39be-47d3-4ac7-893a-5849c1e4570c) ![imagen](https://github.com/user-attachments/assets/115b39be-47d3-4ac7-893a-5849c1e4570c)
@ -61,7 +61,7 @@ makepkg -si
``` git clone https://github.com/nixietab/picodulce ``` ``` git clone https://github.com/nixietab/picodulce ```
### 2. (Optional) Set Up a Virtual Environment ### 2. (Optional) Set Up a Virtual Environment
Setting up a virtual environment is recommended to avoid dependency conflicts. Picodulce relies on the path of the `zucaro` project, and using a virtual environment helps prevent errors. Setting up a virtual environment is recommended to avoid dependency conflicts. Picodulce relies on the path of the `picomc` project, and using a virtual environment helps prevent errors.
Create the virtual environment: Create the virtual environment:
@ -86,7 +86,7 @@ On the venv run it as a normal python script
```python picodulce.py``` ```python picodulce.py```
Just make sure you have Java installed for running the actual game, or check the "manage java" option inside the launcher settings Just make sure you have Java installed for running the actual game
### About the name ### About the name
The name "Picodulce" comes from a popular Argentinian candy. This reflects the enjoyable and user-friendly experience that the launcher aims to provide, making game management straightforward and pleasant. The name "Picodulce" comes from a popular Argentinian candy. This reflects the enjoyable and user-friendly experience that the launcher aims to provide, making game management straightforward and pleasant.

View File

@ -1,29 +1,43 @@
import sys import sys
import json import subprocess
import os import re
import uuid
import asyncio
import aiohttp
from datetime import datetime, timezone
from pathlib import Path
from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QVBoxLayout, from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QVBoxLayout,
QPushButton, QLineEdit, QMessageBox) QPushButton, QLineEdit, QMessageBox)
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from picomc.logging import logger
from picomc.launcher import get_default_root, Launcher
# Constants for Microsoft Authentication class AuthenticationParser:
URL_DEVICE_AUTH = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode" @staticmethod
URL_TOKEN = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token" def clean_ansi(text):
URL_XBL = "https://user.auth.xboxlive.com/user/authenticate" ansi_clean = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
URL_XSTS = "https://xsts.auth.xboxlive.com/xsts/authorize" printable_clean = re.compile(r'[^\x20-\x7E\n]')
URL_MC = "https://api.minecraftservices.com/authentication/login_with_xbox" text = ansi_clean.sub('', text)
URL_PROFILE = "https://api.minecraftservices.com/minecraft/profile" text = printable_clean.sub('', text)
return text.strip()
CLIENT_ID = "c52aed44-3b4d-4215-99c5-824033d2bc0f" @staticmethod
SCOPE = "XboxLive.signin offline_access" def is_auth_error(output):
GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code" cleaned_output = AuthenticationParser.clean_ansi(output)
return "AADSTS70016" in cleaned_output and "not yet been authorized" in cleaned_output
@staticmethod
def parse_auth_output(output):
cleaned_output = AuthenticationParser.clean_ansi(output)
if AuthenticationParser.is_auth_error(cleaned_output):
return None
pattern = r"https://[^\s]+"
code_pattern = r"code\s+([A-Z0-9]+)"
url_match = re.search(pattern, cleaned_output)
code_match = re.search(code_pattern, cleaned_output, re.IGNORECASE)
if url_match and code_match:
return {
'url': url_match.group(0),
'code': code_match.group(1)
}
return None
class AuthDialog(QDialog): class AuthDialog(QDialog):
def __init__(self, url, code, parent=None, error_mode=False): def __init__(self, url, code, parent=None, error_mode=False):
@ -32,10 +46,10 @@ class AuthDialog(QDialog):
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setModal(True) self.setModal(True)
self.setup_ui(url, code, error_mode) self.setup_ui(url, code, error_mode)
def setup_ui(self, url, code, error_mode): def setup_ui(self, url, code, error_mode):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
if error_mode: if error_mode:
error_label = QLabel("Error in Login - Please try again") error_label = QLabel("Error in Login - Please try again")
error_label.setStyleSheet("QLabel { color: red; font-weight: bold; }") error_label.setStyleSheet("QLabel { color: red; font-weight: bold; }")
@ -92,254 +106,117 @@ class AuthenticationThread(QThread):
error_occurred = pyqtSignal(str) error_occurred = pyqtSignal(str)
auth_error_detected = pyqtSignal(str) auth_error_detected = pyqtSignal(str)
finished = pyqtSignal() finished = pyqtSignal()
access_token_received = pyqtSignal(dict)
def __init__(self, username): def __init__(self, account):
super().__init__() super().__init__()
self.username = username self.account = account
self.device_code = None self.process = None
self.is_running = True self.is_running = True
self.current_output = ""
async def _ms_oauth(self): self.waiting_for_auth = False
data = {"client_id": CLIENT_ID, "scope": SCOPE}
async with aiohttp.ClientSession() as session:
async with session.post(URL_DEVICE_AUTH, data=data) as resp:
if resp.status != 200:
raise Exception(f"Failed to get device code: {await resp.text()}")
j = await resp.json()
self.device_code = j["device_code"]
self.auth_data_received.emit({
'url': j["verification_uri"],
'code': j["user_code"]
})
while self.is_running:
data = {
"grant_type": GRANT_TYPE,
"client_id": CLIENT_ID,
"device_code": self.device_code
}
async with session.post(URL_TOKEN, data=data) as resp:
j = await resp.json()
if resp.status == 400:
if j["error"] == "authorization_pending":
await asyncio.sleep(2)
continue
else:
raise Exception(j["error_description"])
elif resp.status != 200:
raise Exception(f"Token request failed: {j}")
return j["access_token"], j["refresh_token"]
async def _xbl_auth(self, access_token):
data = {
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": f"d={access_token}"
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}
async with aiohttp.ClientSession() as session:
async with session.post(URL_XBL, json=data) as resp:
if resp.status != 200:
raise Exception(f"XBL auth failed: {await resp.text()}")
j = await resp.json()
return j["Token"], j["DisplayClaims"]["xui"][0]["uhs"]
async def _xsts_auth(self, xbl_token):
data = {
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [xbl_token]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}
async with aiohttp.ClientSession() as session:
async with session.post(URL_XSTS, json=data) as resp:
if resp.status != 200:
raise Exception(f"XSTS auth failed: {await resp.text()}")
j = await resp.json()
return j["Token"]
async def _mc_auth(self, uhs, xsts_token):
data = {
"identityToken": f"XBL3.0 x={uhs};{xsts_token}"
}
async with aiohttp.ClientSession() as session:
async with session.post(URL_MC, json=data) as resp:
if resp.status != 200:
raise Exception(f"MC auth failed: {await resp.text()}")
j = await resp.json()
return j["access_token"]
async def _get_profile(self, mc_token):
headers = {
"Authorization": f"Bearer {mc_token}"
}
async with aiohttp.ClientSession() as session:
async with session.get(URL_PROFILE, headers=headers) as resp:
if resp.status != 200:
raise Exception(f"Profile request failed: {await resp.text()}")
return await resp.json()
async def _auth_flow(self):
try:
ms_access_token, refresh_token = await self._ms_oauth()
xbl_token, uhs = await self._xbl_auth(ms_access_token)
xsts_token = await self._xsts_auth(xbl_token)
mc_token = await self._mc_auth(uhs, xsts_token)
profile = await self._get_profile(mc_token)
self.access_token_received.emit({
'access_token': mc_token,
'refresh_token': refresh_token,
'profile': profile
})
except Exception as e:
self.error_occurred.emit(str(e))
def run(self): def run(self):
try: try:
loop = asyncio.new_event_loop() command = f'picomc account authenticate {self.account}'
asyncio.set_event_loop(loop)
loop.run_until_complete(self._auth_flow()) self.process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
self.current_output = ""
while self.is_running and self.process.poll() is None:
line = self.process.stdout.readline()
if line:
self.current_output += line
if not self.waiting_for_auth:
parsed_data = AuthenticationParser.parse_auth_output(self.current_output)
if parsed_data:
self.auth_data_received.emit(parsed_data)
self.waiting_for_auth = True
self.current_output = ""
elif AuthenticationParser.is_auth_error(self.current_output):
self.auth_error_detected.emit(self.current_output)
self.waiting_for_auth = False
self.current_output = ""
self.process.wait()
self.finished.emit()
except Exception as e: except Exception as e:
self.error_occurred.emit(str(e)) self.error_occurred.emit(str(e))
finally:
self.finished.emit() self.finished.emit()
def send_enter(self):
if self.process and self.process.poll() is None:
self.process.stdin.write("\n")
self.process.stdin.flush()
def stop(self): def stop(self):
self.is_running = False self.is_running = False
if self.process:
self.process.terminate()
class MinecraftAuthenticator(QObject): class MinecraftAuthenticator(QObject): # Changed to inherit from QObject
auth_finished = pyqtSignal(bool) auth_finished = pyqtSignal(bool) # Add signal for completion
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.auth_thread = None self.auth_thread = None
self.current_auth_data = None
self.auth_dialog = None self.auth_dialog = None
self.success = False self.success = False
self.username = None
# Initialize the launcher to get the correct config path
with Launcher.new() as launcher:
self.config_path = launcher.root
def authenticate(self, username): def authenticate(self, username):
self.username = username """
Start the authentication process for the given username
Returns immediately, authentication result will be emitted via auth_finished signal
"""
self.success = False self.success = False
# Create accounts.json if it doesn't exist
if not self.save_to_accounts_json():
return
self.auth_thread = AuthenticationThread(username) self.auth_thread = AuthenticationThread(username)
self.auth_thread.auth_data_received.connect(self.show_auth_dialog) self.auth_thread.auth_data_received.connect(self.show_auth_dialog)
self.auth_thread.auth_error_detected.connect(self.handle_auth_error)
self.auth_thread.error_occurred.connect(self.show_error) self.auth_thread.error_occurred.connect(self.show_error)
self.auth_thread.access_token_received.connect(self.on_access_token_received)
self.auth_thread.finished.connect(self.on_authentication_finished) self.auth_thread.finished.connect(self.on_authentication_finished)
self.auth_thread.start() self.auth_thread.start()
def show_auth_dialog(self, auth_data): def show_auth_dialog(self, auth_data):
self.current_auth_data = auth_data
if self.auth_dialog is not None: if self.auth_dialog is not None:
self.auth_dialog.close() self.auth_dialog.close()
self.auth_dialog = None
self.auth_dialog = AuthDialog(auth_data['url'], auth_data['code']) self.auth_dialog = AuthDialog(auth_data['url'], auth_data['code'])
if self.auth_dialog.exec_() == QDialog.Accepted:
result = self.auth_dialog.exec_() self.auth_thread.send_enter()
if result != QDialog.Accepted:
self.auth_thread.stop()
def show_error(self, error_msg): def handle_auth_error(self, output):
QMessageBox.critical(None, "Error", error_msg) if self.current_auth_data:
if self.auth_dialog is not None:
self.auth_dialog.close()
self.auth_dialog = None
self.auth_dialog = AuthDialog(
self.current_auth_data['url'],
self.current_auth_data['code'],
error_mode=True
)
if self.auth_dialog.exec_() == QDialog.Accepted:
self.auth_thread.send_enter()
def show_error(self, error_message):
QMessageBox.critical(None, "Error", f"Authentication error: {error_message}")
self.success = False self.success = False
self.auth_finished.emit(False) self.auth_finished.emit(False)
def save_to_accounts_json(self):
try:
accounts_file = Path(self.config_path) / "accounts.json"
if accounts_file.exists():
with open(accounts_file) as f:
config = json.load(f)
else:
config = {
"default": None,
"accounts": {},
"client_token": str(uuid.uuid4())
}
accounts_file.parent.mkdir(parents=True, exist_ok=True)
# Only create/update if account doesn't exist
if self.username not in config["accounts"]:
config["accounts"][self.username] = {
"uuid": "-",
"online": True,
"microsoft": True,
"gname": "-",
"access_token": "-",
"refresh_token": "-",
"is_authenticated": False
}
# Set as default if no default exists
if config["default"] is None:
config["default"] = self.username
with open(accounts_file, 'w') as f:
json.dump(config, f, indent=4)
return True
except Exception as e:
logger.error(f"Failed to initialize account data: {str(e)}")
QMessageBox.critical(None, "Error", f"Failed to initialize account data: {str(e)}")
return False
def on_access_token_received(self, data):
try:
accounts_file = Path(self.config_path) / "accounts.json"
with open(accounts_file) as f:
config = json.load(f)
if self.username in config["accounts"]:
config["accounts"][self.username].update({
"access_token": data['access_token'],
"refresh_token": data['refresh_token'],
"uuid": data['profile']['id'],
"gname": data['profile']['name'],
"is_authenticated": True
})
with open(accounts_file, 'w') as f:
json.dump(config, f, indent=4)
self.success = True
QMessageBox.information(None, "Success",
f"Successfully authenticated account: {self.username}")
else:
raise Exception("Account not found in configuration")
except Exception as e:
logger.error(f"Failed to update account data: {str(e)}")
QMessageBox.critical(None, "Error", f"Failed to update account data: {str(e)}")
self.success = False
self.auth_finished.emit(self.success)
def on_authentication_finished(self): def on_authentication_finished(self):
if self.auth_dialog is not None: if self.auth_dialog is not None:
self.auth_dialog.close() self.auth_dialog.close()
@ -349,8 +226,8 @@ class MinecraftAuthenticator(QObject):
self.auth_thread.stop() self.auth_thread.stop()
self.auth_thread = None self.auth_thread = None
if not self.success: self.success = True
self.auth_finished.emit(False) self.auth_finished.emit(True)
def cleanup(self): def cleanup(self):
if self.auth_dialog is not None: if self.auth_dialog is not None:
@ -361,6 +238,7 @@ class MinecraftAuthenticator(QObject):
self.auth_thread.stop() self.auth_thread.stop()
self.auth_thread.wait() self.auth_thread.wait()
def create_authenticator(): # Example usage
"""Factory function to create a new MinecraftAuthenticator instance""" if __name__ == '__main__':
return MinecraftAuthenticator() authenticator = MinecraftAuthenticator()
authenticator.authenticate("TestUser")

View File

@ -1,54 +1,6 @@
import os import os
import json import json
import shutil import requests
import modulecli
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QLabel, QProgressBar
from PyQt5.QtCore import Qt, QThread, pyqtSignal
import sys
class CopyThread(QThread):
progress_changed = pyqtSignal(int)
finished = pyqtSignal()
def __init__(self, src_dir, dst_dir):
super().__init__()
self.src_dir = src_dir
self.dst_dir = dst_dir
def run(self):
# Gather all files recursively
files_to_copy = []
for root, dirs, files in os.walk(self.src_dir):
for f in files:
full_path = os.path.join(root, f)
relative_path = os.path.relpath(full_path, self.src_dir)
files_to_copy.append(relative_path)
total_files = len(files_to_copy)
copied_files = 0
for relative_path in files_to_copy:
src_path = os.path.join(self.src_dir, relative_path)
dst_path = os.path.join(self.dst_dir, relative_path)
dst_folder = os.path.dirname(dst_path)
if not os.path.exists(dst_folder):
try:
os.makedirs(dst_folder)
except PermissionError:
print(f"Skipping folder {dst_folder} (permission denied)")
continue
try:
shutil.copy2(src_path, dst_path)
except PermissionError:
print(f"Skipping file {dst_path} (permission denied)")
copied_files += 1
progress_percent = int((copied_files / total_files) * 100)
self.progress_changed.emit(progress_percent)
self.finished.emit()
class HealthCheck: class HealthCheck:
@ -62,120 +14,51 @@ class HealthCheck:
"CheckUpdate": False, "CheckUpdate": False,
"IsBleeding": False, "IsBleeding": False,
"LastPlayed": "", "LastPlayed": "",
"TotalPlaytime": 0,
"IsFirstLaunch": True, "IsFirstLaunch": True,
"Instance": "default", "Instance": "default",
"Theme": "Dark.json", "Theme": "Dark.json",
"ThemeBackground": True, "ThemeBackground": True,
"ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json", "ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json",
"Locale": "en", "Locale": "en"
"ManageJava": False,
"MaxRAM": "2G",
"JavaPath": "",
"ZucaroCheck": False,
} }
# Step 1: Check if the file exists; if not, create it with default values
if not os.path.exists(config_path): if not os.path.exists(config_path):
with open(config_path, "w") as config_file: with open(config_path, "w") as config_file:
json.dump(default_config, config_file, indent=4) json.dump(default_config, config_file, indent=4)
self.config = default_config self.config = default_config
return return
# Step 2: Try loading the config file, handle invalid JSON
try: try:
with open(config_path, "r") as config_file: with open(config_path, "r") as config_file:
self.config = json.load(config_file) self.config = json.load(config_file)
except (json.JSONDecodeError, ValueError): except (json.JSONDecodeError, ValueError):
# File is corrupted, overwrite it with default configuration
with open(config_path, "w") as config_file: with open(config_path, "w") as config_file:
json.dump(default_config, config_file, indent=4) json.dump(default_config, config_file, indent=4)
self.config = default_config self.config = default_config
return return
# Step 3: Check for missing keys and add defaults if necessary
updated = False updated = False
for key, value in default_config.items(): for key, value in default_config.items():
if key not in self.config: if key not in self.config: # Field is missing
self.config[key] = value self.config[key] = value
updated = True updated = True
# Step 4: Save the repaired config back to the file
if updated: if updated:
with open(config_path, "w") as config_file: with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4) json.dump(self.config, config_file, indent=4)
def get_folder_size(self, folder_path):
total_size = 0
for dirpath, dirnames, filenames in os.walk(folder_path):
for f in filenames:
fp = os.path.join(dirpath, f)
if os.path.isfile(fp):
total_size += os.path.getsize(fp)
return total_size
def zucaro_health_check(self):
if self.config.get("ZucaroCheck"):
return
output = modulecli.run_command("instance dir").strip()
instance_dir = os.path.abspath(output)
base_dir = os.path.abspath(os.path.join(instance_dir, "..", ".."))
possible_zucaro = [os.path.join(base_dir, "zucaro"), os.path.join(base_dir, ".zucaro")]
possible_picomc = [os.path.join(base_dir, "picomc"), os.path.join(base_dir, ".picomc")]
zucaro_dir = next((d for d in possible_zucaro if os.path.exists(d)), None)
picomc_dir = next((d for d in possible_picomc if os.path.exists(d)), None)
if picomc_dir is None or zucaro_dir is None:
print("Required directories not found. Skipping copy.")
# Mark the check as done so it wont run again
self.config["ZucaroCheck"] = True
with open("config.json", "w") as f:
json.dump(self.config, f, indent=4)
return
picomc_size = self.get_folder_size(picomc_dir)
zucaro_size = self.get_folder_size(zucaro_dir)
if picomc_size <= zucaro_size:
print("No action needed. Zucaro folder is not smaller than Picomc.")
# Update config so the check is considered done
self.config["ZucaroCheck"] = True
with open("config.json", "w") as f:
json.dump(self.config, f, indent=4)
return
print(f"Copying Picomc ({picomc_size} bytes) to Zucaro ({zucaro_size} bytes)...")
app = QApplication.instance() or QApplication(sys.argv)
dialog = QDialog()
dialog.setWindowTitle("Working...")
dialog.setWindowModality(Qt.ApplicationModal)
layout = QVBoxLayout()
label = QLabel("Working on stuff, please wait...")
progress = QProgressBar()
progress.setValue(0)
layout.addWidget(label)
layout.addWidget(progress)
dialog.setLayout(layout)
# Setup copy thread
thread = CopyThread(picomc_dir, zucaro_dir)
thread.progress_changed.connect(progress.setValue)
thread.finished.connect(dialog.accept)
thread.start()
dialog.exec_() # Runs the modal event loop
# Mark as done
self.config["ZucaroCheck"] = True
with open("config.json", "w") as f:
json.dump(self.config, f, indent=4)
print("Copy completed.")
def themes_integrity(self): def themes_integrity(self):
# Define folder and file paths
themes_folder = "themes" themes_folder = "themes"
dark_theme_file = os.path.join(themes_folder, "Dark.json") dark_theme_file = os.path.join(themes_folder, "Dark.json")
native_theme_file = os.path.join(themes_folder, "Native.json") native_theme_file = os.path.join(themes_folder, "Native.json")
# Define the default content for Dark.json
dark_theme_content = { dark_theme_content = {
"manifest": { "manifest": {
"name": "Dark", "name": "Dark",
@ -201,6 +84,7 @@ class HealthCheck:
"background_image_base64": "" "background_image_base64": ""
} }
# Define the default content for Native.json
native_theme_content = { native_theme_content = {
"manifest": { "manifest": {
"name": "Native", "name": "Native",
@ -211,21 +95,25 @@ class HealthCheck:
"palette": {} "palette": {}
} }
# Step 1: Ensure the themes folder exists
if not os.path.exists(themes_folder): if not os.path.exists(themes_folder):
print(f"Creating folder: {themes_folder}") print(f"Creating folder: {themes_folder}")
os.makedirs(themes_folder) os.makedirs(themes_folder)
# Step 2: Ensure Dark.json exists
if not os.path.isfile(dark_theme_file): if not os.path.isfile(dark_theme_file):
print(f"Creating file: {dark_theme_file}") print(f"Creating file: {dark_theme_file}")
with open(dark_theme_file, "w", encoding="utf-8") as file: with open(dark_theme_file, "w", encoding="utf-8") as file:
json.dump(dark_theme_content, file, indent=2) json.dump(dark_theme_content, file, indent=2)
print("Dark.json has been created successfully.") print("Dark.json has been created successfully.")
# Step 3: Ensure Native.json exists
if not os.path.isfile(native_theme_file): if not os.path.isfile(native_theme_file):
print(f"Creating file: {native_theme_file}") print(f"Creating file: {native_theme_file}")
with open(native_theme_file, "w", encoding="utf-8") as file: with open(native_theme_file, "w", encoding="utf-8") as file:
json.dump(native_theme_content, file, indent=2) json.dump(native_theme_content, file, indent=2)
print("Native.json has been created successfully.") print("Native.json has been created successfully.")
# Check if both files exist and print OK message
if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file): if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file):
print("Theme Integrity OK") print("Theme Integrity OK")

View File

@ -1,46 +0,0 @@
from io import StringIO
import sys
import shlex
import gc
def run_command(command="zucaro"):
# Remove all zucaro-related modules from sys.modules BEFORE import
modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')]
for mod in modules_to_remove:
del sys.modules[mod]
gc.collect()
# Import zucaro_cli dynamically
from zucaro.cli.main import zucaro_cli
# Redirect stdout and stderr to capture the command output
old_stdout, old_stderr = sys.stdout, sys.stderr
sys.stdout = mystdout = StringIO()
sys.stderr = mystderr = StringIO()
try:
# Use shlex.split to properly parse the command string
# This will call Click's CLI as if from command line, using args
zucaro_cli.main(args=shlex.split(command))
except SystemExit as e:
if e.code != 0:
print(f"Command exited with code {e.code}", file=sys.stderr)
except Exception as e:
print(f"Unexpected error: {e}", file=sys.stderr)
finally:
# Restore stdout and stderr
sys.stdout = old_stdout
sys.stderr = old_stderr
output = mystdout.getvalue().strip()
error = mystderr.getvalue().strip()
# Cleanup: remove zucaro-related modules from sys.modules and force garbage collection
modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')]
for mod in modules_to_remove:
del sys.modules[mod]
gc.collect()
if not output:
return f"Error: No output from command. Stderr: {error}"
return output

View File

@ -13,14 +13,13 @@ import time
from authser import MinecraftAuthenticator from authser import MinecraftAuthenticator
from healthcheck import HealthCheck from healthcheck import HealthCheck
import modulecli
from PyQt5.QtWidgets import QApplication, QComboBox, QWidget, QInputDialog, QVBoxLayout, QListWidget, QSpinBox, QFileDialog, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QTabWidget, QFrame, QSpacerItem, QSizePolicy, QMainWindow, QGridLayout, QTextEdit, QListWidget, QListWidgetItem, QMenu from PyQt5.QtWidgets import QApplication, QComboBox, QWidget, QInputDialog, QVBoxLayout, QListWidget, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QTabWidget, QFrame, QSpacerItem, QSizePolicy, QMainWindow, QGridLayout, QTextEdit, QListWidget, QListWidgetItem, QMenu
from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QMovie, QPixmap, QDesktopServices, QBrush from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QMovie, QPixmap, QDesktopServices, QBrush
from PyQt5.QtCore import Qt, QObject, pyqtSignal, QThread, QUrl, QMetaObject, Q_ARG, QByteArray, QSize from PyQt5.QtCore import Qt, QObject, pyqtSignal, QThread, QUrl, QMetaObject, Q_ARG, QByteArray, QSize
from datetime import datetime from datetime import datetime
logging.basicConfig(level=logging.ERROR, format='%(levelname)s - %(message)s') logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
class PicomcVersionSelector(QWidget): class PicomcVersionSelector(QWidget):
def __init__(self): def __init__(self):
@ -31,7 +30,6 @@ class PicomcVersionSelector(QWidget):
health_checker = HealthCheck() health_checker = HealthCheck()
health_checker.themes_integrity() health_checker.themes_integrity()
health_checker.check_config_file() health_checker.check_config_file()
health_checker.zucaro_health_check()
self.config = health_checker.config self.config = health_checker.config
themes_folder = "themes" themes_folder = "themes"
@ -128,13 +126,11 @@ class PicomcVersionSelector(QWidget):
try: try:
self.config_path = "config.json" self.config_path = "config.json"
print("Running picomc instance create default command...") print("Running picomc instance create default command...")
# Run the command using subprocess
# Run the command using modulecli result = subprocess.run(["picomc", "instance", "create", "default"], check=True, capture_output=True, text=True)
command = "instance create default"
result = modulecli.run_command(command)
# Print the output of the command # Print the output of the command
print("Command output:", result) print("Command output:", result.stdout)
# Change the value of IsFirstLaunch to False # Change the value of IsFirstLaunch to False
self.config["IsFirstLaunch"] = False self.config["IsFirstLaunch"] = False
@ -145,9 +141,9 @@ class PicomcVersionSelector(QWidget):
json.dump(self.config, f, indent=4) json.dump(self.config, f, indent=4)
print("Configuration saved to", self.config_path) print("Configuration saved to", self.config_path)
except Exception as e: except subprocess.CalledProcessError as e:
print("An error occurred while creating the instance.") print("An error occurred while creating the instance.")
print("Error output:", str(e)) print("Error output:", e.stderr)
def resize_event(self, event): def resize_event(self, event):
if hasattr(self, 'movie_label'): if hasattr(self, 'movie_label'):
@ -297,17 +293,21 @@ class PicomcVersionSelector(QWidget):
def open_settings_dialog(self): def open_settings_dialog(self):
dialog = QDialog(self) dialog = QDialog(self)
dialog.setWindowTitle('Settings') dialog.setWindowTitle('Settings')
# Make the window resizable
dialog.setMinimumSize(400, 300) dialog.setMinimumSize(400, 300)
# Create a Tab Widget
tab_widget = QTabWidget() tab_widget = QTabWidget()
# --- Settings Tab --- # Create the Settings Tab
settings_tab = QWidget() settings_tab = QWidget()
settings_layout = QVBoxLayout() settings_layout = QVBoxLayout()
title_label = QLabel('Settings') title_label = QLabel('Settings')
title_label.setFont(QFont("Arial", 14)) title_label.setFont(QFont("Arial", 14))
# Create checkboxes for settings tab
discord_rcp_checkbox = QCheckBox('Discord Rich Presence') discord_rcp_checkbox = QCheckBox('Discord Rich Presence')
discord_rcp_checkbox.setChecked(self.config.get("IsRCPenabled", False)) discord_rcp_checkbox.setChecked(self.config.get("IsRCPenabled", False))
@ -323,6 +323,7 @@ class PicomcVersionSelector(QWidget):
settings_layout.addWidget(check_updates_checkbox) settings_layout.addWidget(check_updates_checkbox)
settings_layout.addWidget(bleeding_edge_checkbox) settings_layout.addWidget(bleeding_edge_checkbox)
# Add buttons in the settings tab
update_button = QPushButton('Check for updates') update_button = QPushButton('Check for updates')
update_button.clicked.connect(self.check_for_update) update_button.clicked.connect(self.check_for_update)
@ -338,99 +339,53 @@ class PicomcVersionSelector(QWidget):
settings_tab.setLayout(settings_layout) settings_tab.setLayout(settings_layout)
# --- Customization Tab --- # Create the Customization Tab
customization_tab = QWidget() customization_tab = QWidget()
customization_layout = QVBoxLayout() customization_layout = QVBoxLayout()
# Create theme background checkbox for customization tab
theme_background_checkbox = QCheckBox('Theme Background') theme_background_checkbox = QCheckBox('Theme Background')
theme_background_checkbox.setChecked(self.config.get("ThemeBackground", False)) theme_background_checkbox.setChecked(self.config.get("ThemeBackground", False))
# Label to show currently selected theme
theme_filename = self.config.get('Theme', 'Dark.json') theme_filename = self.config.get('Theme', 'Dark.json')
current_theme_label = QLabel(f"Current Theme: {theme_filename}") current_theme_label = QLabel(f"Current Theme: {theme_filename}")
# QListWidget to display available themes
json_files_label = QLabel('Installed Themes:') json_files_label = QLabel('Installed Themes:')
self.json_files_list_widget = QListWidget() self.json_files_list_widget = QListWidget()
self.selected_theme = theme_filename
# Track selected theme
self.selected_theme = theme_filename # Default to current theme
# Build the list of themes
themes_list = self.build_themes_list() themes_list = self.build_themes_list()
# Populate themes initially
self.populate_themes(self.json_files_list_widget, themes_list) self.populate_themes(self.json_files_list_widget, themes_list)
# Update current theme label when a theme is selected
self.json_files_list_widget.itemClicked.connect( self.json_files_list_widget.itemClicked.connect(
lambda: self.on_theme_selected(self.json_files_list_widget, current_theme_label) lambda: self.on_theme_selected(self.json_files_list_widget, current_theme_label)
) )
# Add widgets to the layout
customization_layout.addWidget(theme_background_checkbox) customization_layout.addWidget(theme_background_checkbox)
customization_layout.addWidget(current_theme_label) customization_layout.addWidget(current_theme_label)
customization_layout.addWidget(json_files_label) customization_layout.addWidget(json_files_label)
customization_layout.addWidget(self.json_files_list_widget) customization_layout.addWidget(self.json_files_list_widget)
# Button to download themes
download_themes_button = QPushButton("Download More Themes") download_themes_button = QPushButton("Download More Themes")
download_themes_button.clicked.connect(self.download_themes_window) download_themes_button.clicked.connect(self.download_themes_window)
customization_layout.addWidget(download_themes_button) customization_layout.addWidget(download_themes_button)
customization_tab.setLayout(customization_layout) customization_tab.setLayout(customization_layout)
# --- Java Tab --- # Add the tabs to the TabWidget
java_tab = QWidget()
java_layout = QVBoxLayout()
# Java path input with browse button
java_path_layout = QHBoxLayout()
java_path_input = QLineEdit()
java_path_input.setPlaceholderText("Custom Java Installation Path")
java_path_input.setText(self.config.get("JavaPath", ""))
browse_button = QPushButton("Examine")
browse_button.clicked.connect(lambda: self.browse_java_path(java_path_input))
java_path_layout.addWidget(java_path_input)
java_path_layout.addWidget(browse_button)
ram_layout = QHBoxLayout()
ram_label = QLabel("Assigned RAM:")
ram_selector = QLineEdit()
ram_selector.setPlaceholderText("2G") # Show default placeholder
# RAM selector
ram_layout = QHBoxLayout()
ram_label = QLabel("Assigned RAM:")
ram_selector = QLineEdit()
ram_selector.setPlaceholderText("2G") # Show default placeholder
# Set initial value from config, ensuring it ends with 'G'
initial_ram = self.config.get("MaxRAM", "2G")
if not initial_ram.endswith('G'):
initial_ram += 'G'
ram_selector.setText(initial_ram)
# Ensure 'G' is always present when focus is lost
def ensure_g_suffix():
current_text = ram_selector.text()
if not current_text.endswith('G'):
ram_selector.setText(current_text + 'G')
ram_selector.editingFinished.connect(ensure_g_suffix)
ram_layout.addWidget(ram_label)
ram_layout.addWidget(ram_selector)
# Manage Java checkbox
manage_java_checkbox = QCheckBox("Manage Java")
manage_java_checkbox.setChecked(self.config.get("ManageJava", False))
manage_java_info = QLabel(
"<b>Disclaimer:</b> Experimental feature. Do not change these settings "
"unless you are sure of what you are doing. "
" If Manage Java is enabledthe launcher will download Java binaries for your OS only for Minecraft compatibility purposes.")
manage_java_info.setWordWrap(True)
# Add to layout
java_layout.addLayout(java_path_layout)
java_layout.addLayout(ram_layout)
java_layout.addWidget(manage_java_checkbox)
java_layout.addWidget(manage_java_info)
java_tab.setLayout(java_layout)
# Add all tabs
tab_widget.addTab(settings_tab, "Settings") tab_widget.addTab(settings_tab, "Settings")
tab_widget.addTab(customization_tab, "Customization") tab_widget.addTab(customization_tab, "Customization")
tab_widget.addTab(java_tab, "Java")
# Save button # Save button
save_button = QPushButton('Save') save_button = QPushButton('Save')
@ -439,14 +394,12 @@ class PicomcVersionSelector(QWidget):
discord_rcp_checkbox.isChecked(), discord_rcp_checkbox.isChecked(),
check_updates_checkbox.isChecked(), check_updates_checkbox.isChecked(),
theme_background_checkbox.isChecked(), theme_background_checkbox.isChecked(),
self.selected_theme, self.selected_theme, # Pass the selected theme here
bleeding_edge_checkbox.isChecked(), bleeding_edge_checkbox.isChecked() # Pass the bleeding edge setting here
java_path_input.text(),
ram_selector.text(),
manage_java_checkbox.isChecked()
) )
) )
# Main layout
main_layout = QVBoxLayout() main_layout = QVBoxLayout()
main_layout.addWidget(tab_widget) main_layout.addWidget(tab_widget)
main_layout.addWidget(save_button) main_layout.addWidget(save_button)
@ -454,13 +407,6 @@ class PicomcVersionSelector(QWidget):
dialog.setLayout(main_layout) dialog.setLayout(main_layout)
dialog.exec_() dialog.exec_()
def browse_java_path(self, java_path_input):
path, _ = QFileDialog.getOpenFileName(self, "Select Java Executable")
if path:
java_path_input.setText(path)
def show_bleeding_edge_popup(self, checkbox): def show_bleeding_edge_popup(self, checkbox):
if checkbox.isChecked(): if checkbox.isChecked():
response = QMessageBox.question( response = QMessageBox.question(
@ -689,31 +635,20 @@ class PicomcVersionSelector(QWidget):
## REPOSITORY BLOCK ENDS ## REPOSITORY BLOCK ENDS
def save_settings( def save_settings(self, is_rcp_enabled, check_updates_on_start, theme_background, selected_theme, is_bleeding):
self,
is_rcp_enabled,
check_updates_on_start,
theme_background,
selected_theme,
is_bleeding,
java_path,
ram_allocation,
manage_java_enabled
):
config_path = "config.json" config_path = "config.json"
updated_config = { updated_config = {
"IsRCPenabled": is_rcp_enabled, "IsRCPenabled": is_rcp_enabled,
"CheckUpdate": check_updates_on_start, "CheckUpdate": check_updates_on_start,
"ThemeBackground": theme_background, "ThemeBackground": theme_background,
"Theme": selected_theme, "Theme": selected_theme,
"IsBleeding": is_bleeding, "IsBleeding": is_bleeding
"ManageJava": manage_java_enabled,
"MaxRAM": ram_allocation,
"JavaPath": java_path,
} }
# Update config values
self.config.update(updated_config) self.config.update(updated_config)
# Save updated config to file
with open(config_path, "w") as config_file: with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4) json.dump(self.config, config_file, indent=4)
@ -724,6 +659,20 @@ class PicomcVersionSelector(QWidget):
) )
self.__init__() self.__init__()
def get_palette(self, palette_type):
"""Retrieve the corresponding palette based on the palette type."""
palettes = {
"Dark": self.create_dark_palette,
"Obsidian": self.create_obsidian_palette,
"Redstone": self.create_redstone_palette,
"Alpha": self.create_alpha_palette,
"Strawberry": self.create_strawberry_palette,
"Native": self.create_native_palette,
"Christmas": self.create_christmas_palette,
}
# Default to dark palette if the type is not specified or invalid
return palettes.get(palette_type, self.create_dark_palette)()
def get_system_info(self): def get_system_info(self):
# Get system information # Get system information
java_version = subprocess.getoutput("java -version 2>&1 | head -n 1") java_version = subprocess.getoutput("java -version 2>&1 | head -n 1")
@ -763,16 +712,16 @@ class PicomcVersionSelector(QWidget):
def open_game_directory(self): def open_game_directory(self):
try: try:
# Run the command using modulecli # Run the command and capture the output
command = "instance dir" result = subprocess.run(['picomc', 'instance', 'dir'], capture_output=True, text=True, check=True)
result = modulecli.run_command(command) game_directory = result.stdout.strip()
game_directory = result.strip()
# Open the directory in the system's file explorer # Open the directory in the system's file explorer
QDesktopServices.openUrl(QUrl.fromLocalFile(game_directory)) QDesktopServices.openUrl(QUrl.fromLocalFile(game_directory))
except Exception as e: except subprocess.CalledProcessError as e:
print(f"Error running picomc command: {e}") print(f"Error running picomc command: {e}")
def populate_installed_versions(self): def populate_installed_versions(self):
config_path = "config.json" config_path = "config.json"
@ -793,17 +742,20 @@ class PicomcVersionSelector(QWidget):
# Run the command and capture the output # Run the command and capture the output
try: try:
command = "version list" process = subprocess.Popen(['picomc', 'version', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output = modulecli.run_command(command) output, error = process.communicate()
if not output: if process.returncode != 0:
raise Exception("Failed to get output from modulecli") raise subprocess.CalledProcessError(process.returncode, process.args, output=output, stderr=error)
except Exception as e: except FileNotFoundError:
logging.error("Error running 'picomc': %s", e) logging.error("'picomc' command not found. Please ensure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error running 'picomc': %s", e.stderr)
return return
# Parse the output and replace '[local]' with a space # Parse the output and replace '[local]' with a space
versions = [version.replace('[local]', ' ').strip() for version in output.splitlines() if version.strip()] versions = [version.replace('[local]', ' ').strip() for version in output.splitlines()]
# Get the last played version from the config # Get the last played version from the config
last_played = self.config.get("LastPlayed", "") last_played = self.config.get("LastPlayed", "")
@ -816,26 +768,32 @@ class PicomcVersionSelector(QWidget):
# Populate the installed versions combo box # Populate the installed versions combo box
self.installed_version_combo.clear() self.installed_version_combo.clear()
self.installed_version_combo.addItems(versions) self.installed_version_combo.addItems(versions)
def populate_installed_versions_normal_order(self): def populate_installed_versions_normal_order(self):
# Run the 'picomc instance create default' command at the start # Run the 'picomc instance create default' command at the start
try: try:
command = "instance create default" process = subprocess.Popen(['picomc', 'instance', 'create', 'default'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output = modulecli.run_command(command) output, error = process.communicate()
if not output: if process.returncode != 0:
raise Exception("Failed to get output from modulecli for 'instance create default'") raise subprocess.CalledProcessError(process.returncode, process.args, error)
except Exception as e: except FileNotFoundError:
logging.error("Error creating default instance: %s", str(e)) logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error creating default instance: %s", e.stderr)
return return
# Run the 'picomc version list' command and get the output # Run the 'picomc version list' command and get the output
try: try:
command = "version list" process = subprocess.Popen(['picomc', 'version', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output = modulecli.run_command(command) output, error = process.communicate()
if not output: if process.returncode != 0:
raise Exception("Failed to get output from modulecli for 'version list'") raise subprocess.CalledProcessError(process.returncode, process.args, error)
except Exception as e: except FileNotFoundError:
logging.error("Error: %s", str(e)) logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error: %s", e.stderr)
return return
# Parse the output and replace '[local]' with a space # Parse the output and replace '[local]' with a space
@ -862,7 +820,7 @@ class PicomcVersionSelector(QWidget):
# Check if there are any accounts # Check if there are any accounts
try: try:
account_list_output = modulecli.run_command("account list").strip() account_list_output = subprocess.check_output(["picomc", "account", "list"]).decode("utf-8").strip()
if not account_list_output: if not account_list_output:
QMessageBox.warning(self, "No Account Available", "Please create an account first.") QMessageBox.warning(self, "No Account Available", "Please create an account first.")
return return
@ -871,77 +829,53 @@ class PicomcVersionSelector(QWidget):
if '*' not in account_list_output: if '*' not in account_list_output:
QMessageBox.warning(self, "No Account Selected", "Please select an account.") QMessageBox.warning(self, "No Account Selected", "Please select an account.")
return return
except Exception as e: except subprocess.CalledProcessError as e:
error_message = f"Error fetching accounts: {str(e)}" error_message = f"Error fetching accounts: {str(e)}"
logging.error(error_message) logging.error(error_message)
QMessageBox.critical(self, "Error", error_message) QMessageBox.critical(self, "Error", error_message)
return return
selected_instance = self.installed_version_combo.currentText() selected_instance = self.installed_version_combo.currentText()
logging.info(f"Selected instance from dropdown: {selected_instance}") logging.info(f"Selected instance: {selected_instance}")
# Verify the selected instance value before starting the game
if not selected_instance:
logging.error("No instance selected.")
QMessageBox.warning(self, "No Instance Selected", "Please select an instance.")
return
play_thread = threading.Thread(target=self.run_game, args=(selected_instance,)) play_thread = threading.Thread(target=self.run_game, args=(selected_instance,))
play_thread.start() play_thread.start()
def run_game(self, selected_instance): def run_game(self, selected_instance):
try: try:
# Set current_state to the selected instance
self.current_state = selected_instance self.current_state = selected_instance
self.start_time = time.time()
# Read config # Read the config.json to get the "Instance" value
with open('config.json', 'r') as config_file: with open('config.json', 'r') as config_file:
config = json.load(config_file) config = json.load(config_file)
instance_value = config.get("Instance", "default") instance_value = config.get("Instance", "default") # Default to "default" if not found
max_ram = config.get("MaxRAM", 2)
manage_java = config.get("ManageJava", False)
java_path = config.get("JavaPath", "")
# Update last played on a thread # Update lastplayed field in config.json on a separate thread
update_thread = threading.Thread(target=self.update_last_played, args=(selected_instance,)) update_thread = threading.Thread(target=self.update_last_played, args=(selected_instance,))
update_thread.start() update_thread.start()
# Build command # Run the game subprocess with the instance_value from config.json
command = f"instance launch {instance_value} --version-override {selected_instance} --assigned-ram {max_ram}" subprocess.run(['picomc', 'instance', 'launch', '--version-override', selected_instance, instance_value], check=True)
if manage_java:
command += " --manage-java"
if java_path:
command += f" --java {java_path}"
print(f"Launching command: {command}") except subprocess.CalledProcessError as e:
output = modulecli.run_command(command)
print(f"modulecli output: {output}")
if not output:
raise Exception("Failed to get output from modulecli")
except Exception as e:
error_message = f"Error playing {selected_instance}: {e}" error_message = f"Error playing {selected_instance}: {e}"
print(error_message) # Add this for debugging
logging.error(error_message) logging.error(error_message)
# (Show error in UI if necessary) # Use QMetaObject.invokeMethod to call showError safely
QMetaObject.invokeMethod(
self, "showError", Qt.QueuedConnection,
Q_ARG(str, "Error"), Q_ARG(str, error_message)
)
finally: finally:
# Reset current_state to "menu" after the game closes
self.current_state = "menu" self.current_state = "menu"
self.update_total_playtime(self.start_time)
def update_last_played(self, selected_instance): def update_last_played(self, selected_instance):
config_path = "config.json" config_path = "config.json"
self.config["LastPlayed"] = selected_instance self.config["LastPlayed"] = selected_instance
with open(config_path, "w") as config_file: with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4) json.dump(self.config, config_file, indent=4)
def update_total_playtime(self, start_time):
config_path = "config.json"
self.config["TotalPlaytime"] += time.time() - self.start_time
print("TOTAL PLAYTIME:" + str(self.config["TotalPlaytime"]))
with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4)
def showError(self, title, message): def showError(self, title, message):
QMessageBox.critical(self, title, message) QMessageBox.critical(self, title, message)
@ -1033,16 +967,16 @@ class PicomcVersionSelector(QWidget):
return return
try: try:
command = f"account create {username}" command = ['picomc', 'account', 'create', username]
if is_microsoft: if is_microsoft:
command += " --ms" command.append('--ms')
modulecli.run_command(command) subprocess.run(command, check=True)
QMessageBox.information(dialog, "Success", f"Account '{username}' created successfully!") QMessageBox.information(dialog, "Success", f"Account '{username}' created successfully!")
self.populate_accounts_for_all_dialogs() self.populate_accounts_for_all_dialogs()
dialog.accept() dialog.accept()
except Exception as e: except subprocess.CalledProcessError as e:
error_message = f"Error creating account: {str(e)}" error_message = f"Error creating account: {e.stderr.decode()}"
logging.error(error_message) logging.error(error_message)
QMessageBox.critical(dialog, "Error", error_message) QMessageBox.critical(dialog, "Error", error_message)
@ -1095,21 +1029,23 @@ class PicomcVersionSelector(QWidget):
confirm_dialog = QMessageBox.question(dialog, "Confirm Removal", confirm_message, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) confirm_dialog = QMessageBox.question(dialog, "Confirm Removal", confirm_message, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm_dialog == QMessageBox.Yes: if confirm_dialog == QMessageBox.Yes:
try: try:
command = f"account remove {username}" subprocess.run(['picomc', 'account', 'remove', username], check=True)
modulecli.run_command(command)
QMessageBox.information(dialog, "Success", f"Account '{username}' removed successfully!") QMessageBox.information(dialog, "Success", f"Account '{username}' removed successfully!")
self.populate_accounts_for_all_dialogs() self.populate_accounts_for_all_dialogs()
except Exception as e: except subprocess.CalledProcessError as e:
error_message = f"Error removing account: {str(e)}" error_message = f"Error removing account: {e.stderr.decode()}"
logging.error(error_message) logging.error(error_message)
QMessageBox.critical(dialog, "Error", error_message) QMessageBox.critical(dialog, "Error", error_message)
def populate_accounts(self, account_combo): def populate_accounts(self, account_combo):
# Populate the account dropdown # Populate the account dropdown
try: try:
command = "account list" process = subprocess.Popen(['picomc', 'account', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output = modulecli.run_command(command) output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
# Process accounts, keeping the one with "*" at the top # Process accounts, keeping the one with "*" at the top
accounts = output.splitlines() accounts = output.splitlines()
starred_account = None starred_account = None
@ -1133,8 +1069,10 @@ class PicomcVersionSelector(QWidget):
for account in normal_accounts: for account in normal_accounts:
account_combo.addItem(account) account_combo.addItem(account)
except Exception as e: except FileNotFoundError:
logging.error(f"Error: {str(e)}") logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
except subprocess.CalledProcessError as e:
logging.error(f"Error: {e.stderr}")
def populate_accounts_for_all_dialogs(self): def populate_accounts_for_all_dialogs(self):
# Update account dropdowns in all open dialogs # Update account dropdowns in all open dialogs
@ -1151,31 +1089,14 @@ class PicomcVersionSelector(QWidget):
return return
try: try:
command = f"account setdefault {account_name}" subprocess.run(['picomc', 'account', 'setdefault', account_name], check=True)
modulecli.run_command(command)
QMessageBox.information(self, "Success", f"Account '{account_name}' set as default!") QMessageBox.information(self, "Success", f"Account '{account_name}' set as default!")
self.populate_accounts_for_all_dialogs() self.populate_accounts_for_all_dialogs()
except Exception as e: except subprocess.CalledProcessError as e:
error_message = f"Error setting default account '{account_name}': {str(e)}" error_message = f"Error setting default account '{account_name}': {e.stderr.decode()}"
logging.error(error_message) logging.error(error_message)
QMessageBox.critical(self, "Error", error_message) QMessageBox.critical(self, "Error", error_message)
def get_playtime(self, config_data):
#Gets the playtime from the json and
total_playtime = config_data.get("TotalPlaytime")/60
#if total playtime is over 60 minutes, uses hours instead
if(total_playtime > 60):
total_playtime = total_playtime / 60
playtime_unit = "hours"
else:
playtime_unit = "minutes"
total_playtime = round(total_playtime)
#returs the playtime and the unit used to measure in a string
return(f"{total_playtime} {playtime_unit}")
def show_about_dialog(self): def show_about_dialog(self):
# Load the version number from version.json # Load the version number from version.json
try: try:
@ -1199,16 +1120,13 @@ class PicomcVersionSelector(QWidget):
if is_bleeding and version_bleeding: if is_bleeding and version_bleeding:
version_number = version_bleeding version_number = version_bleeding
about_message = ( about_message = (
f"PicoDulce Launcher (v{version_number})\n\n" f"PicoDulce Launcher (v{version_number})\n\n"
"A simple Minecraft launcher built using Qt, based on the zucaro backend.\n\n" "A simple Minecraft launcher built using Qt, based on the picomc project.\n\n"
"Credits:\n" "Credits:\n"
"Nixietab: Code and UI design\n" "Nixietab: Code and UI design\n"
"Wabaano: Graphic design\n" "Wabaano: Graphic design\n"
"Olinad: Christmas!!!!\n\n" "Olinad: Christmas!!!!"
f"Playtime: {self.get_playtime(config_data)}"
) )
QMessageBox.about(self, "About", about_message) QMessageBox.about(self, "About", about_message)
@ -1395,7 +1313,7 @@ class PicomcVersionSelector(QWidget):
def open_mod_loader_and_version_menu(self): def open_mod_loader_and_version_menu(self):
dialog = ModLoaderAndVersionMenu(parent=self) dialog = ModLoaderAndVersionMenu()
dialog.finished.connect(self.populate_installed_versions) dialog.finished.connect(self.populate_installed_versions)
dialog.exec_() dialog.exec_()
@ -1408,25 +1326,17 @@ class DownloadThread(QThread):
def run(self): def run(self):
try: try:
command = f"version prepare {self.version}" subprocess.run(['picomc', 'version', 'prepare', self.version], check=True)
modulecli.run_command(command)
self.completed.emit(True, f"Version {self.version} prepared successfully!") self.completed.emit(True, f"Version {self.version} prepared successfully!")
except Exception as e: except subprocess.CalledProcessError as e:
error_message = f"Error preparing {self.version}: {str(e)}" error_message = f"Error preparing {self.version}: {e.stderr.decode()}"
self.completed.emit(False, error_message) self.completed.emit(False, error_message)
class ModLoaderAndVersionMenu(QDialog): class ModLoaderAndVersionMenu(QDialog):
def __init__(self, parent=None): def __init__(self):
super().__init__(parent) super().__init__()
self.setWindowTitle("Mod Loader and Version Menu") self.setWindowTitle("Mod Loader and Version Menu")
# Set window position relative to parent self.setGeometry(100, 100, 400, 300)
if parent:
parent_pos = parent.pos()
x = parent_pos.x() + (parent.width() - 400) // 2
y = parent_pos.y() + (parent.height() - 300) // 2
self.setGeometry(x, y, 400, 300)
else:
self.setGeometry(100, 100, 400, 300)
main_layout = QVBoxLayout(self) main_layout = QVBoxLayout(self)
@ -1436,11 +1346,11 @@ class ModLoaderAndVersionMenu(QDialog):
# Create tabs # Create tabs
install_mod_tab = QWidget() install_mod_tab = QWidget()
download_version_tab = QWidget() download_version_tab = QWidget()
instances_tab = QWidget() instances_tab = QWidget() # New tab for instances
tab_widget.addTab(download_version_tab, "Download Version") tab_widget.addTab(download_version_tab, "Download Version")
tab_widget.addTab(install_mod_tab, "Install Mod Loader") tab_widget.addTab(install_mod_tab, "Install Mod Loader")
tab_widget.addTab(instances_tab, "Instances") tab_widget.addTab(instances_tab, "Instances") # Add the new tab
# Add content to "Install Mod Loader" tab # Add content to "Install Mod Loader" tab
self.setup_install_mod_loader_tab(install_mod_tab) self.setup_install_mod_loader_tab(install_mod_tab)
@ -1492,8 +1402,11 @@ class ModLoaderAndVersionMenu(QDialog):
if instance_name: if instance_name:
try: try:
# Run the "picomc instance create" command # Run the "picomc instance create" command
command = f"instance create {instance_name}" process = subprocess.Popen(['picomc', 'instance', 'create', instance_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
modulecli.run_command(command) output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
# Notify the user that the instance was created # Notify the user that the instance was created
QMessageBox.information(self, "Instance Created", f"Instance '{instance_name}' has been created successfully.") QMessageBox.information(self, "Instance Created", f"Instance '{instance_name}' has been created successfully.")
@ -1504,9 +1417,11 @@ class ModLoaderAndVersionMenu(QDialog):
# Optionally select the newly created instance # Optionally select the newly created instance
self.on_instance_selected(self.instances_list_widget.item(self.instances_list_widget.count() - 1)) self.on_instance_selected(self.instances_list_widget.item(self.instances_list_widget.count() - 1))
except Exception as e: except FileNotFoundError:
logging.error("Error creating instance: %s", str(e)) logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
QMessageBox.critical(self, "Error", f"Failed to create instance: {str(e)}") except subprocess.CalledProcessError as e:
logging.error("Error creating instance: %s", e.stderr)
QMessageBox.critical(self, "Error", f"Failed to create instance: {e.stderr}")
else: else:
QMessageBox.warning(self, "Invalid Input", "Please enter a valid instance name.") QMessageBox.warning(self, "Invalid Input", "Please enter a valid instance name.")
@ -1517,8 +1432,14 @@ class ModLoaderAndVersionMenu(QDialog):
try: try:
# Run the "picomc instance rename" command # Run the "picomc instance rename" command
command = f"instance rename {old_instance_name} {new_instance_name}" process = subprocess.Popen(
modulecli.run_command(command) ['picomc', 'instance', 'rename', old_instance_name, new_instance_name],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
QMessageBox.information(self, "Instance Renamed", f"Instance '{old_instance_name}' has been renamed to '{new_instance_name}' successfully.") QMessageBox.information(self, "Instance Renamed", f"Instance '{old_instance_name}' has been renamed to '{new_instance_name}' successfully.")
@ -1530,9 +1451,11 @@ class ModLoaderAndVersionMenu(QDialog):
if matching_items: if matching_items:
self.instances_list_widget.setCurrentItem(matching_items[0]) self.instances_list_widget.setCurrentItem(matching_items[0])
except Exception as e: except FileNotFoundError:
logging.error("Error renaming instance: %s", str(e)) logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
QMessageBox.critical(self, "Error", f"Failed to rename instance: {str(e)}") except subprocess.CalledProcessError as e:
logging.error("Error renaming instance: %s", e.stderr)
QMessageBox.critical(self, "Error", f"Failed to rename instance: {e.stderr}")
def delete_instance(self, instance_name): def delete_instance(self, instance_name):
if instance_name == "default": if instance_name == "default":
@ -1547,8 +1470,11 @@ class ModLoaderAndVersionMenu(QDialog):
if confirm_delete == QMessageBox.Yes: if confirm_delete == QMessageBox.Yes:
try: try:
# Run the "picomc instance delete" command # Run the "picomc instance delete" command
command = f"instance delete {instance_name}" process = subprocess.Popen(['picomc', 'instance', 'delete', instance_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
modulecli.run_command(command) output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
# Notify the user that the instance was deleted # Notify the user that the instance was deleted
QMessageBox.information(self, "Instance Deleted", f"Instance '{instance_name}' has been deleted successfully.") QMessageBox.information(self, "Instance Deleted", f"Instance '{instance_name}' has been deleted successfully.")
@ -1556,16 +1482,19 @@ class ModLoaderAndVersionMenu(QDialog):
# Reload the instances list # Reload the instances list
self.load_instances() self.load_instances()
except Exception as e: except FileNotFoundError:
logging.error("Error deleting instance: %s", str(e)) logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
QMessageBox.critical(self, "Error", f"Failed to delete instance: {str(e)}") except subprocess.CalledProcessError as e:
logging.error("Error deleting instance: %s", e.stderr)
QMessageBox.critical(self, "Error", f"Failed to delete instance: {e.stderr}")
def load_instances(self): def load_instances(self):
try: try:
# Run the "picomc instance list" command process = subprocess.Popen(['picomc', 'instance', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
command = "instance list" output, error = process.communicate()
output = modulecli.run_command(command) if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
# Parse the output and add each instance to the list widget # Parse the output and add each instance to the list widget
instances = output.splitlines() instances = output.splitlines()
self.instances_list_widget.clear() # Clear the previous list self.instances_list_widget.clear() # Clear the previous list
@ -1574,9 +1503,10 @@ class ModLoaderAndVersionMenu(QDialog):
self.instances_list_widget.addItem(item) self.instances_list_widget.addItem(item)
self.add_instance_buttons(item, instance) self.add_instance_buttons(item, instance)
except Exception as e: except FileNotFoundError:
logging.error("Error fetching instances: %s", str(e)) logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
except subprocess.CalledProcessError as e:
logging.error("Error fetching instances: %s", e.stderr)
def add_instance_buttons(self, list_item, instance_name): def add_instance_buttons(self, list_item, instance_name):
widget = QWidget() widget = QWidget()
@ -1732,19 +1662,21 @@ class ModLoaderAndVersionMenu(QDialog):
options.append('--beta') options.append('--beta')
if options: if options:
try: try:
command = 'version list ' + ' '.join(options) process = subprocess.Popen(['picomc', 'version', 'list'] + options, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output = modulecli.run_command(command) output, error = process.communicate()
if "Error" in output: if process.returncode != 0:
logging.error(output) raise subprocess.CalledProcessError(process.returncode, process.args, error)
return except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
# Parse the output and replace '[local]' with a space
versions = output.splitlines()
versions = [version.replace('[local]', ' ').strip() for version in versions]
self.version_combo.addItems(versions)
except Exception as e:
logging.error("Unexpected error: %s", e)
return return
except subprocess.CalledProcessError as e:
logging.error("Error: %s", e.stderr)
return
# Parse the output and replace '[local]' with a space
versions = output.splitlines()
versions = [version.replace('[local]', ' ').strip() for version in versions]
self.version_combo.addItems(versions)
# Update the download button state whenever versions are updated # Update the download button state whenever versions are updated
self.update_download_button_state() self.update_download_button_state()
@ -1761,7 +1693,7 @@ class ModLoaderAndVersionMenu(QDialog):
# Connect the combo box signal to the update function # Connect the combo box signal to the update function
self.version_combo.currentIndexChanged.connect(self.update_download_button_state) self.version_combo.currentIndexChanged.connect(self.update_download_button_state)
def update_download_button_state(self): def update_download_button_state(self):
self.download_button.setEnabled(self.version_combo.currentIndex() != -1) self.download_button.setEnabled(self.version_combo.currentIndex() != -1)
@ -1801,10 +1733,15 @@ class ModLoaderAndVersionMenu(QDialog):
def populate_available_releases(self, version_combo, install_forge, install_fabric): def populate_available_releases(self, version_combo, install_forge, install_fabric):
try: try:
command = "version list --release" process = subprocess.Popen(['picomc', 'version', 'list', '--release'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output = modulecli.run_command(command) output, error = process.communicate()
except Exception as e: if process.returncode != 0:
logging.error("Error: %s", str(e)) raise subprocess.CalledProcessError(process.returncode, process.args, error)
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error: %s", e.stderr)
return return
if install_fabric: if install_fabric:
@ -1834,13 +1771,12 @@ class ModLoaderAndVersionMenu(QDialog):
try: try:
if mod_loader == 'forge': if mod_loader == 'forge':
command = f"mod loader forge install --game {version}" subprocess.run(['picomc', 'mod', 'loader', 'forge', 'install', '--game', version], check=True)
elif mod_loader == 'fabric': elif mod_loader == 'fabric':
command = f"mod loader fabric install {version}" subprocess.run(['picomc', 'mod', 'loader', 'fabric', 'install', version], check=True)
modulecli.run_command(command)
QMessageBox.information(self, "Success", f"{mod_loader.capitalize()} installed successfully for version {version}!") QMessageBox.information(self, "Success", f"{mod_loader.capitalize()} installed successfully for version {version}!")
except Exception as e: except subprocess.CalledProcessError as e:
error_message = f"Error installing {mod_loader} for version {version}: {str(e)}" error_message = f"Error installing {mod_loader} for version {version}: {e.stderr.decode()}"
QMessageBox.critical(self, "Error", error_message) QMessageBox.critical(self, "Error", error_message)
logging.error(error_message) logging.error(error_message)

View File

@ -1,6 +1,5 @@
zucaro picomc
PyQt5 PyQt5
requests requests
aiohttp
pypresence pypresence
tqdm tqdm

View File

@ -1,5 +1,5 @@
{ {
"version": "0.13.5", "version": "0.12.1",
"links": [ "links": [
"https://raw.githubusercontent.com/nixietab/picodulce/main/version.json", "https://raw.githubusercontent.com/nixietab/picodulce/main/version.json",
"https://raw.githubusercontent.com/nixietab/picodulce/main/picodulce.py", "https://raw.githubusercontent.com/nixietab/picodulce/main/picodulce.py",
@ -8,8 +8,7 @@
"https://raw.githubusercontent.com/nixietab/picodulce/main/marroc.py", "https://raw.githubusercontent.com/nixietab/picodulce/main/marroc.py",
"https://raw.githubusercontent.com/nixietab/picodulce/main/holiday.ico", "https://raw.githubusercontent.com/nixietab/picodulce/main/holiday.ico",
"https://raw.githubusercontent.com/nixietab/picodulce/main/authser.py", "https://raw.githubusercontent.com/nixietab/picodulce/main/authser.py",
"https://raw.githubusercontent.com/nixietab/picodulce/main/healthcheck.py", "https://raw.githubusercontent.com/nixietab/picodulce/main/healthcheck.py"
"https://raw.githubusercontent.com/nixietab/picodulce/main/modulecli.py"
], ],
"versionBleeding": "0.13.3-212" "versionBleeding": "0.12-186"
} }