Compare commits

...

63 Commits
0.11.9 ... main

Author SHA1 Message Date
Nix
8f24903fb3
Zucaro replace (#15)
Some checks failed
Version Change Action / version-release (push) Has been cancelled
* Initial commit of the replace

* Delete .github/workflows/Bleeding-Job.yaml

* remove picomc

* Update README.md

* Update version.json
2025-09-15 00:55:06 -03:00
github-actions[bot]
408afef6f1 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-08-12 23:06:17 +00:00
Nix
2b950c3c2d
Update version.json 2025-08-12 20:06:10 -03:00
github-actions[bot]
12b8b67bbd Update version.json with commit count 2025-08-12 23:05:47 +00:00
Nix
aceb899ea6
add zucaro to the requirements 2025-08-12 20:05:33 -03:00
github-actions[bot]
5e27a78ded 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-05-20 02:43:10 +00:00
Nix
aaf5ccbc90
Update version.json 2025-05-19 23:42:58 -03:00
github-actions[bot]
aaf73533c1 Update version.json with commit count 2025-05-20 02:42:43 +00:00
Nix
8a6246cbbb
fix 2025-05-19 23:42:31 -03:00
github-actions[bot]
723cd1dd56 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-23 18:49:27 +00:00
Wabaano
1e30d2a164
PR of recording total playtime (#14)
Added "Playtime" on About menu.
2025-04-23 15:49:13 -03:00
github-actions[bot]
9523f1ab0c 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-15 06:23:51 +00:00
Nix
103927328a
little fix to the version managing window 2025-04-15 03:23:40 -03:00
github-actions[bot]
faefc09aad Update version.json with commit count 2025-04-15 06:13:47 +00:00
Nix
dfc2dac3f5
Update version.json 2025-04-15 03:13:37 -03:00
github-actions[bot]
7942c09082 Update version.json with commit count 2025-04-15 06:13:15 +00:00
Nix
d85cfe2de6
Update requirements.txt 2025-04-15 03:13:08 -03:00
github-actions[bot]
d145fb2df1 Update version.json with commit count 2025-04-15 06:13:02 +00:00
OmeletGit
dedc59d09c
Fix the authentication system dont saving tokens (#13) 2025-04-15 03:12:53 -03:00
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
8 changed files with 1037 additions and 421 deletions

View File

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

View File

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

366
authser.py Normal file
View File

@ -0,0 +1,366 @@
import sys
import json
import os
import uuid
import asyncio
import aiohttp
from datetime import datetime, timezone
from pathlib import Path
from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QVBoxLayout,
QPushButton, QLineEdit, QMessageBox)
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject
from PyQt5.QtGui import QDesktopServices
from picomc.logging import logger
from picomc.launcher import get_default_root, Launcher
# Constants for Microsoft Authentication
URL_DEVICE_AUTH = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"
URL_TOKEN = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
URL_XBL = "https://user.auth.xboxlive.com/user/authenticate"
URL_XSTS = "https://xsts.auth.xboxlive.com/xsts/authorize"
URL_MC = "https://api.minecraftservices.com/authentication/login_with_xbox"
URL_PROFILE = "https://api.minecraftservices.com/minecraft/profile"
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(dict)
def __init__(self, username):
super().__init__()
self.username = username
self.device_code = None
self.is_running = True
async def _ms_oauth(self):
data = {"client_id": CLIENT_ID, "scope": SCOPE}
async with aiohttp.ClientSession() as session:
async with session.post(URL_DEVICE_AUTH, data=data) as resp:
if resp.status != 200:
raise Exception(f"Failed to get device code: {await resp.text()}")
j = await resp.json()
self.device_code = j["device_code"]
self.auth_data_received.emit({
'url': j["verification_uri"],
'code': j["user_code"]
})
while self.is_running:
data = {
"grant_type": GRANT_TYPE,
"client_id": CLIENT_ID,
"device_code": self.device_code
}
async with session.post(URL_TOKEN, data=data) as resp:
j = await resp.json()
if resp.status == 400:
if j["error"] == "authorization_pending":
await asyncio.sleep(2)
continue
else:
raise Exception(j["error_description"])
elif resp.status != 200:
raise Exception(f"Token request failed: {j}")
return j["access_token"], j["refresh_token"]
async def _xbl_auth(self, access_token):
data = {
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": f"d={access_token}"
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}
async with aiohttp.ClientSession() as session:
async with session.post(URL_XBL, json=data) as resp:
if resp.status != 200:
raise Exception(f"XBL auth failed: {await resp.text()}")
j = await resp.json()
return j["Token"], j["DisplayClaims"]["xui"][0]["uhs"]
async def _xsts_auth(self, xbl_token):
data = {
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [xbl_token]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}
async with aiohttp.ClientSession() as session:
async with session.post(URL_XSTS, json=data) as resp:
if resp.status != 200:
raise Exception(f"XSTS auth failed: {await resp.text()}")
j = await resp.json()
return j["Token"]
async def _mc_auth(self, uhs, xsts_token):
data = {
"identityToken": f"XBL3.0 x={uhs};{xsts_token}"
}
async with aiohttp.ClientSession() as session:
async with session.post(URL_MC, json=data) as resp:
if resp.status != 200:
raise Exception(f"MC auth failed: {await resp.text()}")
j = await resp.json()
return j["access_token"]
async def _get_profile(self, mc_token):
headers = {
"Authorization": f"Bearer {mc_token}"
}
async with aiohttp.ClientSession() as session:
async with session.get(URL_PROFILE, headers=headers) as resp:
if resp.status != 200:
raise Exception(f"Profile request failed: {await resp.text()}")
return await resp.json()
async def _auth_flow(self):
try:
ms_access_token, refresh_token = await self._ms_oauth()
xbl_token, uhs = await self._xbl_auth(ms_access_token)
xsts_token = await self._xsts_auth(xbl_token)
mc_token = await self._mc_auth(uhs, xsts_token)
profile = await self._get_profile(mc_token)
self.access_token_received.emit({
'access_token': mc_token,
'refresh_token': refresh_token,
'profile': profile
})
except Exception as e:
self.error_occurred.emit(str(e))
def run(self):
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(self._auth_flow())
except Exception as e:
self.error_occurred.emit(str(e))
finally:
self.finished.emit()
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.auth_dialog = None
self.success = False
self.username = None
# Initialize the launcher to get the correct config path
with Launcher.new() as launcher:
self.config_path = launcher.root
def authenticate(self, username):
self.username = username
self.success = False
# Create accounts.json if it doesn't exist
if not self.save_to_accounts_json():
return
self.auth_thread = AuthenticationThread(username)
self.auth_thread.auth_data_received.connect(self.show_auth_dialog)
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):
if self.auth_dialog is not None:
self.auth_dialog.close()
self.auth_dialog = AuthDialog(auth_data['url'], auth_data['code'])
result = self.auth_dialog.exec_()
if result != QDialog.Accepted:
self.auth_thread.stop()
def show_error(self, error_msg):
QMessageBox.critical(None, "Error", error_msg)
self.success = False
self.auth_finished.emit(False)
def save_to_accounts_json(self):
try:
accounts_file = Path(self.config_path) / "accounts.json"
if accounts_file.exists():
with open(accounts_file) as f:
config = json.load(f)
else:
config = {
"default": None,
"accounts": {},
"client_token": str(uuid.uuid4())
}
accounts_file.parent.mkdir(parents=True, exist_ok=True)
# Only create/update if account doesn't exist
if self.username not in config["accounts"]:
config["accounts"][self.username] = {
"uuid": "-",
"online": True,
"microsoft": True,
"gname": "-",
"access_token": "-",
"refresh_token": "-",
"is_authenticated": False
}
# Set as default if no default exists
if config["default"] is None:
config["default"] = self.username
with open(accounts_file, 'w') as f:
json.dump(config, f, indent=4)
return True
except Exception as e:
logger.error(f"Failed to initialize account data: {str(e)}")
QMessageBox.critical(None, "Error", f"Failed to initialize account data: {str(e)}")
return False
def on_access_token_received(self, data):
try:
accounts_file = Path(self.config_path) / "accounts.json"
with open(accounts_file) as f:
config = json.load(f)
if self.username in config["accounts"]:
config["accounts"][self.username].update({
"access_token": data['access_token'],
"refresh_token": data['refresh_token'],
"uuid": data['profile']['id'],
"gname": data['profile']['name'],
"is_authenticated": True
})
with open(accounts_file, 'w') as f:
json.dump(config, f, indent=4)
self.success = True
QMessageBox.information(None, "Success",
f"Successfully authenticated account: {self.username}")
else:
raise Exception("Account not found in configuration")
except Exception as e:
logger.error(f"Failed to update account data: {str(e)}")
QMessageBox.critical(None, "Error", f"Failed to update account data: {str(e)}")
self.success = False
self.auth_finished.emit(self.success)
def on_authentication_finished(self):
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()
def create_authenticator():
"""Factory function to create a new MinecraftAuthenticator instance"""
return MinecraftAuthenticator()

231
healthcheck.py Normal file
View File

@ -0,0 +1,231 @@
import os
import json
import shutil
import modulecli
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QLabel, QProgressBar
from PyQt5.QtCore import Qt, QThread, pyqtSignal
import sys
class CopyThread(QThread):
progress_changed = pyqtSignal(int)
finished = pyqtSignal()
def __init__(self, src_dir, dst_dir):
super().__init__()
self.src_dir = src_dir
self.dst_dir = dst_dir
def run(self):
# Gather all files recursively
files_to_copy = []
for root, dirs, files in os.walk(self.src_dir):
for f in files:
full_path = os.path.join(root, f)
relative_path = os.path.relpath(full_path, self.src_dir)
files_to_copy.append(relative_path)
total_files = len(files_to_copy)
copied_files = 0
for relative_path in files_to_copy:
src_path = os.path.join(self.src_dir, relative_path)
dst_path = os.path.join(self.dst_dir, relative_path)
dst_folder = os.path.dirname(dst_path)
if not os.path.exists(dst_folder):
try:
os.makedirs(dst_folder)
except PermissionError:
print(f"Skipping folder {dst_folder} (permission denied)")
continue
try:
shutil.copy2(src_path, dst_path)
except PermissionError:
print(f"Skipping file {dst_path} (permission denied)")
copied_files += 1
progress_percent = int((copied_files / total_files) * 100)
self.progress_changed.emit(progress_percent)
self.finished.emit()
class HealthCheck:
def __init__(self):
self.config = None
def check_config_file(self):
config_path = "config.json"
default_config = {
"IsRCPenabled": False,
"CheckUpdate": False,
"IsBleeding": False,
"LastPlayed": "",
"TotalPlaytime": 0,
"IsFirstLaunch": True,
"Instance": "default",
"Theme": "Dark.json",
"ThemeBackground": True,
"ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json",
"Locale": "en",
"ManageJava": False,
"MaxRAM": "2G",
"JavaPath": "",
"ZucaroCheck": False,
}
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
try:
with open(config_path, "r") as config_file:
self.config = json.load(config_file)
except (json.JSONDecodeError, ValueError):
with open(config_path, "w") as config_file:
json.dump(default_config, config_file, indent=4)
self.config = default_config
return
updated = False
for key, value in default_config.items():
if key not in self.config:
self.config[key] = value
updated = True
if updated:
with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4)
def get_folder_size(self, folder_path):
total_size = 0
for dirpath, dirnames, filenames in os.walk(folder_path):
for f in filenames:
fp = os.path.join(dirpath, f)
if os.path.isfile(fp):
total_size += os.path.getsize(fp)
return total_size
def zucaro_health_check(self):
if self.config.get("ZucaroCheck"):
return
output = modulecli.run_command("instance dir").strip()
instance_dir = os.path.abspath(output)
base_dir = os.path.abspath(os.path.join(instance_dir, "..", ".."))
possible_zucaro = [os.path.join(base_dir, "zucaro"), os.path.join(base_dir, ".zucaro")]
possible_picomc = [os.path.join(base_dir, "picomc"), os.path.join(base_dir, ".picomc")]
zucaro_dir = next((d for d in possible_zucaro if os.path.exists(d)), None)
picomc_dir = next((d for d in possible_picomc if os.path.exists(d)), None)
if picomc_dir is None or zucaro_dir is None:
print("Required directories not found. Skipping copy.")
# Mark the check as done so it wont run again
self.config["ZucaroCheck"] = True
with open("config.json", "w") as f:
json.dump(self.config, f, indent=4)
return
picomc_size = self.get_folder_size(picomc_dir)
zucaro_size = self.get_folder_size(zucaro_dir)
if picomc_size <= zucaro_size:
print("No action needed. Zucaro folder is not smaller than Picomc.")
# Update config so the check is considered done
self.config["ZucaroCheck"] = True
with open("config.json", "w") as f:
json.dump(self.config, f, indent=4)
return
print(f"Copying Picomc ({picomc_size} bytes) to Zucaro ({zucaro_size} bytes)...")
app = QApplication.instance() or QApplication(sys.argv)
dialog = QDialog()
dialog.setWindowTitle("Working...")
dialog.setWindowModality(Qt.ApplicationModal)
layout = QVBoxLayout()
label = QLabel("Working on stuff, please wait...")
progress = QProgressBar()
progress.setValue(0)
layout.addWidget(label)
layout.addWidget(progress)
dialog.setLayout(layout)
# Setup copy thread
thread = CopyThread(picomc_dir, zucaro_dir)
thread.progress_changed.connect(progress.setValue)
thread.finished.connect(dialog.accept)
thread.start()
dialog.exec_() # Runs the modal event loop
# Mark as done
self.config["ZucaroCheck"] = True
with open("config.json", "w") as f:
json.dump(self.config, f, indent=4)
print("Copy completed.")
def themes_integrity(self):
themes_folder = "themes"
dark_theme_file = os.path.join(themes_folder, "Dark.json")
native_theme_file = os.path.join(themes_folder, "Native.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": ""
}
native_theme_content = {
"manifest": {
"name": "Native",
"description": "The native looks of your OS",
"author": "Your Qt Style",
"license": "Any"
},
"palette": {}
}
if not os.path.exists(themes_folder):
print(f"Creating folder: {themes_folder}")
os.makedirs(themes_folder)
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.")
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.")
if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file):
print("Theme Integrity OK")

46
modulecli.py Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,12 +1,15 @@
{ {
"version": "0.11.9", "version": "0.13.5",
"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.11.8-149" "versionBleeding": "0.13.3-212"
} }