Advertisement
PaffcioStudio

Untitled

May 11th, 2025
338
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 148.21 KB | None | 0 0
  1. import sys
  2. import os
  3. import json
  4. import requests
  5. import hashlib
  6. import subprocess
  7. import zipfile
  8. import shutil
  9. import glob
  10. import re
  11. from PyQt6.QtWidgets import (
  12.     QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
  13.     QPushButton, QListWidget, QComboBox, QLineEdit, QProgressBar,
  14.     QLabel, QMessageBox, QMenuBar, QDialog, QInputDialog,
  15.     QFileDialog, QCheckBox, QTextEdit, QScrollArea, QSizePolicy,
  16.     QListWidgetItem # Dodano brakujący import
  17. )
  18. from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSize, QTimer
  19. from PyQt6.QtGui import QIcon, QPixmap
  20. import logging
  21. from datetime import datetime, timedelta # Import timedelta
  22. from collections import deque
  23. import time
  24. from pathlib import Path
  25. import signal # Potrzebne do obsługi Ctrl+C
  26.  
  27.  
  28. logging.basicConfig(
  29.     filename="launcher.log",
  30.     level=logging.INFO,
  31.     format="%(asctime)s - %(levelname)s - %(message)s"
  32. )
  33.  
  34.  
  35. CONFIG_DIR = Path.cwd() / "minecraft_launcher"
  36. SETTINGS_FILE = CONFIG_DIR / "settings.json"
  37. ASSETS_DIR = CONFIG_DIR / "assets"
  38. LIBRARIES_DIR = CONFIG_DIR / "libraries"
  39. INSTANCES_DIR = CONFIG_DIR / "instances"
  40. JAVA_DIR = CONFIG_DIR / "java"
  41. MOD_ICONS_DIR = CONFIG_DIR / "mod_icons"
  42.  
  43.  
  44. CURSEFORGE_API_KEY = "$2a$10$dxb5k5YbdGcnXYwM4U7CF.VWOtmsUP3xt3fDssBnjyPwCpEFpJgs."
  45.  
  46.  
  47. DEFAULT_SETTINGS = {
  48.     "theme": "Light",
  49.     "java_path": "",
  50.     "ram": "4G",
  51.     "jvm_args": "-XX:+UnlockExperimentalVMOptions",
  52.     "fullscreen": False,
  53.     "resolution": "1280x720",
  54.     "default_account": "Player"
  55. }
  56.  
  57.  
  58. STYLESHEET = """
  59. QDialog, QMainWindow {
  60.    background-color: #f0f0f0;
  61.    font-family: Arial;
  62. }
  63. QLabel {
  64.    font-size: 14px;
  65.    margin: 5px 0;
  66. }
  67. QProgressBar {
  68.    border: 1px solid #ccc;
  69.    border-radius: 5px;
  70.    text-align: center;
  71.    height: 20px;
  72.    background-color: #e0e0e0;
  73. }
  74. QProgressBar::chunk {
  75.    background-color: #4CAF50;
  76.    border-radius: 3px;
  77. }
  78. QPushButton {
  79.    background-color: #4CAF50;
  80.    color: white;
  81.    border: none;
  82.    padding: 8px;
  83.    border-radius: 5px;
  84. }
  85. QPushButton:hover {
  86.    background-color: #45a049;
  87. }
  88. QPushButton:disabled {
  89.    background-color: #cccccc;
  90. }
  91. QPushButton[deleteButton="true"] {
  92.    background-color: #f44336;
  93. }
  94. QPushButton[deleteButton="true"]:hover {
  95.    background-color: #d32f2f;
  96. }
  97. QPushButton[deleteButton="true"]:disabled {
  98.    background-color: #cccccc;
  99. }
  100. QListWidget {
  101.    border: 1px solid #ccc;
  102.    border-radius: 5px;
  103.    padding: 5px;
  104.    background-color: white;
  105.    alternate-background-color: #f5f5f5;
  106. }
  107. QScrollArea {
  108.    border: none;
  109. }
  110. QLineEdit, QComboBox, QTextEdit {
  111.    padding: 5px;
  112.    border: 1px solid #ccc;
  113.    border-radius: 4px;
  114. }
  115. """
  116.  
  117. # Nowa klasa wątku do pobierania ikon
  118. class IconDownloadThread(QThread):
  119.     # Sygnał emitowany po pobraniu ikony: (mod_id, ścieżka_do_pliku)
  120.     icon_downloaded = pyqtSignal(int, str)
  121.  
  122.     def __init__(self, mod_id, url, dest_path):
  123.         super().__init__()
  124.         self.mod_id = mod_id
  125.         self.url = url
  126.         self.dest_path = Path(dest_path)
  127.         # logging.debug(f"Utworzono wątek pobierania ikony dla mod ID {mod_id} z URL: {url}")
  128.  
  129.     def run(self):
  130.         # Sprawdź jeszcze raz, czy plik nie został utworzony przez inny wątek w międzyczasie
  131.         if self.dest_path.exists():
  132.              logging.debug(f"Icon file already exists for mod ID {self.mod_id}: {self.dest_path.name}. Skipping download.")
  133.              self.icon_downloaded.emit(self.mod_id, str(self.dest_path))
  134.              return
  135.  
  136.         logging.debug(f"Starting icon download for mod ID {self.mod_id} from {self.url}")
  137.         try:
  138.             self.dest_path.parent.mkdir(parents=True, exist_ok=True)
  139.             response = requests.get(self.url, timeout=10) # Krótki timeout dla ikon
  140.             response.raise_for_status()
  141.  
  142.             with open(self.dest_path, "wb") as f:
  143.                 f.write(response.content)
  144.  
  145.             logging.debug(f"Icon downloaded successfully for mod ID {self.mod_id}: {self.dest_path.name}")
  146.             self.icon_downloaded.emit(self.mod_id, str(self.dest_path))
  147.  
  148.         except requests.exceptions.RequestException as e:
  149.             logging.warning(f"Failed to download icon for mod ID {self.mod_id} from {self.url}: {e}")
  150.             # Emituj pustą ścieżkę lub None, aby wskazać błąd
  151.             self.icon_downloaded.emit(self.mod_id, "")
  152.         except Exception as e:
  153.             logging.error(f"Unexpected error during icon download for mod ID {self.mod_id}: {e}")
  154.             self.icon_downloaded.emit(self.mod_id, "")
  155.  
  156. class DownloadThread(QThread):
  157.     progress = pyqtSignal(int)
  158.     total_progress = pyqtSignal(int)
  159.     finished = pyqtSignal(str, bool, str)
  160.     update_status = pyqtSignal(str, str)
  161.     update_speed = pyqtSignal(float, str)
  162.     update_size = pyqtSignal(float, str)
  163.     add_to_total_files = pyqtSignal(int)
  164.  
  165.     def __init__(self, url, dest, download_type, sha1=None):
  166.         super().__init__()
  167.         self.url = url
  168.         self.dest = dest
  169.         self.download_type = download_type
  170.         self.sha1 = sha1
  171.         self.canceled = False
  172.  
  173.     def run(self):
  174.         dest_path = Path(self.dest)
  175.         file_name = os.path.basename(self.dest)
  176.         logging.info(f"Starting download thread for: {self.url} -> {self.dest}")
  177.  
  178.         if dest_path.exists() and self.sha1 and self.validate_sha1(self.dest, self.sha1):
  179.             logging.info(f"File {file_name} already exists and is valid. Skipping download.")
  180.             self.progress.emit(100)
  181.             self.finished.emit(self.dest, True, "skipped")
  182.             return
  183.  
  184.         try:
  185.             dest_path.parent.mkdir(parents=True, exist_ok=True)
  186.             self.update_status.emit(self.download_type, file_name)
  187.  
  188.             response = requests.get(self.url, stream=True, timeout=120)
  189.             response.raise_for_status()
  190.  
  191.             total_size = int(response.headers.get("content-length", 0))
  192.             if total_size > 0:
  193.                 unit, size = self._format_bytes(total_size)
  194.                 self.update_size.emit(size, unit)
  195.             else:
  196.                 self.update_size.emit(0, "Nieznany")
  197.  
  198.             downloaded = 0
  199.             start_time = time.time()
  200.             last_update_time = start_time
  201.             last_downloaded_speed_check = 0
  202.             progress_interval = 0.5
  203.  
  204.             with open(self.dest, "wb") as f:
  205.                 for chunk in response.iter_content(chunk_size=8192):
  206.                     if self.canceled:
  207.                         logging.info(f"Download canceled for {file_name}")
  208.                         try:
  209.                              dest_path.unlink(missing_ok=True)
  210.                         except Exception as cleanup_e:
  211.                              logging.warning(f"Failed to clean up partial download {dest_path}: {cleanup_e}") # Poprawiono zmienną
  212.                         self.finished.emit("", False, "Anulowano")
  213.                         return
  214.                     if chunk:
  215.                         f.write(chunk)
  216.                         downloaded += len(chunk)
  217.  
  218.                         current_time = time.time()
  219.                         if current_time - last_update_time >= progress_interval and total_size > 0:
  220.                             self.progress.emit(int(downloaded / total_size * 100))
  221.  
  222.                             delta_downloaded = downloaded - last_downloaded_speed_check
  223.                             delta_time = current_time - last_update_time
  224.                             if delta_time > 0:
  225.                                 speed = delta_downloaded / delta_time
  226.                                 speed_unit, speed_val = self._format_bytes(speed)
  227.                                 self.update_speed.emit(speed_val, f"{speed_unit}/s")
  228.  
  229.                             last_downloaded_speed_check = downloaded
  230.                             last_update_time = current_time
  231.  
  232.             if total_size > 0:
  233.                  self.progress.emit(100)
  234.             else:
  235.                  self.progress.emit(100)
  236.  
  237.  
  238.             if self.sha1 and not self.validate_sha1(self.dest, self.sha1):
  239.                 logging.error(f"SHA1 validation failed for {file_name}. Expected: {self.sha1}")
  240.                 try:
  241.                      dest_path.unlink(missing_ok=True)
  242.                 except Exception as cleanup_e:
  243.                      logging.warning(f"Failed to clean up invalid file {dest_path}: {cleanup_e}") # Poprawiono zmienną
  244.                 self.finished.emit(self.dest, False, "Walidacja SHA1 nieudana")
  245.                 return
  246.  
  247.             logging.info(f"Download successful: {file_name}")
  248.             self.finished.emit(self.dest, True, "")
  249.  
  250.         except requests.exceptions.Timeout:
  251.             logging.error(f"Download timeout for {file_name}")
  252.             try:
  253.                  dest_path.unlink(missing_ok=True)
  254.             except Exception as cleanup_e:
  255.                  logging.warning(f"Failed to clean up partial download {dest_path}: {cleanup_e}") # Poprawiono zmienną
  256.             self.finished.emit(self.dest, False, "Timeout pobierania")
  257.         except requests.exceptions.RequestException as e:
  258.             logging.error(f"Download error for {file_name}: {e}")
  259.             try:
  260.                  dest_path.unlink(missing_ok=True)
  261.             except Exception as cleanup_e:
  262.                  logging.warning(f"Failed to clean up partial download {dest_path}: {cleanup_e}") # Poprawiono zmienną
  263.             self.finished.emit(self.dest, False, f"Błąd pobierania: {e}")
  264.         except Exception as e:
  265.             logging.error(f"An unexpected error occurred during download of {file_name}: {e}")
  266.             try:
  267.                  dest_path.unlink(missing_ok=True)
  268.             except Exception as cleanup_e:
  269.                  logging.warning(f"Failed to clean up partial download {dest_path}: {cleanup_e}") # Poprawiono zmienną
  270.             self.finished.emit(self.dest, False, f"Nieoczekiwany błąd: {e}")
  271.  
  272.  
  273.     def validate_sha1(self, file_path, expected_sha1):
  274.         sha1 = hashlib.sha1()
  275.         try:
  276.             if not expected_sha1 or not re.match(r'^[a-f0-9]{40}$', expected_sha1):
  277.                  logging.warning(f"SHA1 validation skipped for {Path(file_path).name}: Invalid or missing SHA1 hash provided.")
  278.                  return True
  279.  
  280.             with open(file_path, "rb") as f:
  281.                 for chunk in iter(lambda: f.read(8192), b""):
  282.                     sha1.update(chunk)
  283.             calculated_sha1 = sha1.hexdigest()
  284.             is_valid = calculated_sha1 == expected_sha1
  285.             if not is_valid:
  286.                  logging.warning(f"SHA1 mismatch for {Path(file_path).name}. Expected: {expected_sha1}, Got: {calculated_sha1}")
  287.             return is_valid
  288.         except FileNotFoundError:
  289.             logging.warning(f"SHA1 validation failed: file not found {file_path}")
  290.             return False
  291.         except Exception as e:
  292.             logging.error(f"Error during SHA1 validation for {file_path}: {e}")
  293.             return False
  294.  
  295.     def cancel(self):
  296.         self.canceled = True
  297.  
  298.     def _format_bytes(self, byte_count):
  299.         if byte_count is None or byte_count < 0:
  300.             return "B", 0
  301.         units = ["B", "KB", "MB", "GB", "TB"]
  302.         i = 0
  303.         while byte_count >= 1024 and i < len(units) - 1:
  304.             byte_count /= 1024
  305.             i += 1
  306.         return units[i], byte_count
  307.  
  308.  
  309. class DownloadProgressDialog(QDialog):
  310.     cancel_signal = pyqtSignal()
  311.     download_process_finished = pyqtSignal(bool)
  312.  
  313.     # Dodano parametr launcher
  314.     def __init__(self, launcher, parent=None):
  315.         super().__init__(parent)
  316.         self.launcher = launcher # Przypisano launcher
  317.         self.setWindowTitle("Pobieranie zasobów...")
  318.         self.setMinimumSize(500, 400)
  319.         self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
  320.  
  321.         self.total_files_expected = 0
  322.         self.downloaded_files_count = 0
  323.         self.successful_files_count = 0
  324.         self.skipped_files_count = 0
  325.         self.failed_downloads = []
  326.         self.is_cancelled = False
  327.         self.init_ui()
  328.         self._dialog_closed_by_signal = False
  329.  
  330.     def init_ui(self):
  331.         self.setStyleSheet(STYLESHEET)
  332.         layout = QVBoxLayout(self)
  333.         layout.setSpacing(10)
  334.  
  335.         self.status_label = QLabel("Przygotowanie do pobierania...")
  336.         layout.addWidget(self.status_label)
  337.  
  338.         layout.addWidget(QLabel("Całkowity postęp:"))
  339.         self.total_progress_bar = QProgressBar()
  340.         self.total_progress_bar.setValue(0)
  341.         layout.addWidget(self.total_progress_bar)
  342.  
  343.         layout.addWidget(QLabel("Postęp pliku:"))
  344.         self.file_progress_bar = QProgressBar()
  345.         self.file_progress_bar.setValue(0)
  346.         layout.addWidget(self.file_progress_bar)
  347.  
  348.         self.speed_label = QLabel("Prędkość: 0 KB/s")
  349.         layout.addWidget(self.speed_label)
  350.         self.size_label = QLabel("Rozmiar: Nieznany")
  351.         layout.addWidget(self.size_label)
  352.  
  353.         self.files_label = QLabel("Pliki: 0/0 (0 pominięto)")
  354.         layout.addWidget(self.files_label)
  355.  
  356.         layout.addWidget(QLabel("Aktywne pobieranie:"))
  357.         self.file_list = QListWidget()
  358.         self.file_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
  359.         self.file_list.addItem("Oczekiwanie na start...")
  360.         layout.addWidget(self.file_list)
  361.  
  362.         self.cancel_button = QPushButton("Anuluj")
  363.         self.cancel_button.clicked.connect(self.cancel_downloads)
  364.         layout.addWidget(self.cancel_button)
  365.  
  366.         self.close_button = QPushButton("Gotowe")
  367.         self.close_button.clicked.connect(self.accept)
  368.         self.close_button.setVisible(False)
  369.         self.close_button.setEnabled(False)
  370.  
  371.         layout.addStretch(1)
  372.         button_layout = QHBoxLayout()
  373.         button_layout.addStretch(1)
  374.         button_layout.addWidget(self.cancel_button)
  375.         button_layout.addWidget(self.close_button)
  376.         button_layout.addStretch(1)
  377.         layout.addLayout(button_layout)
  378.  
  379.  
  380.     def set_total_files(self, count):
  381.         self.total_files_expected = count
  382.         self.files_label.setText(f"Pliki: {self.downloaded_files_count}/{self.total_files_expected} ({self.skipped_files_count} pominięto)")
  383.         self.update_total_progress()
  384.  
  385.     def add_total_files(self, count):
  386.          self.total_files_expected += count
  387.          self.files_label.setText(f"Pliki: {self.downloaded_files_count}/{self.total_files_expected} ({self.skipped_files_count} pominięto)")
  388.  
  389.  
  390.     def update_status(self, download_type, file_name):
  391.         self.status_label.setText(f"Pobieranie {download_type}: {file_name}")
  392.         self.file_list.clear()
  393.         self.file_list.addItem(f"{file_name} ({download_type})")
  394.  
  395.  
  396.     def update_progress(self, value):
  397.         self.file_progress_bar.setValue(value)
  398.  
  399.     def update_total_progress(self):
  400.         if self.total_files_expected > 0:
  401.             total_finished = self.downloaded_files_count + self.skipped_files_count
  402.             total_percentage = int((total_finished / self.total_files_expected) * 100)
  403.             self.total_progress_bar.setValue(total_percentage)
  404.         else:
  405.             pass
  406.  
  407.  
  408.     def update_speed(self, speed, unit):
  409.         self.speed_label.setText(f"Prędkość: {speed:.2f} {unit}")
  410.  
  411.     def update_size(self, size, unit):
  412.         self.size_label.setText(f"Rozmiar: {size:.2f} {unit}" if unit != "Nieznany" else "Rozmiar: Nieznany")
  413.  
  414.     def increment_downloaded(self, file_path, success=True, error_msg=""):
  415.         file_name = os.path.basename(file_path) if file_path else "Nieznany plik"
  416.  
  417.         if error_msg == "skipped":
  418.              self.skipped_files_count += 1
  419.              self.successful_files_count += 1
  420.              logging.info(f"File skipped: {file_name}")
  421.         else:
  422.              self.downloaded_files_count += 1
  423.              if success:
  424.                   self.successful_files_count += 1
  425.                   logging.info(f"Download finished: {file_name}")
  426.              else:
  427.                   self.failed_downloads.append(f"{file_name} ({error_msg})")
  428.                   logging.error(f"Download failed: {file_name} - {error_msg}")
  429.  
  430.  
  431.         self.files_label.setText(f"Pliki: {self.downloaded_files_count}/{self.total_files_expected} ({self.skipped_files_count} pominięto)")
  432.         self.update_total_progress()
  433.         self.file_list.clear()
  434.         self.file_list.addItem("Oczekiwanie na następny...")
  435.  
  436.  
  437.         total_finished = self.downloaded_files_count + self.skipped_files_count
  438.         if total_finished >= self.total_files_expected and self.total_files_expected > 0:
  439.              self.on_all_downloads_finished()
  440.         elif self.is_cancelled:
  441.              pass
  442.  
  443.  
  444.     def on_all_downloads_finished(self):
  445.         logging.info("All downloads/checks processed.")
  446.         self.status_label.setText("Pobieranie zakończone!")
  447.         self.total_progress_bar.setValue(100)
  448.         self.file_progress_bar.setValue(100)
  449.         self.speed_label.setText("Prędkość: 0 KB/s")
  450.         self.size_label.setText("Rozmiar: ---")
  451.         self.file_list.clear()
  452.         self.file_list.addItem("Wszystkie pliki przetworzone.")
  453.  
  454.  
  455.         self.cancel_button.setVisible(False)
  456.         self.close_button.setVisible(True)
  457.         self.close_button.setEnabled(True)
  458.  
  459.         overall_success = not self.failed_downloads and not self.is_cancelled
  460.  
  461.         if self.failed_downloads:
  462.              msg = "Niektóre pliki nie zostały pobrane:\n" + "\n".join(self.failed_downloads)
  463.              QMessageBox.warning(self, "Pobieranie zakończone z błędami", msg)
  464.              logging.warning("Download finished with errors.")
  465.         elif self.is_cancelled:
  466.              self.status_label.setText("Pobieranie anulowane!")
  467.              logging.warning("Download process cancelled by user.")
  468.         else:
  469.              self.status_label.setText("Pobieranie zakończone pomyślnie!")
  470.              logging.info("Download finished successfully.")
  471.  
  472.  
  473.         QTimer.singleShot(100, lambda: self._emit_download_process_finished(overall_success))
  474.  
  475.  
  476.     def _emit_download_process_finished(self, success):
  477.         if not self._dialog_closed_by_signal:
  478.              self._dialog_closed_by_signal = True
  479.              self.download_process_finished.emit(success)
  480.              logging.debug(f"Emitted download_process_finished({success})")
  481.  
  482.  
  483.     def cancel_downloads(self):
  484.         if self.is_cancelled:
  485.             return
  486.  
  487.         self.is_cancelled = True
  488.         self.cancel_button.setEnabled(False)
  489.         self.status_label.setText("Anulowanie...")
  490.         logging.info("User requested cancellation.")
  491.         self.cancel_signal.emit()
  492.  
  493.         self.close_button.setVisible(True)
  494.         self.close_button.setEnabled(True)
  495.         self.file_list.clear()
  496.         self.file_list.addItem("Anulowano przez użytkownika.")
  497.  
  498.  
  499.     def closeEvent(self, event):
  500.         total_finished = self.downloaded_files_count + self.skipped_files_count
  501.         # Użyj przypisanego self.launcher
  502.         downloads_pending = total_finished < self.total_files_expected or (self.launcher and self.launcher.current_download_thread)
  503.  
  504.         if downloads_pending and not self.is_cancelled:
  505.             reply = QMessageBox.question(self, "Zamknąć?",
  506.                                          "Pobieranie wciąż trwa. Czy na pewno chcesz zamknąć i anulować?",
  507.                                          QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  508.             if reply == QMessageBox.StandardButton.Yes:
  509.                 self.cancel_downloads()
  510.                 event.accept()
  511.                 if not self._dialog_closed_by_signal:
  512.                      self._emit_download_process_finished(False)
  513.             else:
  514.                 event.ignore()
  515.  
  516.         else:
  517.             if not self._dialog_closed_by_signal:
  518.                  final_success = not self.failed_downloads and not self.is_cancelled
  519.                  self._emit_download_process_finished(final_success)
  520.             event.accept()
  521.  
  522.  
  523. class MinecraftLauncher:
  524.     def __init__(self):
  525.         CONFIG_DIR.mkdir(parents=True, exist_ok=True)
  526.         ASSETS_DIR.mkdir(parents=True, exist_ok=True)
  527.         LIBRARIES_DIR.mkdir(parents=True, exist_ok=True)
  528.         INSTANCES_DIR.mkdir(parents=True, exist_ok=True)
  529.         JAVA_DIR.mkdir(parents=True, exist_ok=True)
  530.         MOD_ICONS_DIR.mkdir(parents=True, exist_ok=True)
  531.  
  532.         self.settings = self.load_settings()
  533.         self.accounts = []
  534.         self.java_versions = self.find_java_versions()
  535.  
  536.         self.download_queue = deque()
  537.         self.current_download_thread = None
  538.         self.progress_dialog = None
  539.  
  540.         self.logged_snapshots_modloader_warning = set()
  541.         self._post_download_data = None
  542.  
  543.     def load_settings(self):
  544.         if SETTINGS_FILE.exists():
  545.             try:
  546.                 with SETTINGS_FILE.open("r") as f:
  547.                     loaded_settings = json.load(f)
  548.                     settings = DEFAULT_SETTINGS.copy()
  549.                     settings.update(loaded_settings)
  550.                     return settings
  551.             except (json.JSONDecodeError, Exception) as e:
  552.                 logging.error(f"Błąd odczytu/parsowania settings.json: {e}. Używam domyślnych.")
  553.         logging.info("settings.json nie znaleziono lub błąd odczytu, używam domyślnych.")
  554.         return DEFAULT_SETTINGS.copy()
  555.  
  556.     def save_settings(self):
  557.         try:
  558.             with SETTINGS_FILE.open("w") as f:
  559.                 json.dump(self.settings, f, indent=4)
  560.             logging.info("Ustawienia zapisane pomyślnie.")
  561.         except Exception as e:
  562.             logging.error(f"Błąd zapisu ustawień: {e}")
  563.  
  564.     def _queue_download(self, url, dest, download_type, sha1=None):
  565.         dest_path = Path(dest)
  566.         temp_validator = DownloadThread(url, dest, download_type, sha1)
  567.         if dest_path.exists() and sha1 and temp_validator.validate_sha1(str(dest_path), sha1):
  568.              logging.info(f"Plik {Path(dest).name} już istnieje i jest poprawny. Pomijanie kolejkowania.")
  569.              return 0
  570.         else:
  571.             dest_path.parent.mkdir(parents=True, exist_ok=True)
  572.             self.download_queue.append((url, str(dest_path), download_type, sha1))
  573.             logging.debug(f"Dodano do kolejki: {Path(dest).name} ({download_type})")
  574.             return 1
  575.  
  576.  
  577.     def process_download_queue(self):
  578.         if self.current_download_thread is None and self.download_queue:
  579.             url, dest, download_type, sha1 = self.download_queue.popleft()
  580.             self.current_download_thread = DownloadThread(url, dest, download_type, sha1)
  581.  
  582.             if self.progress_dialog:
  583.                 try:
  584.                     self.current_download_thread.progress.disconnect()
  585.                     self.current_download_thread.update_status.disconnect()
  586.                     self.current_download_thread.update_speed.disconnect()
  587.                     self.current_download_thread.update_size.disconnect()
  588.                     self.current_download_thread.finished.disconnect()
  589.                 except TypeError:
  590.                     pass
  591.  
  592.                 self.current_download_thread.progress.connect(self.progress_dialog.update_progress)
  593.                 self.current_download_thread.update_status.connect(self.progress_dialog.update_status)
  594.                 self.current_download_thread.update_speed.connect(self.progress_dialog.update_speed)
  595.                 self.current_download_thread.update_size.connect(self.progress_dialog.update_size)
  596.                 self.current_download_thread.finished.connect(self.on_download_thread_finished)
  597.  
  598.  
  599.             logging.debug(f"Starting download thread for: {Path(dest).name}")
  600.             self.current_download_thread.start()
  601.         elif self.current_download_thread is None and not self.download_queue:
  602.              logging.debug("Download queue is empty and no thread active. Download process should be complete.")
  603.              pass
  604.  
  605.  
  606.     def on_download_thread_finished(self, path, success, error_message):
  607.         file_name = os.path.basename(path) if path else "Unknown File"
  608.         logging.debug(f"Download thread finished for {file_name}. Success: {success}, Error: {error_message}")
  609.  
  610.         if self.progress_dialog:
  611.              QTimer.singleShot(0, lambda: self.progress_dialog.increment_downloaded(path, success, error_message))
  612.  
  613.         if self.current_download_thread:
  614.              self.current_download_thread.deleteLater()
  615.         self.current_download_thread = None
  616.  
  617.         self.process_download_queue()
  618.  
  619.  
  620.     def cancel_downloads(self):
  621.         logging.info("Attempting to cancel downloads.")
  622.         if self.current_download_thread:
  623.             self.current_download_thread.cancel()
  624.  
  625.         self.download_queue.clear()
  626.         logging.info("Download queue cleared.")
  627.  
  628.  
  629.     def _download_metadata_sync(self, url, dest, description):
  630.         dest_path = Path(dest)
  631.         logging.info(f"Pobieranie metadanych (synchronicznie): {description} z {url}")
  632.         dest_path.parent.mkdir(parents=True, exist_ok=True)
  633.         try:
  634.             response = requests.get(url, timeout=30)
  635.             response.raise_for_status()
  636.             with open(dest_path, "wb") as f:
  637.                 f.write(response.content)
  638.             logging.info(f"Pobrano metadane: {dest_path.name}")
  639.             return str(dest_path)
  640.         except requests.exceptions.RequestException as e:
  641.             logging.error(f"Błąd pobierania metadanych {description} z {url}: {e}")
  642.             raise ValueError(f"Nie udało się pobrać metadanych {description}: {e}")
  643.         except Exception as e:
  644.              logging.error(f"Nieoczekiwany błąd podczas pobierania metadanych {description}: {e}")
  645.              raise RuntimeError(f"Nieoczekiwany błąd podczas pobierania metadanych {description}: {e}")
  646.  
  647.  
  648.     def _queue_version_files(self, version_id, instance_dir):
  649.         version_dir = Path(instance_dir) / "versions" / version_id
  650.         version_dir.mkdir(parents=True, exist_ok=True)
  651.         queued_count = 0
  652.  
  653.         try:
  654.             try:
  655.                  manifest = self.get_version_manifest()
  656.                  version_info = next((v for v in manifest["versions"] if v["id"] == version_id), None)
  657.                  if not version_info:
  658.                       raise ValueError(f"Wersja {version_id} nie istnieje w manifeście!")
  659.                  version_json_url = version_info["url"]
  660.             except Exception as e:
  661.                  raise ValueError(f"Nie udało się uzyskać URL manifestu wersji dla {version_id}: {e}")
  662.  
  663.             version_json_path = version_dir / f"{version_id}.json"
  664.             self._download_metadata_sync(version_json_url, version_json_path, "version JSON")
  665.  
  666.             try:
  667.                 with version_json_path.open("r", encoding='utf-8') as f:
  668.                     version_data = json.load(f)
  669.             except json.JSONDecodeError as e:
  670.                 logging.error(f"Błąd parsowania {version_json_path}: {e}")
  671.                 raise ValueError(f"Nieprawidłowy plik wersji JSON: {version_json_path}")
  672.  
  673.             client_info = version_data.get("downloads", {}).get("client")
  674.             if client_info:
  675.                 client_url = client_info["url"]
  676.                 client_sha1 = client_info.get("sha1")
  677.                 client_path = version_dir / f"{version_id}.jar"
  678.                 queued_count += self._queue_download(client_url, client_path, "client JAR", client_sha1)
  679.             else:
  680.                 logging.warning(f"Brak danych klienta JAR w JSON dla wersji {version_id}. Kontynuuję bez kolejkowania client.jar")
  681.  
  682.  
  683.             asset_index_info = version_data.get("assetIndex")
  684.             if not asset_index_info:
  685.                  logging.warning(f"Brak danych assetIndex w JSON dla wersji {version_id}. Kontynuuję bez kolejkowania assetów.")
  686.                  asset_data = {}
  687.             else:
  688.                 asset_index_id = asset_index_info["id"]
  689.                 asset_index_url = asset_index_info["url"]
  690.                 asset_index_sha1 = asset_index_info.get("sha1")
  691.                 asset_index_path = Path(ASSETS_DIR) / "indexes" / f"{asset_index_id}.json"
  692.                 self._download_metadata_sync(asset_index_url, asset_index_path, "asset index")
  693.  
  694.                 try:
  695.                     with asset_index_path.open("r", encoding='utf-8') as f:
  696.                         asset_data = json.load(f)
  697.                 except json.JSONDecodeError as e:
  698.                     logging.error(f"Błąd parsowania {asset_index_path}: {e}")
  699.                     raise ValueError(f"Nieprawidłowy plik indexu assetów: {asset_index_path}")
  700.                 except FileNotFoundError:
  701.                      logging.warning(f"Plik indexu assetów nie znaleziono po pobraniu (??): {asset_index_path}")
  702.                      asset_data = {}
  703.  
  704.             for hash_path, info in asset_data.get("objects", {}).items():
  705.                 hash = info["hash"]
  706.                 obj_path = Path(ASSETS_DIR) / "objects" / hash[:2] / hash
  707.                 obj_url = f"https://resources.download.minecraft.net/{hash[:2]}/{hash}"
  708.                 queued_count += self._queue_download(obj_url, obj_path, "asset", hash)
  709.  
  710.             for lib in version_data.get("libraries", []):
  711.                 if self._is_library_applicable(lib):
  712.                     if "downloads" in lib and "artifact" in lib["downloads"]:
  713.                         artifact = lib["downloads"]["artifact"]
  714.                         lib_path = Path(LIBRARIES_DIR) / artifact["path"]
  715.                         queued_count += self._queue_download(artifact["url"], lib_path, "library", artifact.get("sha1"))
  716.  
  717.                     classifiers = lib["downloads"].get("classifiers", {})
  718.                     native_classifier_data = None
  719.  
  720.                     current_os_key = sys.platform
  721.                     if current_os_key == "win32":
  722.                         current_os_key = "windows"
  723.                     elif current_os_key == "darwin":
  724.                         current_os_key = "macos"
  725.                     elif current_os_key.startswith("linux"):
  726.                         current_os_key = "linux"
  727.  
  728.                     arch = '64' if sys.maxsize > 2**32 else '32'
  729.  
  730.                     search_keys = [
  731.                         f"natives-{current_os_key}-{arch}",
  732.                         f"natives-{current_os_key}",
  733.                     ]
  734.  
  735.                     for key in search_keys:
  736.                         if key in classifiers:
  737.                             native_classifier_data = classifiers[key]
  738.                             logging.debug(f"Znaleziono klasyfikator natywny '{key}' dla biblioteki {lib.get('name', 'Nieznana')}")
  739.                             break
  740.  
  741.                     if native_classifier_data:
  742.                         native_url = native_classifier_data["url"]
  743.                         native_sha1 = native_classifier_data.get("sha1")
  744.                         lib_name = lib.get("name", "unknown_lib").replace(":", "_").replace(".", "_")
  745.                         classifier_file_name = Path(native_classifier_data["path"]).name
  746.                         native_zip_path = version_dir / "natives_zips" / f"{lib_name}_{classifier_file_name}"
  747.                         queued_count += self._queue_download(native_url, native_zip_path, "native zip", native_sha1)
  748.                     else:
  749.                         logging.debug(f"Brak natywnego klasyfikatora dla biblioteki {lib.get('name', 'Nieznana')} dla systemu {current_os_key}-{arch}")
  750.  
  751.         except ValueError as e:
  752.             logging.error(f"Błąd podczas kolejkowania plików wersji: {e}")
  753.             raise
  754.         except RuntimeError as e:
  755.              logging.error(f"Błąd krytyczny podczas kolejkowania plików wersji: {e}")
  756.              raise
  757.         except Exception as e:
  758.             logging.error(f"Nieoczekiwany błąd podczas kolejkowania plików wersji: {e}")
  759.             raise
  760.  
  761.         return queued_count
  762.  
  763.     def _extract_natives(self, version_id, instance_dir):
  764.         version_dir = Path(instance_dir) / "versions" / version_id
  765.         natives_zip_dir = version_dir / "natives_zips"
  766.         natives_dir = version_dir / "natives"
  767.        
  768.         # Usuń istniejący katalog natives, jeśli istnieje
  769.         if natives_dir.exists():
  770.             try:
  771.                 shutil.rmtree(natives_dir)
  772.                 logging.debug(f"Usunięto istniejący katalog natives: {natives_dir}")
  773.             except Exception as e:
  774.                 logging.warning(f"Nie udało się usunąć katalogu natives {natives_dir}: {e}")
  775.        
  776.         natives_dir.mkdir(parents=True, exist_ok=True)
  777.         logging.info(f"Rozpakowywanie natywnych bibliotek do: {natives_dir}")
  778.        
  779.         extracted_count = 0
  780.         zip_files = []
  781.        
  782.         if natives_zip_dir.exists():
  783.             zip_files = list(natives_zip_dir.glob("*.zip"))
  784.             if not zip_files:
  785.                 logging.warning(f"Brak plików ZIP natywnych bibliotek w {natives_zip_dir}")
  786.            
  787.             for zip_path in zip_files:
  788.                 try:
  789.                     with zipfile.ZipFile(zip_path, "r") as zip_ref:
  790.                         allowed_extensions = ('.dll', '.so', '.dylib')
  791.                         excluded_names = ('META-INF',)
  792.                        
  793.                         for member_info in zip_ref.infolist():
  794.                             member_path = Path(member_info.filename)
  795.                             is_excluded_path = any(part in member_path.parts for part in excluded_names)
  796.                             if member_path.suffix.lower() in allowed_extensions and not is_excluded_path:
  797.                                 try:
  798.                                     zip_ref.extract(member_info, natives_dir)
  799.                                     extracted_count += 1
  800.                                     logging.debug(f"Rozpakowano: {member_info.filename} z {zip_path.name}")
  801.                                 except Exception as extract_e:
  802.                                     logging.error(f"Błąd rozpakowywania {member_info.filename} z {zip_path.name}: {extract_e}")
  803.                    
  804.                     logging.info(f"Przetworzono plik ZIP: {zip_path.name}")
  805.                
  806.                 except zipfile.BadZipFile:
  807.                     logging.error(f"Uszkodzony plik ZIP: {zip_path}. Pomijam.")
  808.                 except Exception as e:
  809.                     logging.error(f"Błąd przetwarzania {zip_path}: {e}")
  810.        
  811.         # Czyszczenie po rozpakowaniu
  812.         for zip_path in zip_files:
  813.             try:
  814.                 zip_path.unlink(missing_ok=True)
  815.                 logging.debug(f"Usunięto plik ZIP: {zip_path}")
  816.             except Exception as e:
  817.                 logging.warning(f"Nie udało się usunąć pliku ZIP {zip_path}: {e}")
  818.        
  819.         if natives_zip_dir.exists() and not list(natives_zip_dir.iterdir()):
  820.             try:
  821.                 natives_zip_dir.rmdir()
  822.                 logging.debug(f"Usunięto pusty katalog natives_zips: {natives_zip_dir}")
  823.             except Exception as e:
  824.                 logging.warning(f"Nie udało się usunąć katalogu natives_zips {natives_zip_dir}: {e}")
  825.        
  826.         logging.info(f"Zakończono rozpakowywanie natywnych bibliotek. Rozpakowano: {extracted_count} plików")
  827.         if extracted_count == 0:
  828.             logging.warning("Nie rozpakowano żadnych natywnych bibliotek. Sprawdź, czy pliki ZIP były dostępne i poprawne.")
  829.        
  830.         return extracted_count
  831.  
  832.     def validate_modloader(self, modloader, version_id):
  833.         if not version_id: return False
  834.  
  835.         is_snapshot = re.match(r"^\d+w\d+[a-z]$", version_id)
  836.         if is_snapshot:
  837.              if version_id not in self.logged_snapshots_modloader_warning:
  838.                  logging.warning(f"Wersja {version_id} to snapshot, wsparcie modloaderów jest ograniczone lub nieistniejące.")
  839.                  self.logged_snapshots_modloader_warning.add(version_id)
  840.              return False
  841.  
  842.         try:
  843.             parts = version_id.split('.')
  844.             major = int(parts[0])
  845.             minor = int(parts[1]) if len(parts) > 1 else 0
  846.             patch = int(parts[2]) if len(parts) > 2 else 0
  847.             version_tuple = (major, minor, patch)
  848.             version_tuple += (0,) * (3 - len(version_tuple))
  849.  
  850.         except ValueError:
  851.             logging.error(f"Nie można sparsować wersji Minecrafta '{version_id}' dla walidacji modloadera. Zakładam brak wsparcia.")
  852.             return False
  853.  
  854.         if modloader == "forge":
  855.             if version_tuple < (1, 5, 2): return False
  856.  
  857.         elif modloader == "neoforge":
  858.             if version_tuple < (1, 20, 1): return False
  859.  
  860.         elif modloader == "fabric":
  861.             if version_tuple < (1, 14, 0): return False
  862.  
  863.         elif modloader == "quilt":
  864.             if version_tuple < (1, 14, 0): return False
  865.  
  866.         return True
  867.  
  868.     def _queue_modloader_installer(self, modloader, version_id, instance_dir):
  869.         if not self.validate_modloader(modloader, version_id):
  870.             raise ValueError(f"{modloader.capitalize()} prawdopodobnie nie wspiera wersji {version_id}!")
  871.  
  872.         modloader_dir = Path(instance_dir) / "modloaders"
  873.         modloader_dir.mkdir(parents=True, exist_ok=True)
  874.         queued_count = 0
  875.         url = None
  876.         installer_name = None
  877.  
  878.         if modloader in ["forge", "neoforge"]:
  879.              logging.warning(f"Automatyczne pobieranie instalatorów {modloader.capitalize()} nie jest wspierane w tym uproszczonym launcherze. Umieść plik instalatora JAR ręcznie w katalogu: {modloader_dir}")
  880.              pass
  881.  
  882.         elif modloader == "fabric":
  883.             installer_name = "fabric-installer.jar"
  884.             url = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/0.11.2/fabric-installer-0.11.2.jar"
  885.             installer_path = modloader_dir / installer_name
  886.             queued_count += self._queue_download(url, installer_path, "fabric installer")
  887.  
  888.         elif modloader == "quilt":
  889.             installer_name = "quilt-installer.jar"
  890.             url = "https://maven.quiltmc.org/repository/release/org/quiltmc/quilt-installer/0.10.0/quilt-installer-0.10.0.jar"
  891.             installer_path = modloader_dir / installer_name
  892.             queued_count += self._queue_download(url, installer_path, "quilt installer")
  893.  
  894.         else:
  895.             raise ValueError(f"Nieznany modloader: {modloader}")
  896.  
  897.         return queued_count
  898.  
  899.  
  900.     def _run_modloader_installer(self, modloader, version_id, instance_dir):
  901.         modloader_dir = Path(instance_dir) / "modloaders"
  902.         installer_path = None
  903.         installer_args = []
  904.         success_message = ""
  905.         error_message = ""
  906.  
  907.         java_path = self.find_java_for_version(version_id)
  908.         if not java_path or not Path(java_path).exists():
  909.              raise ValueError(f"Nie znaleziono kompatybilnej Javy dla wersji {version_id} lub ścieżka Javy jest niepoprawna. Zainstaluj Javę i/lub sprawdź ustawienia.")
  910.  
  911.         if modloader == "forge":
  912.             forge_installers = list(modloader_dir.glob("forge-*-installer.jar"))
  913.             if not forge_installers:
  914.                  raise FileNotFoundError(f"Nie znaleziono instalatora Forge (.jar) w katalogu instancji: {modloader_dir}. Pobierz go ręcznie i umieść w tym katalogu.")
  915.             if len(forge_installers) > 1:
  916.                  logging.warning(f"Znaleziono wiele plików instalatora Forge w {modloader_dir}. Używam pierwszego: {forge_installers[0].name}")
  917.             installer_path = forge_installers[0]
  918.             installer_args = ["--installClient", str(instance_dir)]
  919.             success_message = f"Zainstalowano Forge dla wersji {version_id}"
  920.             error_message = "Błąd instalacji Forge."
  921.  
  922.         elif modloader == "neoforge":
  923.              neoforge_installers = list(modloader_dir.glob("neoforge-*-installer.jar"))
  924.              if not neoforge_installers:
  925.                  raise FileNotFoundError(f"Nie znaleziono instalatora NeoForge (.jar) w katalogu instancji: {modloader_dir}. Pobierz go ręcznie i umieść w tym katalogu.")
  926.              if len(neoforge_installers) > 1:
  927.                   logging.warning(f"Znaleziono wiele plików instalatora NeoForge w {modloader_dir}. Używam pierwszego: {neoforge_installers[0].name}")
  928.              installer_path = neoforge_installers[0]
  929.              installer_args = ["--installClient", str(instance_dir)]
  930.              success_message = f"Zainstalowano NeoForge dla wersji {version_id}"
  931.              error_message = "Błąd instalacji NeoForge."
  932.  
  933.         elif modloader == "fabric":
  934.             installer_path = modloader_dir / "fabric-installer.jar"
  935.             installer_args = ["client", "-mcversion", version_id, "-dir", str(instance_dir)]
  936.             success_message = f"Zainstalowano Fabric dla wersji {version_id}"
  937.             error_message = "Błąd instalacji Fabric."
  938.  
  939.         elif modloader == "quilt":
  940.             installer_path = modloader_dir / "quilt-installer.jar"
  941.             installer_args = ["install", "client", version_id, "--install-dir", str(instance_dir)]
  942.             success_message = f"Zainstalowano Quilt dla wersji {version_id}"
  943.             error_message = "Błąd instalacji Quilt."
  944.  
  945.         else:
  946.             logging.error(f"Próba uruchomienia instalatora dla nieznanego modloadera: {modloader}")
  947.             return
  948.  
  949.         if installer_path is None or not installer_path.exists():
  950.              if modloader in ["fabric", "quilt"]:
  951.                   raise FileNotFoundError(f"Instalator {modloader.capitalize()} (.jar) nie znaleziono w katalogu: {installer_path}. Pobieranie mogło się nie udać.")
  952.              else:
  953.                   raise FileNotFoundError(f"Instalator modloadera nie znaleziono: {installer_path}")
  954.  
  955.         cmd = [java_path, "-jar", str(installer_path)] + installer_args
  956.         logging.info(f"Uruchamianie instalatora modloadera: {' '.join([str(c) for c in cmd])}")
  957.  
  958.         try:
  959.             result = subprocess.run(cmd, cwd=str(modloader_dir), capture_output=True, text=True, timeout=300)
  960.             logging.info(f"Instalator stdout:\n{result.stdout}")
  961.             logging.info(f"Instalator stderr:\n{result.stderr}")
  962.  
  963.             if result.returncode != 0:
  964.                  detailed_error = f"{error_message} Proces zakończył się kodem {result.returncode}.\nStderr:\n{result.stderr}"
  965.                  raise subprocess.CalledProcessError(result.returncode, cmd, output=result.stdout, stderr=result.stderr)
  966.  
  967.             logging.info(success_message)
  968.  
  969.             if modloader in ["fabric", "quilt"]:
  970.                  try:
  971.                       installer_path.unlink()
  972.                       logging.debug(f"Usunięto instalator: {installer_path}")
  973.                  except Exception as e:
  974.                       logging.warning(f"Nie udało się usunąć instalatora {installer_path}: {e}")
  975.  
  976.         except FileNotFoundError:
  977.             logging.error(f"Plik wykonywalny instalatora lub Javy nie istnieje: {installer_path} lub {java_path}")
  978.             raise FileNotFoundError(f"Plik wykonywalny instalatora modloadera lub Javy nie istnieje. Sprawdź ścieżki.")
  979.         except subprocess.TimeoutExpired:
  980.             logging.error(f"Instalator modloadera przekroczył czas oczekiwania (Timeout).")
  981.             raise TimeoutError(f"Instalator modloadera przekroczył czas oczekiwania. Spróbuj ponownie lub zwiększ limit czasu.")
  982.         except subprocess.CalledProcessError as e:
  983.             logging.error(f"Instalator modloadera zakończył się błędem:\n{e.stderr}\n{e}")
  984.             raise ValueError(f"Instalator modloadera zakończył się błędem (Kod: {e.returncode}). Sprawdź logi lub dane wyjściowe instalatora.")
  985.         except Exception as e:
  986.             logging.error(f"Nieoczekiwany błąd podczas uruchamiania instalatora modloadera: {e}")
  987.             raise RuntimeError(f"Nieoczekiwany błąd podczas uruchamiania instalatora modloadera: {e}")
  988.  
  989.  
  990.     def get_version_manifest(self):
  991.         url = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"
  992.         manifest_path = CONFIG_DIR / "version_manifest_v2.json"
  993.         if manifest_path.exists():
  994.              try:
  995.                  with manifest_path.open("r", encoding='utf-8') as f:
  996.                      manifest_data = json.load(f)
  997.                  if datetime.fromtimestamp(manifest_path.stat().st_mtime) > datetime.now() - timedelta(hours=1):
  998.                       logging.info("Używam cache version_manifest_v2.json")
  999.                       return manifest_data
  1000.              except Exception as e:
  1001.                  logging.warning(f"Błąd odczytu cache manifestu wersji: {e}. Pobieram nowy.")
  1002.  
  1003.         try:
  1004.             logging.info(f"Pobieranie manifestu wersji z: {url}")
  1005.             response = requests.get(url, timeout=15)
  1006.             response.raise_for_status()
  1007.             manifest_data = response.json()
  1008.             try:
  1009.                  with manifest_path.open("w", encoding='utf-8') as f:
  1010.                      json.dump(manifest_data, f, indent=4)
  1011.                  logging.info("Zapisano version_manifest_v2.json do cache.")
  1012.             except Exception as e:
  1013.                  logging.warning(f"Błąd zapisu cache manifestu wersji: {e}")
  1014.  
  1015.             return manifest_data
  1016.         except requests.exceptions.RequestException as e:
  1017.             logging.error(f"Błąd pobierania manifestu wersji: {e}")
  1018.             if manifest_path.exists():
  1019.                  try:
  1020.                      with manifest_path.open("r", encoding='utf-8') as f:
  1021.                           logging.warning("Pobieranie nieudane, używam starego cache manifestu wersji.")
  1022.                           return json.load(f)
  1023.                  except Exception as e_cache:
  1024.                      logging.error(f"Błąd odczytu starego cache manifestu wersji: {e_cache}")
  1025.                      raise ConnectionError("Brak połączenia z internetem i brak dostępnego manifestu wersji!")
  1026.             else:
  1027.                 raise ConnectionError("Brak połączenia z internetem i brak dostępnego manifestu wersji!")
  1028.  
  1029.  
  1030.     def get_curseforge_mods(self, search_query, version_id):
  1031.         headers = {"x-api-key": CURSEFORGE_API_KEY}
  1032.         params = {"gameId": 432, "searchFilter": search_query, "minecraftVersion": version_id, "classId": 6, "sortField": 2}
  1033.         url = "https://api.curseforge.com/v1/mods/search"
  1034.         logging.info(f"Wyszukiwanie modów: '{search_query}' dla wersji {version_id}")
  1035.         try:
  1036.             response = requests.get(url, headers=headers, params=params, timeout=15)
  1037.             response.raise_for_status()
  1038.             data = response.json().get("data", [])
  1039.             logging.info(f"Znaleziono {len(data)} modów dla '{search_query}'")
  1040.             return data
  1041.         except requests.exceptions.RequestException as e:
  1042.             logging.error(f"Błąd wyszukiwania modów z CurseForge: {e}")
  1043.             if hasattr(e, 'response') and e.response is not None:
  1044.                 logging.error(f"CurseForge API Response status: {e.response.status_code}, body: {e.response.text}")
  1045.                 if e.response.status_code == 403:
  1046.                     raise PermissionError("Błąd API CurseForge: Klucz API jest nieprawidłowy lub brak dostępu.")
  1047.                 if e.response.status_code == 429:
  1048.                     raise requests.exceptions.RequestException("Błąd API CurseForge: Limit żądań przekroczony. Spróbuj ponownie później.")
  1049.  
  1050.             raise requests.exceptions.RequestException(f"Nie udało się wyszukać modów: {e}")
  1051.  
  1052.  
  1053.     def _queue_curseforge_mod_files(self, mod_id, version_id, instance_dir, download_dependencies=False, visited_mods=None):
  1054.         if visited_mods is None:
  1055.             visited_mods = set()
  1056.  
  1057.         if mod_id in visited_mods:
  1058.              logging.debug(f"Mod ID {mod_id} już przetworzony, pomijam.")
  1059.              return 0
  1060.  
  1061.         visited_mods.add(mod_id)
  1062.         logging.debug(f"Processing mod ID {mod_id} for version {version_id}")
  1063.  
  1064.         headers = {"x-api-key": CURSEFORGE_API_KEY}
  1065.         total_queued = 0
  1066.         try:
  1067.             files_url = f"https://api.curseforge.com/v1/mods/{mod_id}/files"
  1068.             response = requests.get(files_url, headers=headers, timeout=15)
  1069.             response.raise_for_status()
  1070.             files = response.json().get("data", [])
  1071.  
  1072.             compatible_file = None
  1073.             files.sort(key=lambda x: x.get('fileDate', '1970-01-01T00:00:00Z'), reverse=True)
  1074.  
  1075.             for file in files:
  1076.                 if version_id in file.get("gameVersions", []):
  1077.                     compatible_file = file
  1078.                     break
  1079.  
  1080.             if not compatible_file:
  1081.                 try:
  1082.                      mod_info_resp = requests.get(f"https://api.curseforge.com/v1/mods/{mod_id}", headers=headers, timeout=5)
  1083.                      mod_name = mod_info_resp.json().get("data", {}).get("name", f"ID {mod_id}") if mod_info_resp.status_code == 200 else f"ID {mod_id}"
  1084.                 except Exception:
  1085.                      mod_name = f"ID {mod_id}"
  1086.  
  1087.                 logging.warning(f"Brak kompatybilnego pliku moda dla {mod_name} (ID: {mod_id}) i wersji {version_id}.")
  1088.                 return 0
  1089.  
  1090.             mod_url = compatible_file.get("downloadUrl")
  1091.             mod_name = compatible_file.get("fileName")
  1092.             if not mod_url:
  1093.                  logging.warning(f"Download URL is null for mod file {mod_name} (Mod ID: {mod_id}). Skipping.")
  1094.                  return 0
  1095.  
  1096.             mod_path = Path(instance_dir) / "mods" / mod_name
  1097.             mod_path.parent.mkdir(parents=True, exist_ok=True)
  1098.  
  1099.             total_queued += self._queue_download(mod_url, mod_path, "mod")
  1100.  
  1101.             if download_dependencies:
  1102.                 logging.debug(f"Checking dependencies for mod ID {mod_id}")
  1103.                 for dep in compatible_file.get("dependencies", []):
  1104.                     if dep.get("relationType") == 3:
  1105.                         dep_mod_id = dep.get("modId")
  1106.                         if dep_mod_id:
  1107.                              logging.debug(f"Queueing required dependency ID {dep_mod_id} for mod ID {mod_id}")
  1108.                              total_queued += self._queue_curseforge_mod_files(dep_mod_id, version_id, instance_dir, download_dependencies=True, visited_mods=visited_mods)
  1109.                         else:
  1110.                             logging.warning(f"Dependency found with null modId for mod ID {mod_id}. Skipping.")
  1111.  
  1112.         except requests.exceptions.RequestException as e:
  1113.             logging.error(f"Błąd pobierania plików moda dla mod ID {mod_id}: {e}")
  1114.             return 0
  1115.         except Exception as e:
  1116.             logging.error(f"Nieoczekiwany błąd podczas kolejkowania plików moda dla mod ID {mod_id}: {e}")
  1117.             return 0
  1118.  
  1119.         return total_queued
  1120.  
  1121.  
  1122.     def remove_mod(self, mod_file_name, instance_dir):
  1123.         mod_path = Path(instance_dir) / "mods" / mod_file_name
  1124.         if mod_path.exists():
  1125.             try:
  1126.                 mod_path.unlink()
  1127.                 logging.info(f"Usunięto mod: {mod_path}")
  1128.             except Exception as e:
  1129.                 logging.error(f"Błąd usuwania moda {mod_path}: {e}")
  1130.                 raise IOError(f"Nie udało się usunąć moda: {e}")
  1131.         else:
  1132.             logging.warning(f"Mod {mod_file_name} nie istnieje w {instance_dir}/mods (pomijam usuwanie)")
  1133.             raise FileNotFoundError(f"Mod {mod_file_name} nie znaleziono w katalogu {instance_dir}/mods!")
  1134.  
  1135.  
  1136.     def launch_game(self, instance_dir_path, username):
  1137.         import uuid
  1138.        
  1139.         instance_dir = Path(instance_dir_path)
  1140.         if not instance_dir.exists():
  1141.             raise FileNotFoundError(f"Katalog instancji nie istnieje: {instance_dir_path}")
  1142.  
  1143.         instance_settings_path = instance_dir / "settings.json"
  1144.         if not instance_settings_path.exists():
  1145.             raise FileNotFoundError(f"Plik ustawień instancji nie znaleziono: {instance_settings_path}")
  1146.        
  1147.         try:
  1148.             with instance_settings_path.open("r", encoding='utf-8') as f:
  1149.                 instance_settings = json.load(f)
  1150.         except json.JSONDecodeError as e:
  1151.             logging.error(f"Błąd odczytu settings.json instancji {instance_dir}: {e}")
  1152.             raise ValueError(f"Nie udało się odczytać ustawień instancji: {e}")
  1153.  
  1154.         version_id = instance_settings.get("version")
  1155.         if not version_id:
  1156.             raise ValueError("Instancja nie ma przypisanej wersji gry. Uruchom instalację instancji ponownie.")
  1157.  
  1158.         version_dir = instance_dir / "versions" / version_id
  1159.         version_json_path = version_dir / f"{version_id}.json"
  1160.         if not version_json_path.exists():
  1161.             raise FileNotFoundError(f"Plik wersji gry {version_json_path} nie istnieje. Uruchom instalację instancji ponownie.")
  1162.  
  1163.         try:
  1164.             with version_json_path.open("r", encoding='utf-8') as f:
  1165.                 version_data = json.load(f)
  1166.         except json.JSONDecodeError as e:
  1167.             logging.error(f"Błąd odczytu JSON wersji {version_id}: {e}")
  1168.             raise ValueError(f"Nie udało się odczytać danych wersji: {e}")
  1169.  
  1170.         # --- Budowanie polecenia startowego ---
  1171.  
  1172.         # 1. Java path
  1173.         java_path = self.find_java_for_version(version_id)
  1174.         if not java_path or not Path(java_path).exists():
  1175.             raise FileNotFoundError(f"Nie znaleziono kompatybilnej Javy dla wersji {version_id} lub ścieżka Javy jest niepoprawna.")
  1176.  
  1177.         # 2. Classpath
  1178.         classpath = []
  1179.         modloader = instance_settings.get("modloader")
  1180.         launch_version_id = version_id
  1181.  
  1182.         if modloader:
  1183.             modded_version_jsons = list((instance_dir / "versions").glob(f"{version_id}-*.json"))
  1184.             if modded_version_jsons:
  1185.                 modded_version_jsons.sort(key=lambda p: p.stat().st_mtime, reverse=True)
  1186.                 found_modded_json_path = modded_version_jsons[0]
  1187.                 launch_version_id = found_modded_json_path.stem
  1188.                 logging.info(f"Znaleziono modowany JSON wersji: {found_modded_json_path.name} (ID: {launch_version_id})")
  1189.                 try:
  1190.                     with found_modded_json_path.open("r", encoding='utf-8') as f:
  1191.                         version_data = json.load(f)
  1192.                 except json.JSONDecodeError as e:
  1193.                     logging.error(f"Błąd odczytu modowanego JSON {found_modded_json_path}: {e}")
  1194.                     raise ValueError(f"Nie udało się odczytać danych modowanej wersji: {e}")
  1195.  
  1196.         main_jar_path = instance_dir / "versions" / launch_version_id / f"{launch_version_id}.jar"
  1197.         if main_jar_path.exists():
  1198.             classpath.append(str(main_jar_path))
  1199.         else:
  1200.             client_jar_candidate = list(version_dir.glob("*.jar"))
  1201.             if client_jar_candidate:
  1202.                 main_jar_path = client_jar_candidate[0]
  1203.                 classpath.append(str(main_jar_path))
  1204.                 logging.warning(f"Główny JAR {launch_version_id}.jar nie istnieje, używam: {main_jar_path.name}")
  1205.             else:
  1206.                 raise FileNotFoundError(f"Plik główny gry (.jar) nie istnieje: {main_jar_path}.")
  1207.  
  1208.         for lib in version_data.get("libraries", []):
  1209.             if self._is_library_applicable(lib):
  1210.                 if "downloads" in lib and "artifact" in lib["downloads"]:
  1211.                     artifact = lib["downloads"]["artifact"]
  1212.                     lib_path = Path(LIBRARIES_DIR) / artifact["path"]
  1213.                     if lib_path.exists():
  1214.                         classpath.append(str(lib_path))
  1215.                     else:
  1216.                         logging.warning(f"Brak pliku biblioteki: {lib_path}")
  1217.                 elif "name" in lib:
  1218.                     parts = lib["name"].split(':')
  1219.                     if len(parts) >= 3:
  1220.                         group = parts[0].replace('.', '/')
  1221.                         artifact = parts[1]
  1222.                         version = parts[2]
  1223.                         guessed_path = Path(LIBRARIES_DIR) / group / artifact / version / f"{artifact}-{version}.jar"
  1224.                         if guessed_path.exists():
  1225.                             classpath.append(str(guessed_path))
  1226.                         else:
  1227.                             logging.warning(f"Brak zgadywanej biblioteki: {guessed_path}")
  1228.  
  1229.         classpath_str = ";".join(classpath) if sys.platform == "win32" else ":".join(classpath)
  1230.  
  1231.         # 3. JVM arguments
  1232.         jvm_args = []
  1233.         ram = instance_settings.get("ram", self.settings.get("ram", "4G"))
  1234.         jvm_args.extend([f"-Xmx{ram}", "-Xms512M"])
  1235.        
  1236.         natives_dir = version_dir / "natives"
  1237.         if natives_dir.exists():
  1238.             jvm_args.append(f"-Djava.library.path={natives_dir}")
  1239.         else:
  1240.             logging.warning(f"Katalog natywnych bibliotek nie istnieje: {natives_dir}")
  1241.  
  1242.         jvm_args_extra = instance_settings.get("jvm_args_extra", self.settings.get("jvm_args", ""))
  1243.         if jvm_args_extra:
  1244.             jvm_args.extend(jvm_args_extra.split())
  1245.  
  1246.         # 4. Game arguments
  1247.         game_args = []
  1248.         main_class = version_data.get("mainClass", "net.minecraft.client.main.Main")
  1249.        
  1250.         # Generowanie UUID dla trybu offline
  1251.         offline_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, username))
  1252.        
  1253.         # Wersja Minecrafta jako krotka (major, minor, patch)
  1254.         try:
  1255.             parts = version_id.split('.')
  1256.             major = int(parts[0])
  1257.             minor = int(parts[1]) if len(parts) > 1 else 0
  1258.             patch = int(parts[2]) if len(parts) > 2 else 0
  1259.             version_tuple = (major, minor, patch)
  1260.         except ValueError:
  1261.             version_tuple = (1, 0, 0)
  1262.             logging.warning(f"Nie można sparsować wersji {version_id}, zakładam 1.0.0")
  1263.  
  1264.         # Argumenty w zależności od wersji
  1265.         if version_tuple >= (1, 6, 0):
  1266.             game_args.extend([
  1267.                 "--username", username,
  1268.                 "--version", launch_version_id,
  1269.                 "--gameDir", str(instance_dir),
  1270.                 "--assetsDir", str(ASSETS_DIR),
  1271.                 "--assetIndex", version_data.get("assetIndex", {}).get("id", "legacy"),
  1272.                 "--uuid", offline_uuid,
  1273.                 "--accessToken", "0",
  1274.                 "--userType", "legacy"
  1275.             ])
  1276.         else:
  1277.             game_args.extend([
  1278.                 username,
  1279.                 "0"  # sessionId dla starszych wersji
  1280.             ])
  1281.  
  1282.         # Rozdzielczość i pełny ekran
  1283.         resolution = instance_settings.get("resolution", self.settings.get("resolution", "1280x720"))
  1284.         if resolution and re.match(r"^\d+x\d+$", resolution):
  1285.             width, height = resolution.split('x')
  1286.             game_args.extend(["--width", width, "--height", height])
  1287.        
  1288.         if instance_settings.get("fullscreen", self.settings.get("fullscreen", False)):
  1289.             game_args.append("--fullscreen")
  1290.  
  1291.         # 5. Budowanie pełnego polecenia
  1292.         cmd = [java_path] + jvm_args + ["-cp", classpath_str, main_class] + game_args
  1293.         logging.info(f"Uruchamianie gry: {' '.join([str(c) for c in cmd])}")
  1294.  
  1295.         try:
  1296.             process = subprocess.Popen(
  1297.                 cmd,
  1298.                 cwd=str(instance_dir),
  1299.                 stdout=subprocess.PIPE,
  1300.                 stderr=subprocess.PIPE,
  1301.                 text=True
  1302.             )
  1303.             stdout, stderr = process.communicate(timeout=300)
  1304.             logging.info(f"Gra stdout:\n{stdout}")
  1305.             if stderr:
  1306.                 logging.error(f"Gra stderr:\n{stderr}")
  1307.             if process.returncode != 0:
  1308.                 raise subprocess.CalledProcessError(process.returncode, cmd, stdout, stderr)
  1309.            
  1310.             logging.info(f"Gra uruchomiona pomyślnie (PID: {process.pid})")
  1311.        
  1312.         except subprocess.TimeoutExpired:
  1313.             logging.error("Uruchamianie gry przekroczyło limit czasu.")
  1314.             raise TimeoutError("Uruchamianie gry przekroczyło limit czasu.")
  1315.         except subprocess.CalledProcessError as e:
  1316.             logging.error(f"Błąd uruchamiania gry: Kod {e.returncode}, stderr: {e.stderr}")
  1317.             raise ValueError(f"Błąd uruchamiania gry: {e.stderr}")
  1318.         except Exception as e:
  1319.             logging.error(f"Nieoczekiwany błąd uruchamiania gry: {e}")
  1320.             raise RuntimeError(f"Nieoczekiwany błąd: {e}")
  1321.  
  1322.  
  1323.     def _is_library_applicable(self, library_data):
  1324.         rules = library_data.get('rules')
  1325.         if not rules:
  1326.             return True
  1327.  
  1328.         current_os_name = sys.platform
  1329.         if current_os_name == "win32":
  1330.             current_os_name = "windows"
  1331.         elif current_os_name == "darwin":
  1332.             current_os_name = "osx"
  1333.  
  1334.         for rule in rules:
  1335.             action = rule.get('action')
  1336.             os_info = rule.get('os', {})
  1337.             rule_os_name = os_info.get('name')
  1338.  
  1339.             rule_applies_to_current_os = False
  1340.             if rule_os_name is None:
  1341.                 rule_applies_to_current_os = True
  1342.             elif rule_os_name == current_os_name:
  1343.                  rule_applies_to_current_os = True
  1344.  
  1345.             if rule_applies_to_current_os:
  1346.                 if action == 'disallow':
  1347.                     logging.debug(f"Library rule disallowed: {library_data.get('name', 'Unknown')}")
  1348.                     return False
  1349.  
  1350.         return True
  1351.  
  1352.     def _is_argument_applicable(self, arg_data):
  1353.         rules = arg_data.get('rules')
  1354.         if not rules:
  1355.             return True
  1356.  
  1357.         current_os_name = sys.platform
  1358.         if current_os_name == "win32":
  1359.             current_os_name = "windows"
  1360.         elif current_os_name == "darwin":
  1361.             current_os_name = "osx"
  1362.  
  1363.         disallows_rule_applies = False
  1364.  
  1365.         for rule in rules:
  1366.             action = rule.get('action')
  1367.             os_info = rule.get('os', {})
  1368.             rule_os_name = os_info.get('name')
  1369.  
  1370.             rule_applies_to_current_os = False
  1371.             if rule_os_name is None:
  1372.                 rule_applies_to_current_os = True
  1373.             elif rule_os_name == current_os_name:
  1374.                  rule_applies_to_current_os = True
  1375.  
  1376.             if rule_applies_to_current_os:
  1377.                  if action == 'disallow':
  1378.                       disallows_rule_applies = True
  1379.                       break
  1380.  
  1381.         if disallows_rule_applies:
  1382.             return False
  1383.  
  1384.         return True
  1385.  
  1386.     def find_java(self):
  1387.         return self.java_versions[0][0] if self.java_versions else None
  1388.  
  1389.     def find_java_versions(self):
  1390.         java_versions = []
  1391.         checked_paths = set()
  1392.  
  1393.         try:
  1394.             java_path = "java"
  1395.             find_cmd = ["where", "java"] if sys.platform == "win32" else ["which", "java"]
  1396.             process = subprocess.run(find_cmd, capture_output=True, text=True, timeout=5, check=True)
  1397.             path_output = process.stdout.strip().splitlines()
  1398.             if path_output:
  1399.                  resolved_path = path_output[0]
  1400.                  if Path(resolved_path).is_file():
  1401.                       java_path = resolved_path
  1402.  
  1403.             result = subprocess.run([java_path, "-version"], capture_output=True, text=True, timeout=5, check=True)
  1404.             version_line = result.stderr.splitlines()[0] if result.stderr else ""
  1405.             if java_path not in checked_paths:
  1406.                  java_versions.append((java_path, f"System Java ({version_line.strip()})"))
  1407.                  checked_paths.add(java_path)
  1408.         except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError, Exception) as e:
  1409.             logging.debug(f"System 'java' not found or error: {e}")
  1410.  
  1411.         if sys.platform == "win32":
  1412.             program_files = Path(os.environ.get("ProgramFiles", "C:/Program Files"))
  1413.             program_files_x86 = Path(os.environ.get("ProgramFiles(x86)", "C:/Program Files (x86)"))
  1414.             java_install_dirs = [
  1415.                 program_files / "Java",
  1416.                 program_files_x86 / "Java",
  1417.                 JAVA_DIR
  1418.             ]
  1419.  
  1420.             for base_dir in java_install_dirs:
  1421.                  if not base_dir.exists():
  1422.                       continue
  1423.                  scan_dirs = [base_dir]
  1424.                  try:
  1425.                       for level1 in base_dir.iterdir():
  1426.                            if level1.is_dir():
  1427.                                 scan_dirs.append(level1)
  1428.                                 try:
  1429.                                      for level2 in level1.iterdir():
  1430.                                           if level2.is_dir():
  1431.                                                scan_dirs.append(level2)
  1432.                                 except Exception as e:
  1433.                                      logging.debug(f"Error scanning subdir {level1}: {e}")
  1434.                  except Exception as e:
  1435.                       logging.debug(f"Error scanning base dir {base_dir}: {e}")
  1436.  
  1437.  
  1438.                  for java_dir in scan_dirs:
  1439.                      if java_dir.is_dir():
  1440.                           java_exe = java_dir / "bin" / "java.exe"
  1441.                           if java_exe.exists() and str(java_exe) not in checked_paths:
  1442.                                try:
  1443.                                    result = subprocess.run([str(java_exe), "-version"], capture_output=True, text=True, timeout=5, check=True)
  1444.                                    version_line = result.stderr.splitlines()[0] if result.stderr else ""
  1445.                                    display_name = java_dir.relative_to(base_dir) if java_dir.is_relative_to(base_dir) else java_dir.name
  1446.                                    java_versions.append((str(java_exe), f"{display_name} ({version_line.strip()})"))
  1447.                                    checked_paths.add(str(java_exe))
  1448.                                except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError, Exception) as e:
  1449.                                    logging.debug(f"Error getting version for {java_exe}: {e}")
  1450.  
  1451.         elif sys.platform == "darwin":
  1452.              java_install_dirs = [
  1453.                   Path("/Library/Java/JavaVirtualMachines"),
  1454.                   Path("/usr/local/Cellar"),
  1455.                   Path.home() / ".sdkman" / "candidates" / "java",
  1456.                   JAVA_DIR
  1457.              ]
  1458.              for base_dir in java_install_dirs:
  1459.                  if not base_dir.exists():
  1460.                       continue
  1461.                  try:
  1462.                       for java_dir in base_dir.iterdir():
  1463.                            if java_dir.is_dir():
  1464.                                 java_exe = java_dir / "Contents" / "Home" / "bin" / "java"
  1465.                                 if java_exe.exists() and str(java_exe) not in checked_paths:
  1466.                                      try:
  1467.                                          result = subprocess.run([str(java_exe), "-version"], capture_output=True, text=True, timeout=5, check=True)
  1468.                                          version_line = result.stderr.splitlines()[0] if result.stderr else ""
  1469.                                          display_name = java_dir.name
  1470.                                          java_versions.append((str(java_exe), f"{display_name} ({version_line.strip()})"))
  1471.                                          checked_paths.add(str(java_exe))
  1472.                                      except Exception as e:
  1473.                                           logging.debug(f"Error getting version for {java_exe}: {e}")
  1474.                  except Exception as e:
  1475.                       logging.debug(f"Error scanning base dir {base_dir}: {e}")
  1476.  
  1477.         elif sys.platform.startswith("linux"):
  1478.              java_install_dirs = [
  1479.                  Path("/usr/lib/jvm"),
  1480.                  Path("/opt/java"),
  1481.                  Path.home() / ".sdkman" / "candidates" / "java",
  1482.                  JAVA_DIR
  1483.              ]
  1484.              for base_dir in java_install_dirs:
  1485.                  if not base_dir.exists():
  1486.                       continue
  1487.                  try:
  1488.                       for java_dir in base_dir.iterdir():
  1489.                            if java_dir.is_dir():
  1490.                                 java_exe = java_dir / "bin" / "java"
  1491.                                 if java_exe.exists() and str(java_exe) not in checked_paths:
  1492.                                      try:
  1493.                                          result = subprocess.run([str(java_exe), "-version"], capture_output=True, text=True, timeout=5, check=True)
  1494.                                          version_line = result.stderr.splitlines()[0] if result.stderr else ""
  1495.                                          display_name = java_dir.name
  1496.                                          java_versions.append((str(java_exe), f"{display_name} ({version_line.strip()})"))
  1497.                                          checked_paths.add(str(java_exe))
  1498.                                      except Exception as e:
  1499.                                           logging.debug(f"Error getting version for {java_exe}: {e}")
  1500.                  except Exception as e:
  1501.                       logging.debug(f"Error scanning base dir {base_dir}: {e}")
  1502.  
  1503.  
  1504.         logging.info(f"Znaleziono wersje Javy: {java_versions}")
  1505.         return java_versions
  1506.  
  1507.     def get_java_version_from_path(self, java_path):
  1508.          if not java_path or not Path(java_path).exists():
  1509.              return None
  1510.          try:
  1511.              result = subprocess.run([java_path, "-version"], capture_output=True, text=True, timeout=5, check=True)
  1512.              version_str = result.stderr.splitlines()[0] if result.stderr else ""
  1513.              match = re.search(r"(?:openjdk|java) version \"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version_str)
  1514.              if match:
  1515.                  major = int(match.group(1))
  1516.                  if major == 1 and match.group(2) is not None:
  1517.                       return int(match.group(2))
  1518.                  return major
  1519.              match = re.search(r"openjdk (\d+)(?:\.(\d+))?", version_str)
  1520.              if match:
  1521.                  return int(match.group(1))
  1522.              logging.warning(f"Nie można sparsować wersji Javy z: {version_str} dla {java_path}")
  1523.              return None
  1524.          except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError, Exception) as e:
  1525.              logging.error(f"Błąd podczas odczytu wersji Javy z {java_path}: {e}")
  1526.              return None
  1527.  
  1528.     def get_required_java_version(self, version_id):
  1529.         logging.debug(f"Sprawdzam wymaganą wersję Javy dla {version_id}")
  1530.  
  1531.         try:
  1532.              manifest = self.get_version_manifest()
  1533.              version_info_from_manifest = next((v for v in manifest.get("versions", []) if v["id"] == version_id), None)
  1534.              if version_info_from_manifest:
  1535.                   version_json_url = version_info_from_manifest.get("url")
  1536.                   if version_json_url:
  1537.                        response = requests.get(version_json_url, timeout=10)
  1538.                        response.raise_for_status()
  1539.                        version_data = response.json()
  1540.                        required_java_from_json = version_data.get("javaVersion", {}).get("majorVersion")
  1541.                        if required_java_from_json:
  1542.                            logging.debug(f"Wymagana Java z Version JSON dla {version_id}: {required_java_from_json}")
  1543.                            return required_java_from_json
  1544.                        else:
  1545.                            logging.debug(f"Version JSON dla {version_id} nie zawiera 'javaVersion'.")
  1546.                   else:
  1547.                        logging.warning(f"Manifest wersji dla {version_id} nie zawiera URL do version JSON.")
  1548.              else:
  1549.                   logging.warning(f"Wersja {version_id} nie znaleziona w manifeście wersji.")
  1550.         except Exception as e:
  1551.              logging.debug(f"Nie udało się pobrać/sparsować version JSON dla {version_id} w celu sprawdzenia Javy: {e}. Używam domyślnej logiki.")
  1552.  
  1553.  
  1554.         try:
  1555.             parts = version_id.split('.')
  1556.             if re.match(r"^\d+w\d+[a-z]$", version_id):
  1557.                 logging.debug(f"'{version_id}' to snapshot, szacuję wymaganą Javę.")
  1558.                 try:
  1559.                      year_week_match = re.match(r"^(\d+)w(\d+)", version_id)
  1560.                      if year_week_match:
  1561.                           year = int(year_week_match.group(1))
  1562.                           week = int(year_week_match.group(2))
  1563.                           if year >= 24:
  1564.                               return 21
  1565.                           elif year == 23 and week >= 14:
  1566.                                return 17
  1567.                      return 17
  1568.                 except Exception as e:
  1569.                      logging.warning(f"Błąd parsowania daty snapshota '{version_id}': {e}. Domyślnie Java 17.")
  1570.                      return 17
  1571.  
  1572.             if len(parts) >= 2:
  1573.                  major = int(parts[0])
  1574.                  minor = int(parts[1])
  1575.  
  1576.                  if major >= 2:
  1577.                       return 21
  1578.                  if major == 1:
  1579.                       if minor >= 21:
  1580.                            return 21
  1581.                       elif minor == 20 and (len(parts) < 3 or int(parts[2]) >= 5):
  1582.                            return 21
  1583.                       elif minor >= 18:
  1584.                            return 17
  1585.                       elif minor >= 17:
  1586.                            return 16
  1587.                       elif minor >= 13:
  1588.                            return 8
  1589.                       else:
  1590.                            return 8
  1591.             logging.warning(f"Nieobsługiwany format wersji gry '{version_id}' dla wymaganej Javy. Domyślnie Java 8.")
  1592.             return 8
  1593.  
  1594.         except Exception as e:
  1595.             logging.error(f"Nieoczekiwany błąd podczas określania wymaganej Javy dla '{version_id}': {e}. Domyślnie Java 8.")
  1596.             return 8
  1597.  
  1598.  
  1599.     def find_java_for_version(self, version_id, preferred_java_path=None):
  1600.         required_major_version = self.get_required_java_version(version_id)
  1601.         logging.info(f"Wersja {version_id} najlepiej działa z Javą {required_major_version}+")
  1602.  
  1603.         if preferred_java_path and preferred_java_path.lower() != 'auto':
  1604.              java_path_obj = Path(preferred_java_path)
  1605.              if java_path_obj.exists():
  1606.                  java_major_version = self.get_java_version_from_path(str(java_path_obj))
  1607.                  if java_major_version is not None and java_major_version >= required_major_version:
  1608.                      logging.info(f"Używam preferowanej Javy: {preferred_java_path} (Wersja: {java_major_version})")
  1609.                      return str(java_path_obj)
  1610.                  elif java_major_version is not None:
  1611.                      logging.warning(f"Preferowana Java ({preferred_java_path}, wersja {java_major_version}) nie spełnia minimalnych wymagań ({required_major_version}). Szukam innej.")
  1612.                  else:
  1613.                      logging.warning(f"Nie można odczytać wersji z preferowanej ścieżki Javy: {preferred_java_path}. Sprawdzam inne.")
  1614.              else:
  1615.                  logging.warning(f"Preferowana ścieżka Javy nie istnieje: {preferred_java_path}. Szukam innej.")
  1616.  
  1617.         java_candidates = []
  1618.         for path, version_string in self.java_versions:
  1619.              if path.lower() == (preferred_java_path or '').lower() and (Path(path).exists()):
  1620.                   continue
  1621.  
  1622.              major_v = self.get_java_version_from_path(path)
  1623.              if major_v is not None:
  1624.                   java_candidates.append((path, major_v))
  1625.  
  1626.         java_candidates.sort(key=lambda item: item[1], reverse=True)
  1627.  
  1628.         for path, major_v in java_candidates:
  1629.              if major_v >= required_major_version:
  1630.                   logging.info(f"Znaleziono kompatybilną Javę ({major_v}+): {path}")
  1631.                   return path
  1632.  
  1633.         logging.warning(f"Nie znaleziono automatycznie kompatybilnej Javy ({required_major_version}+).")
  1634.         return None
  1635.  
  1636.  
  1637.     def create_instance(self, name, version_id, modloader=None, ram="4G", java_path_setting=None, jvm_args_extra="", base_instance_dir_input=None, parent_window=None):
  1638.         if not name:
  1639.             raise ValueError("Nazwa instancji nie może być pusta!")
  1640.         if not version_id:
  1641.             raise ValueError("Nie wybrano wersji Minecrafta!")
  1642.  
  1643.         if not re.match(r"^[a-zA-Z0-9._-]+$", version_id):
  1644.              raise ValueError(f"Nieprawidłowy format ID wersji: '{version_id}'.")
  1645.  
  1646.         if modloader and modloader.lower() not in ["forge", "neoforge", "fabric", "quilt"]:
  1647.              raise ValueError(f"Nieznany typ modloadera: '{modloader}'. Obsługiwane: Forge, NeoForge, Fabric, Quilt.")
  1648.  
  1649.         safe_name = re.sub(r'[<>:"/\\|?*]', '_', name)
  1650.         safe_name = safe_name.strip()
  1651.         if not safe_name:
  1652.              raise ValueError("Nazwa instancji po usunięciu nieprawidłowych znaków jest pusta.")
  1653.  
  1654.         if base_instance_dir_input and Path(base_instance_dir_input) != INSTANCES_DIR:
  1655.              instance_dir = Path(base_instance_dir_input)
  1656.              instance_dir = instance_dir / safe_name
  1657.  
  1658.              try:
  1659.                  resolved_instance_dir = instance_dir.resolve()
  1660.                  resolved_instances_dir = INSTANCES_DIR.resolve()
  1661.                  if resolved_instances_dir in resolved_instance_dir.parents or resolved_instance_dir == resolved_instances_dir:
  1662.                       raise ValueError(f"Docelowy katalog instancji '{instance_dir}' znajduje się wewnątrz domyślnego katalogu instancji '{INSTANCES_DIR}'. Wybierz katalog poza domyślnym lub użyj domyślnego sposobu nazewnictwa instancji.")
  1663.              except ValueError:
  1664.                   raise
  1665.              except Exception as e:
  1666.                   logging.error(f"Błąd walidacji ścieżki instancji: {e}")
  1667.                   QMessageBox.critical(parent_window, "Błąd folderu instancji", f"Wystąpił błąd podczas walidacji ścieżki instancji: {e}")
  1668.                   raise
  1669.  
  1670.         else:
  1671.              instance_dir = INSTANCES_DIR / safe_name
  1672.  
  1673.         if instance_dir.exists():
  1674.              is_empty = not any(instance_dir.iterdir())
  1675.              if not is_empty:
  1676.                   raise FileExistsError(f"Katalog docelowy instancji '{instance_dir}' już istnieje i nie jest pusty! Wybierz inną nazwę lub folder.")
  1677.  
  1678.         instance_dir.mkdir(parents=True, exist_ok=True)
  1679.  
  1680.         if self.current_download_thread or self.download_queue:
  1681.             QMessageBox.warning(parent_window, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Spróbuj ponownie później.")
  1682.             return
  1683.  
  1684.         try:
  1685.             logging.info(f"Przygotowanie do pobierania plików dla instancji '{name}' ({version_id})")
  1686.             self.download_queue.clear()
  1687.  
  1688.             queued_version_files_count = self._queue_version_files(version_id, instance_dir)
  1689.  
  1690.             queued_modloader_files_count = 0
  1691.             if modloader:
  1692.                  if not self.validate_modloader(modloader, version_id):
  1693.                       raise ValueError(f"{modloader.capitalize()} prawdopodobnie nie wspiera wersji {version_id}. Wybierz inną wersję gry lub modloader.")
  1694.  
  1695.                  queued_modloader_files_count = self._queue_modloader_installer(modloader, version_id, instance_dir)
  1696.  
  1697.             total_queued_for_download = len(self.download_queue)
  1698.  
  1699.             if total_queued_for_download == 0:
  1700.                  logging.info("Wszystkie pliki do pobrania już istnieją lub nie wymagają pobierania. Przechodzę do konfiguracji.")
  1701.                  self._extract_natives(version_id, instance_dir)
  1702.                  if modloader:
  1703.                       self._run_modloader_installer(modloader, version_id, instance_dir)
  1704.                  self.save_instance_settings(instance_dir, name, version_id, modloader, ram, java_path_setting, jvm_args_extra)
  1705.                  logging.info(f"Instancja '{name}' ({version_id}) stworzona pomyślnie w {instance_dir}")
  1706.                  QMessageBox.information(parent_window, "Sukces", f"Instancja '{name}' stworzona pomyślnie!")
  1707.                  if hasattr(parent_window, 'update_instance_tiles'):
  1708.                       parent_window.update_instance_tiles()
  1709.                  return str(instance_dir)
  1710.  
  1711.             logging.info(f"Kolejka pobierania gotowa. Plików do pobrania: {total_queued_for_download}")
  1712.             self.progress_dialog = DownloadProgressDialog(self, parent_window) # Pass launcher to dialog
  1713.             self.progress_dialog.set_total_files(total_queued_for_download)
  1714.             self.progress_dialog.cancel_signal.connect(self.cancel_downloads)
  1715.             self.progress_dialog.download_process_finished.connect(self._handle_create_instance_post_download)
  1716.  
  1717.             self._post_download_data = {
  1718.                  "instance_dir": str(instance_dir),
  1719.                  "name": name,
  1720.                  "version_id": version_id,
  1721.                  "modloader": modloader,
  1722.                  "ram": ram,
  1723.                  "java_path_setting": java_path_setting,
  1724.                  "jvm_args_extra": jvm_args_extra,
  1725.                  "parent_window": parent_window
  1726.             }
  1727.  
  1728.             self.process_download_queue()
  1729.             self.progress_dialog.exec()
  1730.  
  1731.             return str(instance_dir)
  1732.  
  1733.  
  1734.         except (ValueError, FileExistsError, FileNotFoundError, ConnectionError, PermissionError, Exception) as e:
  1735.             self.download_queue.clear()
  1736.             if self.current_download_thread:
  1737.                 self.current_download_thread.cancel()
  1738.                 self.current_download_thread.wait(2000)
  1739.                 self.current_download_thread = None
  1740.             if self.progress_dialog:
  1741.                  self.progress_dialog.reject()
  1742.                  self.progress_dialog = None
  1743.  
  1744.             if instance_dir.exists():
  1745.                  try:
  1746.                      if not any(instance_dir.iterdir()):
  1747.                           logging.debug(f"Usuwanie pustego katalogu instancji po błędzie: {instance_dir}")
  1748.                           instance_dir.rmdir()
  1749.                      else:
  1750.                           logging.debug(f"Katalog instancji {instance_dir} nie jest pusty, nie usuwam go po błędzie.")
  1751.                  except Exception as cleanup_e:
  1752.                      logging.warning(f"Nie udało się posprzątać katalogu instancji {instance_dir} po błędzie: {cleanup_e}")
  1753.  
  1754.             logging.error(f"Błąd podczas przygotowania instancji: {e}")
  1755.             raise
  1756.  
  1757.  
  1758.     def _handle_create_instance_post_download(self, success):
  1759.          if self.progress_dialog:
  1760.              post_data = self._post_download_data
  1761.              QTimer.singleShot(0, self.progress_dialog.deleteLater)
  1762.              self.progress_dialog = None
  1763.  
  1764.              if post_data is None:
  1765.                   logging.error("Brak danych do konfiguracji po pobraniu. Nie mogę zakończyć tworzenia instancji.")
  1766.                   parent_window = QApplication.activeWindow()
  1767.                   QMessageBox.critical(parent_window, "Błąd konfiguracji instancji", "Wystąpił wewnętrzny błąd po pobraniu. Spróbuj ponownie.")
  1768.                   return
  1769.  
  1770.              instance_dir = Path(post_data["instance_dir"])
  1771.              name = post_data["name"]
  1772.              version_id = post_data["version_id"]
  1773.              modloader = post_data["modloader"]
  1774.              ram = post_data["ram"]
  1775.              java_path_setting = post_data["java_path_setting"]
  1776.              jvm_args_extra = post_data["jvm_args_extra"]
  1777.              parent_window = post_data.get("parent_window")
  1778.  
  1779.              self._post_download_data = None
  1780.  
  1781.              if not success:
  1782.                  logging.warning("Tworzenie instancji anulowane lub zakończone z błędami pobierania.")
  1783.                  QMessageBox.warning(parent_window, "Tworzenie instancji anulowane", "Tworzenie instancji zostało anulowane lub napotkało błędy podczas pobierania. Sprawdź logi.")
  1784.                  return
  1785.  
  1786.              logging.info("Pobieranie dla instancji zakończone pomyślnie. Kontynuuję konfigurację...")
  1787.  
  1788.              try:
  1789.                  self._extract_natives(version_id, instance_dir)
  1790.                  if modloader:
  1791.                      self._run_modloader_installer(modloader, version_id, instance_dir)
  1792.                  self.save_instance_settings(instance_dir, name, version_id, modloader, ram, java_path_setting, jvm_args_extra)
  1793.  
  1794.                  logging.info(f"Instancja '{name}' ({version_id}) stworzona pomyślnie w {instance_dir}")
  1795.                  QMessageBox.information(parent_window, "Sukces", f"Instancja '{name}' stworzona pomyślnie!")
  1796.  
  1797.                  if hasattr(parent_window, 'update_instance_tiles'):
  1798.                      parent_window.update_instance_tiles()
  1799.  
  1800.              except (ValueError, FileNotFoundError, RuntimeError, TimeoutError, IOError, Exception) as e:
  1801.                  logging.error(f"Błąd podczas konfiguracji instancji po pobraniu: {e}")
  1802.                  QMessageBox.critical(parent_window, "Błąd konfiguracji instancji", f"Nie udało się zakończyć konfiguracji instancji: {e}")
  1803.  
  1804.          else:
  1805.               logging.error("Progress dialog finished signal received, but progress_dialog object was None.")
  1806.  
  1807.  
  1808.     def save_instance_settings(self, instance_dir, name, version_id, modloader, ram, java_path_setting, jvm_args_extra):
  1809.          instance_settings_path = instance_dir / "settings.json"
  1810.          settings_to_save = {
  1811.              "name": name,
  1812.              "version": version_id,
  1813.              "modloader": modloader,
  1814.              "ram": ram,
  1815.              "java_path": java_path_setting if java_path_setting is not None else "auto",
  1816.              "jvm_args": jvm_args_extra or DEFAULT_SETTINGS["jvm_args"],
  1817.          }
  1818.  
  1819.          if instance_settings_path.exists():
  1820.              try:
  1821.                  with instance_settings_path.open("r", encoding='utf-8') as f:
  1822.                      existing_settings = json.load(f)
  1823.                      existing_settings.update(settings_to_save)
  1824.                      settings_to_save = existing_settings
  1825.              except json.JSONDecodeError:
  1826.                  logging.warning(f"Nieprawidłowy format settings.json w {instance_dir}, nadpisuję nowymi standardowymi polami.")
  1827.              except Exception as e:
  1828.                  logging.warning(f"Błąd odczytu istniejącego settings.json w {instance_dir}: {e}, nadpisuję nowymi standardowymi polami.")
  1829.  
  1830.          try:
  1831.              with instance_settings_path.open("w", encoding='utf-8') as f:
  1832.                  json.dump(settings_to_save, f, indent=4)
  1833.              logging.info(f"Zapisano ustawienia instancji do {instance_settings_path}")
  1834.          except Exception as e:
  1835.              logging.error(f"Błąd zapisu settings.json instancji {instance_dir}: {e}")
  1836.              raise IOError(f"Nie udało się zapisać ustawień instancji: {e}")
  1837.  
  1838.  
  1839.     def get_instance_list(self):
  1840.         valid_instances = []
  1841.         if not INSTANCES_DIR.exists():
  1842.              return []
  1843.         for item in INSTANCES_DIR.iterdir():
  1844.             settings_path = item / "settings.json"
  1845.             if item.is_dir() and settings_path.exists():
  1846.                 instance_name = item.name
  1847.                 instance_dir_path = str(item)
  1848.                 try:
  1849.                     with settings_path.open("r", encoding='utf-8') as f:
  1850.                         settings = json.load(f)
  1851.                     stored_name = settings.get("name")
  1852.                     if stored_name and stored_name.strip():
  1853.                          instance_name = stored_name.strip()
  1854.  
  1855.                 except (json.JSONDecodeError, Exception) as e:
  1856.                     logging.warning(f"Błąd odczytu nazwy instancji z {settings_path}: {e}. Używam nazwy folderu: {item.name}")
  1857.  
  1858.                 valid_instances.append((instance_name, instance_dir_path))
  1859.  
  1860.         valid_instances.sort(key=lambda x: x[0].lower())
  1861.  
  1862.         return valid_instances
  1863.  
  1864.  
  1865.     def export_instance(self, instance_dir_path, zip_path):
  1866.         instance_dir = Path(instance_dir_path)
  1867.         zip_path = Path(zip_path)
  1868.         if not instance_dir.exists():
  1869.              raise FileNotFoundError(f"Katalog instancji nie istnieje: {instance_dir_path}")
  1870.         logging.info(f"Eksportowanie instancji z {instance_dir} do {zip_path}")
  1871.  
  1872.         zip_path.parent.mkdir(parents=True, exist_ok=True)
  1873.  
  1874.         try:
  1875.             with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
  1876.                 for root, _, files in os.walk(instance_dir):
  1877.                     for file in files:
  1878.                         file_path = Path(root) / file
  1879.                         archive_path = file_path.relative_to(instance_dir)
  1880.                         if "natives_zips" not in archive_path.parts:
  1881.                             zipf.write(file_path, archive_path)
  1882.             logging.info("Eksport zakończony pomyślnie.")
  1883.         except Exception as e:
  1884.             logging.error(f"Błąd eksportu instancji {instance_dir}: {e}")
  1885.             if zip_path.exists():
  1886.                  try:
  1887.                      zip_path.unlink()
  1888.                      logging.warning(f"Usunięto częściowo utworzony plik zip: {zip_path}")
  1889.                  except Exception as cleanup_e:
  1890.                      logging.warning(f"Nie udało się usunąć częściowego pliku zip {zip_path}: {cleanup_e}")
  1891.  
  1892.             raise IOError(f"Nie udało się wyeksportować instancji: {e}")
  1893.  
  1894.     def import_instance(self, zip_path):
  1895.         zip_path = Path(zip_path)
  1896.         if not zip_path.exists():
  1897.              raise FileNotFoundError(f"Plik ZIP instancji nie istnieje: {zip_path}")
  1898.         if not zipfile.is_zipfile(zip_path):
  1899.              raise zipfile.BadZipFile(f"Plik '{zip_path.name}' nie jest prawidłowym plikiem ZIP.")
  1900.  
  1901.         INSTANCES_DIR.mkdir(parents=True, exist_ok=True)
  1902.  
  1903.         try:
  1904.             instance_name_base = zip_path.stem
  1905.             safe_name_base = re.sub(r'[<>:"/\\|?*]', '_', instance_name_base)
  1906.             if not safe_name_base:
  1907.                  safe_name_base = "imported_instance"
  1908.  
  1909.             instance_dir = INSTANCES_DIR / safe_name_base
  1910.             counter = 1
  1911.             while instance_dir.exists():
  1912.                  instance_dir = INSTANCES_DIR / f"{safe_name_base}-{counter}"
  1913.                  counter += 1
  1914.  
  1915.             logging.info(f"Importowanie instancji z {zip_path} do {instance_dir}")
  1916.             instance_dir.mkdir(parents=True)
  1917.  
  1918.             with zipfile.ZipFile(zip_path, "r") as zipf:
  1919.                 zipf.extractall(instance_dir)
  1920.  
  1921.             settings_path = instance_dir / "settings.json"
  1922.             if settings_path.exists():
  1923.                  try:
  1924.                      with settings_path.open("r", encoding='utf-8') as f:
  1925.                           settings = json.load(f)
  1926.                      settings['name'] = instance_dir.name
  1927.                      with settings_path.open("w", encoding='utf-8') as f:
  1928.                           json.dump(settings, f, indent=4)
  1929.                      logging.debug(f"Zaktualizowano nazwę w settings.json importowanej instancji do: {instance_dir.name}")
  1930.                  except (json.JSONDecodeError, Exception) as e:
  1931.                       logging.warning(f"Nie udało się zaktualizować nazwy w settings.json importowanej instancji {instance_dir.name}: {e}")
  1932.  
  1933.             logging.info("Import zakończony pomyślnie.")
  1934.             return str(instance_dir)
  1935.  
  1936.         except (zipfile.BadZipFile, FileNotFoundError, Exception) as e:
  1937.             logging.error(f"Błąd importu instancji z {zip_path}: {e}")
  1938.             if 'instance_dir' in locals() and instance_dir.exists():
  1939.                  try:
  1940.                      shutil.rmtree(instance_dir)
  1941.                      logging.info(f"Usunięto częściowo zaimportowany katalog: {instance_dir}")
  1942.                  except Exception as cleanup_e:
  1943.                      logging.error(f"Błąd podczas czyszczenia katalogu {instance_dir}: {cleanup_e}")
  1944.  
  1945.             if isinstance(e, (zipfile.BadZipFile, FileNotFoundError)):
  1946.                  raise e
  1947.             else:
  1948.                  raise ValueError(f"Nie udało się zaimportować instancji: {e}")
  1949.  
  1950.  
  1951. class CreateInstanceDialog(QDialog):
  1952.     def __init__(self, launcher, parent=None):
  1953.         super().__init__(parent)
  1954.         self.launcher = launcher
  1955.         self.setWindowTitle("Nowa instancja")
  1956.         self.setMinimumWidth(400)
  1957.         self.init_ui()
  1958.         self.version_combo.currentTextChanged.connect(self.update_modloaders)
  1959.         self.populate_versions()
  1960.         self.update_modloaders()
  1961.  
  1962.     def init_ui(self):
  1963.         layout = QVBoxLayout(self)
  1964.         layout.setSpacing(10)
  1965.  
  1966.         self.name_input = QLineEdit()
  1967.         self.name_input.setPlaceholderText("Nazwa instancji (np. MojaWersja)")
  1968.         layout.addWidget(QLabel("Nazwa instancji:"))
  1969.         layout.addWidget(self.name_input)
  1970.  
  1971.         instance_dir_layout = QHBoxLayout()
  1972.         self.instance_dir_input = QLineEdit(str(INSTANCES_DIR))
  1973.         self.instance_dir_input.setReadOnly(True)
  1974.         self.instance_dir_button = QPushButton("Wybierz inny folder docelowy...")
  1975.         self.instance_dir_button.clicked.connect(self.choose_instance_dir)
  1976.         self.use_custom_dir_check = QCheckBox("Użyj innego folderu")
  1977.         self.use_custom_dir_check.setChecked(False)
  1978.         self.use_custom_dir_check.stateChanged.connect(self.toggle_custom_dir_input)
  1979.         instance_dir_layout.addWidget(self.instance_dir_input)
  1980.         instance_dir_layout.addWidget(self.instance_dir_button)
  1981.         self.instance_dir_input.setEnabled(False)
  1982.         self.instance_dir_button.setEnabled(False)
  1983.  
  1984.         layout.addWidget(QLabel("Folder docelowy instancji:"))
  1985.         layout.addWidget(self.use_custom_dir_check)
  1986.         layout.addLayout(instance_dir_layout)
  1987.  
  1988.         self.version_combo = QComboBox()
  1989.         layout.addWidget(QLabel("Wersja Minecrafta:"))
  1990.         layout.addWidget(self.version_combo)
  1991.  
  1992.         self.modloader_combo = QComboBox()
  1993.         layout.addWidget(QLabel("Modloader (dla wybranych wersji):"))
  1994.         layout.addWidget(self.modloader_combo)
  1995.  
  1996.         advanced_group = QWidget()
  1997.         advanced_layout = QVBoxLayout(advanced_group)
  1998.         advanced_layout.setContentsMargins(0, 0, 0, 0)
  1999.  
  2000.         self.ram_input = QLineEdit(self.launcher.settings.get("ram", DEFAULT_SETTINGS["ram"]))
  2001.         advanced_layout.addWidget(QLabel("Maksymalna pamięć RAM (np. 4G, 2048M):"))
  2002.         advanced_layout.addWidget(self.ram_input)
  2003.  
  2004.         self.jvm_args_input = QLineEdit(self.launcher.settings.get("jvm_args", DEFAULT_SETTINGS["jvm_args"]))
  2005.         advanced_layout.addWidget(QLabel("Dodatkowe argumenty JVM:"))
  2006.         advanced_layout.addWidget(self.jvm_args_input)
  2007.  
  2008.         self.java_combo = QComboBox()
  2009.         self.java_combo.addItem("Automatyczny wybór", userData="auto")
  2010.         sorted_java_versions = sorted(self.launcher.java_versions, key=lambda x: self.launcher.get_java_version_from_path(x[0]) or 0, reverse=True)
  2011.         for java_path, version in sorted_java_versions:
  2012.             major_v = self.launcher.get_java_version_from_path(java_path)
  2013.             self.java_combo.addItem(f"{version} (Java {major_v}) - {java_path}", userData=java_path)
  2014.  
  2015.         default_java_setting = self.launcher.settings.get("java_path")
  2016.         if default_java_setting and default_java_setting.lower() != 'auto':
  2017.              default_index = self.java_combo.findData(default_java_setting)
  2018.              if default_index != -1:
  2019.                   self.java_combo.setCurrentIndex(default_index)
  2020.              else:
  2021.                   custom_item_text = f"Zapisana ścieżka: {default_java_setting} (Nieznana wersja)"
  2022.                   self.java_combo.addItem(custom_item_text, userData=default_java_setting)
  2023.                   self.java_combo.setCurrentIndex(self.java_combo.count() - 1)
  2024.                   logging.warning(f"Zapisana ścieżka Javy w ustawieniach nie znaleziona wśród automatycznie wykrytych: {default_java_setting}. Dodano jako opcję niestandardową.")
  2025.  
  2026.         layout.addWidget(QLabel("Wersja Javy (zalecany 'Automatyczny wybór'):"))
  2027.         layout.addWidget(self.java_combo)
  2028.  
  2029.         layout.addWidget(advanced_group)
  2030.  
  2031.         button_layout = QHBoxLayout()
  2032.         create_button = QPushButton("Stwórz instancję")
  2033.         create_button.clicked.connect(self.check_and_accept)
  2034.         cancel_button = QPushButton("Anuluj")
  2035.         cancel_button.clicked.connect(self.reject)
  2036.         button_layout.addStretch(1)
  2037.         button_layout.addWidget(create_button)
  2038.         button_layout.addWidget(cancel_button)
  2039.         layout.addLayout(button_layout)
  2040.  
  2041.     def toggle_custom_dir_input(self, state):
  2042.         enabled = self.use_custom_dir_check.isChecked()
  2043.         self.instance_dir_input.setEnabled(enabled)
  2044.         self.instance_dir_button.setEnabled(enabled)
  2045.         if not enabled:
  2046.              self.instance_dir_input.setText(str(INSTANCES_DIR))
  2047.  
  2048.     def choose_instance_dir(self):
  2049.         current_dir = self.instance_dir_input.text()
  2050.         if not Path(current_dir).exists():
  2051.              current_dir = str(INSTANCES_DIR.parent)
  2052.  
  2053.         folder = QFileDialog.getExistingDirectory(self, "Wybierz folder docelowy dla instancji", current_dir)
  2054.         if folder:
  2055.             self.instance_dir_input.setText(folder)
  2056.  
  2057.     def populate_versions(self):
  2058.          self.version_combo.blockSignals(True)
  2059.          self.version_combo.clear()
  2060.          try:
  2061.              manifest = self.launcher.get_version_manifest()
  2062.              versions = sorted(manifest.get("versions", []), key=lambda x: x.get('releaseTime', '1970-01-01T00:00:00+00:00'), reverse=True)
  2063.  
  2064.              for version in versions:
  2065.                  self.version_combo.addItem(version["id"])
  2066.  
  2067.          except ConnectionError as e:
  2068.              QMessageBox.critical(self.parentWidget(), "Błąd połączenia", f"Nie udało się pobrać listy wersji gry. Sprawdź połączenie z internetem.\n{e}")
  2069.              self.version_combo.addItem("Błąd pobierania listy wersji")
  2070.              self.version_combo.setEnabled(False)
  2071.          except Exception as e:
  2072.              logging.error(f"Nieoczekiwany błąd podczas pobierania listy wersji: {e}")
  2073.              QMessageBox.critical(self.parentWidget(), "Błąd", f"Nie udało się pobrać listy wersji gry: {e}")
  2074.              self.version_combo.addItem("Błąd ładowania listy wersji")
  2075.              self.version_combo.setEnabled(False)
  2076.          finally:
  2077.               self.version_combo.blockSignals(False)
  2078.  
  2079.  
  2080.     def update_modloaders(self):
  2081.         version_id = self.version_combo.currentText()
  2082.         self.modloader_combo.clear()
  2083.         self.modloader_combo.addItem("Brak")
  2084.         if not version_id or version_id.startswith("Błąd"):
  2085.             self.modloader_combo.setEnabled(False)
  2086.             return
  2087.         else:
  2088.              self.modloader_combo.setEnabled(True)
  2089.  
  2090.         supported_modloaders = []
  2091.         for modloader in ["forge", "neoforge", "fabric", "quilt"]:
  2092.             if self.launcher.validate_modloader(modloader, version_id):
  2093.                 supported_modloaders.append(modloader.capitalize())
  2094.  
  2095.         if supported_modloaders:
  2096.              self.modloader_combo.addItems(supported_modloaders)
  2097.         elif re.match(r"^\d+w\d+[a-z]$", version_id):
  2098.              self.modloader_combo.addItem("Brak (Snapshot - brak oficjalnego wsparcia)")
  2099.  
  2100.     def check_and_accept(self):
  2101.         name = self.name_input.text().strip()
  2102.         if not name:
  2103.             QMessageBox.warning(self, "Brak nazwy", "Proszę podać nazwę instancji.")
  2104.             return
  2105.  
  2106.         version_id = self.version_combo.currentText()
  2107.         if not version_id or version_id.startswith("Błąd"):
  2108.              QMessageBox.warning(self, "Brak wersji", "Proszę wybrać poprawną wersję Minecrafta.")
  2109.              return
  2110.  
  2111.         ram_val = self.ram_input.text().strip().upper()
  2112.         if not re.match(r"^\d+[MG]$", ram_val):
  2113.              QMessageBox.warning(self, "Nieprawidłowy format RAM", "Proszę podać RAM w formacie np. '4G' lub '2048M'.")
  2114.              return
  2115.  
  2116.         selected_java_index = self.java_combo.currentIndex()
  2117.         if selected_java_index == -1:
  2118.              QMessageBox.warning(self, "Brak wyboru Javy", "Proszę wybrać wersję Javy lub 'Automatyczny wybór'.")
  2119.              return
  2120.         selected_java_path_data = self.java_combo.itemData(selected_java_index)
  2121.         selected_java_path = selected_java_path_data if selected_java_path_data is not None else "auto"
  2122.  
  2123.         if selected_java_path != 'auto':
  2124.              if not Path(selected_java_path).exists():
  2125.                   QMessageBox.warning(self, "Nieprawidłowa ścieżka Javy", f"Wybrana ścieżka Javy nie istnieje:\n{selected_java_path}. Proszę wybrać inną lub 'Automatyczny wybór'.")
  2126.                   return
  2127.              required_java = self.launcher.get_required_java_version(version_id)
  2128.              selected_java_major = self.launcher.get_java_version_from_path(selected_java_path)
  2129.              if selected_java_major is not None and selected_java_major < required_java:
  2130.                   reply = QMessageBox.question(self, "Niekompatybilna Java?",
  2131.                                                f"Wybrana wersja Javy ({selected_java_major}) może nie być kompatybilna z wersją Minecrafta {version_id} (wymaga {required_java}+). Czy chcesz kontynuować?",
  2132.                                                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  2133.                   if reply == QMessageBox.StandardButton.No:
  2134.                       return
  2135.  
  2136.         if self.use_custom_dir_check.isChecked():
  2137.              chosen_base_dir_str = self.instance_dir_input.text().strip()
  2138.              if not chosen_base_dir_str:
  2139.                   QMessageBox.warning(self, "Brak folderu docelowego", "Proszę wybrać folder docelowy dla instancji.")
  2140.                   return
  2141.  
  2142.         self.accept()
  2143.  
  2144.     def get_data(self):
  2145.         selected_java_index = self.java_combo.currentIndex()
  2146.         selected_java_path_data = self.java_combo.itemData(selected_java_index)
  2147.         java_path_setting_to_save = selected_java_path_data if selected_java_path_data is not None else "auto"
  2148.  
  2149.         base_instance_dir_input_value = self.instance_dir_input.text().strip() if self.use_custom_dir_check.isChecked() else None
  2150.         if self.use_custom_dir_check.isChecked() and not base_instance_dir_input_value:
  2151.              base_instance_dir_input_value = None
  2152.  
  2153.         return {
  2154.             "name": self.name_input.text().strip(),
  2155.             "version": self.version_combo.currentText(),
  2156.             "modloader": self.modloader_combo.currentText().lower() if self.modloader_combo.currentText() != "Brak" and "snapshot" not in self.modloader_combo.currentText().lower() else None,
  2157.             "ram": self.ram_input.text().strip().upper(),
  2158.             "java_path_setting": java_path_setting_to_save,
  2159.             "jvm_args_extra": self.jvm_args_input.text().strip(),
  2160.             "base_instance_dir_input": base_instance_dir_input_value,
  2161.         }
  2162.  
  2163. class ModBrowserDialog(QDialog):
  2164.     def __init__(self, launcher, version_id, instance_dir, parent=None):
  2165.         super().__init__(parent)
  2166.         self.launcher = launcher
  2167.         self.version_id = version_id
  2168.         self.instance_dir = instance_dir
  2169.         self.setWindowTitle(f"Przeglądarka modów dla {version_id}")
  2170.         self.setMinimumSize(800, 600)
  2171.         self.current_mod = None
  2172.         self.selected_compatible_file = None
  2173.         self.init_ui()
  2174.         self.mod_list.clear()
  2175.         self.reset_details()
  2176.  
  2177.     def init_ui(self):
  2178.         self.setStyleSheet(STYLESHEET)
  2179.         layout = QHBoxLayout(self)
  2180.         layout.setSpacing(10)
  2181.  
  2182.         left_panel = QWidget()
  2183.         left_layout = QVBoxLayout(left_panel)
  2184.         left_layout.setSpacing(5)
  2185.  
  2186.         self.search_input = QLineEdit()
  2187.         self.search_input.setPlaceholderText("Szukaj modów...")
  2188.         self.search_input.returnPressed.connect(self.search_mods)
  2189.         left_layout.addWidget(self.search_input)
  2190.  
  2191.         self.mod_list = QListWidget()
  2192.         self.mod_list.setIconSize(QSize(48, 48))
  2193.         self.mod_list.itemClicked.connect(self.show_mod_details)
  2194.         self.mod_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
  2195.         left_layout.addWidget(self.mod_list)
  2196.  
  2197.         layout.addWidget(left_panel, 1)
  2198.  
  2199.         right_panel_scroll = QScrollArea()
  2200.         right_panel_scroll.setWidgetResizable(True)
  2201.         right_panel_scroll.setMinimumWidth(300)
  2202.         right_panel_widget = QWidget()
  2203.         self.details_layout = QVBoxLayout(right_panel_widget)
  2204.         self.details_layout.setSpacing(10)
  2205.         self.details_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
  2206.         right_panel_scroll.setWidget(right_panel_widget)
  2207.  
  2208.         self.mod_icon = QLabel()
  2209.         self.mod_icon.setFixedSize(128, 128)
  2210.         self.mod_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
  2211.         self.mod_icon.setStyleSheet("border: 1px solid #ccc; background-color: #e0e0e0;")
  2212.         self.details_layout.addWidget(self.mod_icon)
  2213.  
  2214.         self.mod_name = QLabel("Wybierz mod z listy")
  2215.         self.mod_name.setStyleSheet("font-size: 18px; font-weight: bold; margin-top: 5px;")
  2216.         self.mod_name.setWordWrap(True)
  2217.         self.details_layout.addWidget(self.mod_name)
  2218.  
  2219.         self.mod_author = QLabel("Autor: Brak")
  2220.         self.details_layout.addWidget(self.mod_author)
  2221.  
  2222.         self.mod_downloads = QLabel("Pobrania: Brak danych")
  2223.         self.details_layout.addWidget(self.mod_downloads)
  2224.  
  2225.         self.mod_date = QLabel("Aktualizacja: Brak danych")
  2226.         self.details_layout.addWidget(self.mod_date)
  2227.  
  2228.         self.mod_version = QLabel("Kompatybilna wersja pliku: Szukam...")
  2229.         self.details_layout.addWidget(self.mod_version)
  2230.  
  2231.         self.mod_description_label = QLabel("Opis:")
  2232.         self.details_layout.addWidget(self.mod_description_label)
  2233.         self.mod_description = QTextEdit()
  2234.         self.mod_description.setReadOnly(True)
  2235.         self.mod_description.setMinimumHeight(150)
  2236.         self.mod_description.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
  2237.         self.details_layout.addWidget(self.mod_description)
  2238.  
  2239.         self.dependency_check = QCheckBox("Pobierz wymagane mody (zalecane)")
  2240.         self.dependency_check.setChecked(True)
  2241.         self.details_layout.addWidget(self.dependency_check)
  2242.  
  2243.         self.details_layout.addStretch(1)
  2244.  
  2245.         button_layout = QHBoxLayout()
  2246.         self.install_button = QPushButton("Zainstaluj")
  2247.         self.install_button.clicked.connect(self.install_mod)
  2248.         self.install_button.setEnabled(False)
  2249.         self.remove_button = QPushButton("Usuń")
  2250.         self.remove_button.setProperty("deleteButton", "true")
  2251.         self.remove_button.clicked.connect(self.remove_mod)
  2252.         self.remove_button.setEnabled(False)
  2253.         self.remove_button.setStyleSheet("background-color: #f44336;")
  2254.         self.remove_button.setStyleSheet(self.remove_button.styleSheet() + """
  2255.            QPushButton:hover { background-color: #d32f2f; }
  2256.            QPushButton:disabled { background-color: #cccccc; }
  2257.        """)
  2258.  
  2259.         button_layout.addWidget(self.install_button)
  2260.         button_layout.addWidget(self.remove_button)
  2261.         self.details_layout.addLayout(button_layout)
  2262.  
  2263.         close_button = QPushButton("Zamknij")
  2264.         close_button.clicked.connect(self.accept)
  2265.         self.details_layout.addWidget(close_button)
  2266.  
  2267.         layout.addWidget(right_panel_scroll, 2)
  2268.  
  2269.     def search_mods(self):
  2270.         query = self.search_input.text().strip()
  2271.         if not query:
  2272.             self.mod_list.clear()
  2273.             self.reset_details()
  2274.             self.mod_name.setText("Wprowadź frazę do wyszukiwania.")
  2275.             return
  2276.  
  2277.         logging.info(f"Wyszukiwanie modów: '{query}' dla wersji {self.version_id}")
  2278.         self.mod_list.clear()
  2279.         self.reset_details()
  2280.         self.mod_name.setText("Szukam modów...")
  2281.         self.setCursor(Qt.CursorShape.WaitCursor)
  2282.  
  2283.         try:
  2284.             mods = self.launcher.get_curseforge_mods(query, self.version_id)
  2285.             self.unsetCursor()
  2286.             if not mods:
  2287.                  self.mod_name.setText("Brak wyników.")
  2288.                  return
  2289.  
  2290.             self.mod_name.setText("Wybierz mod z listy")
  2291.  
  2292.             for mod in mods:
  2293.                 compatible_file = None
  2294.                 files = mod.get("latestFiles", [])
  2295.                 files.sort(key=lambda x: x.get('fileDate', '1970-01-01T00:00:00Z'), reverse=True)
  2296.                 for file in files:
  2297.                     if self.version_id in file.get("gameVersions", []):
  2298.                         compatible_file = file
  2299.                         break
  2300.  
  2301.                 item_text = f"{mod.get('name', 'Nazwa nieznana')}"
  2302.                 if not compatible_file:
  2303.                      item_text += " (Brak wersji dla tej gry)"
  2304.  
  2305.                 list_item = QListWidgetItem(item_text)
  2306.  
  2307.                 item_data = {
  2308.                     'mod': mod,
  2309.                     'compatible_file': compatible_file
  2310.                 }
  2311.                 list_item.setData(Qt.ItemDataRole.UserRole, item_data)
  2312.  
  2313.                 icon_url = mod.get("logo", {}).get("url")
  2314.                 if icon_url:
  2315.                      icon_file_extension = Path(icon_url).suffix or ".png"
  2316.                      icon_dest_path = MOD_ICONS_DIR / f"{mod['id']}{icon_file_extension}"
  2317.  
  2318.                      if icon_dest_path.exists():
  2319.                           list_item.setIcon(QIcon(str(icon_dest_path)))
  2320.                      else:
  2321.                           pass
  2322.  
  2323.                 self.mod_list.addItem(list_item)
  2324.  
  2325.         except (requests.exceptions.RequestException, PermissionError) as e:
  2326.              self.unsetCursor()
  2327.              QMessageBox.critical(self, "Błąd API CurseForge", f"Wystąpił błąd podczas wyszukiwania modów:\n{e}")
  2328.              logging.error(f"Błąd wyszukiwania modów: {e}")
  2329.              self.mod_name.setText("Błąd API CurseForge.")
  2330.         except Exception as e:
  2331.             self.unsetCursor()
  2332.             logging.error(f"Nieoczekiwany błąd podczas wyszukiwania modów: {e}")
  2333.             QMessageBox.critical(self, "Błąd wyszukiwania modów", f"Nie udało się wyszukać modów: {e}")
  2334.             self.mod_name.setText("Błąd wyszukiwania.")
  2335.  
  2336.  
  2337.     def show_mod_details(self, item):
  2338.         item_data = item.data(Qt.ItemDataRole.UserRole)
  2339.         mod = item_data.get('mod')
  2340.         compatible_file = item_data.get('compatible_file')
  2341.  
  2342.         if not mod:
  2343.             self.reset_details()
  2344.             return
  2345.  
  2346.         self.current_mod = mod
  2347.         self.selected_compatible_file = compatible_file
  2348.  
  2349.         self.mod_name.setText(mod.get("name", "Nazwa nieznana"))
  2350.         authors = mod.get("authors", [])
  2351.         self.mod_author.setText(f"Autor: {authors[0].get('name', 'Brak danych') if authors else 'Brak danych'}")
  2352.         self.mod_downloads.setText(f"Pobrania: {mod.get('downloadCount', 'Brak danych')}")
  2353.         try:
  2354.             date_modified_ts = mod.get('dateModified')
  2355.             if date_modified_ts is not None:
  2356.                  date_modified = datetime.fromtimestamp(date_modified_ts / 1000).strftime('%Y-%m-%d %H:%M')
  2357.                  self.mod_date.setText(f"Aktualizacja: {date_modified}")
  2358.             else:
  2359.                  self.mod_date.setText("Aktualizacja: Brak danych")
  2360.         except Exception as e:
  2361.             logging.warning(f"Błąd parsowania daty modyfikacji dla mod ID {mod.get('id')}: {e}")
  2362.             self.mod_date.setText("Aktualizacja: Nieprawidłowa data")
  2363.  
  2364.         if compatible_file:
  2365.              self.mod_version.setText(f"Kompatybilny plik: {compatible_file.get('fileName', 'Brak danych')}")
  2366.              self.install_button.setEnabled(True)
  2367.              mod_file_name = compatible_file.get("fileName")
  2368.              if mod_file_name:
  2369.                   mod_path = Path(self.instance_dir) / "mods" / mod_file_name
  2370.                   self.remove_button.setEnabled(mod_path.exists())
  2371.              else:
  2372.                   self.remove_button.setEnabled(False)
  2373.  
  2374.         else:
  2375.              self.mod_version.setText("Kompatybilny plik: Brak dla tej wersji")
  2376.              self.install_button.setEnabled(False)
  2377.              self.remove_button.setEnabled(False)
  2378.  
  2379.         description_text = mod.get("summary", "")
  2380.         self.mod_description.setHtml(description_text or "Brak opisu.")
  2381.  
  2382.         self.mod_icon.clear()
  2383.         icon_url = mod.get("logo", {}).get("url")
  2384.         if icon_url:
  2385.              icon_file_extension = Path(icon_url).suffix or ".png"
  2386.              icon_dest_path = MOD_ICONS_DIR / f"{mod['id']}{icon_file_extension}"
  2387.  
  2388.              if icon_dest_path.exists():
  2389.                  try:
  2390.                      pixmap = QPixmap(str(icon_dest_path)).scaled(128, 128, Qt.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
  2391.                      self.mod_icon.setPixmap(pixmap)
  2392.                  except Exception as e:
  2393.                      logging.warning(f"Błąd ładowania ikony z pliku {icon_dest_path}: {e}")
  2394.                      self.mod_icon.setText("Błąd ikony")
  2395.              else:
  2396.                  self.mod_icon.setText("Ładowanie ikony...")
  2397.  
  2398.         else:
  2399.             self.mod_icon.setText("Brak ikony")
  2400.  
  2401.     def reset_details(self):
  2402.         self.current_mod = None
  2403.         self.selected_compatible_file = None
  2404.         self.mod_icon.clear()
  2405.         self.mod_icon.setText("Ikona")
  2406.         self.mod_name.setText("Wybierz mod z listy")
  2407.         self.mod_author.setText("Autor: Brak")
  2408.         self.mod_downloads.setText("Pobrania: Brak danych")
  2409.         self.mod_date.setText("Aktualizacja: Brak danych")
  2410.         self.mod_version.setText("Kompatybilny plik: Brak danych")
  2411.         self.mod_description.setHtml("Wybierz mod z listy, aby zobaczyć szczegóły.")
  2412.         self.install_button.setEnabled(False)
  2413.         self.remove_button.setEnabled(False)
  2414.         self.dependency_check.setChecked(True)
  2415.  
  2416.     def install_mod(self):
  2417.         if not self.current_mod or not self.selected_compatible_file:
  2418.             QMessageBox.warning(self, "Błąd", "Proszę wybrać mod do instalacji i upewnić się, że jest dostępna kompatybilna wersja pliku.")
  2419.             return
  2420.  
  2421.         mod_id = self.current_mod.get("id")
  2422.         mod_name_display = self.current_mod.get("name", "Wybrany mod")
  2423.         download_deps = self.dependency_check.isChecked()
  2424.  
  2425.         reply = QMessageBox.question(self, "Potwierdzenie instalacji",
  2426.                                      f"Zainstalować mod '{mod_name_display}' (dla wersji {self.version_id})?",
  2427.                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  2428.         if reply == QMessageBox.StandardButton.No:
  2429.             return
  2430.  
  2431.         if self.launcher.current_download_thread or self.launcher.download_queue:
  2432.             QMessageBox.warning(self, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Poczekaj na jego zakończenie lub anuluj.")
  2433.             return
  2434.  
  2435.         try:
  2436.             logging.info(f"Rozpoczęcie instalacji moda '{mod_name_display}' (ID: {mod_id}) dla wersji {self.version_id}")
  2437.             self.launcher.download_queue.clear()
  2438.  
  2439.             visited_mods_during_install = set()
  2440.             total_queued = self.launcher._queue_curseforge_mod_files(
  2441.                 mod_id, self.version_id, self.instance_dir,
  2442.                 download_dependencies=download_deps,
  2443.                 visited_mods=visited_mods_during_install
  2444.             )
  2445.  
  2446.             if total_queued == 0:
  2447.                  QMessageBox.information(self, "Informacja", f"Brak plików do pobrania dla moda '{mod_name_display}' (ID: {mod_id}). Pliki mogą już istnieć lub brak kompatybilnej wersji.")
  2448.                  logging.warning("Install mod: No files queued.")
  2449.                  self.show_mod_details(self.mod_list.currentItem())
  2450.                  return
  2451.  
  2452.             logging.info(f"Kolejka pobierania modów gotowa. Plików do pobrania: {total_queued}")
  2453.             self.launcher.progress_dialog = DownloadProgressDialog(self.launcher, self) # Pass launcher to dialog
  2454.             self.launcher.progress_dialog.set_total_files(total_queued)
  2455.             self.launcher.progress_dialog.cancel_signal.connect(self.launcher.cancel_downloads)
  2456.             self.launcher.progress_dialog.download_process_finished.connect(self._handle_mod_install_post_download)
  2457.  
  2458.             self._post_mod_install_data = {
  2459.                 "mod_name": mod_name_display,
  2460.                 "mod_id": mod_id,
  2461.                 "parent_dialog": self
  2462.             }
  2463.  
  2464.             self.launcher.process_download_queue()
  2465.             self.launcher.progress_dialog.exec()
  2466.  
  2467.         except (ValueError, requests.exceptions.RequestException, PermissionError, Exception) as e:
  2468.             logging.error(f"Błąd podczas przygotowania instalacji moda '{mod_name_display}': {e}")
  2469.             QMessageBox.critical(self, "Błąd instalacji moda", f"Nie udało się przygotować instalacji moda:\n{e}")
  2470.             self.launcher.download_queue.clear()
  2471.  
  2472.     def _handle_mod_install_post_download(self, success):
  2473.         if self.launcher.progress_dialog:
  2474.             post_data = self._post_mod_install_data
  2475.             QTimer.singleShot(0, self.launcher.progress_dialog.deleteLater)
  2476.             self.launcher.progress_dialog = None
  2477.  
  2478.             if post_data is None:
  2479.                   logging.error("Brak danych do konfiguracji po pobraniu moda. Nie mogę zakończyć instalacji.")
  2480.                   QMessageBox.critical(self, "Błąd instalacji moda", "Wystąpił wewnętrzny błąd po pobraniu moda. Spróbuj ponownie.")
  2481.                   return
  2482.  
  2483.             mod_name = post_data.get("mod_name", "Mod")
  2484.             mod_id = post_data.get("mod_id")
  2485.             parent_dialog = post_data.get("parent_dialog")
  2486.  
  2487.             self._post_mod_install_data = None
  2488.  
  2489.             if success:
  2490.                 logging.info(f"Mod '{mod_name}' zainstalowany pomyślnie.")
  2491.                 QMessageBox.information(parent_dialog, "Sukces", f"Mod '{mod_name}' zainstalowany pomyślnie!")
  2492.                 if mod_id is not None:
  2493.                      for i in range(self.mod_list.count()):
  2494.                           item = self.mod_list.item(i)
  2495.                           item_data = item.data(Qt.ItemDataRole.UserRole)
  2496.                           if item_data and item_data.get('mod', {}).get('id') == mod_id:
  2497.                                self.mod_list.setCurrentItem(item)
  2498.                                self.show_mod_details(item)
  2499.                                break
  2500.             else:
  2501.                 logging.warning(f"Instalacja moda '{mod_name}' anulowana lub zakończona z błędami.")
  2502.                 QMessageBox.warning(parent_dialog, "Instalacja anulowana", f"Instalacja moda '{mod_name}' została anulowana lub napotkała błędy.")
  2503.  
  2504.     def remove_mod(self):
  2505.         if not self.current_mod or not self.selected_compatible_file:
  2506.             QMessageBox.warning(self, "Błąd", "Proszę wybrać mod do usunięcia.")
  2507.             return
  2508.  
  2509.         mod_name_display = self.current_mod.get("name", "Wybrany mod")
  2510.         mod_file_name = self.selected_compatible_file.get("fileName")
  2511.  
  2512.         if not mod_file_name:
  2513.             QMessageBox.warning(self, "Błąd", "Nie można określić nazwy pliku moda do usunięcia.")
  2514.             return
  2515.  
  2516.         mod_path = Path(self.instance_dir) / "mods" / mod_file_name
  2517.         if not mod_path.exists():
  2518.              QMessageBox.warning(self, "Błąd usuwania", f"Plik moda '{mod_file_name}' nie znaleziono w katalogu instancji.\nMożliwe, że został już usunięty lub instalacja nie była kompletna.")
  2519.              self.remove_button.setEnabled(False)
  2520.              return
  2521.  
  2522.         reply = QMessageBox.question(self, "Potwierdzenie usunięcia",
  2523.                                      f"Usunąć mod '{mod_name_display}' ({mod_file_name})?",
  2524.                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  2525.         if reply == QMessageBox.StandardButton.No:
  2526.             return
  2527.  
  2528.         try:
  2529.             self.launcher.remove_mod(mod_file_name, self.instance_dir)
  2530.             QMessageBox.information(self, "Sukces", f"Usunięto mod: {mod_file_name}")
  2531.             self.remove_button.setEnabled(False)
  2532.         except FileNotFoundError:
  2533.              QMessageBox.warning(self, "Błąd usuwania", f"Plik moda nie znaleziono w katalogu: {mod_file_name}")
  2534.              self.remove_button.setEnabled(False)
  2535.         except IOError as e:
  2536.              QMessageBox.critical(self, "Błąd usuwania", f"Wystąpił błąd podczas usuwania pliku:\n{e}")
  2537.         except Exception as e:
  2538.             logging.error(f"Nieoczekiwany błąd podczas usuwania moda {mod_file_name}: {e}")
  2539.             QMessageBox.critical(self, "Błąd usuwania", f"Wystąpił nieoczekiwany błąd podczas usuwania moda:\n{e}")
  2540.  
  2541.  
  2542. class LauncherWindow(QMainWindow):
  2543.     def __init__(self):
  2544.         super().__init__()
  2545.         self.launcher = MinecraftLauncher()
  2546.         self.setWindowTitle("Paffcio's Minecraft Launcher")
  2547.         self.setGeometry(100, 100, 900, 650)
  2548.         self.selected_instance_dir = None
  2549.         self.init_ui()
  2550.         self.apply_theme()
  2551.         self.update_instance_tiles()
  2552.  
  2553.     def init_ui(self):
  2554.         main_widget = QWidget()
  2555.         self.setCentralWidget(main_widget)
  2556.         main_layout = QVBoxLayout(main_widget)
  2557.         main_layout.setContentsMargins(10, 10, 10, 10)
  2558.         main_layout.setSpacing(10)
  2559.  
  2560.         content_layout = QHBoxLayout()
  2561.         content_layout.setSpacing(10)
  2562.  
  2563.         sidebar = QWidget()
  2564.         sidebar.setMinimumWidth(200)
  2565.         sidebar.setMaximumWidth(300)
  2566.         sidebar_layout = QVBoxLayout(sidebar)
  2567.         sidebar_layout.setContentsMargins(0, 0, 0, 0)
  2568.         sidebar_layout.setSpacing(5)
  2569.  
  2570.         sidebar_layout.addWidget(QLabel("Twoje instancje:"))
  2571.         self.instance_list = QListWidget()
  2572.         self.instance_list.itemSelectionChanged.connect(self.handle_instance_selection_change)
  2573.         self.instance_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
  2574.         sidebar_layout.addWidget(self.instance_list)
  2575.  
  2576.         instance_actions_layout = QVBoxLayout()
  2577.         instance_actions_layout.setSpacing(5)
  2578.  
  2579.         self.play_button = QPushButton("Graj")
  2580.         self.play_button.clicked.connect(self.play_instance)
  2581.         self.play_button.setEnabled(False)
  2582.         instance_actions_layout.addWidget(self.play_button)
  2583.  
  2584.         self.mod_browser_button = QPushButton("Przeglądaj mody")
  2585.         self.mod_browser_button.clicked.connect(self.open_mod_browser)
  2586.         self.mod_browser_button.setEnabled(False)
  2587.         instance_actions_layout.addWidget(self.mod_browser_button)
  2588.  
  2589.         self.delete_instance_button = QPushButton("Usuń instancję")
  2590.         self.delete_instance_button.clicked.connect(self.delete_instance)
  2591.         self.delete_instance_button.setEnabled(False)
  2592.         self.delete_instance_button.setStyleSheet("background-color: #f44336;")
  2593.         self.delete_instance_button.setStyleSheet(self.delete_instance_button.styleSheet() + """
  2594.            QPushButton:hover { background-color: #d32f2f; }
  2595.            QPushButton:disabled { background-color: #cccccc; }
  2596.        """)
  2597.         instance_actions_layout.addWidget(self.delete_instance_button)
  2598.  
  2599.         sidebar_layout.addLayout(instance_actions_layout)
  2600.  
  2601.         content_layout.addWidget(sidebar, 1)
  2602.  
  2603.         main_content_area = QWidget()
  2604.         main_content_layout = QVBoxLayout(main_content_area)
  2605.         main_content_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
  2606.         main_content_layout.addStretch(1)
  2607.         self.main_info_label = QLabel("Wybierz instancję z listy po lewej lub stwórz nową instancję (Plik -> Nowa instancja...).")
  2608.         self.main_info_label.setWordWrap(True)
  2609.         self.main_info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
  2610.         main_content_layout.addWidget(self.main_info_label)
  2611.         main_content_layout.addStretch(2)
  2612.  
  2613.         content_layout.addWidget(main_content_area, 3)
  2614.  
  2615.         main_layout.addLayout(content_layout)
  2616.  
  2617.         self.status_label = QLabel("Gotowy.")
  2618.         self.status_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
  2619.         main_layout.addWidget(self.status_label)
  2620.  
  2621.         menubar = self.menuBar()
  2622.         file_menu = menubar.addMenu("Plik")
  2623.         file_menu.addAction("Nowa instancja...", self.create_instance)
  2624.         file_menu.addSeparator()
  2625.         file_menu.addAction("Importuj instancję...", self.import_instance)
  2626.         file_menu.addAction("Eksportuj wybraną instancję...", self.export_instance)
  2627.         file_menu.addSeparator()
  2628.         file_menu.addAction("Zamknij", self.close)
  2629.  
  2630.         settings_menu = menubar.addMenu("Ustawienia")
  2631.         settings_menu.addAction("Ustawienia launchera...", self.open_settings)
  2632.  
  2633.         accounts_menu = menubar.addMenu("Konta")
  2634.         accounts_menu.addAction("Ustaw nazwę konta offline...", self.set_offline_account)
  2635.  
  2636.     def apply_theme(self):
  2637.         theme = self.launcher.settings.get("theme", "Light")
  2638.         if theme == "Light":
  2639.             self.setStyleSheet(STYLESHEET)
  2640.         else:
  2641.             dark_stylesheet = STYLESHEET + """
  2642.            QMainWindow, QDialog, QWidget {
  2643.                background-color: #2e2e2e;
  2644.                color: #cccccc;
  2645.            }
  2646.            QLabel {
  2647.                color: #cccccc;
  2648.            }
  2649.            QListWidget {
  2650.                background-color: #3a3a3a;
  2651.                color: #cccccc;
  2652.                border: 1px solid #555555;
  2653.            }
  2654.            QListWidget::item:selected {
  2655.                background-color: #5a5a5a;
  2656.                color: #ffffff;
  2657.            }
  2658.            QLineEdit, QComboBox, QTextEdit {
  2659.                background-color: #4a4a4a;
  2660.                color: #cccccc;
  2661.                border: 1px solid #666666;
  2662.            }
  2663.            QTextEdit {
  2664.                background-color: #3a3a3a;
  2665.                border: 1px solid #555555;
  2666.            }
  2667.            QPushButton {
  2668.                background-color: #4CAF50;
  2669.                color: white;
  2670.            }
  2671.            QPushButton:hover {
  2672.                background-color: #45a049;
  2673.            }
  2674.            QPushButton:disabled {
  2675.                background-color: #555555;
  2676.                color: #aaaaaa;
  2677.            }
  2678.            QPushButton[deleteButton="true"] {
  2679.                background-color: #c62828;
  2680.            }
  2681.            QPushButton[deleteButton="true"]:hover {
  2682.                background-color: #d32f2f;
  2683.            }
  2684.            QPushButton[deleteButton="true"]:disabled {
  2685.                background-color: #555555;
  2686.            }
  2687.            QProgressBar {
  2688.                background-color: #555555;
  2689.                border: 1px solid #666666;
  2690.            }
  2691.            QProgressBar::chunk {
  2692.                background-color: #4CAF50;
  2693.            }
  2694.            QScrollArea {
  2695.                border: none;
  2696.            }
  2697.            """
  2698.             self.setStyleSheet(dark_stylesheet)
  2699.        
  2700.         # Ustaw atrybut deleteButton dla przycisków usuwania
  2701.         self.delete_instance_button.setProperty("deleteButton", "true")
  2702.         self.delete_instance_button.style().unpolish(self.delete_instance_button)
  2703.         self.delete_instance_button.style().polish(self.delete_instance_button)
  2704.  
  2705.     def update_instance_tiles(self):
  2706.         logging.info("Odświeżanie listy instancji...")
  2707.         current_selection_path = self.selected_instance_dir
  2708.         self.instance_list.clear()
  2709.         self.selected_instance_dir = None
  2710.  
  2711.         instances = self.launcher.get_instance_list()
  2712.  
  2713.         if not instances:
  2714.              self.status_label.setText("Brak instancji. Stwórz nową (Plik -> Nowa instancja...).")
  2715.              self.main_info_label.setText("Brak instancji. Stwórz nową instancję (Plik -> Nowa instancja...).")
  2716.              self.play_button.setEnabled(False)
  2717.              self.mod_browser_button.setEnabled(False)
  2718.              self.delete_instance_button.setEnabled(False)
  2719.              return
  2720.  
  2721.         found_selected_index = -1
  2722.         for i, (name, path) in enumerate(instances):
  2723.             item = QListWidgetItem(name)
  2724.             item.setData(Qt.ItemDataRole.UserRole, path)
  2725.             self.instance_list.addItem(item)
  2726.             if path == current_selection_path:
  2727.                  found_selected_index = i
  2728.  
  2729.         logging.info(f"Znaleziono {len(instances)} instancji.")
  2730.         self.status_label.setText(f"Znaleziono {len(instances)} instancji.")
  2731.         self.main_info_label.setText("Wybierz instancję z listy po lewej lub stwórz nową instancję (Plik -> Nowa instancja...).")
  2732.  
  2733.         if found_selected_index != -1:
  2734.             self.instance_list.setCurrentRow(found_selected_index)
  2735.         else:
  2736.             if self.instance_list.count() > 0:
  2737.                 self.instance_list.setCurrentRow(0)
  2738.             else:
  2739.                 self.play_button.setEnabled(False)
  2740.                 self.mod_browser_button.setEnabled(False)
  2741.                 self.delete_instance_button.setEnabled(False)
  2742.  
  2743.     def handle_instance_selection_change(self):
  2744.         current_item = self.instance_list.currentItem()
  2745.         if current_item:
  2746.              self.load_instance(current_item)
  2747.         else:
  2748.              self.selected_instance_dir = None
  2749.              self.play_button.setEnabled(False)
  2750.              self.mod_browser_button.setEnabled(False)
  2751.              self.delete_instance_button.setEnabled(False)
  2752.              self.status_label.setText("Gotowy.")
  2753.  
  2754.     def create_instance(self):
  2755.         if self.launcher.current_download_thread or self.launcher.download_queue:
  2756.              QMessageBox.warning(self, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Poczekaj na jego zakończenie lub anuluj.")
  2757.              return
  2758.  
  2759.         dialog = CreateInstanceDialog(self.launcher, self)
  2760.         if dialog.exec():
  2761.             data = dialog.get_data()
  2762.             try:
  2763.                 self.launcher.create_instance(
  2764.                     name=data["name"],
  2765.                     version_id=data["version"],
  2766.                     modloader=data["modloader"],
  2767.                     ram=data["ram"],
  2768.                     java_path_setting=data["java_path_setting"],
  2769.                     jvm_args_extra=data["jvm_args_extra"],
  2770.                     base_instance_dir_input=data["base_instance_dir_input"],
  2771.                     parent_window=self
  2772.                 )
  2773.  
  2774.             except (ValueError, FileExistsError, FileNotFoundError, ConnectionError, PermissionError, Exception) as e:
  2775.                 error_title = "Błąd tworzenia instancji"
  2776.                 if isinstance(e, FileExistsError):
  2777.                      error_title = "Katalog instancji już istnieje"
  2778.                 elif isinstance(e, FileNotFoundError):
  2779.                      error_title = "Wymagany plik/folder nie znaleziono"
  2780.                 elif isinstance(e, ConnectionError):
  2781.                      error_title = "Błąd połączenia sieciowego"
  2782.                 elif isinstance(e, PermissionError):
  2783.                      error_title = "Błąd uprawnień (klucz API?)"
  2784.  
  2785.                 QMessageBox.critical(self, error_title, f"Nie udało się przygotować instancji:\n{e}")
  2786.                 self.update_instance_tiles()
  2787.  
  2788.     def import_instance(self):
  2789.         file, _ = QFileDialog.getOpenFileName(self, "Importuj instancję", "", "Archiwa ZIP (*.zip);;Wszystkie pliki (*)")
  2790.         if file:
  2791.             if self.launcher.current_download_thread or self.launcher.download_queue:
  2792.                  QMessageBox.warning(self, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Poczekaj na jego zakończenie lub anuluj.")
  2793.                  return
  2794.  
  2795.             try:
  2796.                 imported_dir = self.launcher.import_instance(file)
  2797.                 QMessageBox.information(self, "Sukces", f"Instancja zaimportowana pomyślnie do:\n{imported_dir}")
  2798.                 self.update_instance_tiles()
  2799.             except (FileNotFoundError, zipfile.BadZipFile, ValueError, Exception) as e:
  2800.                  error_title = "Błąd importu instancji"
  2801.                  if isinstance(e, FileNotFoundError):
  2802.                      error_title = "Plik nie znaleziono"
  2803.                  elif isinstance(e, zipfile.BadZipFile):
  2804.                      error_title = "Nieprawidłowy plik ZIP"
  2805.                  QMessageBox.critical(self, error_title, f"Nie udało się zaimportować instancji:\n{e}")
  2806.  
  2807.     def export_instance(self):
  2808.         current_item = self.instance_list.currentItem()
  2809.         if not current_item or not self.selected_instance_dir:
  2810.             QMessageBox.warning(self, "Brak wybranej instancji", "Proszę wybrać instancję do eksportu.")
  2811.             return
  2812.  
  2813.         instance_name = current_item.text()
  2814.         instance_dir_path = self.selected_instance_dir
  2815.  
  2816.         if not Path(instance_dir_path).exists():
  2817.              self.update_instance_tiles()
  2818.              return
  2819.  
  2820.         default_filename = f"{instance_name}.zip"
  2821.         start_dir = str(Path(instance_dir_path).parent)
  2822.         if not Path(start_dir).exists():
  2823.              start_dir = str(Path.home())
  2824.  
  2825.         file, _ = QFileDialog.getSaveFileName(self, f"Eksportuj instancję '{instance_name}'", os.path.join(start_dir, default_filename), "Archiwa ZIP (*.zip);;Wszystkie pliki (*)")
  2826.         if file:
  2827.             if not file.lower().endswith('.zip'):
  2828.                  file += '.zip'
  2829.             try:
  2830.                 self.launcher.export_instance(instance_dir_path, file)
  2831.                 QMessageBox.information(self, "Sukces", f"Instancja '{instance_name}' wyeksportowana pomyślnie do:\n{file}")
  2832.             except (FileNotFoundError, IOError, Exception) as e:
  2833.                  error_title = "Błąd eksportu instancji"
  2834.                  if isinstance(e, FileNotFoundError):
  2835.                       error_title = "Katalog instancji nie znaleziono"
  2836.                  elif isinstance(e, IOError):
  2837.                       error_title = "Błąd zapisu pliku"
  2838.  
  2839.                  QMessageBox.critical(self, error_title, f"Nie udało się wyeksportować instancji '{instance_name}':\n{e}")
  2840.  
  2841.     def delete_instance(self):
  2842.         current_item = self.instance_list.currentItem()
  2843.         if not current_item or not self.selected_instance_dir:
  2844.             QMessageBox.warning(self, "Brak wybranej instancji", "Proszę wybrać instancję do usunięcia.")
  2845.             self.delete_instance_button.setEnabled(False)
  2846.             return
  2847.  
  2848.         instance_name = current_item.text()
  2849.         instance_dir_path = self.selected_instance_dir
  2850.         instance_dir = Path(instance_dir_path)
  2851.  
  2852.         if not instance_dir.exists():
  2853.              QMessageBox.warning(self, "Błąd", "Katalog instancji już nie istnieje. Odświeżam listę.")
  2854.              self.update_instance_tiles()
  2855.              return
  2856.  
  2857.         reply = QMessageBox.question(self, "Potwierdzenie usunięcia",
  2858.                                      f"Czy na pewno chcesz usunąć instancję '{instance_name}'?\n\nTa operacja jest nieodwracalna i usunie wszystkie pliki instancji (zapisy gry, mody, ustawienia itp.) z katalogu:\n{instance_dir_path}",
  2859.                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
  2860.                                      QMessageBox.StandardButton.No)
  2861.  
  2862.         if reply == QMessageBox.StandardButton.Yes:
  2863.             try:
  2864.                 logging.info(f"Usuwanie instancji: {instance_dir_path}")
  2865.                 shutil.rmtree(instance_dir)
  2866.                 logging.info("Instancja usunięta pomyślnie.")
  2867.                 QMessageBox.information(self, "Sukces", f"Instancja '{instance_name}' została usunięta.")
  2868.                 self.update_instance_tiles()
  2869.             except Exception as e:
  2870.                 logging.error(f"Błąd podczas usuwania instancji {instance_dir_path}: {e}")
  2871.                 QMessageBox.critical(self, "Błąd usuwania instancji", f"Nie udało się usunąć instancji '{instance_name}':\n{e}")
  2872.  
  2873.  
  2874.     def load_instance(self, item):
  2875.         instance_name = item.text()
  2876.         instance_dir_path = item.data(Qt.ItemDataRole.UserRole)
  2877.         logging.info(f"Wybrano instancję: '{instance_name}' w katalogu {instance_dir_path}")
  2878.  
  2879.         instance_dir = Path(instance_dir_path)
  2880.         if not instance_dir.exists():
  2881.             logging.error(f"Katalog instancji nie istnieje: {instance_dir_path}")
  2882.             self.instance_list.takeItem(self.instance_list.row(item))
  2883.             self.handle_instance_selection_change()
  2884.             return
  2885.  
  2886.         self.selected_instance_dir = instance_dir_path
  2887.  
  2888.         settings_path = instance_dir / "settings.json"
  2889.         has_settings = False
  2890.         version_id = None
  2891.  
  2892.         if settings_path.exists():
  2893.              try:
  2894.                  with settings_path.open("r", encoding='utf-8') as f:
  2895.                      settings = json.load(f)
  2896.                  version_id = settings.get("version")
  2897.                  has_settings = True
  2898.              except json.JSONDecodeError as e:
  2899.                  logging.error(f"Błąd odczytu settings.json dla instancji {instance_name}: {e}")
  2900.                  QMessageBox.critical(self, "Błąd ładowania instancji", f"Nie udało się odczytać ustawień instancji '{instance_name}'. Funkcje 'Graj' i 'Mody' mogą być niedostępne.")
  2901.              except Exception as e:
  2902.                  logging.error(f"Nieoczekiwany błąd podczas ładowania settings.json dla instancji {instance_name}: {e}")
  2903.                  QMessageBox.critical(self, "Błąd ładowania instancji", f"Wystąpił nieoczekiwany błąd podczas odczytu ustawień instancji '{instance_name}'.")
  2904.  
  2905.         self.play_button.setEnabled(has_settings and version_id is not None)
  2906.         self.mod_browser_button.setEnabled(has_settings and version_id is not None)
  2907.         self.delete_instance_button.setEnabled(True)
  2908.  
  2909.     def play_instance(self):
  2910.         current_item = self.instance_list.currentItem()
  2911.         instance_dir_path = self.selected_instance_dir
  2912.  
  2913.         if not current_item or not instance_dir_path:
  2914.             QMessageBox.warning(self, "Brak wybranej instancji", "Proszę wybrać instancję do uruchomienia.")
  2915.             self.play_button.setEnabled(False)
  2916.             return
  2917.  
  2918.         instance_name = current_item.text()
  2919.  
  2920.         if not Path(instance_dir_path).exists():
  2921.              QMessageBox.critical(self, "Błąd uruchamiania", "Katalog wybranej instancji nie istnieje! Odświeżam listę.")
  2922.              self.update_instance_tiles()
  2923.              return
  2924.  
  2925.         if self.launcher.current_download_thread or self.launcher.download_queue:
  2926.             QMessageBox.warning(self, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Proszę poczekać na jego zakończenie przed uruchomieniem gry.")
  2927.             return
  2928.  
  2929.         username = self.launcher.settings.get("default_account", DEFAULT_SETTINGS["default_account"])
  2930.         if not username or not username.strip():
  2931.              username, ok = QInputDialog.getText(self, "Nazwa gracza offline", "Wprowadź domyślną nazwę użytkownika offline:", text="Player")
  2932.              if not ok or not username.strip():
  2933.                   QMessageBox.warning(self, "Anulowano", "Nazwa gracza jest wymagana do uruchomienia gry offline.")
  2934.                   self.status_label.setText("Uruchomienie anulowane (brak nazwy gracza).")
  2935.                   return
  2936.              username = username.strip()
  2937.  
  2938.         try:
  2939.             self.status_label.setText(f"Uruchamiam instancję: {instance_name}...")
  2940.             self.launcher.launch_game(instance_dir_path, username)
  2941.             self.status_label.setText(f"Uruchomiono instancję: {instance_name}")
  2942.  
  2943.         except (FileNotFoundError, ValueError, RuntimeError, TimeoutError, Exception) as e:
  2944.             error_title = "Błąd uruchamiania"
  2945.             if isinstance(e, FileNotFoundError):
  2946.                  error_title = "Brak wymaganych plików"
  2947.             elif isinstance(e, ValueError):
  2948.                  error_title = "Błąd konfiguracji instancji"
  2949.             elif isinstance(e, TimeoutError):
  2950.                  error_title = "Przekroczono czas oczekiwania"
  2951.  
  2952.             logging.error(f"Błąd podczas uruchamiania instancji {instance_name}: {e}")
  2953.             QMessageBox.critical(self, error_title, f"Nie udało się uruchomić gry:\n{e}\nSprawdź logi launchera.")
  2954.             self.status_label.setText(f"Błąd uruchamiania instancji {instance_name}.")
  2955.  
  2956.     def open_mod_browser(self):
  2957.         current_item = self.instance_list.currentItem()
  2958.         instance_dir_path = self.selected_instance_dir
  2959.  
  2960.         if not current_item or not instance_dir_path:
  2961.             QMessageBox.warning(self, "Brak wybranej instancji", "Proszę wybrać instancję, dla której chcesz przeglądać mody.")
  2962.             self.mod_browser_button.setEnabled(False)
  2963.             return
  2964.  
  2965.         instance_name = current_item.text()
  2966.  
  2967.         if not Path(instance_dir_path).exists():
  2968.              QMessageBox.critical(self, "Błąd przeglądania modów", "Katalog wybranej instancji nie istnieje! Odświeżam listę.")
  2969.              self.update_instance_tiles()
  2970.              return
  2971.  
  2972.         settings_path = Path(instance_dir_path) / "settings.json"
  2973.         if not settings_path.exists():
  2974.              QMessageBox.warning(self, "Ustawienia instancji", f"Brak pliku settings.json dla instancji '{instance_name}'. Nie można przeglądać modów.")
  2975.              self.mod_browser_button.setEnabled(False)
  2976.              return
  2977.  
  2978.         try:
  2979.              with settings_path.open("r", encoding='utf-8') as f:
  2980.                  settings = json.load(f)
  2981.              version_id = settings.get("version")
  2982.  
  2983.              if not version_id:
  2984.                  QMessageBox.warning(self, "Wersja nieznana", f"Wersja gry dla instancji '{instance_name}' nie została poprawnie skonfigurowana. Nie można przeglądać modów.")
  2985.                  self.mod_browser_button.setEnabled(False)
  2986.                  return
  2987.  
  2988.              if self.launcher.current_download_thread or self.launcher.download_queue:
  2989.                   QMessageBox.warning(self, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Proszę poczekać na jego zakończenie.")
  2990.                   return
  2991.  
  2992.              dialog = ModBrowserDialog(self.launcher, version_id, instance_dir_path, self)
  2993.              dialog.exec()
  2994.  
  2995.         except json.JSONDecodeError as e:
  2996.              logging.error(f"Błąd odczytu settings.json dla instancji {instance_name}: {e}")
  2997.              QMessageBox.critical(self, "Błąd ładowania instancji", f"Nie udało się odczytać ustawień instancji '{instance_name}'.")
  2998.         except Exception as e:
  2999.             logging.error(f"Nieoczekiwany błąd podczas otwierania przeglądarki modów dla instancji {instance_name}: {e}")
  3000.             QMessageBox.critical(self, "Błąd przeglądania modów", f"Wystąpił nieoczekiwany błąd: {e}")
  3001.  
  3002.     def set_offline_account(self):
  3003.         current_username = self.launcher.settings.get("default_account", "")
  3004.         username, ok = QInputDialog.getText(self, "Ustaw nazwę konta offline", "Wprowadź domyślną nazwę użytkownika offline:", text=current_username)
  3005.         if ok and username:
  3006.             username = username.strip()
  3007.             if username:
  3008.                 self.launcher.settings["default_account"] = username
  3009.                 self.launcher.save_settings()
  3010.                 QMessageBox.information(self, "Ustawiono konto", f"Domyślne konto offline ustawione na: '{username}'.")
  3011.                 logging.info(f"Ustawiono domyślne konto offline: {username}")
  3012.             else:
  3013.                 self.launcher.settings["default_account"] = ""
  3014.                 self.launcher.save_settings()
  3015.                 QMessageBox.information(self, "Ustawiono konto", "Domyślne konto offline zostało zresetowane. Nazwa będzie pytana przy uruchomieniu lub użyta domyślna 'Player'.")
  3016.                 logging.info("Domyślne konto offline zresetowane.")
  3017.  
  3018.     def open_settings(self):
  3019.         dialog = QDialog(self)
  3020.         dialog.setWindowTitle("Ustawienia launchera")
  3021.         dialog.setMinimumWidth(400)
  3022.         layout = QVBoxLayout(dialog)
  3023.         layout.setSpacing(10)
  3024.  
  3025.         layout.addWidget(QLabel("Motyw interfejsu:"))
  3026.         theme_combo = QComboBox()
  3027.         theme_combo.addItems(["Light", "Night"])
  3028.         theme_combo.setCurrentText(self.launcher.settings.get("theme", "Light"))
  3029.         layout.addWidget(theme_combo)
  3030.  
  3031.         layout.addWidget(QLabel("Domyślna wersja Javy dla nowych instancji:"))
  3032.         java_combo = QComboBox()
  3033.         java_combo.addItem("Automatyczny wybór", userData="auto")
  3034.         sorted_java_versions = sorted(self.launcher.java_versions, key=lambda x: self.launcher.get_java_version_from_path(x[0]) or 0, reverse=True)
  3035.         for java_path, version in sorted_java_versions:
  3036.             major_v = self.launcher.get_java_version_from_path(java_path)
  3037.             java_combo.addItem(f"{version} (Java {major_v}) - {java_path}", userData=java_path)
  3038.  
  3039.         current_java_setting = self.launcher.settings.get("java_path", "auto")
  3040.         if current_java_setting.lower() == 'auto':
  3041.              java_combo.setCurrentText("Automatyczny wybór")
  3042.         else:
  3043.              found_index = java_combo.findData(current_java_setting)
  3044.              if found_index != -1:
  3045.                   java_combo.setCurrentIndex(found_index)
  3046.              else:
  3047.                   custom_item_text = f"Zapisana ścieżka: {current_java_setting} (Nieznana wersja)"
  3048.                   java_combo.addItem(custom_item_text, userData=current_java_setting)
  3049.                   java_combo.setCurrentIndex(java_combo.count() - 1)
  3050.                   logging.warning(f"Zapisana ścieżka Javy w ustawieniach nie znaleziona wśród automatycznie wykrytych: {current_java_setting}. Dodano jako opcję niestandardową.")
  3051.  
  3052.         layout.addWidget(java_combo)
  3053.  
  3054.         layout.addWidget(QLabel("Domyślna pamięć RAM (np. 4G, 2048M):"))
  3055.         ram_input = QLineEdit(self.launcher.settings.get("ram", DEFAULT_SETTINGS["ram"]))
  3056.         layout.addWidget(ram_input)
  3057.  
  3058.         layout.addWidget(QLabel("Domyślne dodatkowe argumenty JVM:"))
  3059.         jvm_args_input = QLineEdit(self.launcher.settings.get("jvm_args", DEFAULT_SETTINGS["jvm_args"]))
  3060.         layout.addWidget(jvm_args_input)
  3061.  
  3062.         fullscreen_check = QCheckBox("Domyślnie pełny ekran")
  3063.         fullscreen_check.setChecked(self.launcher.settings.get("fullscreen", DEFAULT_SETTINGS["fullscreen"]))
  3064.         layout.addWidget(fullscreen_check)
  3065.  
  3066.         layout.addWidget(QLabel("Domyślna rozdzielczość (np. 1280x720):"))
  3067.         resolution_input = QLineEdit(self.launcher.settings.get("resolution", DEFAULT_SETTINGS["resolution"]))
  3068.         layout.addWidget(resolution_input)
  3069.  
  3070.         current_account_label = QLabel(f"Domyślne konto offline: {self.launcher.settings.get('default_account', 'Brak')}")
  3071.         layout.addWidget(current_account_label)
  3072.  
  3073.         button_layout = QHBoxLayout()
  3074.         save_button = QPushButton("Zapisz ustawienia")
  3075.         save_button.clicked.connect(dialog.accept)
  3076.  
  3077.         cancel_button = QPushButton("Anuluj")
  3078.         cancel_button.clicked.connect(dialog.reject)
  3079.  
  3080.         button_layout.addStretch(1)
  3081.         button_layout.addWidget(save_button)
  3082.         button_layout.addWidget(cancel_button)
  3083.         layout.addLayout(button_layout)
  3084.  
  3085.         if dialog.exec():
  3086.             selected_theme = theme_combo.currentText()
  3087.  
  3088.             selected_java_index = java_combo.currentIndex()
  3089.             selected_java_path_data = java_combo.itemData(selected_java_index)
  3090.             selected_java_path_to_save = selected_java_path_data if selected_java_path_data is not None else "auto"
  3091.  
  3092.             selected_ram = ram_input.text().strip().upper()
  3093.             selected_jvm_args = jvm_args_input.text().strip()
  3094.             selected_fullscreen = fullscreen_check.isChecked()
  3095.             selected_resolution = resolution_input.text().strip()
  3096.  
  3097.             if not re.match(r"^\d+[MG]$", selected_ram):
  3098.                  QMessageBox.warning(dialog, "Nieprawidłowy format RAM", "Nieprawidłowy format pamięci RAM. Ustawienia nie zostały zapisane.")
  3099.                  return
  3100.  
  3101.             if not re.match(r"^\d+x\d+$", selected_resolution):
  3102.                 QMessageBox.warning(dialog, "Nieprawidłowy format rozdzielczości", "Nieprawidłowy format rozdzielczości. Ustawienia nie zostały zapisane.")
  3103.                 return
  3104.  
  3105.             if selected_java_path_to_save != 'auto' and not Path(selected_java_path_to_save).exists():
  3106.                  QMessageBox.warning(dialog, "Nieprawidłowa ścieżka Javy", f"Wybrana ścieżka Javy nie istnieje:\n{selected_java_path_to_save}. Ustawienia nie zostały zapisane.")
  3107.                  return
  3108.  
  3109.             self.launcher.settings["theme"] = selected_theme
  3110.             self.launcher.settings["java_path"] = selected_java_path_to_save
  3111.             self.launcher.settings["ram"] = selected_ram
  3112.             self.launcher.settings["jvm_args"] = selected_jvm_args
  3113.             self.launcher.settings["fullscreen"] = selected_fullscreen
  3114.             self.launcher.settings["resolution"] = selected_resolution
  3115.  
  3116.             self.launcher.save_settings()
  3117.  
  3118.             self.apply_theme()
  3119.             logging.info("Ustawienia launchera zaktualizowane.")
  3120.             QMessageBox.information(self, "Sukces", "Ustawienia zostały zapisane.")
  3121.  
  3122.     def closeEvent(self, event):
  3123.         if self.launcher.progress_dialog and self.launcher.progress_dialog.isVisible():
  3124.             reply = QMessageBox.question(self, "Zamknąć?",
  3125.                                          "Pobieranie wciąż trwa. Czy na pewno chcesz zamknąć launcher i anulować pobieranie?",
  3126.                                          QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  3127.             if reply == QMessageBox.StandardButton.Yes:
  3128.                 self.launcher.progress_dialog.cancel_downloads()
  3129.                 event.accept()
  3130.             else:
  3131.                 event.ignore()
  3132.         else:
  3133.             event.accept()
  3134.  
  3135. if __name__ == "__main__":
  3136.     signal.signal(signal.SIGINT, signal.SIG_DFL)
  3137.  
  3138.     app = QApplication(sys.argv)
  3139.     window = LauncherWindow()
  3140.     window.show()
  3141.     sys.exit(app.exec())
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement