Advertisement
PaffcioStudio

Untitled

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