Compare commits

...

5 Commits
0.13.8 ... main

Author SHA1 Message Date
Nix
0014978163
Better fallbacks in the login system 2025-12-10 21:18:42 -03:00
Nixietab
01587aa5e7 simple changes to the version managment menu
Some checks failed
Version Change Action / version-release (push) Has been cancelled
2025-12-08 18:38:54 -03:00
Nixietab
41bca4d23b bumped version, removed drums 2025-12-08 16:07:25 -03:00
Nixietab
9bac8e2d6d added actual progressbar to the version prepare menu 2025-12-08 16:06:16 -03:00
Nix
da0f4449ea
Return None instead of error message when no output 2025-12-08 15:47:15 -03:00
6 changed files with 236 additions and 73 deletions

View File

@ -134,6 +134,8 @@ class AuthenticationThread(QThread):
return j["access_token"], j["refresh_token"]
raise Exception("Authentication cancelled by user")
async def _xbl_auth(self, access_token):
data = {
"Properties": {
@ -223,13 +225,14 @@ class AuthenticationThread(QThread):
self.is_running = False
class MinecraftAuthenticator(QObject):
auth_finished = pyqtSignal(bool)
auth_finished = pyqtSignal(bool, str)
def __init__(self, parent=None):
super().__init__(parent)
self.auth_thread = None
self.auth_dialog = None
self.success = False
self.error_message = None
self.username = None
# Initialize the launcher to get the correct config path
@ -242,6 +245,12 @@ class MinecraftAuthenticator(QObject):
# Create accounts.json if it doesn't exist
if not self.save_to_accounts_json():
self.auth_finished.emit(False, self.error_message)
return
# Check if account is online
if not self.validate_account_type():
self.auth_finished.emit(False, "Cannot authenticate an offline account")
return
self.auth_thread = AuthenticationThread(username)
@ -263,9 +272,24 @@ class MinecraftAuthenticator(QObject):
self.auth_thread.stop()
def show_error(self, error_msg):
QMessageBox.critical(None, "Error", error_msg)
self.error_message = error_msg
self.success = False
self.auth_finished.emit(False)
def validate_account_type(self):
try:
accounts_file = Path(self.config_path) / "accounts.json"
if accounts_file.exists():
with open(accounts_file) as f:
config = json.load(f)
if self.username in config["accounts"]:
return config["accounts"][self.username].get("microsoft", False)
return True # New account, will be created as microsoft
except Exception as e:
logger.error(f"Failed to validate account type: {str(e)}")
return False
def save_to_accounts_json(self):
try:
@ -305,7 +329,7 @@ class MinecraftAuthenticator(QObject):
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)}")
self.error_message = f"Failed to initialize account data: {str(e)}"
return False
def on_access_token_received(self, data):
@ -328,17 +352,15 @@ class MinecraftAuthenticator(QObject):
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.error_message = f"Failed to update account data: {str(e)}"
self.success = False
self.auth_finished.emit(self.success)
# We don't emit here, we wait for the thread to finish
def on_authentication_finished(self):
if self.auth_dialog is not None:
@ -350,7 +372,9 @@ class MinecraftAuthenticator(QObject):
self.auth_thread = None
if not self.success:
self.auth_finished.emit(False)
self.auth_finished.emit(False, self.error_message)
else:
self.auth_finished.emit(True, f"Successfully authenticated account: {self.username}")
def cleanup(self):
if self.auth_dialog is not None:
@ -363,4 +387,4 @@ class MinecraftAuthenticator(QObject):
def create_authenticator():
"""Factory function to create a new MinecraftAuthenticator instance"""
return MinecraftAuthenticator()
return MinecraftAuthenticator()

