Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- from PyQt6.QtWidgets import (
- QApplication, QMainWindow, QToolBar, QWidget, QLabel,
- QPushButton, QHBoxLayout, QVBoxLayout, QScrollArea,
- QGridLayout, QMenu, QDialog, QFormLayout, QLineEdit,
- QComboBox, QCheckBox, QMessageBox, QGroupBox, QRadioButton,
- QDialogButtonBox, QSizePolicy, QProgressBar, QFileDialog
- )
- from PyQt6.QtGui import QIcon, QAction, QDrag, QDesktopServices
- from PyQt6.QtCore import Qt, QMimeData, QUrl, QThread, pyqtSignal, QProcess
- import sys
- import os
- import json
- import logging
- import configparser
- from datetime import datetime
- import requests
- import shutil
- import platform
- import subprocess
- from pathlib import Path
- # Konfiguracja logowania
- def setup_logging():
- log_dir = "minecraft/logs"
- os.makedirs(log_dir, exist_ok=True)
- logging.basicConfig(
- filename=f"{log_dir}/launcher.log",
- level=logging.INFO,
- format="%(asctime)s - %(levelname)s - %(message)s"
- )
- for log_file in ["error.log", "download.log"]:
- with open(f"{log_dir}/{log_file}", "a") as f:
- pass
- # Tworzenie struktury folderów
- def setup_directories():
- directories = [
- "minecraft/assets/indexes",
- "minecraft/assets/objects",
- "minecraft/libraries",
- "minecraft/instances",
- "minecraft/javas",
- "minecraft/versions"
- ]
- for directory in directories:
- os.makedirs(directory, exist_ok=True)
- # Zapisywanie stanu pobierania
- def save_download_state(download_type, file_path, file_url):
- state_file = "minecraft/downloads.json"
- state = {}
- try:
- with open(state_file, "r") as f:
- state = json.load(f)
- except FileNotFoundError:
- pass
- if download_type not in state:
- state[download_type] = {}
- state[download_type][file_path] = file_url
- with open(state_file, "w") as f:
- json.dump(state, f, indent=4)
- # Wczytywanie stanu pobierania
- def load_download_state():
- state_file = "minecraft/downloads.json"
- try:
- with open(state_file, "r") as f:
- return json.load(f)
- except FileNotFoundError:
- return {}
- # Klasa do pobierania plików w tle
- class DownloadWorker(QThread):
- progress_updated = pyqtSignal(str, int, int, int, str, float, float) # typ, procent, pobrane, całkowite, nazwa pliku, MB pobrane, MB całkowite
- download_finished = pyqtSignal(bool, str) # sukces, wiadomość
- file_downloaded = pyqtSignal(str, str) # typ, ścieżka pliku
- def __init__(self, files_to_download, download_type, parent=None):
- super().__init__(parent)
- self.files_to_download = files_to_download
- self.download_type = download_type
- self.canceled = False
- def run(self):
- try:
- total_files = len(self.files_to_download)
- completed_files = 0
- for file_path, file_url in self.files_to_download.items():
- if self.canceled:
- break
- if os.path.exists(file_path):
- completed_files += 1
- self.file_downloaded.emit(self.download_type, file_path)
- self.progress_updated.emit(self.download_type, int((completed_files / total_files) * 100), completed_files, total_files, os.path.basename(file_path), 0, 0)
- continue
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
- with requests.get(file_url, stream=True) as r:
- r.raise_for_status()
- total_size = int(r.headers.get('content-length', 0))
- total_mb = total_size / (1024 * 1024)
- downloaded = 0
- with open(file_path, 'wb') as f:
- for chunk in r.iter_content(chunk_size=8192):
- if self.canceled:
- break
- f.write(chunk)
- downloaded += len(chunk)
- downloaded_mb = downloaded / (1024 * 1024)
- self.progress_updated.emit(
- self.download_type,
- int((completed_files / total_files) * 100),
- completed_files,
- total_files,
- os.path.basename(file_path),
- round(downloaded_mb, 2),
- round(total_mb, 2)
- )
- if not self.canceled:
- save_download_state(self.download_type, file_path, file_url)
- completed_files += 1
- self.file_downloaded.emit(self.download_type, file_path)
- self.progress_updated.emit(
- self.download_type,
- int((completed_files / total_files) * 100),
- completed_files,
- total_files,
- os.path.basename(file_path),
- round(downloaded_mb, 2),
- round(total_mb, 2)
- )
- if not self.canceled:
- self.download_finished.emit(True, f"Pobieranie {self.download_type} zakończone!")
- else:
- self.download_finished.emit(False, f"Pobieranie {self.download_type} przerwane")
- except Exception as e:
- logging.error(f"Błąd podczas pobierania {self.download_type}: {str(e)}")
- self.download_finished.emit(False, f"Błąd: {str(e)}")
- def cancel(self):
- self.canceled = True
- # Klasa do uruchamiania Minecrafta
- class MinecraftRunner(QThread):
- output_received = pyqtSignal(str)
- finished = pyqtSignal(int)
- def __init__(self, command, working_dir, parent=None):
- super().__init__(parent)
- self.command = command
- self.working_dir = working_dir
- self.process = None
- def run(self):
- try:
- self.process = QProcess()
- self.process.setWorkingDirectory(self.working_dir)
- self.process.readyReadStandardOutput.connect(self.handle_output)
- self.process.readyReadStandardError.connect(self.handle_error)
- self.process.finished.connect(self.on_finished)
- self.process.start(self.command[0], self.command[1:])
- self.process.waitForFinished(-1)
- except Exception as e:
- self.output_received.emit(f"Błąd podczas uruchamiania: {str(e)}")
- self.finished.emit(1)
- def handle_output(self):
- output = self.process.readAllStandardOutput().data().decode().strip()
- if output:
- self.output_received.emit(output)
- def handle_error(self):
- error = self.process.readAllStandardError().data().decode().strip()
- if error:
- self.output_received.emit(error)
- def on_finished(self, exit_code):
- self.finished.emit(exit_code)
- def stop(self):
- if self.process:
- self.process.terminate()
- # Wyszukiwanie Java
- class JavaFinder:
- @staticmethod
- def find_java_for_version(version_info):
- required_java = version_info.get("javaVersion", {}).get("majorVersion", 8)
- logging.info(f"Szukam Java w wersji {required_java}")
- javas_dir = "minecraft/javas"
- if os.path.exists(javas_dir):
- for root, dirs, _ in os.walk(javas_dir):
- for dir_name in dirs:
- if f"java-{required_java}" in dir_name.lower():
- java_path = os.path.join(root, dir_name, "bin", "java.exe" if platform.system() == "Windows" else "java")
- if os.path.exists(java_path):
- return java_path
- java_home = os.environ.get("JAVA_HOME")
- if java_home:
- java_path = os.path.join(java_home, "bin", "java.exe" if platform.system() == "Windows" else "java")
- if os.path.exists(java_path):
- return java_path
- try:
- java_path = shutil.which("java")
- if java_path:
- version_output = subprocess.check_output([java_path, "-version"], stderr=subprocess.STDOUT).decode()
- if f'version "{required_java}' in version_output or f'version "1.{required_java}' in version_output:
- return java_path
- except:
- pass
- return None
- # Klasa kafelka instancji
- class InstanceTile(QWidget):
- def __init__(self, name, version, group=None, parent=None):
- super().__init__()
- self.name = name
- self.version = version
- self.group = group
- self.parent = parent
- self.is_selected = False
- self.is_running = False
- self.setFixedSize(120, 120)
- layout = QVBoxLayout()
- layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
- layout.setSpacing(8)
- self.icon_container = QWidget()
- icon_layout = QVBoxLayout()
- icon_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
- self.icon = QLabel("🟩")
- self.icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
- self.icon.setStyleSheet("font-size: 48px; border: none;")
- self.status_icon = QLabel("")
- self.status_icon.setAlignment(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignRight)
- self.status_icon.setStyleSheet("font-size: 16px; position: absolute; margin-bottom: -10px; margin-right: -10px;")
- icon_layout.addWidget(self.icon)
- icon_layout.addWidget(self.status_icon)
- self.icon_container.setLayout(icon_layout)
- self.version_label = QLabel(version)
- self.version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
- self.version_label.setStyleSheet("font-size: 10px; color: gray;")
- self.name_label = QLabel(name)
- self.name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
- self.name_label.setStyleSheet("font-size: 12px;")
- layout.addWidget(self.version_label)
- layout.addWidget(self.icon_container)
- layout.addWidget(self.name_label)
- self.setLayout(layout)
- self.update_style()
- self.setMouseTracking(True)
- def mousePressEvent(self, event):
- if event.button() == Qt.MouseButton.LeftButton:
- if self.parent.selected_tile:
- self.parent.selected_tile.is_selected = False
- self.parent.selected_tile.update_style()
- self.is_selected = True
- self.parent.selected_tile = self
- self.update_style()
- self.parent.update_right_panel()
- logging.info(f"Zaznaczono instancję: {self.name}")
- print(f"[INFO] Zaznaczono instancję: {self.name}")
- drag = QDrag(self)
- mime = QMimeData()
- mime.setText(self.name)
- drag.setMimeData(mime)
- drag.exec(Qt.DropAction.MoveAction)
- def mouseDoubleClickEvent(self, event):
- if event.button() == Qt.MouseButton.LeftButton:
- self.parent.launch_selected()
- def keyPressEvent(self, event):
- if event.key() == Qt.Key.Key_F2:
- self.edit_name()
- def update_style(self):
- if self.is_selected:
- self.setStyleSheet("background-color: #d3e3fd; border: none; border-radius: 5px;")
- else:
- self.setStyleSheet("background-color: white; border: none; border-radius: 5px;")
- def contextMenuEvent(self, event):
- menu = QMenu(self)
- run_action = menu.addAction(QIcon.fromTheme("media-playback-start"), "Uruchom")
- kill_action = menu.addAction(QIcon.fromTheme("process-stop"), "Zakończ")
- edit_action = menu.addAction(QIcon.fromTheme("document-edit"), "Edytuj")
- rename_action = menu.addAction(QIcon.fromTheme("edit-rename"), "Zmień nazwę")
- change_group_action = menu.addAction(QIcon.fromTheme("list"), "Zmień grupę")
- folder_action = menu.addAction(QIcon.fromTheme("folder"), "Folder")
- export_menu = menu.addMenu("Eksportuj")
- export_menu.addAction("Eksportuj ZIP")
- export_menu.addAction("Eksportuj JSON")
- copy_action = menu.addAction(QIcon.fromTheme("edit-copy"), "Kopiuj")
- delete_action = menu.addAction(QIcon.fromTheme("edit-delete"), "Usuń")
- shortcut_action = menu.addAction(QIcon.fromTheme("link"), "Utwórz skrót")
- action = menu.exec(event.globalPos())
- if action == run_action:
- self.parent.launch_selected()
- elif action == kill_action:
- self.parent.kill_selected()
- elif action == edit_action:
- logging.info(f"Edytowanie instancji: {self.name}")
- print(f"[INFO] Edytowanie instancji: {self.name}")
- elif action == rename_action:
- self.edit_name()
- elif action == change_group_action:
- logging.info(f"Zmiana grupy dla: {self.name}")
- print(f"[INFO] Zmiana grupy dla: {self.name}")
- elif action == folder_action:
- instance_dir = f"minecraft/instances/{self.name}"
- if os.path.exists(instance_dir):
- QDesktopServices.openUrl(QUrl.fromLocalFile(instance_dir))
- logging.info(f"Otwieranie folderu instancji: {self.name}")
- print(f"[INFO] Otwieranie folderu instancji: {self.name}")
- def edit_name(self):
- dialog = QDialog(self)
- dialog.setWindowTitle("Zmień nazwę")
- layout = QFormLayout()
- new_name = QLineEdit(self.name)
- layout.addRow("Nowa nazwa:", new_name)
- button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- button_box.accepted.connect(dialog.accept)
- button_box.rejected.connect(dialog.reject)
- layout.addWidget(button_box)
- dialog.setLayout(layout)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- old_name = self.name
- self.name = new_name.text()
- self.name_label.setText(self.name)
- self.parent.update_instance_name(old_name, self.name, self.group)
- logging.info(f"Zmieniono nazwę instancji na: {self.name}")
- print(f"[INFO] Zmieniono nazwę instancji na: {self.name}")
- # Sekcja grupy instancji
- class GroupSection(QWidget):
- def __init__(self, group_name, tiles, parent=None):
- super().__init__()
- self.group_name = group_name
- self.tiles = tiles
- self.parent = parent
- self.setAcceptDrops(True)
- self.setStyleSheet("background-color: white;")
- layout = QVBoxLayout()
- self.grid_widget = QWidget()
- self.grid_layout = QGridLayout()
- self.grid_layout.setSpacing(2)
- for i, tile in enumerate(self.tiles):
- self.grid_layout.addWidget(tile, i // 4, i % 4)
- self.grid_widget.setLayout(self.grid_layout)
- layout.addWidget(self.grid_widget)
- self.setLayout(layout)
- def dragEnterEvent(self, event):
- if event.mimeData().hasText():
- self.setStyleSheet("background-color: lightgray;")
- event.acceptProposedAction()
- def dragLeaveEvent(self, event):
- self.setStyleSheet("background-color: white;")
- def dropEvent(self, event):
- source_name = event.mimeData().text()
- source_tile = None
- for group, tiles in self.parent.groups.items():
- for tile in tiles:
- if tile.name == source_name:
- source_tile = tile
- break
- if source_tile:
- break
- if source_tile and source_tile.group != self.group_name:
- old_group = source_tile.group
- self.parent.groups[old_group].remove(source_tile)
- source_tile.group = self.group_name
- self.parent.groups[self.group_name].append(source_tile)
- self.parent.update_instance_group(source_name, old_group, self.group_name)
- self.parent.refresh_instances()
- logging.info(f"Przeniesiono instancję {source_name} do grupy {self.group_name}")
- print(f"[INFO] Przeniesiono instancję {source_name} do grupy {self.group_name}")
- self.setStyleSheet("background-color: white;")
- # Dialog dodawania instancji
- class AddInstanceDialog(QDialog):
- def __init__(self, groups, versions_data, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Dodaj Instancję")
- self.setGeometry(200, 200, 500, 400)
- self.versions_data = versions_data
- self.parent = parent
- form_layout = QFormLayout()
- self.instance_name = QLineEdit()
- form_layout.addRow("Nazwa instancji:", self.instance_name)
- self.version_combo = QComboBox()
- for version in self.versions_data["versions"]:
- self.version_combo.addItem(version["id"], version["id"])
- form_layout.addRow("Wersja Minecraft:", self.version_combo)
- self.instance_type = QComboBox()
- self.instance_type.addItems(["Vanilla", "Forge", "Fabric"])
- form_layout.addRow("Typ instancji:", self.instance_type)
- self.group_combo = QComboBox()
- self.group_combo.addItems(groups)
- form_layout.addRow("Grupa:", self.group_combo)
- self.mod_loader_checkbox = QCheckBox("Zainstaluj mod loadera")
- form_layout.addRow(self.mod_loader_checkbox)
- self.group_box = QGroupBox("Zarządzanie modami")
- group_layout = QVBoxLayout()
- self.enable_mods_radio = QRadioButton("Włącz mody")
- self.disable_mods_radio = QRadioButton("Wyłącz mody")
- self.disable_mods_radio.setChecked(True)
- group_layout.addWidget(self.enable_mods_radio)
- group_layout.addWidget(self.disable_mods_radio)
- self.group_box.setLayout(group_layout)
- form_layout.addRow(self.group_box)
- button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
- button_box.accepted.connect(self.accept)
- button_box.rejected.connect(self.reject)
- form_layout.addWidget(button_box)
- self.setLayout(form_layout)
- def accept(self):
- instance_name = self.instance_name.text()
- if not instance_name:
- QMessageBox.warning(self, "Błąd", "Nazwa instancji nie może być pusta!")
- return
- version_id = self.version_combo.currentData()
- version_info = next((v for v in self.versions_data["versions"] if v["id"] == version_id), None)
- if not version_info:
- QMessageBox.warning(self, "Błąd", "Nie można znaleźć informacji o wybranej wersji!")
- return
- super().accept()
- # Dialog dodawania grupy
- class AddGroupDialog(QDialog):
- def __init__(self):
- super().__init__()
- self.setWindowTitle("Dodaj Grupę")
- self.setGeometry(200, 200, 300, 150)
- form_layout = QFormLayout()
- self.group_name = QLineEdit()
- form_layout.addRow("Nazwa grupy:", self.group_name)
- button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
- button_box.accepted.connect(self.accept)
- button_box.rejected.connect(self.reject)
- form_layout.addWidget(button_box)
- self.setLayout(form_layout)
- # Dialog ustawień
- class SettingsDialog(QDialog):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Ustawienia")
- self.setGeometry(250, 250, 500, 400)
- form_layout = QFormLayout()
- self.username_edit = QLineEdit()
- self.username_edit.setText(parent.username or "Brak użytkownika")
- form_layout.addRow("Nazwa użytkownika:", self.username_edit)
- self.theme_combo = QComboBox()
- self.theme_combo.addItems(["Jasny", "Ciemny"])
- self.theme_combo.setCurrentText(parent.theme)
- form_layout.addRow("Motyw:", self.theme_combo)
- java_group = QGroupBox("Ustawienia Java")
- java_layout = QFormLayout()
- self.java_path_edit = QLineEdit()
- self.java_path_edit.setText(parent.settings.get("java_path", ""))
- java_layout.addRow("Ścieżka do Java:", self.java_path_edit)
- browse_btn = QPushButton("Przeglądaj...")
- browse_btn.clicked.connect(self.browse_java_path)
- java_layout.addRow(browse_btn)
- self.auto_java_check = QCheckBox("Automatycznie wykrywaj Java")
- self.auto_java_check.setChecked(parent.settings.get("auto_java", True))
- java_layout.addRow(self.auto_java_check)
- java_group.setLayout(java_layout)
- form_layout.addWidget(java_group)
- button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
- button_box.accepted.connect(self.accept)
- button_box.rejected.connect(self.reject)
- form_layout.addWidget(button_box)
- self.setLayout(form_layout)
- def browse_java_path(self):
- file_path, _ = QFileDialog.getOpenFileName(self, "Wybierz plik Java", "", "Java (java java.exe)")
- if file_path:
- self.java_path_edit.setText(file_path)
- # Główna klasa launchera
- class MCLauncher(QMainWindow):
- def __init__(self):
- super().__init__()
- self.setWindowTitle("MCLauncher")
- self.setGeometry(100, 100, 1000, 700)
- self.username = None
- self.theme = "Jasny"
- self.groups = {}
- self.group_widgets = {}
- self.selected_tile = None
- self.collapsed_groups = set()
- self.settings = {}
- self.download_worker = None
- self.minecraft_runner = None
- self.versions_data = None
- self.current_downloads = {}
- self.download_counts = {
- 'assets': {'completed': 0, 'total': 0},
- 'libraries': {'completed': 0, 'total': 0},
- 'natives': {'completed': 0, 'total': 0}
- }
- setup_directories()
- setup_logging()
- self.load_settings()
- self.fetch_minecraft_versions()
- self.load_instgroups()
- self.apply_theme()
- self.create_toolbar()
- self.create_main_area()
- self.create_status_bar()
- def fetch_minecraft_versions(self):
- try:
- response = requests.get("https://launchermeta.mojang.com/mc/game/version_manifest.json")
- response.raise_for_status()
- self.versions_data = response.json()
- with open("minecraft/versions/version_manifest.json", "w") as f:
- json.dump(self.versions_data, f)
- logging.info("Pobrano listę wersji Minecraft")
- except Exception as e:
- logging.error(f"Błąd podczas pobierania wersji Minecraft: {str(e)}")
- try:
- with open("minecraft/versions/version_manifest.json", "r") as f:
- self.versions_data = json.load(f)
- logging.info("Wczytano lokalną listę wersji Minecraft")
- except:
- self.versions_data = {"versions": [{"id": "1.21.4", "url": ""}]}
- logging.warning("Użyto domyślnej wersji Minecraft")
- def load_instgroups(self):
- try:
- with open("minecraft/instances/instgroups.json", "r") as f:
- data = json.load(f)
- if data["formatVersion"] != "1":
- raise ValueError("Nieobsługiwana wersja formatu instgroups.json")
- self.groups = {}
- for group_name, group_data in data["groups"].items():
- self.groups[group_name] = []
- if not group_data["hidden"]:
- for instance_name in group_data["instances"]:
- instance_dir = f"minecraft/instances/{instance_name}"
- version = "1.21.4"
- if os.path.exists(f"{instance_dir}/instance.cfg"):
- config = configparser.ConfigParser()
- config.read(f"{instance_dir}/instance.cfg")
- if "General" in config and "InstanceType" in config["General"]:
- version = config["General"].get("InstanceType", "1.21.4")
- self.groups[group_name].append(InstanceTile(instance_name, version, group_name, self))
- except FileNotFoundError:
- self.groups = {"Modpack": [], "Vanilla-like": []}
- self.save_instgroups()
- def save_instgroups(self):
- data = {"formatVersion": "1", "groups": {}}
- for group_name, tiles in self.groups.items():
- data["groups"][group_name] = {
- "hidden": group_name in self.collapsed_groups,
- "instances": [tile.name for tile in tiles]
- }
- with open("minecraft/instances/instgroups.json", "w") as f:
- json.dump(data, f, indent=4)
- def update_instance_name(self, old_name, new_name, group_name):
- if os.path.exists(f"minecraft/instances/{old_name}"):
- os.rename(f"minecraft/instances/{old_name}", f"minecraft/instances/{new_name}")
- config = configparser.ConfigParser()
- config.read(f"minecraft/instances/{new_name}/instance.cfg")
- if "General" in config:
- config["General"]["name"] = new_name
- with open(f"minecraft/instances/{new_name}/instance.cfg", "w") as f:
- config.write(f)
- for tile in self.groups[group_name]:
- if tile.name == old_name:
- tile.name = new_name
- break
- self.save_instgroups()
- def update_instance_group(self, instance_name, old_group, new_group):
- self.save_instgroups()
- def create_toolbar(self):
- toolbar = QToolBar()
- self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar)
- logo_btn = QPushButton("MCLauncher")
- logo_btn.setStyleSheet("font-weight: bold; padding: 4px 10px;")
- toolbar.addWidget(logo_btn)
- toolbar.addSeparator()
- add_action = QAction(QIcon.fromTheme("list-add"), "Dodaj Instalację", self)
- add_action.triggered.connect(self.open_add_instance_dialog)
- toolbar.addAction(add_action)
- add_group_action = QAction(QIcon.fromTheme("folder-new"), "Dodaj Grupę", self)
- add_group_action.triggered.connect(self.open_add_group_dialog)
- toolbar.addAction(add_group_action)
- folder_menu = QMenu("Foldery", self)
- folder_menu.addAction("Folder instancji").triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile("minecraft/instances")))
- folder_menu.addAction("Folder modów").triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile("minecraft/mods")))
- folder_menu.addAction("Folder logów").triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile("minecraft/logs")))
- folder_menu.addAction("Folder assets").triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile("minecraft/assets")))
- folder_menu.addAction("Folder bibliotek").triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile("minecraft/libraries")))
- folder_menu.addAction("Folder Javy").triggered.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile("minecraft/javas")))
- folder_btn = QPushButton("Foldery")
- folder_btn.setMenu(folder_menu)
- toolbar.addWidget(folder_btn)
- settings_action = QAction(QIcon.fromTheme("preferences-system"), "Ustawienia", self)
- settings_action.triggered.connect(self.open_settings_dialog)
- toolbar.addAction(settings_action)
- help_action = QAction(QIcon.fromTheme("help-about"), "Pomoc", self)
- toolbar.addAction(help_action)
- update_action = QAction(QIcon.fromTheme("view-refresh"), "Aktualizuj", self)
- update_action.triggered.connect(self.refresh_launcher)
- toolbar.addAction(update_action)
- spacer = QWidget()
- spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
- toolbar.addWidget(spacer)
- self.user_btn = QPushButton(self.username or "Dodaj użytkownika")
- toolbar.addWidget(self.user_btn)
- def create_main_area(self):
- central_widget = QWidget()
- self.setCentralWidget(central_widget)
- layout = QHBoxLayout()
- self.main_area = QScrollArea()
- self.main_area.setWidgetResizable(True)
- content_widget = QWidget()
- self.grid_layout = QVBoxLayout()
- for group in self.groups.keys():
- group_section = GroupSection(group, self.groups[group], self)
- self.group_widgets[group] = {"header": None, "section": group_section}
- self.refresh_instances()
- content_widget.setLayout(self.grid_layout)
- self.main_area.setWidget(content_widget)
- layout.addWidget(self.main_area, stretch=3)
- self.right_panel = QWidget()
- right_layout = QVBoxLayout()
- self.right_icon = QLabel("🟩")
- self.right_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
- self.right_icon.setStyleSheet("font-size: 64px; border: none;")
- right_layout.addWidget(self.right_icon)
- self.launch_btn = QPushButton("Uruchom")
- self.launch_btn.clicked.connect(self.launch_selected)
- right_layout.addWidget(self.launch_btn)
- self.kill_btn = QPushButton("Zakończ")
- self.kill_btn.clicked.connect(self.kill_selected)
- right_layout.addWidget(self.kill_btn)
- self.edit_btn = QPushButton("Edytuj")
- self.edit_btn.clicked.connect(self.edit_selected)
- right_layout.addWidget(self.edit_btn)
- self.change_group_btn = QPushButton("Zmień grupę")
- right_layout.addWidget(self.change_group_btn)
- self.folder_btn = QPushButton("Folder")
- self.folder_btn.clicked.connect(self.open_instance_folder)
- right_layout.addWidget(self.folder_btn)
- self.export_btn = QPushButton("Eksportuj")
- right_layout.addWidget(self.export_btn)
- self.copy_btn = QPushButton("Kopiuj")
- right_layout.addWidget(self.copy_btn)
- self.delete_btn = QPushButton("Usuń")
- self.delete_btn.clicked.connect(self.delete_instance)
- right_layout.addWidget(self.delete_btn)
- self.shortcut_btn = QPushButton("Utwórz skrót")
- right_layout.addWidget(self.shortcut_btn)
- self.right_panel.setLayout(right_layout)
- layout.addWidget(self.right_panel, stretch=1)
- central_widget.setLayout(layout)
- def create_status_bar(self):
- self.status_bar = self.statusBar()
- self.progress_bar = QProgressBar()
- self.progress_bar.setFixedHeight(20)
- self.progress_bar.setFixedWidth(400)
- self.progress_bar.setTextVisible(False)
- self.status_bar.addPermanentWidget(self.progress_bar, 1)
- self.status_label = QLabel("Gotowy")
- self.status_bar.addPermanentWidget(self.status_label, 1)
- self.download_info = QLabel("")
- self.status_bar.addPermanentWidget(self.download_info, 2)
- self.progress_bar.hide()
- def refresh_instances(self):
- for i in reversed(range(self.grid_layout.count())):
- widget = self.grid_layout.itemAt(i).widget()
- if widget:
- self.grid_layout.removeWidget(widget)
- widget.hide()
- for group in self.groups.keys():
- if not self.group_widgets[group]["header"]:
- header_widget = QWidget()
- header_layout = QHBoxLayout()
- collapse_btn = QPushButton("▼" if group not in self.collapsed_groups else "▶")
- collapse_btn.setFixedSize(20, 20)
- collapse_btn.clicked.connect(lambda checked, g=group: self.toggle_group(g))
- header_layout.addWidget(collapse_btn)
- group_label = QLabel(group)
- header_layout.addWidget(group_label)
- header_widget.setLayout(header_layout)
- self.group_widgets[group]["header"] = header_widget
- self.grid_layout.addWidget(self.group_widgets[group]["header"])
- self.group_widgets[group]["header"].show()
- if group not in self.collapsed_groups:
- self.grid_layout.addWidget(self.group_widgets[group]["section"])
- self.group_widgets[group]["section"].show()
- def toggle_group(self, group):
- if group in self.collapsed_groups:
- self.collapsed_groups.remove(group)
- else:
- self.collapsed_groups.add(group)
- self.save_instgroups()
- self.refresh_instances()
- def update_right_panel(self):
- if self.selected_tile:
- self.right_icon.setText(self.selected_tile.icon.text())
- else:
- self.right_icon.setText("🟩")
- def launch_selected(self):
- if not self.selected_tile:
- QMessageBox.warning(self, "Błąd", "Nie wybrano instancji!")
- return
- instance_name = self.selected_tile.name
- instance_dir = f"minecraft/instances/{instance_name}"
- if not os.path.exists(instance_dir):
- QMessageBox.warning(self, "Błąd", f"Folder instancji {instance_name} nie istnieje!")
- return
- version = self.selected_tile.version
- version_info = next((v for v in self.versions_data["versions"] if v["id"] == version), None)
- if not version_info:
- QMessageBox.warning(self, "Błąd", f"Nie można znaleźć informacji o wersji {version}!")
- return
- try:
- version_details = self.get_version_details(version_info["url"])
- except Exception as e:
- QMessageBox.warning(self, "Błąd", f"Nie można pobrać szczegółów wersji: {str(e)}")
- return
- missing_files = self.check_required_files(version_details, instance_name)
- if missing_files:
- total_missing = sum(len(files) for files in missing_files.values())
- reply = QMessageBox.question(
- self, "Pobieranie plików",
- f"Brakuje {total_missing} plików (assets, libraries, natives). Czy chcesz je pobrać?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
- )
- if reply == QMessageBox.StandardButton.Yes:
- self.download_required_files(missing_files, instance_name, version_details)
- return
- self.run_minecraft(instance_name, version_details)
- def get_version_details(self, version_url):
- version_id = version_url.split("/")[-2]
- version_file = f"minecraft/versions/{version_id}/{version_id}.json"
- if os.path.exists(version_file):
- with open(version_file, "r") as f:
- return json.load(f)
- response = requests.get(version_url)
- response.raise_for_status()
- version_details = response.json()
- os.makedirs(os.path.dirname(version_file), exist_ok=True)
- with open(version_file, "w") as f:
- json.dump(version_details, f)
- return version_details
- def check_required_files(self, version_details, instance_name):
- missing_files = {'assets': {}, 'libraries': {}, 'natives': {}}
- download_state = load_download_state()
- # Sprawdzanie client.jar
- version_id = version_details['id']
- client_jar = os.path.join("minecraft", "versions", version_id, f"{version_id}.jar")
- if not os.path.exists(client_jar):
- client_url = version_details["downloads"]["client"]["url"]
- missing_files['assets'][client_jar] = client_url
- # Sprawdzanie bibliotek
- for library in version_details.get("libraries", []):
- if "rules" in library and not self.check_library_rules(library["rules"]):
- continue
- path = self.get_library_path(library)
- url = self.get_library_url(library)
- if not os.path.exists(path):
- if 'libraries' not in download_state or path not in download_state.get('libraries', {}):
- missing_files['libraries'][path] = url
- elif download_state['libraries'].get(path) == url and not os.path.exists(path):
- missing_files['libraries'][path] = url
- # Sprawdzanie natywnych bibliotek
- for library in version_details.get("libraries", []):
- if "natives" in library and "rules" in library and self.check_library_rules(library["rules"]):
- native = library["natives"].get(platform.system().lower())
- if native:
- artifact = library["downloads"]["classifiers"].get(native)
- if artifact:
- path = os.path.join("minecraft", "instances", instance_name, "natives", os.path.basename(artifact["path"]))
- if not os.path.exists(path):
- if 'natives' not in download_state or path not in download_state.get('natives', {}):
- missing_files['natives'][path] = artifact["url"]
- elif download_state['natives'].get(path) == artifact["url"] and not os.path.exists(path):
- missing_files['natives'][path] = artifact["url"]
- # Sprawdzanie asset index
- asset_index = version_details.get("assetIndex", {}).get("id", "legacy")
- asset_index_file = os.path.join("minecraft", "assets", "indexes", f"{asset_index}.json")
- if not os.path.exists(asset_index_file):
- if 'assets' not in download_state or asset_index_file not in download_state.get('assets', {}):
- missing_files['assets'][asset_index_file] = version_details["assetIndex"]["url"]
- elif download_state['assets'].get(asset_index_file) == version_details["assetIndex"]["url"] and not os.path.exists(asset_index_file):
- missing_files['assets'][asset_index_file] = version_details["assetIndex"]["url"]
- # Sprawdzanie obiektów assets
- if os.path.exists(asset_index_file):
- with open(asset_index_file, "r") as f:
- asset_index = json.load(f)
- objects = asset_index.get("objects", {})
- for asset_name, asset_data in objects.items():
- hash = asset_data["hash"]
- asset_path = os.path.join("minecraft", "assets", "objects", hash[:2], hash)
- url = f"https://resources.download.minecraft.net/{hash[:2]}/{hash}"
- if not os.path.exists(asset_path):
- if 'assets' not in download_state or asset_path not in download_state.get('assets', {}):
- missing_files['assets'][asset_path] = url
- elif download_state['assets'].get(asset_path) == url and not os.path.exists(asset_path):
- missing_files['assets'][asset_path] = url
- return missing_files
- def check_library_rules(self, rules):
- for rule in rules:
- if rule.get("action") == "allow":
- if "os" in rule and rule["os"]["name"] != platform.system().lower():
- return False
- elif rule.get("action") == "disallow":
- if "os" in rule and rule["os"]["name"] == platform.system().lower():
- return False
- return True
- def get_library_path(self, library):
- lib_name = library["name"]
- parts = lib_name.split(":")
- base_path = os.path.join("minecraft", "libraries", *parts[0].split("."), parts[1], parts[2])
- artifact = library.get("downloads", {}).get("artifact", {})
- if artifact and "path" in artifact:
- return os.path.join(base_path, artifact["path"])
- return os.path.join(base_path, f"{parts[1]}-{parts[2]}.jar")
- def get_library_url(self, library):
- artifact = library.get("downloads", {}).get("artifact", {})
- if artifact and "url" in artifact:
- return artifact["url"]
- lib_name = library["name"]
- parts = lib_name.split(":")
- base_url = f"https://libraries.minecraft.net/{parts[0].replace('.', '/')}/{parts[1]}/{parts[2]}"
- return f"{base_url}/{parts[1]}-{parts[2]}.jar"
- def download_required_files(self, files_to_download, instance_name, version_details):
- self.current_downloads = files_to_download
- for download_type, files in files_to_download.items():
- if files:
- self.download_counts[download_type]['total'] = len(files)
- self.download_counts[download_type]['completed'] = 0
- self.download_worker = DownloadWorker(files, download_type)
- self.download_worker.progress_updated.connect(self.update_download_progress)
- self.download_worker.download_finished.connect(self.download_finished)
- self.download_worker.file_downloaded.connect(self.file_downloaded)
- self.progress_bar.show()
- self.status_label.setText(f"[{download_type}]: pobieranie 0% | 0 / {len(files)}")
- self.download_worker.start()
- break
- def update_download_progress(self, download_type, percent, completed, total, filename, downloaded_mb, total_mb):
- self.download_counts[download_type]['completed'] = completed
- self.download_counts[download_type]['total'] = total
- self.progress_bar.setValue(percent)
- self.status_label.setText(f"[{download_type}]: pobieranie {percent}% | {completed} / {total}")
- self.download_info.setText(f"{filename}: {downloaded_mb} mb / {total_mb} mb")
- def file_downloaded(self, download_type, file_path):
- if "assets/indexes" in file_path:
- self.process_asset_index(file_path)
- def process_asset_index(self, index_file):
- try:
- with open(index_file, "r") as f:
- asset_index = json.load(f)
- objects = asset_index.get("objects", {})
- files_to_download = {}
- for asset_name, asset_data in objects.items():
- hash = asset_data["hash"]
- asset_path = os.path.join("minecraft", "assets", "objects", hash[:2], hash)
- if not os.path.exists(asset_path):
- url = f"https://resources.download.minecraft.net/{hash[:2]}/{hash}"
- files_to_download[asset_path] = url
- if files_to_download:
- self.current_downloads['assets'].update(files_to_download)
- self.download_counts['assets']['total'] += len(files_to_download)
- self.download_worker.files_to_download.update(files_to_download)
- self.progress_bar.setMaximum(self.download_counts['assets']['total'] * 100)
- except Exception as e:
- logging.error(f"Błąd podczas przetwarzania asset index: {str(e)}")
- def download_finished(self, success, message):
- self.progress_bar.hide()
- self.status_label.setText(message)
- self.download_info.setText("")
- if success:
- instance_name = self.selected_tile.name
- instance_dir = f"minecraft/instances/{instance_name}"
- version = self.selected_tile.version
- version_info = next((v for v in self.versions_data["versions"] if v["id"] == version), None)
- version_details = self.get_version_details(version_info["url"])
- self.run_minecraft(instance_name, version_details)
- else:
- QMessageBox.warning(self, "Błąd pobierania", message)
- def run_minecraft(self, instance_name, version_details):
- instance_dir = f"minecraft/instances/{instance_name}"
- version_id = version_details["id"]
- java_path = self.find_java_for_version(version_details)
- if not java_path:
- QMessageBox.warning(self, "Błąd", "Nie znaleziono odpowiedniej wersji Java!")
- return
- args = self.prepare_launch_arguments(java_path, instance_name, version_details)
- self.minecraft_runner = MinecraftRunner(args, instance_dir)
- self.minecraft_runner.output_received.connect(self.handle_minecraft_output)
- self.minecraft_runner.finished.connect(self.handle_minecraft_finished)
- self.minecraft_runner.start()
- self.selected_tile.is_running = True
- self.selected_tile.status_icon.setText("▶")
- self.status_label.setText(f"Uruchomiono {instance_name}")
- if os.path.exists(f"{instance_dir}/instance.cfg"):
- config = configparser.ConfigParser()
- config.read(f"{instance_dir}/instance.cfg")
- if "General" in config:
- config["General"]["lastLaunchTime"] = str(int(datetime.now().timestamp() * 1000))
- with open(f"{instance_dir}/instance.cfg", "w") as f:
- config.write(f)
- logging.info(f"Uruchomiono instancję: {instance_name}")
- print(f"[INFO] Uruchomiono instancję: {instance_name}")
- def find_java_for_version(self, version_details):
- if not self.settings.get("auto_java", True) and self.settings.get("java_path"):
- return self.settings["java_path"]
- java_path = JavaFinder.find_java_for_version(version_details)
- if java_path:
- return java_path
- reply = QMessageBox.question(
- self, "Brak Java",
- "Nie znaleziono odpowiedniej wersji Java. Czy chcesz pobrać wymaganą wersję?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
- )
- if reply == QMessageBox.StandardButton.Yes:
- self.download_java(version_details.get("javaVersion", {}).get("majorVersion", 8))
- return None
- return None
- def download_java(self, java_version):
- QMessageBox.information(self, "Informacja", "Pobieranie Java będzie dostępne w przyszłej wersji.")
- def prepare_launch_arguments(self, java_path, instance_name, version_details):
- args = [java_path]
- jvm_args = version_details.get("arguments", {}).get("jvm", [])
- for arg in jvm_args:
- if isinstance(arg, dict):
- continue
- args.append(arg.replace("${natives_directory}", f"minecraft/instances/{instance_name}/natives")
- .replace("${launcher_name}", "MCLauncher")
- .replace("${launcher_version}", "1.0.0"))
- classpath = self.build_classpath(version_details, instance_name)
- args.extend(["-cp", classpath])
- main_class = version_details.get("mainClass", "net.minecraft.client.main.Main")
- args.append(main_class)
- game_args = version_details.get("arguments", {}).get("game", [])
- for arg in game_args:
- if isinstance(arg, dict):
- continue
- args.append(arg.replace("${version_name}", version_details["id"])
- .replace("${game_directory}", f"minecraft/instances/{instance_name}")
- .replace("${assets_root}", "minecraft/assets")
- .replace("${assets_index_name}", version_details.get("assetIndex", {}).get("id", "legacy"))
- .replace("${auth_player_name}", self.username or "Player")
- .replace("${version_type}", version_details.get("type", "release")))
- return args
- def build_classpath(self, version_details, instance_name):
- classpath = []
- version_id = version_details["id"]
- classpath.append(f"minecraft/versions/{version_id}/{version_id}.jar")
- for library in version_details.get("libraries", []):
- if "rules" in library and not self.check_library_rules(library["rules"]):
- continue
- path = self.get_library_path(library)
- if os.path.exists(path):
- classpath.append(path)
- separator = ";" if platform.system() == "Windows" else ":"
- return separator.join(classpath)
- def handle_minecraft_output(self, output):
- print(f"[Minecraft] {output}")
- def handle_minecraft_finished(self, exit_code):
- if self.selected_tile:
- self.selected_tile.is_running = False
- self.selected_tile.status_icon.setText("")
- self.status_label.setText(f"Gra zakończona z kodem {exit_code}")
- logging.info(f"Gra zakończona z kodem {exit_code}")
- def kill_selected(self):
- if self.selected_tile and self.selected_tile.is_running:
- if self.minecraft_runner:
- self.minecraft_runner.stop()
- self.selected_tile.is_running = False
- self.selected_tile.status_icon.setText("")
- self.status_label.setText("Zatrzymano grę")
- logging.info(f"Zakończono instancję: {self.selected_tile.name}")
- print(f"[INFO] Zakończono instancję: {self.selected_tile.name}")
- def edit_selected(self):
- if self.selected_tile:
- logging.info(f"Edytowanie instancji: {self.selected_tile.name}")
- print(f"[INFO] Edytowanie instancji: {self.selected_tile.name}")
- def open_instance_folder(self):
- if self.selected_tile:
- instance_dir = f"minecraft/instances/{self.selected_tile.name}"
- if os.path.exists(instance_dir):
- QDesktopServices.openUrl(QUrl.fromLocalFile(instance_dir))
- logging.info(f"Otwieranie folderu instancji: {self.selected_tile.name}")
- print(f"[INFO] Otwieranie folderu instancji: {self.selected_tile.name}")
- def delete_instance(self):
- if not self.selected_tile:
- return
- instance_name = self.selected_tile.name
- reply = QMessageBox.question(
- self, "Usuwanie instancji",
- f"Czy na pewno chcesz usunąć instancję '{instance_name}'?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
- )
- if reply == QMessageBox.StandardButton.Yes:
- for group, tiles in self.groups.items():
- if self.selected_tile in tiles:
- tiles.remove(self.selected_tile)
- break
- instance_dir = f"minecraft/instances/{instance_name}"
- if os.path.exists(instance_dir):
- shutil.rmtree(instance_dir)
- self.save_instgroups()
- self.refresh_instances()
- self.selected_tile = None
- self.update_right_panel()
- logging.info(f"Usunięto instancję: {instance_name}")
- print(f"[INFO] Usunięto instancję: {instance_name}")
- def open_add_instance_dialog(self):
- if not self.versions_data:
- QMessageBox.warning(self, "Błąd", "Nie można załadować listy wersji Minecraft!")
- return
- dialog = AddInstanceDialog(list(self.groups.keys()), self.versions_data, self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- instance_name = dialog.instance_name.text()
- version_id = dialog.version_combo.currentData()
- group_name = dialog.group_combo.currentText()
- new_tile = InstanceTile(instance_name, version_id, group_name, self)
- self.groups[group_name].append(new_tile)
- self.group_widgets[group_name]["section"] = GroupSection(group_name, self.groups[group_name], self)
- self.create_instance_folder(instance_name, version_id)
- self.save_instgroups()
- self.refresh_instances()
- logging.info(f"Dodano instancję: {instance_name}, wersja: {version_id}")
- print(f"[INFO] Dodano instancję: {instance_name}, wersja: {version_id}")
- def create_instance_folder(self, instance_name, version_id):
- instance_dir = f"minecraft/instances/{instance_name}"
- os.makedirs(instance_dir, exist_ok=True)
- os.makedirs(f"{instance_dir}/minecraft", exist_ok=True)
- os.makedirs(f"{instance_dir}/natives", exist_ok=True)
- config = configparser.ConfigParser()
- config['General'] = {
- 'ConfigVersion': '1.2',
- 'ManagedPack': 'false',
- 'iconKey': 'default',
- 'name': instance_name,
- 'InstanceType': version_id,
- 'lastLaunchTime': str(int(datetime.now().timestamp() * 1000)),
- 'lastTimePlayed': '0',
- 'totalTimePlayed': '0'
- }
- with open(f"{instance_dir}/instance.cfg", "w") as config_file:
- config.write(config_file)
- def open_add_group_dialog(self):
- dialog = AddGroupDialog()
- if dialog.exec() == QDialog.DialogCode.Accepted:
- group_name = dialog.group_name.text()
- if group_name and group_name not in self.groups:
- self.groups[group_name] = []
- self.group_widgets[group_name] = {
- "header": None,
- "section": GroupSection(group_name, self.groups[group_name], self)
- }
- self.save_instgroups()
- self.refresh_instances()
- logging.info(f"Dodano grupę: {group_name}")
- print(f"[INFO] Dodano grupę: {group_name}")
- def open_settings_dialog(self):
- dialog = SettingsDialog(self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- self.username = dialog.username_edit.text()
- self.theme = dialog.theme_combo.currentText()
- self.settings = {
- "auto_java": dialog.auto_java_check.isChecked(),
- "java_path": dialog.java_path_edit.text()
- }
- self.user_btn.setText(self.username or "Dodaj użytkownika")
- self.save_settings()
- self.apply_theme()
- logging.info(f"Zaktualizowano ustawienia: użytkownik={self.username}, motyw={self.theme}")
- print(f"[INFO] Zaktualizowano ustawienia: użytkownik={self.username}, motyw={self.theme}")
- def load_settings(self):
- try:
- with open("minecraft/settings.json", "r") as f:
- settings = json.load(f)
- self.username = settings.get("username", None)
- self.theme = settings.get("theme", "Jasny")
- self.settings = settings.get("settings", {})
- except FileNotFoundError:
- self.save_settings()
- def save_settings(self):
- settings = {
- "username": self.username,
- "theme": self.theme,
- "settings": self.settings
- }
- with open("minecraft/settings.json", "w") as f:
- json.dump(settings, f, indent=4)
- def apply_theme(self):
- if self.theme == "Ciemny":
- self.setStyleSheet("""
- QMainWindow { background-color: #2b2b2b; color: #ffffff; }
- QWidget { background-color: #2b2b2b; color: #ffffff; }
- QPushButton { background-color: #3c3c3c; border: 1px solid #555555; color: #ffffff; }
- QLineEdit, QComboBox { background-color: #3c3c3c; color: #ffffff; }
- QScrollArea { background-color: #2b2b2b; }
- QProgressBar { background-color: #3c3c3c; border: 1px solid #555555; }
- """)
- else:
- self.setStyleSheet("")
- def refresh_launcher(self):
- self.fetch_minecraft_versions()
- QMessageBox.information(self, "Odświeżono", "Lista wersji Minecraft została odświeżona.")
- if __name__ == "__main__":
- app = QApplication(sys.argv)
- launcher = MCLauncher()
- launcher.show()
- sys.exit(app.exec())
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement