picodulce/picodulce.py
2024-05-06 00:09:08 -03:00

520 lines
23 KiB
Python

import sys
import subprocess
import threading
import logging
import shutil
import json
import os
import requests
from PyQt5.QtWidgets import QApplication, QComboBox, QWidget, QVBoxLayout, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox
from PyQt5.QtGui import QFont, QIcon, QColor, QPalette
from PyQt5.QtCore import Qt
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
class PicomcVersionSelector(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('PicoDulce Launcher') # Change window title
self.setWindowIcon(QIcon('launcher_icon.ico')) # Set window icon
self.setGeometry(100, 100, 400, 250)
# Set application style and palette
app_style = QApplication.setStyle("Fusion")
dark_palette = self.create_dark_palette()
QApplication.instance().setPalette(dark_palette)
# Set window border color to dark mode on Windows
if sys.platform == 'win32':
self.setStyleSheet("QMainWindow { border: 2px solid rgb(53, 53, 53); }")
# Create title label
title_label = QLabel('PicoDulce Launcher') # Change label text
title_label.setFont(QFont("Arial", 24, QFont.Bold))
# Create installed versions section
installed_versions_label = QLabel('Installed Versions:')
installed_versions_label.setFont(QFont("Arial", 14))
self.installed_version_combo = QComboBox()
self.installed_version_combo.setMinimumWidth(200)
self.populate_installed_versions()
# Create buttons layout
buttons_layout = QVBoxLayout()
# Create play button for installed versions
self.play_button = QPushButton('Play')
self.play_button.setFocusPolicy(Qt.NoFocus) # Set focus policy to prevent highlighting
self.play_button.clicked.connect(self.play_instance)
self.play_button.setStyleSheet("background-color: #4bb679; color: white;")
buttons_layout.addWidget(self.play_button)
# Create button to open version download menu
self.version_download_button = QPushButton('Download Version')
self.version_download_button.clicked.connect(self.open_version_menu)
buttons_layout.addWidget(self.version_download_button)
# Create button to manage accounts
self.manage_accounts_button = QPushButton('Manage Accounts')
self.manage_accounts_button.clicked.connect(self.manage_accounts)
buttons_layout.addWidget(self.manage_accounts_button)
# Create button to install mod loader
self.install_mod_loader_button = QPushButton('Install Mod Loader')
self.install_mod_loader_button.clicked.connect(self.open_mod_loader_menu)
buttons_layout.addWidget(self.install_mod_loader_button)
# Create a button for the marroc mod loader
self.open_marroc_button = QPushButton('Marroc Mod Manager')
self.open_marroc_button.clicked.connect(self.open_marroc_script)
buttons_layout.addWidget(self.open_marroc_button)
# Create Update button
self.update_button = QPushButton('Update')
self.update_button.clicked.connect(self.check_for_update)
buttons_layout.addWidget(self.update_button)
# Create About button
self.about_button = QPushButton('About')
self.about_button.clicked.connect(self.show_about_dialog)
buttons_layout.addWidget(self.about_button)
# Set buttons layout alignment and spacing
buttons_layout.setAlignment(Qt.AlignTop)
buttons_layout.setSpacing(10)
# Set layout
main_layout = QVBoxLayout()
main_layout.addWidget(title_label, alignment=Qt.AlignCenter)
main_layout.addWidget(installed_versions_label)
main_layout.addWidget(self.installed_version_combo)
main_layout.addLayout(buttons_layout)
main_layout.setAlignment(Qt.AlignCenter)
main_layout.setSpacing(20)
self.setLayout(main_layout)
def populate_installed_versions(self):
# Run the command and get the output
try:
process = subprocess.Popen(['picomc', 'version', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
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
# Parse the output and replace '[local]' with a space
versions = output.splitlines()
versions = [version.replace('[local]', ' ').strip() for version in versions]
# Populate installed versions combo box
self.installed_version_combo.clear()
self.installed_version_combo.addItems(versions)
def open_marroc_script(self):
try:
# Replace 'path_to_marroc.py' with the actual path to marroc.py
subprocess.Popen(['python', 'marroc.py'])
except FileNotFoundError:
logging.error("'marroc.py' not found.")
QMessageBox.critical(self, "Error", "'marroc.py' not found.")
def play_instance(self):
if self.installed_version_combo.count() == 0:
QMessageBox.warning(self, "No Version Available", "Please download a version first.")
return
selected_instance = self.installed_version_combo.currentText()
# Create a separate thread to run the game process
play_thread = threading.Thread(target=self.run_game, args=(selected_instance,))
play_thread.start()
def run_game(self, selected_instance):
try:
subprocess.run(['picomc', 'play', selected_instance], check=True)
except subprocess.CalledProcessError as e:
error_message = f"Error playing {selected_instance}: {e.stderr.decode()}"
logging.error(error_message)
QMessageBox.critical(self, "Error", error_message)
def open_version_menu(self):
dialog = QDialog(self)
dialog.setWindowTitle('Download Version')
dialog.setFixedSize(300, 250)
# Create title label
title_label = QLabel('Download Version')
title_label.setFont(QFont("Arial", 14))
# Create checkboxes for different version types
release_checkbox = QCheckBox('Releases')
snapshot_checkbox = QCheckBox('Snapshots')
alpha_checkbox = QCheckBox('Alpha')
beta_checkbox = QCheckBox('Beta')
# Create dropdown menu for versions
version_combo = QComboBox()
def update_versions():
version_combo.clear()
options = []
if release_checkbox.isChecked():
options.append('--release')
if snapshot_checkbox.isChecked():
options.append('--snapshot')
if alpha_checkbox.isChecked():
options.append('--alpha')
if beta_checkbox.isChecked():
options.append('--beta')
if options:
try:
process = subprocess.Popen(['picomc', 'version', 'list'] + options, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
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
# Parse the output and replace '[local]' with a space
versions = output.splitlines()
versions = [version.replace('[local]', ' ').strip() for version in versions]
version_combo.addItems(versions)
release_checkbox.clicked.connect(update_versions)
snapshot_checkbox.clicked.connect(update_versions)
alpha_checkbox.clicked.connect(update_versions)
beta_checkbox.clicked.connect(update_versions)
# Set layout
layout = QVBoxLayout()
layout.addWidget(title_label)
layout.addWidget(release_checkbox)
layout.addWidget(snapshot_checkbox)
layout.addWidget(alpha_checkbox)
layout.addWidget(beta_checkbox)
layout.addWidget(version_combo)
# Create download button
download_button = QPushButton('Download')
download_button.clicked.connect(lambda: self.prepare_version(dialog, version_combo.currentText()))
layout.addWidget(download_button)
dialog.setLayout(layout)
dialog.exec_()
def prepare_version(self, dialog, version):
dialog.close()
try:
subprocess.run(['picomc', 'version', 'prepare', version], check=True)
QMessageBox.information(self, "Success", f"Version {version} prepared successfully!")
self.populate_installed_versions() # Refresh the installed versions list after downloading
except subprocess.CalledProcessError as e:
error_message = f"Error preparing {version}: {e.stderr.decode()}"
logging.error(error_message)
QMessageBox.critical(self, "Error", error_message)
def manage_accounts(self):
dialog = QDialog(self)
dialog.setWindowTitle('Manage Accounts')
dialog.setFixedSize(400, 250)
# Create title label
title_label = QLabel('Manage Accounts')
title_label.setFont(QFont("Arial", 14))
# Create dropdown menu for accounts
account_combo = QComboBox()
self.populate_accounts(account_combo)
# Create select button
select_button = QPushButton('Select')
select_button.clicked.connect(lambda: self.set_default_account(dialog, account_combo.currentText()))
# Create input field for account name
account_input = QLineEdit()
account_input.setPlaceholderText('Xx_PussySlayer_xX')
# Create button to create new account
create_account_button = QPushButton('Create')
create_account_button.clicked.connect(lambda: self.create_account(dialog, account_input.text(), microsoft_checkbox.isChecked()))
# Create checkbox for Microsoft account
microsoft_checkbox = QCheckBox('Microsoft Account')
# Create button to authenticate
authenticate_button = QPushButton('Authenticate')
authenticate_button.clicked.connect(lambda: self.authenticate_account(dialog, account_combo.currentText()))
# Create button to remove account
remove_account_button = QPushButton('Remove Account')
remove_account_button.clicked.connect(lambda: self.remove_account(dialog, account_combo.currentText()))
# Create layout for account selection
account_selection_layout = QVBoxLayout()
account_selection_layout.addWidget(title_label)
account_selection_layout.addWidget(account_combo)
account_selection_layout.addWidget(select_button)
# Create layout for account creation
create_account_layout = QHBoxLayout()
create_account_layout.addWidget(account_input)
create_account_layout.addWidget(create_account_button)
# Create layout for Microsoft account checkbox and authenticate button
microsoft_layout = QVBoxLayout()
microsoft_layout.addWidget(microsoft_checkbox)
microsoft_layout.addWidget(authenticate_button)
# Create layout for remove account button
remove_account_layout = QVBoxLayout()
remove_account_layout.addWidget(remove_account_button)
# Create main layout
main_layout = QVBoxLayout()
main_layout.addLayout(account_selection_layout)
main_layout.addLayout(create_account_layout)
main_layout.addLayout(microsoft_layout)
# main_layout.addStretch(1)
main_layout.addLayout(remove_account_layout)
dialog.setLayout(main_layout)
dialog.exec_()
def authenticate_account(self, dialog, account_name):
# Remove leading " * " from the account name
account_name = account_name.strip().lstrip(" * ")
try:
subprocess.run(['picomc', 'account', 'authenticate', account_name], check=True)
QMessageBox.information(self, "Success", f"Account '{account_name}' authenticated successfully!")
except subprocess.CalledProcessError as e:
error_message = f"Error authenticating account '{account_name}': {e.stderr.decode()}"
logging.error(error_message)
QMessageBox.critical(self, "Error", error_message)
def populate_accounts(self, account_combo):
# Run the command and get the output
try:
process = subprocess.Popen(['picomc', 'account', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
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
# Parse the output and remove ' *' from account names
accounts = output.splitlines()
accounts = [account.replace(' *', '').strip() for account in accounts]
# Populate accounts combo box
account_combo.clear()
account_combo.addItems(accounts)
def create_account(self, dialog, username, is_microsoft):
if username.strip() == '':
QMessageBox.warning(dialog, "Warning", "Username cannot be blank.")
return
try:
if is_microsoft:
subprocess.run(['picomc', 'account', 'create', username, '--ms'], check=True)
else:
subprocess.run(['picomc', 'account', 'create', username], check=True)
QMessageBox.information(self, "Success", f"Account {username} created successfully!")
self.populate_accounts(dialog.findChild(QComboBox))
except subprocess.CalledProcessError as e:
error_message = f"Error creating account: {e.stderr.decode()}"
logging.error(error_message)
QMessageBox.critical(self, "Error", error_message)
def set_default_account(self, dialog, account):
dialog.close()
try:
subprocess.run(['picomc', 'account', 'setdefault', account], check=True)
QMessageBox.information(self, "Success", f"Default account set to {account}!")
except subprocess.CalledProcessError as e:
error_message = f"Error setting default account: {e.stderr.decode()}"
logging.error(error_message)
QMessageBox.critical(self, "Error", error_message)
def remove_account(self, dialog, username):
if username.strip() == '':
QMessageBox.warning(dialog, "Warning", "Please select an account to remove.")
return
# Remove any leading " * " from the username
username = username.strip().lstrip(" * ")
# Ask for confirmation twice before removing the account
confirm_message = f"Are you sure you want to remove the account '{username}'?\nThis action cannot be undone."
confirm_dialog = QMessageBox.question(self, "Confirm Removal", confirm_message, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm_dialog == QMessageBox.Yes:
confirm_message_again = "This action is irreversible. Are you absolutely sure?"
confirm_dialog_again = QMessageBox.question(self, "Confirm Removal", confirm_message_again, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm_dialog_again == QMessageBox.Yes:
try:
subprocess.run(['picomc', 'account', 'remove', username], check=True)
QMessageBox.information(self, "Success", f"Account '{username}' removed successfully!")
self.populate_accounts(dialog.findChild(QComboBox))
except subprocess.CalledProcessError as e:
error_message = f"Error removing account '{username}': {e.stderr.decode()}"
logging.error(error_message)
QMessageBox.critical(self, "Error", error_message)
def open_mod_loader_menu(self):
dialog = QDialog(self)
dialog.setWindowTitle('Install Mod Loader')
dialog.setFixedSize(300, 200)
# Create title label
title_label = QLabel('Mod Loader Options')
title_label.setFont(QFont("Arial", 14))
# Create buttons for Fabric and Forge
fabric_button = QPushButton('Fabric')
fabric_button.clicked.connect(lambda: self.install_mod_loader(dialog, 'fabric'))
forge_button = QPushButton('Forge')
forge_button.clicked.connect(lambda: self.install_mod_loader(dialog, 'forge'))
# Create layout
layout = QVBoxLayout()
layout.addWidget(title_label)
layout.addWidget(fabric_button)
layout.addWidget(forge_button)
dialog.setLayout(layout)
dialog.exec_()
def install_mod_loader(self, dialog, mod_loader):
dialog.close()
try:
subprocess.run(['picomc', 'install', mod_loader], check=True)
QMessageBox.information(self, "Success", f"{mod_loader.capitalize()} installed successfully!")
except subprocess.CalledProcessError as e:
error_message = f"Error installing {mod_loader}: {e.stderr.decode()}"
logging.error(error_message)
QMessageBox.critical(self, "Error", error_message)
def create_dark_palette(self):
dark_palette = QPalette()
dark_palette.setColor(QPalette.Window, QColor(53, 53, 53))
dark_palette.setColor(QPalette.WindowText, Qt.white)
dark_palette.setColor(QPalette.Base, QColor(25, 25, 25))
dark_palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
dark_palette.setColor(QPalette.ToolTipBase, Qt.white)
dark_palette.setColor(QPalette.ToolTipText, Qt.white)
dark_palette.setColor(QPalette.Text, Qt.white)
dark_palette.setColor(QPalette.Button, QColor(53, 53, 53))
dark_palette.setColor(QPalette.ButtonText, Qt.white)
dark_palette.setColor(QPalette.BrightText, Qt.red)
dark_palette.setColor(QPalette.Link, QColor(42, 130, 218))
dark_palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
dark_palette.setColor(QPalette.HighlightedText, Qt.black)
return dark_palette
def show_about_dialog(self):
about_message = """
PicoDulce Launcher - Version 1.0
Developed by Your Name
This launcher allows you to manage your Minecraft versions,
accounts, and mod loaders with ease.
"""
QMessageBox.about(self, "About", about_message)
def check_for_update(self):
try:
with open("version.json") as f:
local_version_info = json.load(f)
local_version = local_version_info.get("version")
logging.info(f"Local version: {local_version}")
if local_version:
remote_version_info = self.fetch_remote_version()
remote_version = remote_version_info.get("version")
logging.info(f"Remote version: {remote_version}")
if remote_version and remote_version != local_version:
update_message = f"A new version ({remote_version}) is available!\nDo you want to download it now?"
update_dialog = QMessageBox.question(self, "Update Available", update_message, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if update_dialog == QMessageBox.Yes:
# Download and apply the update
self.download_update(remote_version_info)
else:
QMessageBox.information(self, "Up to Date", "You already have the latest version!")
else:
logging.error("Failed to read local version information.")
QMessageBox.critical(self, "Error", "Failed to check for updates.")
except Exception as e:
logging.error("Error checking for updates: %s", str(e))
QMessageBox.critical(self, "Error", "Failed to check for updates.")
def fetch_remote_version(self):
try:
update_url = "https://raw.githubusercontent.com/nixietab/picodulce/main/version.json"
response = requests.get(update_url)
if response.status_code == 200:
remote_version_info = response.json()
return remote_version_info
else:
logging.error("Failed to fetch update information.")
return None
except Exception as e:
logging.error("Error fetching remote version: %s", str(e))
return None
def download_update(self, version_info):
try:
update_folder = "update"
if not os.path.exists(update_folder):
os.makedirs(update_folder)
for link in version_info.get("links", []):
filename = os.path.basename(link)
response = requests.get(link, stream=True)
if response.status_code == 200:
with open(os.path.join(update_folder, filename), 'wb') as f:
for chunk in response.iter_content(chunk_size=1024):
f.write(chunk)
else:
QMessageBox.critical(self, "Error", f"Failed to download update file: {filename}")
# Move downloaded files one directory up
for file in os.listdir(update_folder):
src = os.path.join(update_folder, file)
dst = os.path.join(os.path.dirname(update_folder), file)
shutil.move(src, dst)
# Remove the update folder
shutil.rmtree(update_folder)
QMessageBox.information(self, "Update", "Updates downloaded successfully.")
except Exception as e:
logging.error("Error downloading updates: %s", str(e))
QMessageBox.critical(self, "Error", "Failed to download updates.")
if __name__ == '__main__':
app = QApplication(sys.argv)
window = PicomcVersionSelector()
window.show()
sys.exit(app.exec_())