Compare commits

..

No commits in common. "main" and "release" have entirely different histories.

17 changed files with 198 additions and 2869 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

@ -1,69 +0,0 @@
name: Version Change Action
on:
push:
paths:
- version.json # Trigger on changes to version.json
jobs:
version-release:
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:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x' # Specify the Python version you need
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyqt5 requests pywin32 pyinstaller pillow # Install specific dependencies
- name: Create actions-temp folder
run: mkdir actions-temp # Create the folder called actions-temp
- name: Download picoBuild.py script
run: curl -L -o actions-temp/picoBuild.py https://raw.githubusercontent.com/nixietab/picodulce-build-script/refs/heads/main/picoBuild.py
- name: Run picoBuild.py script
run: python actions-temp/picoBuild.py
- name: Show directory structure
run: |
dir actions-temp
dir
- name: Get version and name from version.json
id: version_info
run: |
$versionJson = Get-Content version.json | ConvertFrom-Json
echo "RELEASE_NAME=Release $($versionJson.version)" >> $env:GITHUB_ENV
echo "RELEASE_TAG=$($versionJson.version)" >> $env:GITHUB_ENV
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1
with:
tag_name: ${{ env.RELEASE_TAG }}
release_name: ${{ env.RELEASE_NAME }}
body: "This release was created automatically by a GitHub Action."
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/2hsu.exe
asset_name: 2hsu.exe
asset_content_type: application/octet-stream
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,76 +0,0 @@
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