BIN
drums.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -3,6 +3,7 @@ import threading
import time
import shlex
import gc
import re
from io import StringIO
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QPushButton, QHBoxLayout
from PyQt5.QtCore import Qt, pyqtSignal, QObject, QTimer, QEvent
@ -40,7 +41,7 @@ class StreamingCapture(StringIO):
# Signal/Object might be deleted
pass
if not self.launch_detected:
if not self.launch_detected and self.launch_signal:
lower_text = text.lower()
if "launching" in lower_text and ("game" in lower_text or "version" in lower_text or "minecraft" in lower_text):
self.launch_detected = True
@ -193,8 +194,174 @@ class LaunchWindow(QDialog):
pass
class PrepareWindow(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Preparing Version")
self.setModal(True)
self.resize(400, 150)
layout = QVBoxLayout()
self.status_label = QLabel("Initializing...")
self.status_label.setWordWrap(True)
self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label)
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 0) # Indeterminate progress
self.progress_bar.setTextVisible(False)
layout.addWidget(self.progress_bar)
# Add a manual close/cancel button
button_layout = QHBoxLayout()
button_layout.addStretch()
self.cancel_button = QPushButton("Cancel")
self.cancel_button.clicked.connect(self.request_abort)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
self.signals = LaunchSignals()
self.signals.log_update.connect(self.update_status)
self.signals.launch_complete.connect(self.on_prepare_complete)
self.signals.launch_aborted.connect(self.on_prepare_aborted)
self.signals.cleanup_done.connect(self.on_cleanup_done)
self.aborting = False
self.capture_streams = []
self.thread_running = False
self.success = False
def update_status(self, text):
# Parse output for progress updates
if "Downloading" in text and "libraries" in text:
try:
count = int(re.search(r'\d+', text).group())
self.status_label.setText(f"Downloading {count} libraries...")
except:
self.status_label.setText(text)
elif "Checking" in text and "assets" in text:
try:
count = int(re.search(r'\d+', text).group())
self.status_label.setText(f"Checking {count} assets...")
except:
self.status_label.setText(text)
elif "Jar file" in text and "downloaded" in text:
self.status_label.setText("Downloading game jar...")
elif "Checking libraries" in text:
self.status_label.setText("Checking libraries...")
else:
if len(text) > 100:
text = text[:97] + "..."
self.status_label.setText(text)
def on_prepare_complete(self):
if not self.aborting:
self.success = True
self.status_label.setText("Version prepared successfully!")
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(100)
self.cancel_button.setEnabled(False)
QTimer.singleShot(1500, self.accept)
def on_prepare_aborted(self):
self.status_label.setText("Preparation Aborted.")
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.success = False
def on_cleanup_done(self):
self.thread_running = False
if not self.success:
super().reject()
def request_abort(self):
if self.thread_running and not self.aborting:
self.aborting = True
self.status_label.setText("Aborting...")
self.cancel_button.setEnabled(False)
# Signal streams to stop
for stream in self.capture_streams:
stream.abort_requested = True
elif not self.thread_running:
super().reject()
def reject(self):
self.request_abort()
def closeEvent(self, event):
if self.thread_running:
event.ignore()
self.request_abort()
else:
event.accept()
def prepare_version(self, version):
command = f"version prepare {version}"
self.thread_running = True
thread = threading.Thread(target=self._run_prepare, args=(command,), daemon=True)
thread.start()
def _run_prepare(self, command):
try:
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()
from zucaro.cli.main import zucaro_cli
old_stdout, old_stderr = sys.stdout, sys.stderr
stdout_capture = StreamingCapture(self.signals.log_update, None)
stderr_capture = StreamingCapture(self.signals.log_update, None)
self.capture_streams = [stdout_capture, stderr_capture]
sys.stdout = stdout_capture
sys.stderr = stderr_capture
try:
zucaro_cli.main(args=shlex.split(command), standalone_mode=False)
except AbortException:
self.signals.launch_aborted.emit()
except SystemExit:
pass
except Exception as e:
self.signals.log_update.emit(f"Error: {str(e)}")
finally:
sys.stdout = old_stdout
sys.stderr = old_stderr
if not stdout_capture.abort_requested:
self.signals.launch_complete.emit()
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()
self.signals.cleanup_done.emit()
except Exception as e:
try:
self.signals.log_update.emit(f"Error preparing version: {str(e)}")
self.signals.cleanup_done.emit()
except:
pass
def launch_instance_with_window(command, parent=None):
window = LaunchWindow(parent)
window.launch_game(command)
window.exec_()
return window
def prepare_version_with_window(version, parent=None):
window = PrepareWindow(parent)
window.prepare_version(version)
result = window.exec_()
return window.success

