Initial commit of the replace

This commit is contained in:
Nix 2025-09-10 03:50:59 -03:00 committed by GitHub
parent 408afef6f1
commit 8b6683475a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 257 additions and 67 deletions

View File

@ -1,6 +1,54 @@
import os
import json
import requests
import shutil
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:
@ -22,47 +70,112 @@ class HealthCheck:
"ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json",
"Locale": "en",
"ManageJava": False,
"MaxRAM": 2,
"JavaPath": ""
"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):
with open(config_path, "w") as config_file:
json.dump(default_config, config_file, indent=4)
self.config = default_config
return
# Step 2: Try loading the config file, handle invalid JSON
try:
with open(config_path, "r") as config_file:
self.config = json.load(config_file)
except (json.JSONDecodeError, ValueError):
# File is corrupted, overwrite it with default configuration
with open(config_path, "w") as config_file:
json.dump(default_config, config_file, indent=4)
self.config = default_config
return
# Step 3: Check for missing keys and add defaults if necessary
updated = False
for key, value in default_config.items():
if key not in self.config: # Field is missing
if key not in self.config:
self.config[key] = value
updated = True
# Step 4: Save the repaired config back to the file
if updated:
with open(config_path, "w") as config_file:
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):
# Define folder and file paths
themes_folder = "themes"
dark_theme_file = os.path.join(themes_folder, "Dark.json")
native_theme_file = os.path.join(themes_folder, "Native.json")
# Define the default content for Dark.json
dark_theme_content = {
"manifest": {
"name": "Dark",
@ -88,7 +201,6 @@ class HealthCheck:
"background_image_base64": ""
}
# Define the default content for Native.json
native_theme_content = {
"manifest": {
"name": "Native",
@ -99,25 +211,21 @@ class HealthCheck:
"palette": {}
}
# Step 1: Ensure the themes folder exists
if not os.path.exists(themes_folder):
print(f"Creating folder: {themes_folder}")
os.makedirs(themes_folder)
# Step 2: Ensure Dark.json exists
if not os.path.isfile(dark_theme_file):
print(f"Creating file: {dark_theme_file}")
with open(dark_theme_file, "w", encoding="utf-8") as file:
json.dump(dark_theme_content, file, indent=2)
print("Dark.json has been created successfully.")
# Step 3: Ensure Native.json exists
if not os.path.isfile(native_theme_file):
print(f"Creating file: {native_theme_file}")
with open(native_theme_file, "w", encoding="utf-8") as file:
json.dump(native_theme_content, file, indent=2)
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):
print("Theme Integrity OK")

View File

@ -1,16 +1,27 @@
import click
from picomc.cli.main import picomc_cli
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
def run_command(command="picomc"):
# 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:
picomc_cli.main(args=command.split())
# 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)
@ -24,7 +35,12 @@ def run_command(command="picomc"):
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

@ -15,7 +15,7 @@ from authser import MinecraftAuthenticator
from healthcheck import HealthCheck
import modulecli
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.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.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 datetime import datetime
@ -31,6 +31,7 @@ class PicomcVersionSelector(QWidget):
health_checker = HealthCheck()
health_checker.themes_integrity()
health_checker.check_config_file()
health_checker.zucaro_health_check()
self.config = health_checker.config
themes_folder = "themes"
@ -296,21 +297,17 @@ class PicomcVersionSelector(QWidget):
def open_settings_dialog(self):
dialog = QDialog(self)
dialog.setWindowTitle('Settings')
# Make the window resizable
dialog.setMinimumSize(400, 300)
# Create a Tab Widget
tab_widget = QTabWidget()
# Create the Settings Tab
# --- Settings Tab ---
settings_tab = QWidget()
settings_layout = QVBoxLayout()
title_label = QLabel('Settings')
title_label.setFont(QFont("Arial", 14))
# Create checkboxes for settings tab
discord_rcp_checkbox = QCheckBox('Discord Rich Presence')
discord_rcp_checkbox.setChecked(self.config.get("IsRCPenabled", False))
@ -326,7 +323,6 @@ class PicomcVersionSelector(QWidget):
settings_layout.addWidget(check_updates_checkbox)
settings_layout.addWidget(bleeding_edge_checkbox)
# Add buttons in the settings tab
update_button = QPushButton('Check for updates')
update_button.clicked.connect(self.check_for_update)
@ -342,53 +338,99 @@ class PicomcVersionSelector(QWidget):
settings_tab.setLayout(settings_layout)
# Create the Customization Tab
# --- Customization Tab ---
customization_tab = QWidget()
customization_layout = QVBoxLayout()
# Create theme background checkbox for customization tab
theme_background_checkbox = QCheckBox('Theme Background')
theme_background_checkbox.setChecked(self.config.get("ThemeBackground", False))
# Label to show currently selected theme
theme_filename = self.config.get('Theme', 'Dark.json')
current_theme_label = QLabel(f"Current Theme: {theme_filename}")
# QListWidget to display available themes
json_files_label = QLabel('Installed Themes:')
self.json_files_list_widget = QListWidget()
# Track selected theme
self.selected_theme = theme_filename # Default to current theme
# Build the list of themes
self.selected_theme = theme_filename
themes_list = self.build_themes_list()
# Populate themes initially
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(
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(current_theme_label)
customization_layout.addWidget(json_files_label)
customization_layout.addWidget(self.json_files_list_widget)
# Button to download themes
download_themes_button = QPushButton("Download More Themes")
download_themes_button.clicked.connect(self.download_themes_window)
customization_layout.addWidget(download_themes_button)
customization_tab.setLayout(customization_layout)
# Add the tabs to the TabWidget
# --- Java Tab ---
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(customization_tab, "Customization")
tab_widget.addTab(java_tab, "Java")
# Save button
save_button = QPushButton('Save')
@ -398,11 +440,13 @@ class PicomcVersionSelector(QWidget):
check_updates_checkbox.isChecked(),
theme_background_checkbox.isChecked(),
self.selected_theme,
bleeding_edge_checkbox.isChecked()
bleeding_edge_checkbox.isChecked(),
java_path_input.text(),
ram_selector.text(),
manage_java_checkbox.isChecked()
)
)
# Main layout
main_layout = QVBoxLayout()
main_layout.addWidget(tab_widget)
main_layout.addWidget(save_button)
@ -410,6 +454,13 @@ class PicomcVersionSelector(QWidget):
dialog.setLayout(main_layout)
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):
if checkbox.isChecked():
response = QMessageBox.question(
@ -638,20 +689,31 @@ class PicomcVersionSelector(QWidget):
## REPOSITORY BLOCK ENDS
def save_settings(self, is_rcp_enabled, check_updates_on_start, theme_background, selected_theme, is_bleeding):
def save_settings(
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"
updated_config = {
"IsRCPenabled": is_rcp_enabled,
"CheckUpdate": check_updates_on_start,
"ThemeBackground": theme_background,
"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)
# Save updated config to file
with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4)
@ -830,40 +892,44 @@ class PicomcVersionSelector(QWidget):
def run_game(self, selected_instance):
try:
# Set current_state to the selected instance
self.current_state = selected_instance
self.start_time = time.time()
# Read the config.json to get the "Instance" value
# Read config
with open('config.json', 'r') as config_file:
config = json.load(config_file)
instance_value = config.get("Instance", "default") # Default to "default" if not found
instance_value = config.get("Instance", "default")
max_ram = config.get("MaxRAM", 2)
manage_java = config.get("ManageJava", False)
java_path = config.get("JavaPath", "")
# Update lastplayed field in config.json on a separate thread
# Update last played on a thread
update_thread = threading.Thread(target=self.update_last_played, args=(selected_instance,))
update_thread.start()
# Run the game using the modulecli module
command = f"instance launch --version-override {selected_instance} {instance_value}"
output = modulecli.run_command(command)
# Build command
command = f"instance launch {instance_value} --version-override {selected_instance} --assigned-ram {max_ram}"
if manage_java:
command += " --manage-java"
if java_path:
command += f" --java {java_path}"
print(f"Launching command: {command}")
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}"
print(error_message) # Add this for debugging
logging.error(error_message)
# Use QMetaObject.invokeMethod to call showError safely
QMetaObject.invokeMethod(
self, "showError", Qt.QueuedConnection,
Q_ARG(str, "Error"), Q_ARG(str, error_message)
)
# (Show error in UI if necessary)
finally:
# Reset current_state to "menu" after the game closes
self.current_state = "menu"
self.update_total_playtime(self.start_time)
def update_last_played(self, selected_instance):
config_path = "config.json"
self.config["LastPlayed"] = selected_instance
@ -1137,7 +1203,7 @@ class PicomcVersionSelector(QWidget):
about_message = (
f"PicoDulce Launcher (v{version_number})\n\n"
"A simple Minecraft launcher built using Qt, based on the picomc project.\n\n"
"A simple Minecraft launcher built using Qt, based on the zucaro backend.\n\n"
"Credits:\n"
"Nixietab: Code and UI design\n"
"Wabaano: Graphic design\n"