@ -1,92 +1,12 @@
<p align="center">
<img src="https://github.com/nixietab/picodulce/assets/75538775/36fee78f-fb46-400c-8b14-dda5ec6191ef" alt="launcher_icon">
</p>
# picodulce
<h1 align="center">Picodulce Launcher</h1>
a launcher for block game made in Qt5 and a GUI for the picomc project.
<p align="center">The simple FOSS launcher you been looking for</p>
it should download all the versions and start them as well. it works offline and online
<p align="center">
<a href="https://github.com/nixietab/picodulce/releases">
<img src="https://img.shields.io/github/v/release/nixietab/picodulce" alt="Latest Release">
</a>
<a href="https://github.com/nixietab/picodulce/issues">
<img src="https://img.shields.io/github/issues/nixietab/picodulce" alt="Issues">
</a>
<a href="https://github.com/nixietab/picodulce/pulls">
<img src="https://img.shields.io/github/issues-pr/nixietab/picodulce" alt="Pull Requests">
</a>
<a href="https://github.com/nixietab/picodulce/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/nixietab/picodulce" alt="License">
</a>
<a href="https://github.com/nixietab/picodulce">
<img src="https://img.shields.io/github/stars/nixietab/picodulce?style=social" alt="GitHub Stars">
</a>
</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.
![imagen](https://github.com/user-attachments/assets/115b39be-47d3-4ac7-893a-5849c1e4570c)
## Key Features
- **Version Management**: Picodulce is designed to download and launch all available game versions, ensuring users have easy access to the latest updates as well as older versions.
- **Offline and Online Support**: Whether you're connected to Microsoft or not, Picodulce ensures you can still enjoy your game by supporting both offline and online modes.
- **Integrated Mod Manager**: Includes the [Marroc Mod Manager](https://github.com/nixietab/marroc), enabling users to effortlessly manage and customize their game with mods and texturepacks.
- **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
## 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
``` git clone https://github.com/nixietab/picodulce ```
### 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.
Create the virtual environment:
``` python -m venv venv ```
- **Linux/Mac:**
`source venv/bin/activate`
- **Windows:**
`.\\venv\\Scripts\\activate`
### Install requirements
Now on the venv you can install the requirements safely
```pip install -r requirements.txt ```
### Running the launcher
On the venv run it as a normal python script
```python picodulce.py```
Just make sure you have Java installed for running the actual game
![a capture of the launcher](https://i.imgur.com/Ui92vta.png)
### 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.
pico dulce is the name of a well known argentinean candy and i think is apropiate say that this is a really "sweetened" way of using picomc

View File

@ -1,366 +0,0 @@
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()

BIN
drums.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,119 +0,0 @@
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")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

416
marroc.py
View File

@ -1,416 +0,0 @@
import sys
import os
import shutil
import json
import threading
import requests
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem, QMessageBox, QComboBox, QDialog, QTabWidget, QMainWindow, QSpacerItem, QSizePolicy
from PyQt5.QtCore import Qt, QSize, QObject, pyqtSignal
from PyQt5.QtGui import QIcon, QPalette, QColor, QPixmap
CONFIG_FILE = "config.json"
class IconLoader(QObject, threading.Thread):
icon_loaded = pyqtSignal(QPixmap)
def __init__(self, icon_url):
super().__init__()
threading.Thread.__init__(self)
self.icon_url = icon_url
def run(self):
try:
response = requests.get(self.icon_url)
if response.status_code == 200:
pixmap = QPixmap()
pixmap.loadFromData(response.content)
self.icon_loaded.emit(pixmap.scaled(QSize(42, 42), Qt.KeepAspectRatio, Qt.SmoothTransformation))
else:
self.icon_loaded.emit(QPixmap("missing.png"))
except Exception as e:
print("Error loading icon:", e)
self.icon_loaded.emit(QPixmap("missing.png"))
class ModrinthSearchApp(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Marroc Mod Manager")
self.setGeometry(100, 100, 500, 400)
self.ensure_directories_exist()
layout = QVBoxLayout()
tab_widget = QTabWidget()
self.search_tab = QWidget()
self.mods_tab = QWidget()
tab_widget.addTab(self.search_tab, "Search")
tab_widget.addTab(self.mods_tab, "Manager")
self.init_search_tab()
self.init_mods_tab()
layout.addWidget(tab_widget)
self.setLayout(layout)
def keyPressEvent(self, event):
focus_widget = self.focusWidget()
if event.key() == Qt.Key_Down:
self.focusNextChild() # Move focus to the next widget
elif event.key() == Qt.Key_Up:
self.focusPreviousChild() # Move focus to the previous widget
elif event.key() in [Qt.Key_Return, Qt.Key_Enter]:
if isinstance(focus_widget, QPushButton):
focus_widget.click() # Trigger the button click
elif isinstance(focus_widget, QComboBox):
focus_widget.showPopup() # Show dropdown for combo box
else:
super().keyPressEvent(event)
def ensure_directories_exist(self):
directories = ["marroc/mods", "marroc/resourcepacks"]
for directory in directories:
if not os.path.exists(directory):
os.makedirs(directory)
def init_search_tab(self):
layout = QVBoxLayout()
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Enter a search term...")
search_layout.addWidget(self.search_input)
self.search_button = QPushButton("Search")
self.search_button.clicked.connect(self.search_mods)
search_layout.addWidget(self.search_button)
self.search_type_dropdown = QComboBox()
self.search_type_dropdown.addItems(["Mod", "Texture Pack"])
search_layout.addWidget(self.search_type_dropdown)
layout.addLayout(search_layout)
self.mods_list = QListWidget()
layout.addWidget(self.mods_list)
self.select_button = QPushButton("Select")
self.select_button.clicked.connect(self.show_mod_details_window)
layout.addWidget(self.select_button)
self.selected_mod = None
self.search_tab.setLayout(layout)
def init_mods_tab(self):
layout = QVBoxLayout()
self.mod_manager_window = ModManagerWindow()
layout.addWidget(self.mod_manager_window)
self.mods_tab.setLayout(layout)
def search_mods(self):
self.mods_list.clear()
mod_name = self.search_input.text()
search_type = self.search_type_dropdown.currentText().lower()
if search_type == "texture pack":
api_url = f"https://api.modrinth.com/v2/search?query={mod_name}&limit=20&facets=%5B%5B%22project_type%3Aresourcepack%22%5D%5D"
else:
api_url = f"https://api.modrinth.com/v2/search?query={mod_name}&limit=20&facets=%5B%5B%22project_type%3A{search_type}%22%5D%5D"
response = requests.get(api_url)
if response.status_code == 200:
mods_data = json.loads(response.text)
for mod in mods_data['hits']:
mod_name = mod['title']
mod_description = mod['description']
icon_url = mod['icon_url']
item = QListWidgetItem(f"Title: {mod_name}\nDescription: {mod_description}")
item.setSizeHint(QSize(200, 50))
icon_loader = IconLoader(icon_url)
icon_loader.icon_loaded.connect(lambda pixmap, item=item: self.set_item_icon(item, pixmap))
icon_loader.start()
item.mod_data = mod
self.mods_list.addItem(item)
else:
self.mods_list.addItem("Failed to fetch mods. Please try again later.")
def set_item_icon(self, item, pixmap):
if pixmap:
item.setData(Qt.DecorationRole, pixmap)
else:
# Set a default icon if loading failed
item.setIcon(QIcon("missing.png"))
def show_mod_details_window(self):
selected_item = self.mods_list.currentItem()
if selected_item is not None:
mod_data = selected_item.mod_data
mod_slug = mod_data.get('slug')
if mod_slug:
api_url = f"https://api.modrinth.com/v2/project/{mod_slug}"
response = requests.get(api_url)
if response.status_code == 200:
mod_info = json.loads(response.text)
icon_url = mod_info.get('icon_url')
mod_versions = self.get_mod_versions(mod_slug)
mod_details_window = ModDetailsWindow(mod_data, icon_url, mod_versions)
mod_details_window.exec_()
else:
QMessageBox.warning(self, "Failed to Fetch Mod Details", "Failed to fetch mod details. Please try again later.")
else:
QMessageBox.warning(self, "No Mod Slug", "Selected mod has no slug.")
else:
QMessageBox.warning(self, "No Mod Selected", "Please select a mod first.")
def get_mod_versions(self, mod_slug):
api_url = f"https://api.modrinth.com/v2/project/{mod_slug}/version"
response = requests.get(api_url)
if response.status_code == 200:
versions = json.loads(response.text)
mod_versions = []
for version in versions:
version_name = version['name']
version_files = version.get('files', [])
if version_files:
file_urls = [file['url'] for file in version_files]
mod_versions.append({'version': version_name, 'files': file_urls})
else:
mod_versions.append({'version': version_name, 'files': []})
return mod_versions
else:
return []
class ModManagerWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Mod Manager")
self.setGeometry(100, 100, 600, 400)
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout = QHBoxLayout(self.central_widget)
self.file_type_combo_box = QComboBox()
self.file_type_combo_box.addItems(["Mods", "Resource Packs"])
self.file_type_combo_box.currentIndexChanged.connect(self.load_files)
self.available_files_widget = QListWidget()
self.installed_files_widget = QListWidget()
self.button_dropdown_layout = QVBoxLayout()
self.button_dropdown_layout.addWidget(self.file_type_combo_box)
self.button_dropdown_layout.addSpacerItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))
self.move_right_button = QPushButton(">")
self.move_right_button.clicked.connect(self.move_right)
self.button_dropdown_layout.addWidget(self.move_right_button)
self.move_left_button = QPushButton("<")
self.move_left_button.clicked.connect(self.move_left)
self.button_dropdown_layout.addWidget(self.move_left_button)
self.delete_button = QPushButton("Delete")
self.delete_button.clicked.connect(self.delete_selected_item)
self.button_dropdown_layout.addWidget(self.delete_button)
self.button_dropdown_layout.addSpacerItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))
self.layout.addWidget(self.available_files_widget)
self.layout.addLayout(self.button_dropdown_layout)
self.layout.addWidget(self.installed_files_widget)
self.load_files()
def load_files(self):
file_type = self.file_type_combo_box.currentText()
if file_type == "Mods":
self.load_mods()
elif file_type == "Resource Packs":
self.load_resource_packs()
def load_mods(self):
mods_directory = "marroc/mods"
if os.path.exists(mods_directory) and os.path.isdir(mods_directory):
mods = os.listdir(mods_directory)
self.available_files_widget.clear()
self.available_files_widget.addItems(mods)
self.load_installed_mods("mods")
def load_resource_packs(self):
resource_packs_directory = "marroc/resourcepacks"
if os.path.exists(resource_packs_directory) and os.path.isdir(resource_packs_directory):
resource_packs = os.listdir(resource_packs_directory)
self.available_files_widget.clear()
self.available_files_widget.addItems(resource_packs)
self.load_installed_mods("resourcepacks")
def load_installed_mods(self, file_type):
if sys.platform.startswith('linux'):
minecraft_directory = os.path.expanduser("~/.local/share/picomc/instances/default/minecraft")
elif sys.platform.startswith('win'):
minecraft_directory = os.path.join(os.getenv('APPDATA'), '.picomc/instances/default/minecraft')
else:
minecraft_directory = ""
if minecraft_directory:
installed_files_directory = os.path.join(minecraft_directory, file_type)
if os.path.exists(installed_files_directory) and os.path.isdir(installed_files_directory):
installed_files = os.listdir(installed_files_directory)
self.installed_files_widget.clear()
self.installed_files_widget.addItems(installed_files)
def move_right(self):
selected_item = self.available_files_widget.currentItem()
if selected_item:
source_directory = self.get_source_directory()
destination_directory = self.get_destination_directory()
file_name = selected_item.text()
source_path = os.path.join(source_directory, file_name)
destination_path = os.path.join(destination_directory, file_name)
shutil.move(source_path, destination_path)
self.load_files()
def move_left(self):
selected_item = self.installed_files_widget.currentItem()
if selected_item:
source_directory = self.get_destination_directory()
destination_directory = self.get_source_directory()
file_name = selected_item.text()
source_path = os.path.join(source_directory, file_name)
destination_path = os.path.join(destination_directory, file_name)
shutil.move(source_path, destination_path)
self.load_files()
def delete_selected_item(self):
selected_item = self.available_files_widget.currentItem() or self.installed_files_widget.currentItem()
if selected_item:
file_name = selected_item.text()
reply = QMessageBox.question(self, 'Delete Item', f'Are you sure you want to delete "{file_name}"?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
file_type = self.file_type_combo_box.currentText()
if file_type == "Mods":
directory = "marroc/mods"
elif file_type == "Resource Packs":
directory = "marroc/resourcepacks"
else:
return
file_path = os.path.join(directory, file_name)
if os.path.exists(file_path):
os.remove(file_path)
self.load_files()
else:
QMessageBox.warning(self, 'File Not Found', 'The selected file does not exist.')
def get_source_directory(self):
file_type = self.file_type_combo_box.currentText()
if file_type == "Mods":
return "marroc/mods"
elif file_type == "Resource Packs":
return "marroc/resourcepacks"
else:
return ""
def get_destination_directory(self):
file_type = self.file_type_combo_box.currentText()
if file_type == "Mods":
if sys.platform.startswith('linux'):
return os.path.expanduser("~/.local/share/picomc/instances/default/minecraft/mods")
elif sys.platform.startswith('win'):
return os.path.join(os.getenv('APPDATA'), '.picomc/instances/default/minecraft/mods')
elif file_type == "Resource Packs":
if sys.platform.startswith('linux'):
return os.path.expanduser("~/.local/share/picomc/instances/default/minecraft/resourcepacks")
elif sys.platform.startswith('win'):
return os.path.join(os.getenv('APPDATA'), '.picomc/instances/default/minecraft/resourcepacks')
else:
return ""
class ModDetailsWindow(QDialog):
def __init__(self, mod_data, icon_url, mod_versions):
super().__init__()
self.setWindowTitle("Mod Details")
self.setGeometry(100, 100, 400, 300)
self.mod_data = mod_data
layout = QVBoxLayout()
mod_name_label = QLabel(f"<h2>{mod_data['title']}</h2>")
mod_name_label.setAlignment(Qt.AlignCenter)
layout.addWidget(mod_name_label)
mod_description_label = QLabel(mod_data['description'])
mod_description_label.setWordWrap(True)
layout.addWidget(mod_description_label)
icon_pixmap = self.load_icon(icon_url)
icon_label = QLabel()
if icon_pixmap:
icon_label.setPixmap(icon_pixmap)
icon_label.setAlignment(Qt.AlignCenter)
layout.addWidget(icon_label)
self.version_dropdown = QComboBox()
for version in mod_versions:
self.version_dropdown.addItem(version['version'])
self.version_dropdown.setItemData(self.version_dropdown.count() - 1, version['files'], Qt.UserRole)
layout.addWidget(self.version_dropdown)
self.download_button = QPushButton("Download")
self.download_button.clicked.connect(self.download_mod)
layout.addWidget(self.download_button)
self.download_url_label = QLabel()
self.download_url_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.download_url_label)
layout.addStretch(1)
self.setLayout(layout)
def load_icon(self, icon_url):
try:
response = requests.get(icon_url)
if response.status_code == 200:
pixmap = QPixmap()
pixmap.loadFromData(response.content)
return pixmap.scaled(QSize(128, 128), Qt.KeepAspectRatio, Qt.SmoothTransformation)
else:
return None
except Exception as e:
print("Error loading icon:", e)
return None
def download_mod(self):
selected_version_index = self.version_dropdown.currentIndex()
selected_version_files = self.version_dropdown.itemData(selected_version_index, Qt.UserRole)
if selected_version_files:
for file_url in selected_version_files:
filename = os.path.basename(file_url)
try:
response = requests.get(file_url)
response.raise_for_status()
save_dir = "marroc/mods" if filename.endswith('.jar') else "marroc/resourcepacks"
with open(os.path.join(save_dir, filename), 'wb') as f:
f.write(response.content)
QMessageBox.information(self, "Download Mod", f"Downloaded {filename} successfully.")
return
except requests.exceptions.RequestException as e:
QMessageBox.warning(self, "Download Error", f"Error downloading mod: {e}")
return
QMessageBox.warning(self, "Download Mod", "Failed to download the mod.")
if __name__ == "__main__":
app = QApplication(sys.argv)
app_icon = QIcon('marroc.ico')
app.setWindowIcon(app_icon)
window = ModrinthSearchApp()
window.show()
sys.exit(app.exec_())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 B

View File

@ -1,30 +0,0 @@
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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,15 +0,0 @@
{
"version": "0.13.1",
"links": [
"https://raw.githubusercontent.com/nixietab/picodulce/main/version.json",
"https://raw.githubusercontent.com/nixietab/picodulce/main/picodulce.py",
"https://raw.githubusercontent.com/nixietab/picodulce/main/requirements.txt",
"https://raw.githubusercontent.com/nixietab/picodulce/main/drums.gif",
"https://raw.githubusercontent.com/nixietab/picodulce/main/marroc.py",
"https://raw.githubusercontent.com/nixietab/picodulce/main/holiday.ico",
"https://raw.githubusercontent.com/nixietab/picodulce/main/authser.py",
"https://raw.githubusercontent.com/nixietab/picodulce/main/healthcheck.py",
"https://raw.githubusercontent.com/nixietab/picodulce/main/modulecli.py"
],
"versionBleeding": "0.13.1-202"
}