mirror of
https://github.com/nixietab/picodulce.git
synced 2025-04-19 06:45:31 +01:00
Compare commits
No commits in common. "main" and "release" have entirely different histories.
36
.github/workflows/Bleeding-Job.yaml
vendored
36
.github/workflows/Bleeding-Job.yaml
vendored
@ -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 }}
|
|
69
.github/workflows/Build.yml
vendored
69
.github/workflows/Build.yml
vendored
@ -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 }}
|
|
76
PKGBUILD
76
PKGBUILD
@ -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:
|
|
92
README.md
92
README.md
@ -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.
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
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
|
||||||
|
366
authser.py
366
authser.py
@ -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()
|
|
119
healthcheck.py
119
healthcheck.py
@ -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")
|
|
BIN
holiday.ico
BIN
holiday.ico
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 |
BIN
marroc.ico
BIN
marroc.ico
Binary file not shown.
Before Width: | Height: | Size: 7.2 KiB |
416
marroc.py
416
marroc.py
@ -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_())
|
|
BIN
missing.png
BIN
missing.png
Binary file not shown.
Before Width: | Height: | Size: 330 B |
30
modulecli.py
30
modulecli.py
@ -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
|
|
1842
picodulce.py
1842
picodulce.py
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,2 @@
|
|||||||
picomc
|
picomc
|
||||||
PyQt5
|
PyQt5
|
||||||
requests
|
|
||||||
aiohttp
|
|
||||||
pypresence
|
|
||||||
tqdm
|
|
15
version.json
15
version.json
@ -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"
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user