View File

@ -42,5 +42,5 @@ def run_command(command="zucaro"):
gc.collect()
if not output:
return f"Error: No output from command. Stderr: {error}"
return output
return None
return output

View File

@ -16,7 +16,7 @@ from healthcheck import HealthCheck
import modulecli
import loaddaemon
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, QSpinBox, QFileDialog, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QTabWidget, QFrame, QSpacerItem, QSizePolicy, QMainWindow, QGridLayout, QTextEdit, QListWidget, QListWidgetItem, QMenu, QRadioButton
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
@ -1074,11 +1074,11 @@ class zucaroVersionSelector(QWidget):
logging.error(error_message)
QMessageBox.critical(dialog, "Error", error_message)
def _on_auth_finished(self, success):
def _on_auth_finished(self, success, message):
if success:
QMessageBox.information(self, "Success", "Account authenticated successfully!")
QMessageBox.information(self, "Success", message)
else:
QMessageBox.critical(self, "Error", "Failed to authenticate account")
QMessageBox.critical(self, "Error", message)
# Cleanup
if self.authenticator:
@ -1402,21 +1402,7 @@ class zucaroVersionSelector(QWidget):
dialog.finished.connect(self.populate_installed_versions)
dialog.exec_()
class DownloadThread(QThread):
completed = pyqtSignal(bool, str)
def __init__(self, version):
super().__init__()
self.version = version
def run(self):
try:
command = f"version prepare {self.version}"
modulecli.run_command(command)
self.completed.emit(True, f"Version {self.version} prepared successfully!")
except Exception as e:
error_message = f"Error preparing {self.version}: {str(e)}"
self.completed.emit(False, error_message)
class ModLoaderAndVersionMenu(QDialog):
def __init__(self, parent=None):
@ -1696,11 +1682,13 @@ class ModLoaderAndVersionMenu(QDialog):
title_label.setFont(QFont("Arial", 14))
layout.addWidget(title_label)
# Create checkboxes for mod loaders
self.forge_checkbox = QCheckBox('Forge')
self.fabric_checkbox = QCheckBox('Fabric')
# Create radio buttons for mod loaders
self.forge_checkbox = QRadioButton('Forge')
self.fabric_checkbox = QRadioButton('Fabric')
self.quilt_checkbox = QRadioButton('Quilt')
layout.addWidget(self.forge_checkbox)
layout.addWidget(self.fabric_checkbox)
layout.addWidget(self.quilt_checkbox)
# Create dropdown menu for versions
self.version_combo_mod = QComboBox()
@ -1709,19 +1697,23 @@ class ModLoaderAndVersionMenu(QDialog):
def update_versions():
self.version_combo_mod.clear()
if self.forge_checkbox.isChecked():
self.populate_available_releases(self.version_combo_mod, True, False)
self.populate_available_releases(self.version_combo_mod, True, False, False)
elif self.fabric_checkbox.isChecked():
self.populate_available_releases(self.version_combo_mod, False, True)
self.populate_available_releases(self.version_combo_mod, False, True, False)
elif self.quilt_checkbox.isChecked():
self.populate_available_releases(self.version_combo_mod, False, False, True)
self.forge_checkbox.clicked.connect(update_versions)
self.fabric_checkbox.clicked.connect(update_versions)
self.quilt_checkbox.clicked.connect(update_versions)
# Create install button
install_button = QPushButton('Install')
install_button.clicked.connect(lambda: self.install_mod_loader(
self.version_combo_mod.currentText(),
self.forge_checkbox.isChecked(),
self.fabric_checkbox.isChecked()
self.fabric_checkbox.isChecked(),
self.quilt_checkbox.isChecked()
))
layout.addWidget(install_button)
@ -1735,6 +1727,7 @@ class ModLoaderAndVersionMenu(QDialog):
# Create checkboxes for different version types
self.release_checkbox = QCheckBox('Releases')
self.release_checkbox.setChecked(True)
self.snapshot_checkbox = QCheckBox('Snapshots')
self.alpha_checkbox = QCheckBox('Alpha')
self.beta_checkbox = QCheckBox('Beta')
@ -1792,44 +1785,20 @@ class ModLoaderAndVersionMenu(QDialog):
# Connect the combo box signal to the update function
self.version_combo.currentIndexChanged.connect(self.update_download_button_state)
# Initial update
update_versions()
def update_download_button_state(self):
self.download_button.setEnabled(self.version_combo.currentIndex() != -1)
def show_popup(self):
self.popup = QDialog(self)
self.popup.setWindowTitle("Installing Version")
layout = QVBoxLayout(self.popup)
label = QLabel("The version is being installed...")
layout.addWidget(label)
movie = QMovie("drums.gif")
gif_label = QLabel()
gif_label.setMovie(movie)
layout.addWidget(gif_label)
movie.start()
self.popup.setGeometry(200, 200, 300, 200)
self.popup.setWindowModality(Qt.ApplicationModal)
self.popup.show()
def download_version(self, version):
# Show the popup in the main thread
self.show_popup()
self.download_thread = DownloadThread(version)
self.download_thread.completed.connect(self.on_download_completed)
self.download_thread.start()
def on_download_completed(self, success, message):
self.popup.close()
success = loaddaemon.prepare_version_with_window(version, self)
if success:
QMessageBox.information(self, "Success", message)
QMessageBox.information(self, "Success", f"Version {version} prepared successfully!")
else:
QMessageBox.critical(self, "Error", message)
logging.error(message)
QMessageBox.critical(self, "Error", f"Failed to prepare version {version}.")
def populate_available_releases(self, version_combo, install_forge, install_fabric):
def populate_available_releases(self, version_combo, install_forge, install_fabric, install_quilt):
try:
command = "version list --release"
output = modulecli.run_command(command)
@ -1840,7 +1809,7 @@ class ModLoaderAndVersionMenu(QDialog):
logging.error("Error: %s", str(e))
return
if install_fabric:
if install_fabric or install_quilt:
releases = [version for version in output.splitlines() if version.startswith("1.") and int(version.split('.')[1]) >= 14]
elif install_forge:
releases = [version for version in output.splitlines() if version.startswith("1.") and float(version.split('.')[1]) >= 5]
@ -1850,8 +1819,8 @@ class ModLoaderAndVersionMenu(QDialog):
version_combo.clear()
version_combo.addItems(releases)
def install_mod_loader(self, version, install_forge, install_fabric):
if not install_forge and not install_fabric:
def install_mod_loader(self, version, install_forge, install_fabric, install_quilt):
if not install_forge and not install_fabric and not install_quilt:
QMessageBox.warning(self, "Select Mod Loader", "Please select at least one mod loader.")
return
@ -1860,6 +1829,8 @@ class ModLoaderAndVersionMenu(QDialog):
mod_loader = 'forge'
elif install_fabric:
mod_loader = 'fabric'
elif install_quilt:
mod_loader = 'quilt'
if not mod_loader:
QMessageBox.warning(self, "Select Mod Loader", "Please select at least one mod loader.")
@ -1870,6 +1841,8 @@ class ModLoaderAndVersionMenu(QDialog):
command = f"mod loader forge install --game {version}"
elif mod_loader == 'fabric':
command = f"mod loader fabric install {version}"
elif mod_loader == 'quilt':
command = f"mod loader quilt install {version}"
modulecli.run_command(command)
QMessageBox.information(self, "Success", f"{mod_loader.capitalize()} installed successfully for version {version}!")
except Exception as e:

View File

@ -1,10 +1,9 @@
{
"version": "0.13.8",
"version": "0.13.10",
"links": [
"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/requirements.txt",
"https://raw.githubusercontent.com/nixietab/picodulce/main/drums.gif",
"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/authser.py",