Advertisement
PaffcioStudio

Untitled

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