Compare commits

...

62 Commits
0.11.7 ... main

Author SHA1 Message Date
github-actions[bot]
32e4783218 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-04-07 21:05:29 +00:00
Nix
502e64df83
Update version.json 2025-04-07 18:05:20 -03:00
github-actions[bot]
1cfb6ffcb6 Update version.json with commit count 2025-04-07 20:53:17 +00:00
Nix
77291ad89e
Update version.json 2025-04-07 17:53:08 -03:00
github-actions[bot]
3baf6e0b1d Update version.json with commit count 2025-04-07 20:52:52 +00:00
Nix
0768897706
stoped parsing commands (#12)
* Add files via upload

* auth done even better!
2025-04-07 17:52:43 -03:00
github-actions[bot]
ba8072c669 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-03-03 04:12:50 +00:00
Nix
785e9be9f9
Update version.json 2025-03-03 01:12:37 -03:00
github-actions[bot]
0cbd000be4 Update version.json with commit count 2025-03-03 04:11:40 +00:00
Nixietab
52b635285e Moved the health checks to a OOP method 2025-03-03 01:10:53 -03:00
github-actions[bot]
67a16c008a Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-03-01 08:34:21 +00:00
Nix
a4bd707461
Delete healtcheck.py 2025-03-01 05:34:12 -03:00
github-actions[bot]
1b27fffc96 Update version.json with commit count 2025-03-01 08:34:00 +00:00
Nix
fade5f86b7
Update picodulce.py 2025-03-01 05:33:51 -03:00
github-actions[bot]
823b438840 Update version.json with commit count 2025-03-01 08:33:36 +00:00
Nix
9a8c3f44d0
Update version.json 2025-03-01 05:33:25 -03:00
github-actions[bot]
6b65fb0d1e Update version.json with commit count 2025-03-01 08:33:18 +00:00
Nix
8247009d60
Delete locales directory 2025-03-01 05:33:07 -03:00
github-actions[bot]
e5c395d031 Update version.json with commit count 2025-03-01 08:06:58 +00:00
Nix
263e6eae07
Added rudimentary translations (#11)
* Update picodulce.py

* Update picodulce.py

* Update picodulce.py

* Update picodulce.py

* Create healtcheck.py

* Update picodulce.py

* Update version.json

* Create locales-go-here

* Add files via upload

* Update version.json

* Update picodulce.py

* Update healtcheck.py

* Update picodulce.py

* Added more locales

* Update version.json

* Update picodulce.py
2025-03-01 05:06:48 -03:00
github-actions[bot]
ec99488326 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-02-23 04:26:44 +00:00
Nix
61cd427beb
Delete locales directory 2025-02-23 01:26:35 -03:00
github-actions[bot]
cb2f5b52b3 Update version.json with commit count 2025-02-23 04:26:29 +00:00
Nix
ba40354a5d
Create bogosbinted.json 2025-02-23 01:26:19 -03:00
github-actions[bot]
0c151b058e Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-02-12 19:13:11 +00:00
Nix
fc7f47d273
put the loading theme background in a separate function 2025-02-12 16:12:59 -03:00
github-actions[bot]
4f4ff35ee5 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-02-11 11:52:38 +00:00
Nix
8b9827b422
Update version.json 2025-02-11 08:52:27 -03:00
github-actions[bot]
892cbc4d07 Update version.json with commit count 2025-02-11 11:51:51 +00:00
Nix
f61f15fe7e
Update version.json 2025-02-11 08:51:39 -03:00
github-actions[bot]
d077a922c0 Update version.json with commit count 2025-02-11 11:50:38 +00:00
Nix
9b70503d26
auth done right 2025-02-11 08:50:27 -03:00
github-actions[bot]
ae9f25a7a8 Update version.json with commit count 2025-02-11 11:49:55 +00:00
Nix
00ed5f97b9
authentication done right 2025-02-11 08:49:44 -03:00
github-actions[bot]
5dbbfd5d87 Update version.json with commit count 2025-02-11 05:45:27 +00:00
Nix
37a1c5b0df
Update version.json 2025-02-11 02:45:16 -03:00
github-actions[bot]
f2a1989993 Update version.json with commit count 2025-02-11 05:38:13 +00:00
Nix
3d40ce7df3
fixxed the need of re-opening the settings menu to refresh the themes list 2025-02-11 02:38:02 -03:00
github-actions[bot]
36ff8896ef Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-02-04 16:42:58 +00:00
Nix
5f59acf0b4
Merge pull request #10 from refrigerador67/main
Removed popup when checking for updates on startup when in latest version
2025-02-04 13:42:46 -03:00
github-actions[bot]
c48a193d9a Update version.json with commit count 2025-02-04 16:39:08 +00:00
refrigerador67
47a843c669
Removed popup at latest version when checking updates at startup 2025-02-04 13:38:57 -03:00
github-actions[bot]
52be28bb6c Update version.json with commit count 2025-02-04 16:30:53 +00:00
github-actions[bot]
6522b70066 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-01-29 02:03:17 +00:00
Nix
8d486a9af2
Update version.json 2025-01-28 23:03:07 -03:00
github-actions[bot]
97393e4ae7 Update version.json with commit count 2025-01-29 02:02:18 +00:00
Nix
e35120bb36
fixxed bleeding edge 2025-01-28 23:02:07 -03:00
github-actions[bot]
f2cfb3ceb3 Update version.json with commit count 2025-01-29 01:47:18 +00:00
Nix
15246cd535
Create Bleeding-Job.yaml 2025-01-28 22:47:06 -03:00
Nix
3edcd10c12
Update Build.yml 2025-01-28 22:43:44 -03:00
Nix
874e513b47
Update requirements.txt 2025-01-21 10:05:08 -03:00
Nix
a10318e00d
Update picodulce.py 2025-01-20 15:11:04 -03:00
Nix
7608b647fe
minnor fix to the instance stuff
Some checks failed
Version Change Action / version-release (push) Has been cancelled
2025-01-14 02:22:32 -03:00
Nix
60d16326b0
Update picodulce.py 2025-01-14 02:21:55 -03:00
Nix
0d300f0435
Merge pull request #9 from refrigerador67/main
Update README.md
2025-01-14 01:56:33 -03:00
refrigerador67
3123ed30cf
Update README.md 2025-01-14 04:50:05 +00:00
Nix
db41858aae
Update picodulce.py 2025-01-14 01:07:50 -03:00
Nix
8c0a794202
Merge pull request #8 from refrigerador67/main
Added PKGBUILD
2025-01-14 00:29:43 -03:00
refrigerador67
514f6427ab
Update README.md 2025-01-14 03:25:54 +00:00
refrigerador67
9920636b9c
Merge branch 'nixietab:main' into main 2025-01-14 03:22:20 +00:00
refrigerador67
5182e42f81
Added PKGBUILD 2025-01-14 03:21:48 +00:00
refrigerador67
a19b8a1545
Minor UI changes 2024-12-18 23:11:00 -03:00
10 changed files with 862 additions and 335 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

@ -9,6 +9,8 @@ jobs:
version-release: version-release:
runs-on: windows-latest # Use Windows 10 runner runs-on: windows-latest # Use Windows 10 runner
if: github.actor != 'github-actions[bot]' # Only run if the actor is not the GitHub Actions bot
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3

76
PKGBUILD Normal file
View File

@ -0,0 +1,76 @@
pkgname=picodulce
pkgver=0.11.7
pkgrel=1
pkgdesc="Launcher for Minecraft based on the picomc library"
arch=('x86_64')
OPTIONS=(!strip !docs libtool emptydirs)
url="https://github.com/nixietab/picodulce"
license=('MIT') # Replace with your project's license
depends=('python' 'python-virtualenv' 'xdg-utils')
makedepends=('git')
source=("git+https://github.com/nixietab/picodulce.git")
sha256sums=('SKIP')
package() {
cd "$srcdir/$pkgname"
# Create a directory for the application in the user's home directory
install -dm755 "$pkgdir/usr/share/$pkgname"
# Copy all project files to the created directory
cp -r . "$pkgdir/usr/share/$pkgname"
# Create a virtual environment
python -m venv "$pkgdir/usr/share/$pkgname/venv"
# Activate the virtual environment and install dependencies
source "$pkgdir/usr/share/$pkgname/venv/bin/activate"
pip install -r requirements.txt
# Create a run.sh script
install -Dm755 /dev/stdin "$pkgdir/usr/share/$pkgname/run.sh" <<EOF
#!/bin/bash
if [ ! -d "venv" ]; then
echo "venv folder does not exist. Creating virtual environment..."
python3 -m venv venv
source venv/bin/activate
echo "Installing required packages..."
pip install -r requirements.txt
else
source venv/bin/activate
fi
python picodulce.py
EOF
# Make the run.sh script executable
chmod +x "$pkgdir/usr/share/$pkgname/run.sh"
# Create a desktop entry for the application
install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/$pkgname.desktop" <<EOF
[Desktop Entry]
Name=Picodulce
Exec=/usr/share/picodulce/run.sh
Icon=/usr/share/picodulce/launcher_icon.ico
Terminal=true
Type=Application
Comment=Picodulce Launcher
Categories=Game;
EOF
# Ensure the normal user has permission to write to the picodulce folder
chown -R "$USER:$USER" "$pkgdir/usr/share/$pkgname"
chmod -R u+w "$pkgdir/usr/share/$pkgname"
#Install into bin
install -Dm755 /dev/stdin "$pkgdir/usr/bin/picodulce" <<EOF
#!/bin/bash
cd /usr/share/picodulce/
exec ./run.sh
EOF
}
# vim:set ts=2 sw=2 et:

View File

@ -40,7 +40,21 @@
- **Custom Theme Support**: Create and apply personalized themes with ease. A dedicated repository and guide are [available to help you get started.](https://github.com/nixietab/picodulce-themes) - **Custom Theme Support**: Create and apply personalized themes with ease. A dedicated repository and guide are [available to help you get started.](https://github.com/nixietab/picodulce-themes)
# Installation # Installation
If you are on windows you may be more interested in a [installer](https://github.com/nixietab/picodulce/releases/latest)
## Windows
For Windows systems using the [installer](https://github.com/nixietab/picodulce/releases/latest) is recommended
## Arch Linux
The package is available in the [AUR](https://aur.archlinux.org/packages/picodulce) as ```picodulce```
For installing on Arch without using an AUR helper a PKGBUILD is provided
```
git clone https://aur.archlinux.org/picodulce.git
cd picodulce
makepkg -si
```
## Other OS
### 1. Clone the repository ### 1. Clone the repository

239
authser.py Normal file
View File

@ -0,0 +1,239 @@
import sys
import re
import colorama
import requests
from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QVBoxLayout,
QPushButton, QLineEdit, QMessageBox)
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject, QTimer
from PyQt5.QtGui import QDesktopServices
from picomc.logging import logger
# Constants
URL_DEVICE_AUTH = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"
URL_TOKEN = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
CLIENT_ID = "c52aed44-3b4d-4215-99c5-824033d2bc0f"
SCOPE = "XboxLive.signin offline_access"
GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
class AuthDialog(QDialog):
def __init__(self, url, code, parent=None, error_mode=False):
super().__init__(parent)
self.setWindowTitle("Microsoft Authentication")
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setModal(True)
self.setup_ui(url, code, error_mode)
def setup_ui(self, url, code, error_mode):
layout = QVBoxLayout(self)
if error_mode:
error_label = QLabel("Error in Login - Please try again")
error_label.setStyleSheet("QLabel { color: red; font-weight: bold; }")
layout.addWidget(error_label)
instructions = QLabel(
"To authenticate your Microsoft Account:\n\n"
"1. Click 'Open Authentication Page' or visit:\n"
"2. Copy the code below\n"
"3. Paste the code on the Microsoft website\n"
"4. After completing authentication, click 'I've Completed Authentication'"
)
instructions.setWordWrap(True)
layout.addWidget(instructions)
url_label = QLabel(url)
url_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
url_label.setWordWrap(True)
layout.addWidget(url_label)
self.code_input = QLineEdit(code)
self.code_input.setReadOnly(True)
self.code_input.setAlignment(Qt.AlignCenter)
self.code_input.setStyleSheet("""
QLineEdit {
font-size: 16pt;
font-weight: bold;
padding: 5px;
}
""")
layout.addWidget(self.code_input)
copy_button = QPushButton("Copy Code")
copy_button.clicked.connect(self.copy_code)
layout.addWidget(copy_button)
open_url_button = QPushButton("Open Authentication Page")
open_url_button.clicked.connect(lambda: self.open_url(url))
layout.addWidget(open_url_button)
continue_button = QPushButton("I've Completed Authentication")
continue_button.clicked.connect(self.accept)
layout.addWidget(continue_button)
def copy_code(self):
clipboard = QApplication.clipboard()
clipboard.setText(self.code_input.text())
def open_url(self, url):
QDesktopServices.openUrl(QUrl(url))
class AuthenticationThread(QThread):
auth_data_received = pyqtSignal(dict)
error_occurred = pyqtSignal(str)
auth_error_detected = pyqtSignal(str)
finished = pyqtSignal()
access_token_received = pyqtSignal(str, str)
def __init__(self, account):
super().__init__()
self.account = account
self.device_code = None
self.is_running = True
def run(self):
try:
self.authenticate(self.account)
except Exception as e:
self.error_occurred.emit(str(e))
self.finished.emit()
def authenticate(self, account):
try:
data = {"client_id": CLIENT_ID, "scope": SCOPE}
# Request device code
resp = requests.post(URL_DEVICE_AUTH, data)
resp.raise_for_status()
j = resp.json()
self.device_code = j["device_code"]
user_code = j["user_code"]
link = j["verification_uri"]
# Format message with colorama
msg = j["message"]
msg = msg.replace(
user_code, colorama.Fore.RED + user_code + colorama.Fore.RESET
).replace(link, colorama.Style.BRIGHT + link + colorama.Style.NORMAL)
# Emit auth data received signal
self.auth_data_received.emit({'url': link, 'code': user_code})
except requests.exceptions.RequestException as e:
logger.error(f"Request failed: {e}")
self.error_occurred.emit(str(e))
self.finished.emit()
def poll_for_token(self):
try:
data = {"code": self.device_code, "grant_type": GRANT_TYPE, "client_id": CLIENT_ID}
resp = requests.post(URL_TOKEN, data)
if resp.status_code == 400:
j = resp.json()
logger.debug(j)
if j["error"] == "authorization_pending":
logger.warning(j["error_description"])
self.auth_error_detected.emit(j["error_description"])
return
else:
raise Exception(j["error_description"])
resp.raise_for_status()
j = resp.json()
access_token = j["access_token"]
refresh_token = j["refresh_token"]
logger.debug("OAuth device code flow successful")
self.access_token_received.emit(access_token, refresh_token)
self.finished.emit()
except requests.exceptions.RequestException as e:
logger.error(f"Request failed: {e}")
self.error_occurred.emit(str(e))
self.finished.emit()
def send_enter(self):
self.poll_for_token()
def stop(self):
self.is_running = False
class MinecraftAuthenticator(QObject):
auth_finished = pyqtSignal(bool)
def __init__(self, parent=None):
super().__init__(parent)
self.auth_thread = None
self.current_auth_data = None
self.auth_dialog = None
self.success = False
def authenticate(self, username):
self.success = False
self.auth_thread = AuthenticationThread(username)
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.access_token_received.connect(self.on_access_token_received)
self.auth_thread.finished.connect(self.on_authentication_finished)
self.auth_thread.start()
def show_auth_dialog(self, auth_data):
self.current_auth_data = auth_data
if self.auth_dialog is not None:
self.auth_dialog.close()
self.auth_dialog = None
self.auth_dialog = AuthDialog(auth_data['url'], auth_data['code'])
if self.auth_dialog.exec_() == QDialog.Accepted:
self.auth_thread.send_enter()
def handle_auth_error(self, output):
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.auth_finished.emit(False)
def on_access_token_received(self, access_token, refresh_token):
QMessageBox.information(None, "Success", "Authentication successful!")
self.success = True
self.auth_finished.emit(True)
def on_authentication_finished(self):
if self.auth_dialog is not None:
self.auth_dialog.close()
self.auth_dialog = None
if self.auth_thread:
self.auth_thread.stop()
self.auth_thread = None
if not self.success:
self.auth_finished.emit(False)
def cleanup(self):
if self.auth_dialog is not None:
self.auth_dialog.close()
self.auth_dialog = None
if self.auth_thread and self.auth_thread.isRunning():
self.auth_thread.stop()
self.auth_thread.wait()
# Example usage
if __name__ == '__main__':
app = QApplication(sys.argv)
authenticator = MinecraftAuthenticator()
authenticator.authenticate("TestUser")
sys.exit(app.exec_())

119
healthcheck.py Normal file
View File

@ -0,0 +1,119 @@
import os
import json
import requests
class HealthCheck:
def __init__(self):
self.config = None
def check_config_file(self):
config_path = "config.json"
default_config = {
"IsRCPenabled": False,
"CheckUpdate": False,
"IsBleeding": False,
"LastPlayed": "",
"IsFirstLaunch": True,
"Instance": "default",
"Theme": "Dark.json",
"ThemeBackground": True,
"ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json",
"Locale": "en"
}
# 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
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 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",
"description": "The default picodulce launcher theme",
"author": "Nixietab",
"license": "MIT"
},
"palette": {
"Window": "#353535",
"WindowText": "#ffffff",
"Base": "#191919",
"AlternateBase": "#353535",
"ToolTipBase": "#ffffff",
"ToolTipText": "#ffffff",
"Text": "#ffffff",
"Button": "#353535",
"ButtonText": "#ffffff",
"BrightText": "#ff0000",
"Link": "#2a82da",
"Highlight": "#4bb679",
"HighlightedText": "#ffffff"
},
"background_image_base64": ""
}
# Define the default content for Native.json
native_theme_content = {
"manifest": {
"name": "Native",
"description": "The native looks of your OS",
"author": "Your Qt Style",
"license": "Any"
},
"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")

30
modulecli.py Normal file
View File

@ -0,0 +1,30 @@
import click
from picomc.cli.main import picomc_cli
from io import StringIO
import sys
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())
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()
if not output:
return f"Error: No output from command. Stderr: {error}"
return output

View File

@ -10,21 +10,30 @@ import requests
import json import json
import os import os
import time import time
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, 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='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.ERROR, format='%(levelname)s - %(message)s')
class PicomcVersionSelector(QWidget): class PicomcVersionSelector(QWidget):
def __init__(self): def __init__(self):
self.current_state = "menu" self.current_state = "menu"
self.open_dialogs = [] self.open_dialogs = []
self.check_config_file()
self.themes_integrity()
themes_folder = "themes"
# Set up and use the health_check module
health_checker = HealthCheck()
health_checker.themes_integrity()
health_checker.check_config_file()
self.config = health_checker.config
themes_folder = "themes"
theme_file = self.config.get("Theme", "Dark.json") theme_file = self.config.get("Theme", "Dark.json")
# Ensure the theme file exists in the themes directory # Ensure the theme file exists in the themes directory
@ -48,6 +57,13 @@ class PicomcVersionSelector(QWidget):
discord_rcp_thread.daemon = True # Make the thread a daemon so it terminates when the main program exits discord_rcp_thread.daemon = True # Make the thread a daemon so it terminates when the main program exits
discord_rcp_thread.start() discord_rcp_thread.start()
if self.config.get("IsFirstLaunch", False):
self.FirstLaunch()
self.authenticator = MinecraftAuthenticator(self)
self.authenticator.auth_finished.connect(self._on_auth_finished)
def load_theme_from_file(self, file_path, app): def load_theme_from_file(self, file_path, app):
self.theme = {} self.theme = {}
# Check if the file exists, else load 'Dark.json' # Check if the file exists, else load 'Dark.json'
@ -107,98 +123,47 @@ class PicomcVersionSelector(QWidget):
else: else:
print("Theme dosn't seem to have a stylesheet") print("Theme dosn't seem to have a stylesheet")
def themes_integrity(self): def FirstLaunch(self):
# Define folder and file paths try:
themes_folder = "themes" self.config_path = "config.json"
dark_theme_file = os.path.join(themes_folder, "Dark.json") print("Running picomc instance create default command...")
native_theme_file = os.path.join(themes_folder, "Native.json")
# Define the default content for Dark.json # Run the command using modulecli
dark_theme_content = { command = "instance create default"
"manifest": { result = modulecli.run_command(command)
"name": "Dark",
"description": "The default picodulce launcher theme",
"author": "Nixietab",
"license": "MIT"
},
"palette": {
"Window": "#353535",
"WindowText": "#ffffff",
"Base": "#191919",
"AlternateBase": "#353535",
"ToolTipBase": "#ffffff",
"ToolTipText": "#ffffff",
"Text": "#ffffff",
"Button": "#353535",
"ButtonText": "#ffffff",
"BrightText": "#ff0000",
"Link": "#2a82da",
"Highlight": "#4bb679",
"HighlightedText": "#ffffff"
},
"background_image_base64": ""
}
# Define the default content for Native.json # Print the output of the command
native_theme_content = { print("Command output:", result)
"manifest": {
"name": "Native",
"description": "The native looks of your OS",
"author": "Your Qt Style",
"license": "Any"
},
"palette": {}
}
# Step 1: Ensure the themes folder exists # Change the value of IsFirstLaunch to False
if not os.path.exists(themes_folder): self.config["IsFirstLaunch"] = False
print(f"Creating folder: {themes_folder}") print("IsFirstLaunch set to False")
os.makedirs(themes_folder)
# Step 2: Ensure Dark.json exists # Save the updated config to the config.json file
if not os.path.isfile(dark_theme_file): with open(self.config_path, 'w') as f:
print(f"Creating file: {dark_theme_file}") json.dump(self.config, f, indent=4)
with open(dark_theme_file, "w", encoding="utf-8") as file: print("Configuration saved to", self.config_path)
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")
except Exception as e:
print("An error occurred while creating the instance.")
print("Error output:", str(e))
def resize_event(self, event): def resize_event(self, event):
if hasattr(self, 'movie_label'): if hasattr(self, 'movie_label'):
self.movie_label.setGeometry(0, 0, self.width(), self.height()) self.movie_label.setGeometry(0, 0, 400, 320)
event.accept() # Accept the resize event event.accept() # Accept the resize event
def load_theme_background(self):
"""Load and set the theme background image from base64 data in the theme configuration."""
if not self.config.get("ThemeBackground", False): # Default to False if ThemeBackground is missing
return
def init_ui(self):
self.setWindowTitle('PicoDulce Launcher') # Change window title
current_date = datetime.now()
if (current_date.month == 12 and current_date.day >= 8) or (current_date.month == 1 and current_date.day <= 1):
self.setWindowIcon(QIcon('holiday.ico')) # Set holiday icon
else:
self.setWindowIcon(QIcon('launcher_icon.ico')) # Set regular icon
self.setGeometry(100, 100, 400, 250)
# Set application style and theme
QApplication.setStyle("Fusion")
with open("config.json", "r") as config_file:
config = json.load(config_file)
if self.config.get("ThemeBackground", False): # Default to False if ThemeBackground is missing
# Get the base64 string for the background image from the theme file # Get the base64 string for the background image from the theme file
theme_background_base64 = self.theme.get("background_image_base64", "") theme_background_base64 = self.theme.get("background_image_base64", "")
if theme_background_base64: if not theme_background_base64:
print("No background GIF base64 string found in the theme file.")
return
try: try:
# Decode the base64 string to get the binary data # Decode the base64 string to get the binary data
background_image_data = QByteArray.fromBase64(theme_background_base64.encode()) background_image_data = QByteArray.fromBase64(theme_background_base64.encode())
@ -231,8 +196,24 @@ class PicomcVersionSelector(QWidget):
print("Error: Failed to load background GIF from base64 string.") print("Error: Failed to load background GIF from base64 string.")
except Exception as e: except Exception as e:
print(f"Error: Failed to decode and set background GIF. {e}") print(f"Error: Failed to decode and set background GIF. {e}")
def init_ui(self):
self.setWindowTitle('PicoDulce Launcher') # Change window title
current_date = datetime.now()
if (current_date.month == 12 and current_date.day >= 8) or (current_date.month == 1 and current_date.day <= 1):
self.setWindowIcon(QIcon('holiday.ico')) # Set holiday icon
else: else:
print("No background GIF base64 string found in the theme file.") self.setWindowIcon(QIcon('launcher_icon.ico')) # Set regular icon
self.setGeometry(100, 100, 400, 250)
# Set application style and theme
QApplication.setStyle("Fusion")
with open("config.json", "r") as config_file:
config = json.load(config_file)
# Load theme background
self.load_theme_background()
# Create title label # Create title label
title_label = QLabel('PicoDulce Launcher') # Change label text title_label = QLabel('PicoDulce Launcher') # Change label text
@ -312,48 +293,6 @@ class PicomcVersionSelector(QWidget):
else: else:
super().keyPressEvent(event) super().keyPressEvent(event)
def check_config_file(self):
config_path = "config.json"
default_config = {
"IsRCPenabled": False,
"CheckUpdate": False,
"LastPlayed": "",
"Instance": "default",
"Theme": "Dark.json",
"ThemeBackground": True,
"ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json"
}
# 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
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 open_settings_dialog(self): def open_settings_dialog(self):
dialog = QDialog(self) dialog = QDialog(self)
dialog.setWindowTitle('Settings') dialog.setWindowTitle('Settings')
@ -378,9 +317,14 @@ class PicomcVersionSelector(QWidget):
check_updates_checkbox = QCheckBox('Check Updates on Start') check_updates_checkbox = QCheckBox('Check Updates on Start')
check_updates_checkbox.setChecked(self.config.get("CheckUpdate", False)) check_updates_checkbox.setChecked(self.config.get("CheckUpdate", False))
bleeding_edge_checkbox = QCheckBox('Bleeding Edge')
bleeding_edge_checkbox.setChecked(self.config.get("IsBleeding", False))
bleeding_edge_checkbox.stateChanged.connect(lambda: self.show_bleeding_edge_popup(bleeding_edge_checkbox))
settings_layout.addWidget(title_label) settings_layout.addWidget(title_label)
settings_layout.addWidget(discord_rcp_checkbox) settings_layout.addWidget(discord_rcp_checkbox)
settings_layout.addWidget(check_updates_checkbox) settings_layout.addWidget(check_updates_checkbox)
settings_layout.addWidget(bleeding_edge_checkbox)
# Add buttons in the settings tab # Add buttons in the settings tab
update_button = QPushButton('Check for updates') update_button = QPushButton('Check for updates')
@ -412,24 +356,27 @@ class PicomcVersionSelector(QWidget):
# QListWidget to display available themes # QListWidget to display available themes
json_files_label = QLabel('Installed Themes:') json_files_label = QLabel('Installed Themes:')
json_files_list_widget = QListWidget() self.json_files_list_widget = QListWidget()
# Track selected theme # Track selected theme
self.selected_theme = theme_filename # Default to current theme self.selected_theme = theme_filename # Default to current theme
# Build the list of themes
themes_list = self.build_themes_list()
# Populate themes initially # Populate themes initially
self.populate_themes(json_files_list_widget) self.populate_themes(self.json_files_list_widget, themes_list)
# Update current theme label when a theme is selected # Update current theme label when a theme is selected
json_files_list_widget.itemClicked.connect( self.json_files_list_widget.itemClicked.connect(
lambda: self.on_theme_selected(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 # 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(json_files_list_widget) customization_layout.addWidget(self.json_files_list_widget)
# Button to download themes # Button to download themes
download_themes_button = QPushButton("Download More Themes") download_themes_button = QPushButton("Download More Themes")
@ -450,7 +397,8 @@ 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 # Pass the selected theme here self.selected_theme,
bleeding_edge_checkbox.isChecked()
) )
) )
@ -462,9 +410,20 @@ class PicomcVersionSelector(QWidget):
dialog.setLayout(main_layout) dialog.setLayout(main_layout)
dialog.exec_() dialog.exec_()
def populate_themes(self, json_files_list_widget): def show_bleeding_edge_popup(self, checkbox):
if checkbox.isChecked():
response = QMessageBox.question(
self,
"Bleeding Edge Feature",
"Enabling 'Bleeding Edge' mode may expose you to unstable and experimental features. Do you want to enable it anyway? In normal mode, updates are only downloaded when a stable release is made.",
QMessageBox.Yes | QMessageBox.No
)
if response == QMessageBox.No:
checkbox.setChecked(False)
def build_themes_list(self):
themes_folder = os.path.join(os.getcwd(), "themes") themes_folder = os.path.join(os.getcwd(), "themes")
json_files_list_widget.clear() themes_list = []
if os.path.exists(themes_folder): if os.path.exists(themes_folder):
json_files = [f for f in os.listdir(themes_folder) if f.endswith('.json')] json_files = [f for f in os.listdir(themes_folder) if f.endswith('.json')]
for json_file in json_files: for json_file in json_files:
@ -480,6 +439,12 @@ class PicomcVersionSelector(QWidget):
# Create display text and list item # Create display text and list item
display_text = f"{name}\n{description}\nBy: {author}" display_text = f"{name}\n{description}\nBy: {author}"
themes_list.append((display_text, json_file))
return themes_list
def populate_themes(self, json_files_list_widget, themes_list):
json_files_list_widget.clear()
for display_text, json_file in themes_list:
list_item = QListWidgetItem(display_text) list_item = QListWidgetItem(display_text)
list_item.setData(Qt.UserRole, json_file) # Store the JSON filename as metadata list_item.setData(Qt.UserRole, json_file) # Store the JSON filename as metadata
@ -544,9 +509,16 @@ class PicomcVersionSelector(QWidget):
main_layout.addLayout(right_layout) main_layout.addLayout(right_layout)
dialog.setLayout(main_layout) dialog.setLayout(main_layout)
dialog.finished.connect(lambda: self.update_themes_list())
self.load_themes() self.load_themes()
dialog.exec_() dialog.exec_()
def update_themes_list(self):
themes_list = self.build_themes_list()
self.populate_themes(self.json_files_list_widget, themes_list)
def fetch_themes(self): def fetch_themes(self):
try: try:
with open("config.json", "r") as config_file: with open("config.json", "r") as config_file:
@ -666,13 +638,14 @@ class PicomcVersionSelector(QWidget):
## REPOSITORY BLOCK ENDS ## REPOSITORY BLOCK ENDS
def save_settings(self, is_rcp_enabled, check_updates_on_start, theme_background, selected_theme): def save_settings(self, is_rcp_enabled, check_updates_on_start, theme_background, selected_theme, is_bleeding):
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
} }
# Update config values # Update config values
@ -689,20 +662,6 @@ 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")
@ -742,16 +701,16 @@ class PicomcVersionSelector(QWidget):
def open_game_directory(self): def open_game_directory(self):
try: try:
# Run the command and capture the output # Run the command using modulecli
result = subprocess.run(['picomc', 'instance', 'dir'], capture_output=True, text=True, check=True) command = "instance dir"
game_directory = result.stdout.strip() result = modulecli.run_command(command)
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 subprocess.CalledProcessError as e: except Exception 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"
@ -772,20 +731,17 @@ class PicomcVersionSelector(QWidget):
# Run the command and capture the output # Run the command and capture the output
try: try:
process = subprocess.Popen(['picomc', 'version', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) command = "version list"
output, error = process.communicate() output = modulecli.run_command(command)
if process.returncode != 0: if not output:
raise subprocess.CalledProcessError(process.returncode, process.args, output=output, stderr=error) raise Exception("Failed to get output from modulecli")
except FileNotFoundError: except Exception as e:
logging.error("'picomc' command not found. Please ensure it's installed and in your PATH.") logging.error("Error running 'picomc': %s", e)
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()] versions = [version.replace('[local]', ' ').strip() for version in output.splitlines() if version.strip()]
# 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", "")
@ -802,28 +758,22 @@ class PicomcVersionSelector(QWidget):
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:
process = subprocess.Popen(['picomc', 'instance', 'create', 'default'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) command = "instance create default"
output, error = process.communicate() output = modulecli.run_command(command)
if process.returncode != 0: if not output:
raise subprocess.CalledProcessError(process.returncode, process.args, error) raise Exception("Failed to get output from modulecli for 'instance create default'")
except FileNotFoundError: except Exception as e:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.") logging.error("Error creating default instance: %s", str(e))
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:
process = subprocess.Popen(['picomc', 'version', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) command = "version list"
output, error = process.communicate() output = modulecli.run_command(command)
if process.returncode != 0: if not output:
raise subprocess.CalledProcessError(process.returncode, process.args, error) raise Exception("Failed to get output from modulecli for 'version list'")
except FileNotFoundError: except Exception as e:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.") logging.error("Error: %s", str(e))
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
@ -850,7 +800,7 @@ class PicomcVersionSelector(QWidget):
# Check if there are any accounts # Check if there are any accounts
try: try:
account_list_output = subprocess.check_output(["picomc", "account", "list"]).decode("utf-8").strip() account_list_output = modulecli.run_command("account list").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
@ -859,18 +809,25 @@ 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 subprocess.CalledProcessError as e: except Exception 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: {selected_instance}") logging.info(f"Selected instance from dropdown: {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 # Set current_state to the selected instance
@ -885,10 +842,14 @@ class PicomcVersionSelector(QWidget):
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()
# Run the game subprocess with the instance_value from config.json # Run the game using the modulecli module
subprocess.run(['picomc', 'instance', 'launch', '--version-override', selected_instance, instance_value], check=True) command = f"instance launch --version-override {selected_instance} {instance_value}"
output = modulecli.run_command(command)
except subprocess.CalledProcessError as e: 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}"
logging.error(error_message) logging.error(error_message)
# Use QMetaObject.invokeMethod to call showError safely # Use QMetaObject.invokeMethod to call showError safely
@ -900,6 +861,7 @@ class PicomcVersionSelector(QWidget):
# Reset current_state to "menu" after the game closes # Reset current_state to "menu" after the game closes
self.current_state = "menu" self.current_state = "menu"
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
@ -997,16 +959,16 @@ class PicomcVersionSelector(QWidget):
return return
try: try:
command = ['picomc', 'account', 'create', username] command = f"account create {username}"
if is_microsoft: if is_microsoft:
command.append('--ms') command += " --ms"
subprocess.run(command, check=True) modulecli.run_command(command)
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 subprocess.CalledProcessError as e: except Exception as e:
error_message = f"Error creating account: {e.stderr.decode()}" error_message = f"Error creating account: {str(e)}"
logging.error(error_message) logging.error(error_message)
QMessageBox.critical(dialog, "Error", error_message) QMessageBox.critical(dialog, "Error", error_message)
@ -1017,19 +979,36 @@ class PicomcVersionSelector(QWidget):
return False return False
def authenticate_account(self, dialog, account_name): def authenticate_account(self, dialog, account_name):
# Authenticate a selected account # Clean up the account name
account_name = account_name.strip().lstrip(" * ") account_name = account_name.strip().lstrip(" * ")
if not account_name: if not account_name:
QMessageBox.warning(dialog, "Warning", "Please select an account to authenticate.") QMessageBox.warning(dialog, "Warning", "Please select an account to authenticate.")
return return
try: try:
subprocess.run(['picomc', 'account', 'authenticate', account_name], check=True) # Create authenticator instance if it doesn't exist
QMessageBox.information(self, "Success", f"Account '{account_name}' authenticated successfully!") if self.authenticator is None:
except subprocess.CalledProcessError as e: self.authenticator = MinecraftAuthenticator(self)
error_message = f"Error authenticating account '{account_name}': {e.stderr.decode()}" self.authenticator.auth_finished.connect(self._on_auth_finished)
# Start authentication process
self.authenticator.authenticate(account_name)
except Exception as e:
error_message = f"Error authenticating account '{account_name}': {str(e)}"
logging.error(error_message) logging.error(error_message)
QMessageBox.critical(self, "Error", error_message) QMessageBox.critical(dialog, "Error", error_message)
def _on_auth_finished(self, success):
if success:
QMessageBox.information(self, "Success", "Account authenticated successfully!")
else:
QMessageBox.critical(self, "Error", "Failed to authenticate account")
# Cleanup
if self.authenticator:
self.authenticator.cleanup()
self.authenticator = None
def remove_account(self, dialog, username): def remove_account(self, dialog, username):
# Remove a selected account # Remove a selected account
@ -1042,22 +1021,20 @@ 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:
subprocess.run(['picomc', 'account', 'remove', username], check=True) command = f"account remove {username}"
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 subprocess.CalledProcessError as e: except Exception as e:
error_message = f"Error removing account: {e.stderr.decode()}" error_message = f"Error removing account: {str(e)}"
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:
process = subprocess.Popen(['picomc', 'account', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) command = "account list"
output, error = process.communicate() output = modulecli.run_command(command)
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()
@ -1082,10 +1059,8 @@ class PicomcVersionSelector(QWidget):
for account in normal_accounts: for account in normal_accounts:
account_combo.addItem(account) account_combo.addItem(account)
except FileNotFoundError: except Exception as e:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.") logging.error(f"Error: {str(e)}")
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
@ -1102,22 +1077,38 @@ class PicomcVersionSelector(QWidget):
return return
try: try:
subprocess.run(['picomc', 'account', 'setdefault', account_name], check=True) command = f"account setdefault {account_name}"
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 subprocess.CalledProcessError as e: except Exception as e:
error_message = f"Error setting default account '{account_name}': {e.stderr.decode()}" error_message = f"Error setting default account '{account_name}': {str(e)}"
logging.error(error_message) logging.error(error_message)
QMessageBox.critical(self, "Error", error_message) QMessageBox.critical(self, "Error", error_message)
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:
with open('version.json', 'r') as version_file: with open('version.json', 'r') as version_file:
version_data = json.load(version_file) version_data = json.load(version_file)
version_number = version_data.get('version', 'unknown version') version_number = version_data.get('version', 'unknown version')
version_bleeding = version_data.get('versionBleeding', None)
except (FileNotFoundError, json.JSONDecodeError): except (FileNotFoundError, json.JSONDecodeError):
version_number = 'unknown version' version_number = 'unknown version'
version_bleeding = None
# Check the configuration for IsBleeding
try:
with open('config.json', 'r') as config_file:
config_data = json.load(config_file)
is_bleeding = config_data.get('IsBleeding', False)
except (FileNotFoundError, json.JSONDecodeError):
is_bleeding = False
# Use versionBleeding if IsBleeding is true
if is_bleeding and 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"
@ -1134,19 +1125,39 @@ class PicomcVersionSelector(QWidget):
with open("version.json") as f: with open("version.json") as f:
local_version_info = json.load(f) local_version_info = json.load(f)
local_version = local_version_info.get("version") local_version = local_version_info.get("version")
local_version_bleeding = local_version_info.get("versionBleeding")
logging.info(f"Local version: {local_version}") logging.info(f"Local version: {local_version}")
logging.info(f"Local bleeding version: {local_version_bleeding}")
with open("config.json") as config_file:
config = json.load(config_file)
is_bleeding = config.get("IsBleeding", False)
if local_version: if local_version:
remote_version_info = self.fetch_remote_version() remote_version_info = self.fetch_remote_version()
remote_version = remote_version_info.get("version") remote_version = remote_version_info.get("version")
remote_version_bleeding = remote_version_info.get("versionBleeding")
logging.info(f"Remote version: {remote_version}") logging.info(f"Remote version: {remote_version}")
if remote_version and remote_version != local_version: logging.info(f"Remote bleeding version: {remote_version_bleeding}")
if is_bleeding:
remote_version_to_check = remote_version_bleeding
local_version_to_check = local_version_bleeding
else:
remote_version_to_check = remote_version
local_version_to_check = local_version
if remote_version_to_check and (remote_version_to_check != local_version_to_check):
if is_bleeding:
update_message = f"Do you want to update to the bleeding edge version ({remote_version_bleeding})?"
else:
update_message = f"A new version ({remote_version}) is available!\nDo you want to download it now?" 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) update_dialog = QMessageBox.question(self, "Update Available", update_message, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if update_dialog == QMessageBox.Yes: if update_dialog == QMessageBox.Yes:
# Download and apply the update # Download and apply the update
self.download_update(remote_version_info) self.download_update(remote_version_info)
else: else:
print("Up to Date", "You already have the latest version!") print(f"You already have the latest version!")
else: else:
logging.error("Failed to read local version information.") logging.error("Failed to read local version information.")
QMessageBox.critical(self, "Error", "Failed to check for updates.") QMessageBox.critical(self, "Error", "Failed to check for updates.")
@ -1159,12 +1170,32 @@ class PicomcVersionSelector(QWidget):
with open("version.json") as f: with open("version.json") as f:
local_version_info = json.load(f) local_version_info = json.load(f)
local_version = local_version_info.get("version") local_version = local_version_info.get("version")
local_version_bleeding = local_version_info.get("versionBleeding")
logging.info(f"Local version: {local_version}") logging.info(f"Local version: {local_version}")
logging.info(f"Local bleeding version: {local_version_bleeding}")
with open("config.json") as config_file:
config = json.load(config_file)
is_bleeding = config.get("IsBleeding", False)
if local_version: if local_version:
remote_version_info = self.fetch_remote_version() remote_version_info = self.fetch_remote_version()
remote_version = remote_version_info.get("version") remote_version = remote_version_info.get("version")
remote_version_bleeding = remote_version_info.get("versionBleeding")
logging.info(f"Remote version: {remote_version}") logging.info(f"Remote version: {remote_version}")
if remote_version and remote_version != local_version: logging.info(f"Remote bleeding version: {remote_version_bleeding}")
if is_bleeding:
remote_version_to_check = remote_version_bleeding
local_version_to_check = local_version_bleeding
else:
remote_version_to_check = remote_version
local_version_to_check = local_version
if remote_version_to_check and (remote_version_to_check != local_version_to_check):
if is_bleeding:
update_message = f"Do you want to update to the bleeding edge version ({remote_version_bleeding})?"
else:
update_message = f"A new version ({remote_version}) is available!\nDo you want to download it now?" 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) update_dialog = QMessageBox.question(self, "Update Available", update_message, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if update_dialog == QMessageBox.Yes: if update_dialog == QMessageBox.Yes:
@ -1285,10 +1316,11 @@ class DownloadThread(QThread):
def run(self): def run(self):
try: try:
subprocess.run(['picomc', 'version', 'prepare', self.version], check=True) command = f"version prepare {self.version}"
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 subprocess.CalledProcessError as e: except Exception as e:
error_message = f"Error preparing {self.version}: {e.stderr.decode()}" error_message = f"Error preparing {self.version}: {str(e)}"
self.completed.emit(False, error_message) self.completed.emit(False, error_message)
class ModLoaderAndVersionMenu(QDialog): class ModLoaderAndVersionMenu(QDialog):
@ -1305,11 +1337,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() # New tab for instances instances_tab = QWidget()
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") # Add the new tab tab_widget.addTab(instances_tab, "Instances")
# 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)
@ -1361,11 +1393,8 @@ class ModLoaderAndVersionMenu(QDialog):
if instance_name: if instance_name:
try: try:
# Run the "picomc instance create" command # Run the "picomc instance create" command
process = subprocess.Popen(['picomc', 'instance', 'create', instance_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) command = f"instance create {instance_name}"
output, error = process.communicate() modulecli.run_command(command)
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.")
@ -1376,11 +1405,9 @@ 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 FileNotFoundError: except Exception as e:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.") logging.error("Error creating instance: %s", str(e))
except subprocess.CalledProcessError as e: QMessageBox.critical(self, "Error", f"Failed to create instance: {str(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.")
@ -1391,14 +1418,8 @@ class ModLoaderAndVersionMenu(QDialog):
try: try:
# Run the "picomc instance rename" command # Run the "picomc instance rename" command
process = subprocess.Popen( command = f"instance rename {old_instance_name} {new_instance_name}"
['picomc', 'instance', 'rename', old_instance_name, new_instance_name], modulecli.run_command(command)
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.")
@ -1410,11 +1431,9 @@ 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 FileNotFoundError: except Exception as e:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.") logging.error("Error renaming instance: %s", str(e))
except subprocess.CalledProcessError as e: QMessageBox.critical(self, "Error", f"Failed to rename instance: {str(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":
@ -1429,11 +1448,8 @@ 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
process = subprocess.Popen(['picomc', 'instance', 'delete', instance_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) command = f"instance delete {instance_name}"
output, error = process.communicate() modulecli.run_command(command)
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.")
@ -1441,18 +1457,15 @@ class ModLoaderAndVersionMenu(QDialog):
# Reload the instances list # Reload the instances list
self.load_instances() self.load_instances()
except FileNotFoundError: except Exception as e:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.") logging.error("Error deleting instance: %s", str(e))
except subprocess.CalledProcessError as e: QMessageBox.critical(self, "Error", f"Failed to delete instance: {str(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:
process = subprocess.Popen(['picomc', 'instance', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) # Run the "picomc instance list" command
output, error = process.communicate() command = "instance list"
if process.returncode != 0: output = modulecli.run_command(command)
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()
@ -1462,10 +1475,9 @@ 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 FileNotFoundError: except Exception as e:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.") logging.error("Error fetching instances: %s", str(e))
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()
@ -1621,21 +1633,19 @@ class ModLoaderAndVersionMenu(QDialog):
options.append('--beta') options.append('--beta')
if options: if options:
try: try:
process = subprocess.Popen(['picomc', 'version', 'list'] + options, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) command = 'version list ' + ' '.join(options)
output, error = process.communicate() output = modulecli.run_command(command)
if process.returncode != 0: if "Error" in output:
raise subprocess.CalledProcessError(process.returncode, process.args, error) logging.error(output)
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
# Parse the output and replace '[local]' with a space # Parse the output and replace '[local]' with a space
versions = output.splitlines() versions = output.splitlines()
versions = [version.replace('[local]', ' ').strip() for version in versions] versions = [version.replace('[local]', ' ').strip() for version in versions]
self.version_combo.addItems(versions) self.version_combo.addItems(versions)
except Exception as e:
logging.error("Unexpected error: %s", e)
return
# 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()
@ -1692,15 +1702,10 @@ 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:
process = subprocess.Popen(['picomc', 'version', 'list', '--release'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) command = "version list --release"
output, error = process.communicate() output = modulecli.run_command(command)
if process.returncode != 0: except Exception as e:
raise subprocess.CalledProcessError(process.returncode, process.args, error) logging.error("Error: %s", str(e))
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:
@ -1730,12 +1735,13 @@ class ModLoaderAndVersionMenu(QDialog):
try: try:
if mod_loader == 'forge': if mod_loader == 'forge':
subprocess.run(['picomc', 'mod', 'loader', 'forge', 'install', '--game', version], check=True) command = f"mod loader forge install --game {version}"
elif mod_loader == 'fabric': elif mod_loader == 'fabric':
subprocess.run(['picomc', 'mod', 'loader', 'fabric', 'install', version], check=True) command = f"mod loader fabric install {version}"
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 subprocess.CalledProcessError as e: except Exception as e:
error_message = f"Error installing {mod_loader} for version {version}: {e.stderr.decode()}" error_message = f"Error installing {mod_loader} for version {version}: {str(e)}"
QMessageBox.critical(self, "Error", error_message) QMessageBox.critical(self, "Error", error_message)
logging.error(error_message) logging.error(error_message)

View File

@ -2,3 +2,4 @@ picomc
PyQt5 PyQt5
requests requests
pypresence pypresence
tqdm

View File

@ -1,11 +1,15 @@
{ {
"version": "0.11.7", "version": "0.13",
"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",
"https://raw.githubusercontent.com/nixietab/picodulce/main/requirements.txt", "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/drums.gif",
"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/healthcheck.py",
"https://raw.githubusercontent.com/nixietab/picodulce/main/modulecli.py"
],
"versionBleeding": "0.13-194"
} }