Advertisement
PaffcioStudio

Untitled

May 11th, 2025
308
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 158.58 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 _extract_natives(self, version_id, instance_dir):
  797.         version_dir = Path(instance_dir) / "versions" / version_id
  798.         natives_zip_dir = version_dir / "natives_zips"
  799.         natives_dir = version_dir / "natives"
  800.        
  801.         # Usuń istniejący katalog natives, jeśli istnieje
  802.         if natives_dir.exists():
  803.             try:
  804.                 shutil.rmtree(natives_dir)
  805.                 logging.debug(f"Usunięto istniejący katalog natives: {natives_dir}")
  806.             except Exception as e:
  807.                 logging.warning(f"Nie udało się usunąć katalogu natives {natives_dir}: {e}")
  808.        
  809.         natives_dir.mkdir(parents=True, exist_ok=True)
  810.         logging.info(f"Rozpakowywanie natywnych bibliotek do: {natives_dir}")
  811.        
  812.         extracted_count = 0
  813.         zip_files = []
  814.        
  815.         if natives_zip_dir.exists():
  816.             zip_files = list(natives_zip_dir.glob("*.zip"))
  817.             if not zip_files:
  818.                 logging.warning(f"Brak plików ZIP natywnych bibliotek w {natives_zip_dir}")
  819.            
  820.             for zip_path in zip_files:
  821.                 try:
  822.                     with zipfile.ZipFile(zip_path, "r") as zip_ref:
  823.                         allowed_extensions = ('.dll', '.so', '.dylib')
  824.                         excluded_names = ('META-INF',)
  825.                        
  826.                         for member_info in zip_ref.infolist():
  827.                             member_path = Path(member_info.filename)
  828.                             is_excluded_path = any(part in member_path.parts for part in excluded_names)
  829.                             if member_path.suffix.lower() in allowed_extensions and not is_excluded_path:
  830.                                 try:
  831.                                     zip_ref.extract(member_info, natives_dir)
  832.                                     extracted_count += 1
  833.                                     logging.debug(f"Rozpakowano: {member_info.filename} z {zip_path.name}")
  834.                                 except Exception as extract_e:
  835.                                     logging.error(f"Błąd rozpakowywania {member_info.filename} z {zip_path.name}: {extract_e}")
  836.                    
  837.                     logging.info(f"Przetworzono plik ZIP: {zip_path.name}")
  838.                
  839.                 except zipfile.BadZipFile:
  840.                     logging.error(f"Uszkodzony plik ZIP: {zip_path}. Pomijam.")
  841.                 except Exception as e:
  842.                     logging.error(f"Błąd przetwarzania {zip_path}: {e}")
  843.        
  844.         # Czyszczenie po rozpakowaniu
  845.         for zip_path in zip_files:
  846.             try:
  847.                 zip_path.unlink(missing_ok=True)
  848.                 logging.debug(f"Usunięto plik ZIP: {zip_path}")
  849.             except Exception as e:
  850.                 logging.warning(f"Nie udało się usunąć pliku ZIP {zip_path}: {e}")
  851.        
  852.         if natives_zip_dir.exists() and not list(natives_zip_dir.iterdir()):
  853.             try:
  854.                 natives_zip_dir.rmdir()
  855.                 logging.debug(f"Usunięto pusty katalog natives_zips: {natives_zip_dir}")
  856.             except Exception as e:
  857.                 logging.warning(f"Nie udało się usunąć katalogu natives_zips {natives_zip_dir}: {e}")
  858.        
  859.         logging.info(f"Zakończono rozpakowywanie natywnych bibliotek. Rozpakowano: {extracted_count} plików")
  860.         if extracted_count == 0:
  861.             logging.warning("Nie rozpakowano żadnych natywnych bibliotek. Sprawdź, czy pliki ZIP były dostępne i poprawne.")
  862.        
  863.         return extracted_count
  864.  
  865.     def validate_modloader(self, modloader, version_id):
  866.         if not version_id: return False
  867.  
  868.         is_snapshot = re.match(r"^\d+w\d+[a-z]$", version_id)
  869.         if is_snapshot:
  870.              if version_id not in self.logged_snapshots_modloader_warning:
  871.                  logging.warning(f"Wersja {version_id} to snapshot, wsparcie modloaderów jest ograniczone lub nieistniejące.")
  872.                  self.logged_snapshots_modloader_warning.add(version_id)
  873.              return False
  874.  
  875.         try:
  876.             parts = version_id.split('.')
  877.             major = int(parts[0])
  878.             minor = int(parts[1]) if len(parts) > 1 else 0
  879.             patch = int(parts[2]) if len(parts) > 2 else 0
  880.             version_tuple = (major, minor, patch)
  881.             version_tuple += (0,) * (3 - len(version_tuple))
  882.  
  883.         except ValueError:
  884.             logging.error(f"Nie można sparsować wersji Minecrafta '{version_id}' dla walidacji modloadera. Zakładam brak wsparcia.")
  885.             return False
  886.  
  887.         if modloader == "forge":
  888.             if version_tuple < (1, 5, 2): return False
  889.  
  890.         elif modloader == "neoforge":
  891.             if version_tuple < (1, 20, 1): return False
  892.  
  893.         elif modloader == "fabric":
  894.             if version_tuple < (1, 14, 0): return False
  895.  
  896.         elif modloader == "quilt":
  897.             if version_tuple < (1, 14, 0): return False
  898.  
  899.         return True
  900.  
  901.     def _queue_modloader_installer(self, modloader, version_id, instance_dir):
  902.         if not self.validate_modloader(modloader, version_id):
  903.             raise ValueError(f"{modloader.capitalize()} prawdopodobnie nie wspiera wersji {version_id}!")
  904.  
  905.         modloader_dir = Path(instance_dir) / "modloaders"
  906.         modloader_dir.mkdir(parents=True, exist_ok=True)
  907.         queued_count = 0
  908.         url = None
  909.         installer_name = None
  910.  
  911.         if modloader in ["forge", "neoforge"]:
  912.              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}")
  913.              pass
  914.  
  915.         elif modloader == "fabric":
  916.             installer_name = "fabric-installer.jar"
  917.             url = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/0.11.2/fabric-installer-0.11.2.jar"
  918.             installer_path = modloader_dir / installer_name
  919.             queued_count += self._queue_download(url, installer_path, "fabric installer")
  920.  
  921.         elif modloader == "quilt":
  922.             installer_name = "quilt-installer.jar"
  923.             url = "https://maven.quiltmc.org/repository/release/org/quiltmc/quilt-installer/0.10.0/quilt-installer-0.10.0.jar"
  924.             installer_path = modloader_dir / installer_name
  925.             queued_count += self._queue_download(url, installer_path, "quilt installer")
  926.  
  927.         else:
  928.             raise ValueError(f"Nieznany modloader: {modloader}")
  929.  
  930.         return queued_count
  931.  
  932.  
  933.     def _run_modloader_installer(self, modloader, version_id, instance_dir):
  934.         modloader_dir = Path(instance_dir) / "modloaders"
  935.         installer_path = None
  936.         installer_args = []
  937.         success_message = ""
  938.         error_message = ""
  939.  
  940.         java_path = self.find_java_for_version(version_id)
  941.         if not java_path or not Path(java_path).exists():
  942.              raise ValueError(f"Nie znaleziono kompatybilnej Javy dla wersji {version_id} lub ścieżka Javy jest niepoprawna. Zainstaluj Javę i/lub sprawdź ustawienia.")
  943.  
  944.         if modloader == "forge":
  945.             forge_installers = list(modloader_dir.glob("forge-*-installer.jar"))
  946.             if not forge_installers:
  947.                  raise FileNotFoundError(f"Nie znaleziono instalatora Forge (.jar) w katalogu instancji: {modloader_dir}. Pobierz go ręcznie i umieść w tym katalogu.")
  948.             if len(forge_installers) > 1:
  949.                  logging.warning(f"Znaleziono wiele plików instalatora Forge w {modloader_dir}. Używam pierwszego: {forge_installers[0].name}")
  950.             installer_path = forge_installers[0]
  951.             installer_args = ["--installClient", str(instance_dir)]
  952.             success_message = f"Zainstalowano Forge dla wersji {version_id}"
  953.             error_message = "Błąd instalacji Forge."
  954.  
  955.         elif modloader == "neoforge":
  956.              neoforge_installers = list(modloader_dir.glob("neoforge-*-installer.jar"))
  957.              if not neoforge_installers:
  958.                  raise FileNotFoundError(f"Nie znaleziono instalatora NeoForge (.jar) w katalogu instancji: {modloader_dir}. Pobierz go ręcznie i umieść w tym katalogu.")
  959.              if len(neoforge_installers) > 1:
  960.                   logging.warning(f"Znaleziono wiele plików instalatora NeoForge w {modloader_dir}. Używam pierwszego: {neoforge_installers[0].name}")
  961.              installer_path = neoforge_installers[0]
  962.              installer_args = ["--installClient", str(instance_dir)]
  963.              success_message = f"Zainstalowano NeoForge dla wersji {version_id}"
  964.              error_message = "Błąd instalacji NeoForge."
  965.  
  966.         elif modloader == "fabric":
  967.             installer_path = modloader_dir / "fabric-installer.jar"
  968.             installer_args = ["client", "-mcversion", version_id, "-dir", str(instance_dir)]
  969.             success_message = f"Zainstalowano Fabric dla wersji {version_id}"
  970.             error_message = "Błąd instalacji Fabric."
  971.  
  972.         elif modloader == "quilt":
  973.             installer_path = modloader_dir / "quilt-installer.jar"
  974.             installer_args = ["install", "client", version_id, "--install-dir", str(instance_dir)]
  975.             success_message = f"Zainstalowano Quilt dla wersji {version_id}"
  976.             error_message = "Błąd instalacji Quilt."
  977.  
  978.         else:
  979.             logging.error(f"Próba uruchomienia instalatora dla nieznanego modloadera: {modloader}")
  980.             return
  981.  
  982.         if installer_path is None or not installer_path.exists():
  983.              if modloader in ["fabric", "quilt"]:
  984.                   raise FileNotFoundError(f"Instalator {modloader.capitalize()} (.jar) nie znaleziono w katalogu: {installer_path}. Pobieranie mogło się nie udać.")
  985.              else:
  986.                   raise FileNotFoundError(f"Instalator modloadera nie znaleziono: {installer_path}")
  987.  
  988.         cmd = [java_path, "-jar", str(installer_path)] + installer_args
  989.         logging.info(f"Uruchamianie instalatora modloadera: {' '.join([str(c) for c in cmd])}")
  990.  
  991.         try:
  992.             result = subprocess.run(cmd, cwd=str(modloader_dir), capture_output=True, text=True, timeout=300)
  993.             logging.info(f"Instalator stdout:\n{result.stdout}")
  994.             logging.info(f"Instalator stderr:\n{result.stderr}")
  995.  
  996.             if result.returncode != 0:
  997.                  detailed_error = f"{error_message} Proces zakończył się kodem {result.returncode}.\nStderr:\n{result.stderr}"
  998.                  raise subprocess.CalledProcessError(result.returncode, cmd, output=result.stdout, stderr=result.stderr)
  999.  
  1000.             logging.info(success_message)
  1001.  
  1002.             if modloader in ["fabric", "quilt"]:
  1003.                  try:
  1004.                       installer_path.unlink()
  1005.                       logging.debug(f"Usunięto instalator: {installer_path}")
  1006.                  except Exception as e:
  1007.                       logging.warning(f"Nie udało się usunąć instalatora {installer_path}: {e}")
  1008.  
  1009.         except FileNotFoundError:
  1010.             logging.error(f"Plik wykonywalny instalatora lub Javy nie istnieje: {installer_path} lub {java_path}")
  1011.             raise FileNotFoundError(f"Plik wykonywalny instalatora modloadera lub Javy nie istnieje. Sprawdź ścieżki.")
  1012.         except subprocess.TimeoutExpired:
  1013.             logging.error(f"Instalator modloadera przekroczył czas oczekiwania (Timeout).")
  1014.             raise TimeoutError(f"Instalator modloadera przekroczył czas oczekiwania. Spróbuj ponownie lub zwiększ limit czasu.")
  1015.         except subprocess.CalledProcessError as e:
  1016.             logging.error(f"Instalator modloadera zakończył się błędem:\n{e.stderr}\n{e}")
  1017.             raise ValueError(f"Instalator modloadera zakończył się błędem (Kod: {e.returncode}). Sprawdź logi lub dane wyjściowe instalatora.")
  1018.         except Exception as e:
  1019.             logging.error(f"Nieoczekiwany błąd podczas uruchamiania instalatora modloadera: {e}")
  1020.             raise RuntimeError(f"Nieoczekiwany błąd podczas uruchamiania instalatora modloadera: {e}")
  1021.  
  1022.  
  1023.     def get_version_manifest(self):
  1024.         url = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"
  1025.         manifest_path = CONFIG_DIR / "version_manifest_v2.json"
  1026.         if manifest_path.exists():
  1027.              try:
  1028.                  with manifest_path.open("r", encoding='utf-8') as f:
  1029.                      manifest_data = json.load(f)
  1030.                  if datetime.fromtimestamp(manifest_path.stat().st_mtime) > datetime.now() - timedelta(hours=1):
  1031.                       logging.info("Używam cache version_manifest_v2.json")
  1032.                       return manifest_data
  1033.              except Exception as e:
  1034.                  logging.warning(f"Błąd odczytu cache manifestu wersji: {e}. Pobieram nowy.")
  1035.  
  1036.         try:
  1037.             logging.info(f"Pobieranie manifestu wersji z: {url}")
  1038.             response = requests.get(url, timeout=15)
  1039.             response.raise_for_status()
  1040.             manifest_data = response.json()
  1041.             try:
  1042.                  with manifest_path.open("w", encoding='utf-8') as f:
  1043.                      json.dump(manifest_data, f, indent=4)
  1044.                  logging.info("Zapisano version_manifest_v2.json do cache.")
  1045.             except Exception as e:
  1046.                  logging.warning(f"Błąd zapisu cache manifestu wersji: {e}")
  1047.  
  1048.             return manifest_data
  1049.         except requests.exceptions.RequestException as e:
  1050.             logging.error(f"Błąd pobierania manifestu wersji: {e}")
  1051.             if manifest_path.exists():
  1052.                  try:
  1053.                      with manifest_path.open("r", encoding='utf-8') as f:
  1054.                           logging.warning("Pobieranie nieudane, używam starego cache manifestu wersji.")
  1055.                           return json.load(f)
  1056.                  except Exception as e_cache:
  1057.                      logging.error(f"Błąd odczytu starego cache manifestu wersji: {e_cache}")
  1058.                      raise ConnectionError("Brak połączenia z internetem i brak dostępnego manifestu wersji!")
  1059.             else:
  1060.                 raise ConnectionError("Brak połączenia z internetem i brak dostępnego manifestu wersji!")
  1061.  
  1062.  
  1063.     def get_curseforge_mods(self, search_query, version_id):
  1064.         headers = {"x-api-key": CURSEFORGE_API_KEY}
  1065.         params = {"gameId": 432, "searchFilter": search_query, "minecraftVersion": version_id, "classId": 6, "sortField": 2}
  1066.         url = "https://api.curseforge.com/v1/mods/search"
  1067.         logging.info(f"Wyszukiwanie modów: '{search_query}' dla wersji {version_id}")
  1068.         try:
  1069.             response = requests.get(url, headers=headers, params=params, timeout=15)
  1070.             response.raise_for_status()
  1071.             data = response.json().get("data", [])
  1072.             logging.info(f"Znaleziono {len(data)} modów dla '{search_query}'")
  1073.             return data
  1074.         except requests.exceptions.RequestException as e:
  1075.             logging.error(f"Błąd wyszukiwania modów z CurseForge: {e}")
  1076.             if hasattr(e, 'response') and e.response is not None:
  1077.                 logging.error(f"CurseForge API Response status: {e.response.status_code}, body: {e.response.text}")
  1078.                 if e.response.status_code == 403:
  1079.                     raise PermissionError("Błąd API CurseForge: Klucz API jest nieprawidłowy lub brak dostępu.")
  1080.                 if e.response.status_code == 429:
  1081.                     raise requests.exceptions.RequestException("Błąd API CurseForge: Limit żądań przekroczony. Spróbuj ponownie później.")
  1082.  
  1083.             raise requests.exceptions.RequestException(f"Nie udało się wyszukać modów: {e}")
  1084.  
  1085.  
  1086.     def _queue_curseforge_mod_files(self, mod_id, version_id, instance_dir, download_dependencies=False, visited_mods=None):
  1087.         if visited_mods is None:
  1088.             visited_mods = set()
  1089.  
  1090.         if mod_id in visited_mods:
  1091.              logging.debug(f"Mod ID {mod_id} już przetworzony, pomijam.")
  1092.              return 0
  1093.  
  1094.         visited_mods.add(mod_id)
  1095.         logging.debug(f"Processing mod ID {mod_id} for version {version_id}")
  1096.  
  1097.         headers = {"x-api-key": CURSEFORGE_API_KEY}
  1098.         total_queued = 0
  1099.         try:
  1100.             files_url = f"https://api.curseforge.com/v1/mods/{mod_id}/files"
  1101.             response = requests.get(files_url, headers=headers, timeout=15)
  1102.             response.raise_for_status()
  1103.             files = response.json().get("data", [])
  1104.  
  1105.             compatible_file = None
  1106.             files.sort(key=lambda x: x.get('fileDate', '1970-01-01T00:00:00Z'), reverse=True)
  1107.  
  1108.             for file in files:
  1109.                 if version_id in file.get("gameVersions", []):
  1110.                     compatible_file = file
  1111.                     break
  1112.  
  1113.             if not compatible_file:
  1114.                 try:
  1115.                      mod_info_resp = requests.get(f"https://api.curseforge.com/v1/mods/{mod_id}", headers=headers, timeout=5)
  1116.                      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}"
  1117.                 except Exception:
  1118.                      mod_name = f"ID {mod_id}"
  1119.  
  1120.                 logging.warning(f"Brak kompatybilnego pliku moda dla {mod_name} (ID: {mod_id}) i wersji {version_id}.")
  1121.                 return 0
  1122.  
  1123.             mod_url = compatible_file.get("downloadUrl")
  1124.             mod_name = compatible_file.get("fileName")
  1125.             if not mod_url:
  1126.                  logging.warning(f"Download URL is null for mod file {mod_name} (Mod ID: {mod_id}). Skipping.")
  1127.                  return 0
  1128.  
  1129.             mod_path = Path(instance_dir) / "mods" / mod_name
  1130.             mod_path.parent.mkdir(parents=True, exist_ok=True)
  1131.  
  1132.             total_queued += self._queue_download(mod_url, mod_path, "mod")
  1133.  
  1134.             if download_dependencies:
  1135.                 logging.debug(f"Checking dependencies for mod ID {mod_id}")
  1136.                 for dep in compatible_file.get("dependencies", []):
  1137.                     if dep.get("relationType") == 3:
  1138.                         dep_mod_id = dep.get("modId")
  1139.                         if dep_mod_id:
  1140.                              logging.debug(f"Queueing required dependency ID {dep_mod_id} for mod ID {mod_id}")
  1141.                              total_queued += self._queue_curseforge_mod_files(dep_mod_id, version_id, instance_dir, download_dependencies=True, visited_mods=visited_mods)
  1142.                         else:
  1143.                             logging.warning(f"Dependency found with null modId for mod ID {mod_id}. Skipping.")
  1144.  
  1145.         except requests.exceptions.RequestException as e:
  1146.             logging.error(f"Błąd pobierania plików moda dla mod ID {mod_id}: {e}")
  1147.             return 0
  1148.         except Exception as e:
  1149.             logging.error(f"Nieoczekiwany błąd podczas kolejkowania plików moda dla mod ID {mod_id}: {e}")
  1150.             return 0
  1151.  
  1152.         return total_queued
  1153.  
  1154.  
  1155.     def remove_mod(self, mod_file_name, instance_dir):
  1156.         mod_path = Path(instance_dir) / "mods" / mod_file_name
  1157.         if mod_path.exists():
  1158.             try:
  1159.                 mod_path.unlink()
  1160.                 logging.info(f"Usunięto mod: {mod_path}")
  1161.             except Exception as e:
  1162.                 logging.error(f"Błąd usuwania moda {mod_path}: {e}")
  1163.                 raise IOError(f"Nie udało się usunąć moda: {e}")
  1164.         else:
  1165.             logging.warning(f"Mod {mod_file_name} nie istnieje w {instance_dir}/mods (pomijam usuwanie)")
  1166.             raise FileNotFoundError(f"Mod {mod_file_name} nie znaleziono w katalogu {instance_dir}/mods!")
  1167.  
  1168.     def extract_natives(self, instance_dir, version_id):
  1169.         """
  1170.        Rozpakowuje pliki JAR z natives_zips do folderu natives dla danej instancji.
  1171.        """
  1172.         natives_zips_dir = Path(instance_dir) / "versions" / version_id / "natives_zips"
  1173.         natives_dir = Path(instance_dir) / "versions" / version_id / "natives"
  1174.        
  1175.         # Tworzenie folderu natives, jeśli nie istnieje
  1176.         natives_dir.mkdir(parents=True, exist_ok=True)
  1177.         logging.debug(f"Tworzenie folderu natives: {natives_dir}")
  1178.  
  1179.         # Sprawdzenie, czy folder natives_zips istnieje
  1180.         if not natives_zips_dir.exists():
  1181.             logging.warning(f"Folder natives_zips nie istnieje: {natives_zips_dir}. Brak natives do rozpakowania.")
  1182.             return
  1183.  
  1184.         # Pobieranie listy plików JAR w natives_zips
  1185.         jar_files = list(natives_zips_dir.glob("*.jar"))
  1186.         if not jar_files:
  1187.             logging.warning(f"Brak plików JAR w folderze natives_zips: {natives_zips_dir}")
  1188.             return
  1189.  
  1190.         # Rozpakowywanie każdego pliku JAR
  1191.         for jar_file in jar_files:
  1192.             logging.debug(f"Rozpakowywanie pliku JAR: {jar_file}")
  1193.             try:
  1194.                 with zipfile.ZipFile(jar_file, "r") as zip_ref:
  1195.                     # Pobieranie listy plików w JAR, pomijając META-INF
  1196.                     file_list = [f for f in zip_ref.namelist() if not f.startswith("META-INF/")]
  1197.                     for file_name in file_list:
  1198.                         # Rozpakowywanie pliku do natives
  1199.                         zip_ref.extract(file_name, natives_dir)
  1200.                         logging.debug(f"Rozpakowano plik: {file_name} do {natives_dir}")
  1201.             except zipfile.BadZipFile:
  1202.                 logging.error(f"Uszkodzony plik JAR: {jar_file}. Pomijanie.")
  1203.                 continue
  1204.             except Exception as e:
  1205.                 logging.error(f"Błąd podczas rozpakowywania pliku JAR {jar_file}: {e}")
  1206.                 continue
  1207.  
  1208.         logging.info(f"Zakończono rozpakowywanie natives dla wersji {version_id} do {natives_dir}")
  1209.  
  1210.     def launch_game(self, instance_dir_path, username):
  1211.         import uuid
  1212.        
  1213.         instance_dir = Path(instance_dir_path)
  1214.         if not instance_dir.exists():
  1215.             raise FileNotFoundError(f"Katalog instancji nie istnieje: {instance_dir_path}")
  1216.  
  1217.         instance_settings_path = instance_dir / "settings.json"
  1218.         if not instance_settings_path.exists():
  1219.             raise FileNotFoundError(f"Plik ustawień instancji nie znaleziono: {instance_settings_path}")
  1220.        
  1221.         try:
  1222.             with instance_settings_path.open("r", encoding='utf-8') as f:
  1223.                 instance_settings = json.load(f)
  1224.         except json.JSONDecodeError as e:
  1225.             logging.error(f"Błąd odczytu settings.json instancji {instance_dir}: {e}")
  1226.             raise ValueError(f"Nie udało się odczytać ustawień instancji: {e}")
  1227.  
  1228.         version_id = instance_settings.get("version")
  1229.         if not version_id:
  1230.             raise ValueError("Instancja nie ma przypisanej wersji gry. Uruchom instalację instancji ponownie.")
  1231.  
  1232.         version_dir = instance_dir / "versions" / version_id
  1233.         version_json_path = version_dir / f"{version_id}.json"
  1234.         if not version_json_path.exists():
  1235.             raise FileNotFoundError(f"Plik wersji gry {version_json_path} nie istnieje. Uruchom instalację instancji ponownie.")
  1236.  
  1237.         try:
  1238.             with version_json_path.open("r", encoding='utf-8') as f:
  1239.                 version_data = json.load(f)
  1240.         except json.JSONDecodeError as e:
  1241.             logging.error(f"Błąd odczytu JSON wersji {version_id}: {e}")
  1242.             raise ValueError(f"Nie udało się odczytać danych wersji: {e}")
  1243.  
  1244.         # --- Budowanie polecenia startowego ---
  1245.  
  1246.         # 1. Java path
  1247.         java_path = self.find_java_for_version(version_id)
  1248.         if not java_path or not Path(java_path).exists():
  1249.             raise FileNotFoundError(f"Nie znaleziono kompatybilnej Javy dla wersji {version_id} lub ścieżka Javy jest niepoprawna.")
  1250.  
  1251.         # 2. Classpath
  1252.         classpath = []
  1253.         modloader = instance_settings.get("modloader")
  1254.         launch_version_id = version_id
  1255.  
  1256.         if modloader:
  1257.             modded_version_jsons = list((instance_dir / "versions").glob(f"{version_id}-*.json"))
  1258.             if modded_version_jsons:
  1259.                 modded_version_jsons.sort(key=lambda p: p.stat().st_mtime, reverse=True)
  1260.                 found_modded_json_path = modded_version_jsons[0]
  1261.                 launch_version_id = found_modded_json_path.stem
  1262.                 logging.info(f"Znaleziono modowany JSON wersji: {found_modded_json_path.name} (ID: {launch_version_id})")
  1263.                 try:
  1264.                     with found_modded_json_path.open("r", encoding='utf-8') as f:
  1265.                         version_data = json.load(f)
  1266.                 except json.JSONDecodeError as e:
  1267.                     logging.error(f"Błąd odczytu modowanego JSON {found_modded_json_path}: {e}")
  1268.                     raise ValueError(f"Nie udało się odczytać danych modowanej wersji: {e}")
  1269.  
  1270.         main_jar_path = instance_dir / "versions" / launch_version_id / f"{launch_version_id}.jar"
  1271.         if main_jar_path.exists():
  1272.             classpath.append(str(main_jar_path))
  1273.         else:
  1274.             client_jar_candidate = list(version_dir.glob("*.jar"))
  1275.             if client_jar_candidate:
  1276.                 main_jar_path = client_jar_candidate[0]
  1277.                 classpath.append(str(main_jar_path))
  1278.                 logging.warning(f"Główny JAR {launch_version_id}.jar nie istnieje, używam: {main_jar_path.name}")
  1279.             else:
  1280.                 raise FileNotFoundError(f"Plik główny gry (.jar) nie istnieje: {main_jar_path}.")
  1281.  
  1282.         for lib in version_data.get("libraries", []):
  1283.             if self._is_library_applicable(lib):
  1284.                 if "downloads" in lib and "artifact" in lib["downloads"]:
  1285.                     artifact = lib["downloads"]["artifact"]
  1286.                     lib_path = Path(LIBRARIES_DIR) / artifact["path"]
  1287.                     if lib_path.exists():
  1288.                         classpath.append(str(lib_path))
  1289.                     else:
  1290.                         logging.warning(f"Brak pliku biblioteki: {lib_path}")
  1291.                 elif "name" in lib:
  1292.                     parts = lib["name"].split(':')
  1293.                     if len(parts) >= 3:
  1294.                         group = parts[0].replace('.', '/')
  1295.                         artifact = parts[1]
  1296.                         version = parts[2]
  1297.                         guessed_path = Path(LIBRARIES_DIR) / group / artifact / version / f"{artifact}-{version}.jar"
  1298.                         if guessed_path.exists():
  1299.                             classpath.append(str(guessed_path))
  1300.                         else:
  1301.                             logging.warning(f"Brak zgadywanej biblioteki: {guessed_path}")
  1302.  
  1303.         classpath_str = ";".join(classpath) if sys.platform == "win32" else ":".join(classpath)
  1304.  
  1305.         # 3. JVM arguments
  1306.         jvm_args = []
  1307.         ram = instance_settings.get("ram", self.settings.get("ram", "4G"))
  1308.         jvm_args.extend([f"-Xmx{ram}", "-Xms512M"])
  1309.        
  1310.         natives_dir = version_dir / "natives"
  1311.         if natives_dir.exists():
  1312.             jvm_args.append(f"-Djava.library.path={natives_dir}")
  1313.         else:
  1314.             logging.warning(f"Katalog natywnych bibliotek nie istnieje: {natives_dir}")
  1315.  
  1316.         jvm_args_extra = instance_settings.get("jvm_args_extra", self.settings.get("jvm_args", ""))
  1317.         if jvm_args_extra:
  1318.             jvm_args.extend(jvm_args_extra.split())
  1319.  
  1320.         # 4. Game arguments
  1321.         game_args = []
  1322.         main_class = version_data.get("mainClass", "net.minecraft.client.main.Main")
  1323.        
  1324.         # Generowanie UUID dla trybu offline
  1325.         offline_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, username))
  1326.        
  1327.         # Rozpoznanie typu wersji
  1328.         version_tuple, modern_args = parse_version_type(version_id)
  1329.  
  1330.         # Argumenty w zależności od wersji
  1331.         if modern_args:
  1332.             game_args.extend([
  1333.                 "--username", username,
  1334.                 "--version", launch_version_id,
  1335.                 "--gameDir", str(instance_dir),
  1336.                 "--assetsDir", str(ASSETS_DIR),
  1337.                 "--assetIndex", version_data.get("assetIndex", {}).get("id", "legacy"),
  1338.                 "--uuid", offline_uuid,
  1339.                 "--accessToken", "0",
  1340.                 "--userType", "legacy"
  1341.             ])
  1342.         else:
  1343.             game_args.extend([
  1344.                 username,
  1345.                 "0"  # sessionId dla starszych wersji
  1346.             ])
  1347.  
  1348.         # Rozdzielczość i pełny ekran
  1349.         resolution = instance_settings.get("resolution", self.settings.get("resolution", "1280x720"))
  1350.         if resolution and re.match(r"^\d+x\d+$", resolution):
  1351.             width, height = resolution.split('x')
  1352.             game_args.extend(["--width", width, "--height", height])
  1353.        
  1354.         if instance_settings.get("fullscreen", self.settings.get("fullscreen", False)):
  1355.             game_args.append("--fullscreen")
  1356.  
  1357.         # 5. Budowanie pełnego polecenia
  1358.         cmd = [java_path] + jvm_args + ["-cp", classpath_str, main_class] + game_args
  1359.         logging.info(f"Uruchamianie gry: {' '.join([str(c) for c in cmd])}")
  1360.  
  1361.         try:
  1362.             process = subprocess.Popen(
  1363.                 cmd,
  1364.                 cwd=str(instance_dir),
  1365.                 stdout=subprocess.PIPE,
  1366.                 stderr=subprocess.PIPE,
  1367.                 text=True
  1368.             )
  1369.             stdout, stderr = process.communicate(timeout=300)
  1370.             logging.info(f"Gra stdout:\n{stdout}")
  1371.             if stderr:
  1372.                 logging.error(f"Gra stderr:\n{stderr}")
  1373.             if process.returncode != 0:
  1374.                 raise subprocess.CalledProcessError(process.returncode, cmd, stdout, stderr)
  1375.            
  1376.             logging.info(f"Gra uruchomiona pomyślnie (PID: {process.pid})")
  1377.        
  1378.         except subprocess.TimeoutExpired:
  1379.             logging.error("Uruchamianie gry przekroczyło limit czasu.")
  1380.             raise TimeoutError("Uruchamianie gry przekroczyło limit czasu.")
  1381.         except subprocess.CalledProcessError as e:
  1382.             logging.error(f"Błąd uruchamiania gry: Kod {e.returncode}, stderr: {e.stderr}")
  1383.             raise ValueError(f"Błąd uruchamiania gry: {e.stderr}")
  1384.         except Exception as e:
  1385.             logging.error(f"Nieoczekiwany błąd uruchamiania gry: {e}")
  1386.             raise RuntimeError(f"Nieoczekiwany błąd: {e}")
  1387.  
  1388.  
  1389.     def _is_library_applicable(self, library_data):
  1390.         rules = library_data.get('rules')
  1391.         if not rules:
  1392.             return True
  1393.  
  1394.         current_os_name = sys.platform
  1395.         if current_os_name == "win32":
  1396.             current_os_name = "windows"
  1397.         elif current_os_name == "darwin":
  1398.             current_os_name = "osx"
  1399.  
  1400.         for rule in rules:
  1401.             action = rule.get('action')
  1402.             os_info = rule.get('os', {})
  1403.             rule_os_name = os_info.get('name')
  1404.  
  1405.             rule_applies_to_current_os = False
  1406.             if rule_os_name is None:
  1407.                 rule_applies_to_current_os = True
  1408.             elif rule_os_name == current_os_name:
  1409.                  rule_applies_to_current_os = True
  1410.  
  1411.             if rule_applies_to_current_os:
  1412.                 if action == 'disallow':
  1413.                     logging.debug(f"Library rule disallowed: {library_data.get('name', 'Unknown')}")
  1414.                     return False
  1415.  
  1416.         return True
  1417.  
  1418.     def _is_argument_applicable(self, arg_data):
  1419.         rules = arg_data.get('rules')
  1420.         if not rules:
  1421.             return True
  1422.  
  1423.         current_os_name = sys.platform
  1424.         if current_os_name == "win32":
  1425.             current_os_name = "windows"
  1426.         elif current_os_name == "darwin":
  1427.             current_os_name = "osx"
  1428.  
  1429.         disallows_rule_applies = False
  1430.  
  1431.         for rule in rules:
  1432.             action = rule.get('action')
  1433.             os_info = rule.get('os', {})
  1434.             rule_os_name = os_info.get('name')
  1435.  
  1436.             rule_applies_to_current_os = False
  1437.             if rule_os_name is None:
  1438.                 rule_applies_to_current_os = True
  1439.             elif rule_os_name == current_os_name:
  1440.                  rule_applies_to_current_os = True
  1441.  
  1442.             if rule_applies_to_current_os:
  1443.                  if action == 'disallow':
  1444.                       disallows_rule_applies = True
  1445.                       break
  1446.  
  1447.         if disallows_rule_applies:
  1448.             return False
  1449.  
  1450.         return True
  1451.  
  1452.     def find_java(self):
  1453.         return self.java_versions[0][0] if self.java_versions else None
  1454.  
  1455.     def find_java_versions(self):
  1456.         java_versions = []
  1457.         checked_paths = set()
  1458.  
  1459.         try:
  1460.             java_path = "java"
  1461.             find_cmd = ["where", "java"] if sys.platform == "win32" else ["which", "java"]
  1462.             process = subprocess.run(find_cmd, capture_output=True, text=True, timeout=5, check=True)
  1463.             path_output = process.stdout.strip().splitlines()
  1464.             if path_output:
  1465.                  resolved_path = path_output[0]
  1466.                  if Path(resolved_path).is_file():
  1467.                       java_path = resolved_path
  1468.  
  1469.             result = subprocess.run([java_path, "-version"], capture_output=True, text=True, timeout=5, check=True)
  1470.             version_line = result.stderr.splitlines()[0] if result.stderr else ""
  1471.             if java_path not in checked_paths:
  1472.                  java_versions.append((java_path, f"System Java ({version_line.strip()})"))
  1473.                  checked_paths.add(java_path)
  1474.         except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError, Exception) as e:
  1475.             logging.debug(f"System 'java' not found or error: {e}")
  1476.  
  1477.         if sys.platform == "win32":
  1478.             program_files = Path(os.environ.get("ProgramFiles", "C:/Program Files"))
  1479.             program_files_x86 = Path(os.environ.get("ProgramFiles(x86)", "C:/Program Files (x86)"))
  1480.             java_install_dirs = [
  1481.                 program_files / "Java",
  1482.                 program_files_x86 / "Java",
  1483.                 JAVA_DIR
  1484.             ]
  1485.  
  1486.             for base_dir in java_install_dirs:
  1487.                  if not base_dir.exists():
  1488.                       continue
  1489.                  scan_dirs = [base_dir]
  1490.                  try:
  1491.                       for level1 in base_dir.iterdir():
  1492.                            if level1.is_dir():
  1493.                                 scan_dirs.append(level1)
  1494.                                 try:
  1495.                                      for level2 in level1.iterdir():
  1496.                                           if level2.is_dir():
  1497.                                                scan_dirs.append(level2)
  1498.                                 except Exception as e:
  1499.                                      logging.debug(f"Error scanning subdir {level1}: {e}")
  1500.                  except Exception as e:
  1501.                       logging.debug(f"Error scanning base dir {base_dir}: {e}")
  1502.  
  1503.  
  1504.                  for java_dir in scan_dirs:
  1505.                      if java_dir.is_dir():
  1506.                           java_exe = java_dir / "bin" / "java.exe"
  1507.                           if java_exe.exists() and str(java_exe) not in checked_paths:
  1508.                                try:
  1509.                                    result = subprocess.run([str(java_exe), "-version"], capture_output=True, text=True, timeout=5, check=True)
  1510.                                    version_line = result.stderr.splitlines()[0] if result.stderr else ""
  1511.                                    display_name = java_dir.relative_to(base_dir) if java_dir.is_relative_to(base_dir) else java_dir.name
  1512.                                    java_versions.append((str(java_exe), f"{display_name} ({version_line.strip()})"))
  1513.                                    checked_paths.add(str(java_exe))
  1514.                                except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError, Exception) as e:
  1515.                                    logging.debug(f"Error getting version for {java_exe}: {e}")
  1516.  
  1517.         elif sys.platform == "darwin":
  1518.              java_install_dirs = [
  1519.                   Path("/Library/Java/JavaVirtualMachines"),
  1520.                   Path("/usr/local/Cellar"),
  1521.                   Path.home() / ".sdkman" / "candidates" / "java",
  1522.                   JAVA_DIR
  1523.              ]
  1524.              for base_dir in java_install_dirs:
  1525.                  if not base_dir.exists():
  1526.                       continue
  1527.                  try:
  1528.                       for java_dir in base_dir.iterdir():
  1529.                            if java_dir.is_dir():
  1530.                                 java_exe = java_dir / "Contents" / "Home" / "bin" / "java"
  1531.                                 if java_exe.exists() and str(java_exe) not in checked_paths:
  1532.                                      try:
  1533.                                          result = subprocess.run([str(java_exe), "-version"], capture_output=True, text=True, timeout=5, check=True)
  1534.                                          version_line = result.stderr.splitlines()[0] if result.stderr else ""
  1535.                                          display_name = java_dir.name
  1536.                                          java_versions.append((str(java_exe), f"{display_name} ({version_line.strip()})"))
  1537.                                          checked_paths.add(str(java_exe))
  1538.                                      except Exception as e:
  1539.                                           logging.debug(f"Error getting version for {java_exe}: {e}")
  1540.                  except Exception as e:
  1541.                       logging.debug(f"Error scanning base dir {base_dir}: {e}")
  1542.  
  1543.         elif sys.platform.startswith("linux"):
  1544.              java_install_dirs = [
  1545.                  Path("/usr/lib/jvm"),
  1546.                  Path("/opt/java"),
  1547.                  Path.home() / ".sdkman" / "candidates" / "java",
  1548.                  JAVA_DIR
  1549.              ]
  1550.              for base_dir in java_install_dirs:
  1551.                  if not base_dir.exists():
  1552.                       continue
  1553.                  try:
  1554.                       for java_dir in base_dir.iterdir():
  1555.                            if java_dir.is_dir():
  1556.                                 java_exe = java_dir / "bin" / "java"
  1557.                                 if java_exe.exists() and str(java_exe) not in checked_paths:
  1558.                                      try:
  1559.                                          result = subprocess.run([str(java_exe), "-version"], capture_output=True, text=True, timeout=5, check=True)
  1560.                                          version_line = result.stderr.splitlines()[0] if result.stderr else ""
  1561.                                          display_name = java_dir.name
  1562.                                          java_versions.append((str(java_exe), f"{display_name} ({version_line.strip()})"))
  1563.                                          checked_paths.add(str(java_exe))
  1564.                                      except Exception as e:
  1565.                                           logging.debug(f"Error getting version for {java_exe}: {e}")
  1566.                  except Exception as e:
  1567.                       logging.debug(f"Error scanning base dir {base_dir}: {e}")
  1568.  
  1569.  
  1570.         logging.info(f"Znaleziono wersje Javy: {java_versions}")
  1571.         return java_versions
  1572.  
  1573.     def get_java_version_from_path(self, java_path):
  1574.          if not java_path or not Path(java_path).exists():
  1575.              return None
  1576.          try:
  1577.              result = subprocess.run([java_path, "-version"], capture_output=True, text=True, timeout=5, check=True)
  1578.              version_str = result.stderr.splitlines()[0] if result.stderr else ""
  1579.              match = re.search(r"(?:openjdk|java) version \"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version_str)
  1580.              if match:
  1581.                  major = int(match.group(1))
  1582.                  if major == 1 and match.group(2) is not None:
  1583.                       return int(match.group(2))
  1584.                  return major
  1585.              match = re.search(r"openjdk (\d+)(?:\.(\d+))?", version_str)
  1586.              if match:
  1587.                  return int(match.group(1))
  1588.              logging.warning(f"Nie można sparsować wersji Javy z: {version_str} dla {java_path}")
  1589.              return None
  1590.          except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError, Exception) as e:
  1591.              logging.error(f"Błąd podczas odczytu wersji Javy z {java_path}: {e}")
  1592.              return None
  1593.  
  1594.     def get_required_java_version(self, version_id):
  1595.         logging.debug(f"Sprawdzam wymaganą wersję Javy dla {version_id}")
  1596.  
  1597.         try:
  1598.              manifest = self.get_version_manifest()
  1599.              version_info_from_manifest = next((v for v in manifest.get("versions", []) if v["id"] == version_id), None)
  1600.              if version_info_from_manifest:
  1601.                   version_json_url = version_info_from_manifest.get("url")
  1602.                   if version_json_url:
  1603.                        response = requests.get(version_json_url, timeout=10)
  1604.                        response.raise_for_status()
  1605.                        version_data = response.json()
  1606.                        required_java_from_json = version_data.get("javaVersion", {}).get("majorVersion")
  1607.                        if required_java_from_json:
  1608.                            logging.debug(f"Wymagana Java z Version JSON dla {version_id}: {required_java_from_json}")
  1609.                            return required_java_from_json
  1610.                        else:
  1611.                            logging.debug(f"Version JSON dla {version_id} nie zawiera 'javaVersion'.")
  1612.                   else:
  1613.                        logging.warning(f"Manifest wersji dla {version_id} nie zawiera URL do version JSON.")
  1614.              else:
  1615.                   logging.warning(f"Wersja {version_id} nie znaleziona w manifeście wersji.")
  1616.         except Exception as e:
  1617.              logging.debug(f"Nie udało się pobrać/sparsować version JSON dla {version_id} w celu sprawdzenia Javy: {e}. Używam domyślnej logiki.")
  1618.  
  1619.  
  1620.         try:
  1621.             parts = version_id.split('.')
  1622.             if re.match(r"^\d+w\d+[a-z]$", version_id):
  1623.                 logging.debug(f"'{version_id}' to snapshot, szacuję wymaganą Javę.")
  1624.                 try:
  1625.                      year_week_match = re.match(r"^(\d+)w(\d+)", version_id)
  1626.                      if year_week_match:
  1627.                           year = int(year_week_match.group(1))
  1628.                           week = int(year_week_match.group(2))
  1629.                           if year >= 24:
  1630.                               return 21
  1631.                           elif year == 23 and week >= 14:
  1632.                                return 17
  1633.                      return 17
  1634.                 except Exception as e:
  1635.                      logging.warning(f"Błąd parsowania daty snapshota '{version_id}': {e}. Domyślnie Java 17.")
  1636.                      return 17
  1637.  
  1638.             if len(parts) >= 2:
  1639.                  major = int(parts[0])
  1640.                  minor = int(parts[1])
  1641.  
  1642.                  if major >= 2:
  1643.                       return 21
  1644.                  if major == 1:
  1645.                       if minor >= 21:
  1646.                            return 21
  1647.                       elif minor == 20 and (len(parts) < 3 or int(parts[2]) >= 5):
  1648.                            return 21
  1649.                       elif minor >= 18:
  1650.                            return 17
  1651.                       elif minor >= 17:
  1652.                            return 16
  1653.                       elif minor >= 13:
  1654.                            return 8
  1655.                       else:
  1656.                            return 8
  1657.             logging.warning(f"Nieobsługiwany format wersji gry '{version_id}' dla wymaganej Javy. Domyślnie Java 8.")
  1658.             return 8
  1659.  
  1660.         except Exception as e:
  1661.             logging.error(f"Nieoczekiwany błąd podczas określania wymaganej Javy dla '{version_id}': {e}. Domyślnie Java 8.")
  1662.             return 8
  1663.  
  1664.  
  1665.     def find_java_for_version(self, version_id):
  1666.         """
  1667.        Znajduje odpowiednią wersję Javy dla danej wersji Minecrafta.
  1668.        """
  1669.         # Mapowanie wymagań Javy
  1670.         java_requirements = {
  1671.             (1, 0): 8,   # Wersje 1.0-1.16.5 -> Java 8
  1672.             (1, 17): 17, # Wersje 1.17-1.20 -> Java 17
  1673.             (1, 21): 21, # Wersje 1.21+ i snapshoty -> Java 21
  1674.         }
  1675.  
  1676.         # Rozpoznanie typu wersji
  1677.         version_tuple, _ = parse_version_type(version_id)
  1678.         required_java = 8  # Domyślnie Java 8
  1679.  
  1680.         # Snapshoty z 2025 zakładamy jako >= 1.21
  1681.         if "w" in version_id:
  1682.             required_java = 21
  1683.         else:
  1684.             for (major, minor), java_ver in java_requirements.items():
  1685.                 if version_tuple >= (major, minor):
  1686.                     required_java = java_ver
  1687.  
  1688.         logging.info(f"Wersja {version_id} wymaga Javy {required_java}+")
  1689.  
  1690.         # Szukanie Javy
  1691.         possible_java_paths = [
  1692.             shutil.which("java"),
  1693.             r"C:\Program Files\Java\jdk-{}\bin\java.exe".format(required_java),
  1694.             r"C:\Program Files\Java\jre-{}\bin\java.exe".format(required_java),
  1695.             r"C:\Program Files\AdoptOpenJDK\jdk-{}-hotspot\bin\java.exe".format(required_java),
  1696.             r"/usr/lib/jvm/java-{}-openjdk/bin/java".format(required_java),
  1697.             r"/usr/lib/jvm/java-{}-openjdk-amd64/bin/java".format(required_java),
  1698.         ]
  1699.  
  1700.         for path in possible_java_paths:
  1701.             if path and Path(path).exists():
  1702.                 try:
  1703.                     result = subprocess.run(
  1704.                         [path, "-version"],
  1705.                         capture_output=True,
  1706.                         text=True,
  1707.                         check=True
  1708.                     )
  1709.                     version_match = re.search(r'version "(\d+)(?:\.(\d+))?', result.stderr)
  1710.                     if version_match:
  1711.                         java_major = int(version_match.group(1))
  1712.                         if java_major >= required_java:
  1713.                             logging.info(f"Znaleziono kompatybilną Javę ({java_major}): {path}")
  1714.                             return path
  1715.                         else:
  1716.                             logging.warning(f"Java {java_major} w {path} jest za stara, wymagana {required_java}")
  1717.                 except (subprocess.CalledProcessError, FileNotFoundError):
  1718.                     logging.warning(f"Ścieżka Javy {path} jest nieprawidłowa lub nie działa")
  1719.        
  1720.         # Fallback na dowolną Javę
  1721.         logging.warning(f"Nie znaleziono Javy {required_java}+, próbuję dowolnej wersji")
  1722.         for path in possible_java_paths:
  1723.             if path and Path(path).exists():
  1724.                 logging.info(f"Używam fallback Javy: {path}")
  1725.                 return path
  1726.        
  1727.         logging.error(f"Nie znaleziono żadnej wersji Javy dla wersji {version_id}")
  1728.         return None
  1729.  
  1730.  
  1731.     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):
  1732.         if not name:
  1733.             raise ValueError("Nazwa instancji nie może być pusta!")
  1734.         if not version_id:
  1735.             raise ValueError("Nie wybrano wersji Minecrafta!")
  1736.  
  1737.         if not re.match(r"^[a-zA-Z0-9._-]+$", version_id):
  1738.              raise ValueError(f"Nieprawidłowy format ID wersji: '{version_id}'.")
  1739.  
  1740.         if modloader and modloader.lower() not in ["forge", "neoforge", "fabric", "quilt"]:
  1741.              raise ValueError(f"Nieznany typ modloadera: '{modloader}'. Obsługiwane: Forge, NeoForge, Fabric, Quilt.")
  1742.  
  1743.         safe_name = re.sub(r'[<>:"/\\|?*]', '_', name)
  1744.         safe_name = safe_name.strip()
  1745.         if not safe_name:
  1746.              raise ValueError("Nazwa instancji po usunięciu nieprawidłowych znaków jest pusta.")
  1747.  
  1748.         if base_instance_dir_input and Path(base_instance_dir_input) != INSTANCES_DIR:
  1749.              instance_dir = Path(base_instance_dir_input)
  1750.              instance_dir = instance_dir / safe_name
  1751.  
  1752.              try:
  1753.                  resolved_instance_dir = instance_dir.resolve()
  1754.                  resolved_instances_dir = INSTANCES_DIR.resolve()
  1755.                  if resolved_instances_dir in resolved_instance_dir.parents or resolved_instance_dir == resolved_instances_dir:
  1756.                       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.")
  1757.              except ValueError:
  1758.                   raise
  1759.              except Exception as e:
  1760.                   logging.error(f"Błąd walidacji ścieżki instancji: {e}")
  1761.                   QMessageBox.critical(parent_window, "Błąd folderu instancji", f"Wystąpił błąd podczas walidacji ścieżki instancji: {e}")
  1762.                   raise
  1763.  
  1764.         else:
  1765.              instance_dir = INSTANCES_DIR / safe_name
  1766.  
  1767.         if instance_dir.exists():
  1768.              is_empty = not any(instance_dir.iterdir())
  1769.              if not is_empty:
  1770.                   raise FileExistsError(f"Katalog docelowy instancji '{instance_dir}' już istnieje i nie jest pusty! Wybierz inną nazwę lub folder.")
  1771.  
  1772.         instance_dir.mkdir(parents=True, exist_ok=True)
  1773.  
  1774.         if self.current_download_thread or self.download_queue:
  1775.             QMessageBox.warning(parent_window, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Spróbuj ponownie później.")
  1776.             return
  1777.  
  1778.         try:
  1779.             logging.info(f"Przygotowanie do pobierania plików dla instancji '{name}' ({version_id})")
  1780.             self.download_queue.clear()
  1781.  
  1782.             queued_version_files_count = self._queue_version_files(version_id, instance_dir)
  1783.  
  1784.             queued_modloader_files_count = 0
  1785.             if modloader:
  1786.                  if not self.validate_modloader(modloader, version_id):
  1787.                       raise ValueError(f"{modloader.capitalize()} prawdopodobnie nie wspiera wersji {version_id}. Wybierz inną wersję gry lub modloader.")
  1788.  
  1789.                  queued_modloader_files_count = self._queue_modloader_installer(modloader, version_id, instance_dir)
  1790.  
  1791.             total_queued_for_download = len(self.download_queue)
  1792.  
  1793.             if total_queued_for_download == 0:
  1794.                  logging.info("Wszystkie pliki do pobrania już istnieją lub nie wymagają pobierania. Przechodzę do konfiguracji.")
  1795.                  self._extract_natives(version_id, instance_dir)
  1796.                  if modloader:
  1797.                       self._run_modloader_installer(modloader, version_id, instance_dir)
  1798.                  self.save_instance_settings(instance_dir, name, version_id, modloader, ram, java_path_setting, jvm_args_extra)
  1799.                  logging.info(f"Instancja '{name}' ({version_id}) stworzona pomyślnie w {instance_dir}")
  1800.                  QMessageBox.information(parent_window, "Sukces", f"Instancja '{name}' stworzona pomyślnie!")
  1801.                  if hasattr(parent_window, 'update_instance_tiles'):
  1802.                       parent_window.update_instance_tiles()
  1803.                  return str(instance_dir)
  1804.  
  1805.             logging.info(f"Kolejka pobierania gotowa. Plików do pobrania: {total_queued_for_download}")
  1806.             self.progress_dialog = DownloadProgressDialog(self, parent_window) # Pass launcher to dialog
  1807.             self.progress_dialog.set_total_files(total_queued_for_download)
  1808.             self.progress_dialog.cancel_signal.connect(self.cancel_downloads)
  1809.             self.progress_dialog.download_process_finished.connect(self._handle_create_instance_post_download)
  1810.  
  1811.             self._post_download_data = {
  1812.                  "instance_dir": str(instance_dir),
  1813.                  "name": name,
  1814.                  "version_id": version_id,
  1815.                  "modloader": modloader,
  1816.                  "ram": ram,
  1817.                  "java_path_setting": java_path_setting,
  1818.                  "jvm_args_extra": jvm_args_extra,
  1819.                  "parent_window": parent_window
  1820.             }
  1821.  
  1822.             self.process_download_queue()
  1823.             self.progress_dialog.exec()
  1824.  
  1825.             return str(instance_dir)
  1826.  
  1827.  
  1828.         except (ValueError, FileExistsError, FileNotFoundError, ConnectionError, PermissionError, Exception) as e:
  1829.             self.download_queue.clear()
  1830.             if self.current_download_thread:
  1831.                 self.current_download_thread.cancel()
  1832.                 self.current_download_thread.wait(2000)
  1833.                 self.current_download_thread = None
  1834.             if self.progress_dialog:
  1835.                  self.progress_dialog.reject()
  1836.                  self.progress_dialog = None
  1837.  
  1838.             if instance_dir.exists():
  1839.                  try:
  1840.                      if not any(instance_dir.iterdir()):
  1841.                           logging.debug(f"Usuwanie pustego katalogu instancji po błędzie: {instance_dir}")
  1842.                           instance_dir.rmdir()
  1843.                      else:
  1844.                           logging.debug(f"Katalog instancji {instance_dir} nie jest pusty, nie usuwam go po błędzie.")
  1845.                  except Exception as cleanup_e:
  1846.                      logging.warning(f"Nie udało się posprzątać katalogu instancji {instance_dir} po błędzie: {cleanup_e}")
  1847.  
  1848.             logging.error(f"Błąd podczas przygotowania instancji: {e}")
  1849.             raise
  1850.  
  1851.  
  1852.     def _handle_create_instance_post_download(self, success):
  1853.          if self.progress_dialog:
  1854.              post_data = self._post_download_data
  1855.              QTimer.singleShot(0, self.progress_dialog.deleteLater)
  1856.              self.progress_dialog = None
  1857.  
  1858.              if post_data is None:
  1859.                   logging.error("Brak danych do konfiguracji po pobraniu. Nie mogę zakończyć tworzenia instancji.")
  1860.                   parent_window = QApplication.activeWindow()
  1861.                   QMessageBox.critical(parent_window, "Błąd konfiguracji instancji", "Wystąpił wewnętrzny błąd po pobraniu. Spróbuj ponownie.")
  1862.                   return
  1863.  
  1864.              instance_dir = Path(post_data["instance_dir"])
  1865.              name = post_data["name"]
  1866.              version_id = post_data["version_id"]
  1867.              modloader = post_data["modloader"]
  1868.              ram = post_data["ram"]
  1869.              java_path_setting = post_data["java_path_setting"]
  1870.              jvm_args_extra = post_data["jvm_args_extra"]
  1871.              parent_window = post_data.get("parent_window")
  1872.  
  1873.              self._post_download_data = None
  1874.  
  1875.              if not success:
  1876.                  logging.warning("Tworzenie instancji anulowane lub zakończone z błędami pobierania.")
  1877.                  QMessageBox.warning(parent_window, "Tworzenie instancji anulowane", "Tworzenie instancji zostało anulowane lub napotkało błędy podczas pobierania. Sprawdź logi.")
  1878.                  return
  1879.  
  1880.              logging.info("Pobieranie dla instancji zakończone pomyślnie. Kontynuuję konfigurację...")
  1881.  
  1882.              try:
  1883.                  self._extract_natives(version_id, instance_dir)
  1884.                  if modloader:
  1885.                      self._run_modloader_installer(modloader, version_id, instance_dir)
  1886.                  self.save_instance_settings(instance_dir, name, version_id, modloader, ram, java_path_setting, jvm_args_extra)
  1887.  
  1888.                  logging.info(f"Instancja '{name}' ({version_id}) stworzona pomyślnie w {instance_dir}")
  1889.                  QMessageBox.information(parent_window, "Sukces", f"Instancja '{name}' stworzona pomyślnie!")
  1890.  
  1891.                  if hasattr(parent_window, 'update_instance_tiles'):
  1892.                      parent_window.update_instance_tiles()
  1893.  
  1894.              except (ValueError, FileNotFoundError, RuntimeError, TimeoutError, IOError, Exception) as e:
  1895.                  logging.error(f"Błąd podczas konfiguracji instancji po pobraniu: {e}")
  1896.                  QMessageBox.critical(parent_window, "Błąd konfiguracji instancji", f"Nie udało się zakończyć konfiguracji instancji: {e}")
  1897.  
  1898.          else:
  1899.               logging.error("Progress dialog finished signal received, but progress_dialog object was None.")
  1900.  
  1901.  
  1902.     def save_instance_settings(self, instance_dir, name, version_id, modloader, ram, java_path_setting, jvm_args_extra):
  1903.          instance_settings_path = instance_dir / "settings.json"
  1904.          settings_to_save = {
  1905.              "name": name,
  1906.              "version": version_id,
  1907.              "modloader": modloader,
  1908.              "ram": ram,
  1909.              "java_path": java_path_setting if java_path_setting is not None else "auto",
  1910.              "jvm_args": jvm_args_extra or DEFAULT_SETTINGS["jvm_args"],
  1911.          }
  1912.  
  1913.          if instance_settings_path.exists():
  1914.              try:
  1915.                  with instance_settings_path.open("r", encoding='utf-8') as f:
  1916.                      existing_settings = json.load(f)
  1917.                      existing_settings.update(settings_to_save)
  1918.                      settings_to_save = existing_settings
  1919.              except json.JSONDecodeError:
  1920.                  logging.warning(f"Nieprawidłowy format settings.json w {instance_dir}, nadpisuję nowymi standardowymi polami.")
  1921.              except Exception as e:
  1922.                  logging.warning(f"Błąd odczytu istniejącego settings.json w {instance_dir}: {e}, nadpisuję nowymi standardowymi polami.")
  1923.  
  1924.          try:
  1925.              with instance_settings_path.open("w", encoding='utf-8') as f:
  1926.                  json.dump(settings_to_save, f, indent=4)
  1927.              logging.info(f"Zapisano ustawienia instancji do {instance_settings_path}")
  1928.          except Exception as e:
  1929.              logging.error(f"Błąd zapisu settings.json instancji {instance_dir}: {e}")
  1930.              raise IOError(f"Nie udało się zapisać ustawień instancji: {e}")
  1931.  
  1932.  
  1933.     def get_instance_list(self):
  1934.         valid_instances = []
  1935.         if not INSTANCES_DIR.exists():
  1936.              return []
  1937.         for item in INSTANCES_DIR.iterdir():
  1938.             settings_path = item / "settings.json"
  1939.             if item.is_dir() and settings_path.exists():
  1940.                 instance_name = item.name
  1941.                 instance_dir_path = str(item)
  1942.                 try:
  1943.                     with settings_path.open("r", encoding='utf-8') as f:
  1944.                         settings = json.load(f)
  1945.                     stored_name = settings.get("name")
  1946.                     if stored_name and stored_name.strip():
  1947.                          instance_name = stored_name.strip()
  1948.  
  1949.                 except (json.JSONDecodeError, Exception) as e:
  1950.                     logging.warning(f"Błąd odczytu nazwy instancji z {settings_path}: {e}. Używam nazwy folderu: {item.name}")
  1951.  
  1952.                 valid_instances.append((instance_name, instance_dir_path))
  1953.  
  1954.         valid_instances.sort(key=lambda x: x[0].lower())
  1955.  
  1956.         return valid_instances
  1957.  
  1958.  
  1959.     def export_instance(self, instance_dir_path, zip_path):
  1960.         instance_dir = Path(instance_dir_path)
  1961.         zip_path = Path(zip_path)
  1962.         if not instance_dir.exists():
  1963.              raise FileNotFoundError(f"Katalog instancji nie istnieje: {instance_dir_path}")
  1964.         logging.info(f"Eksportowanie instancji z {instance_dir} do {zip_path}")
  1965.  
  1966.         zip_path.parent.mkdir(parents=True, exist_ok=True)
  1967.  
  1968.         try:
  1969.             with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
  1970.                 for root, _, files in os.walk(instance_dir):
  1971.                     for file in files:
  1972.                         file_path = Path(root) / file
  1973.                         archive_path = file_path.relative_to(instance_dir)
  1974.                         if "natives_zips" not in archive_path.parts:
  1975.                             zipf.write(file_path, archive_path)
  1976.             logging.info("Eksport zakończony pomyślnie.")
  1977.         except Exception as e:
  1978.             logging.error(f"Błąd eksportu instancji {instance_dir}: {e}")
  1979.             if zip_path.exists():
  1980.                  try:
  1981.                      zip_path.unlink()
  1982.                      logging.warning(f"Usunięto częściowo utworzony plik zip: {zip_path}")
  1983.                  except Exception as cleanup_e:
  1984.                      logging.warning(f"Nie udało się usunąć częściowego pliku zip {zip_path}: {cleanup_e}")
  1985.  
  1986.             raise IOError(f"Nie udało się wyeksportować instancji: {e}")
  1987.  
  1988.     def import_instance(self, zip_path):
  1989.         zip_path = Path(zip_path)
  1990.         if not zip_path.exists():
  1991.              raise FileNotFoundError(f"Plik ZIP instancji nie istnieje: {zip_path}")
  1992.         if not zipfile.is_zipfile(zip_path):
  1993.              raise zipfile.BadZipFile(f"Plik '{zip_path.name}' nie jest prawidłowym plikiem ZIP.")
  1994.  
  1995.         INSTANCES_DIR.mkdir(parents=True, exist_ok=True)
  1996.  
  1997.         try:
  1998.             instance_name_base = zip_path.stem
  1999.             safe_name_base = re.sub(r'[<>:"/\\|?*]', '_', instance_name_base)
  2000.             if not safe_name_base:
  2001.                  safe_name_base = "imported_instance"
  2002.  
  2003.             instance_dir = INSTANCES_DIR / safe_name_base
  2004.             counter = 1
  2005.             while instance_dir.exists():
  2006.                  instance_dir = INSTANCES_DIR / f"{safe_name_base}-{counter}"
  2007.                  counter += 1
  2008.  
  2009.             logging.info(f"Importowanie instancji z {zip_path} do {instance_dir}")
  2010.             instance_dir.mkdir(parents=True)
  2011.  
  2012.             with zipfile.ZipFile(zip_path, "r") as zipf:
  2013.                 zipf.extractall(instance_dir)
  2014.  
  2015.             settings_path = instance_dir / "settings.json"
  2016.             if settings_path.exists():
  2017.                  try:
  2018.                      with settings_path.open("r", encoding='utf-8') as f:
  2019.                           settings = json.load(f)
  2020.                      settings['name'] = instance_dir.name
  2021.                      with settings_path.open("w", encoding='utf-8') as f:
  2022.                           json.dump(settings, f, indent=4)
  2023.                      logging.debug(f"Zaktualizowano nazwę w settings.json importowanej instancji do: {instance_dir.name}")
  2024.                  except (json.JSONDecodeError, Exception) as e:
  2025.                       logging.warning(f"Nie udało się zaktualizować nazwy w settings.json importowanej instancji {instance_dir.name}: {e}")
  2026.  
  2027.             logging.info("Import zakończony pomyślnie.")
  2028.             return str(instance_dir)
  2029.  
  2030.         except (zipfile.BadZipFile, FileNotFoundError, Exception) as e:
  2031.             logging.error(f"Błąd importu instancji z {zip_path}: {e}")
  2032.             if 'instance_dir' in locals() and instance_dir.exists():
  2033.                  try:
  2034.                      shutil.rmtree(instance_dir)
  2035.                      logging.info(f"Usunięto częściowo zaimportowany katalog: {instance_dir}")
  2036.                  except Exception as cleanup_e:
  2037.                      logging.error(f"Błąd podczas czyszczenia katalogu {instance_dir}: {cleanup_e}")
  2038.  
  2039.             if isinstance(e, (zipfile.BadZipFile, FileNotFoundError)):
  2040.                  raise e
  2041.             else:
  2042.                  raise ValueError(f"Nie udało się zaimportować instancji: {e}")
  2043.  
  2044.  
  2045. class CreateInstanceDialog(QDialog):
  2046.     def __init__(self, launcher, parent=None):
  2047.         super().__init__(parent)
  2048.         self.launcher = launcher
  2049.         self.setWindowTitle("Nowa instancja")
  2050.         self.setMinimumWidth(400)
  2051.         self.init_ui()
  2052.         self.version_combo.currentTextChanged.connect(self.update_modloaders)
  2053.         self.populate_versions()
  2054.         self.update_modloaders()
  2055.  
  2056.     def init_ui(self):
  2057.         layout = QVBoxLayout(self)
  2058.         layout.setSpacing(10)
  2059.  
  2060.         self.name_input = QLineEdit()
  2061.         self.name_input.setPlaceholderText("Nazwa instancji (np. MojaWersja)")
  2062.         layout.addWidget(QLabel("Nazwa instancji:"))
  2063.         layout.addWidget(self.name_input)
  2064.  
  2065.         instance_dir_layout = QHBoxLayout()
  2066.         self.instance_dir_input = QLineEdit(str(INSTANCES_DIR))
  2067.         self.instance_dir_input.setReadOnly(True)
  2068.         self.instance_dir_button = QPushButton("Wybierz inny folder docelowy...")
  2069.         self.instance_dir_button.clicked.connect(self.choose_instance_dir)
  2070.         self.use_custom_dir_check = QCheckBox("Użyj innego folderu")
  2071.         self.use_custom_dir_check.setChecked(False)
  2072.         self.use_custom_dir_check.stateChanged.connect(self.toggle_custom_dir_input)
  2073.         instance_dir_layout.addWidget(self.instance_dir_input)
  2074.         instance_dir_layout.addWidget(self.instance_dir_button)
  2075.         self.instance_dir_input.setEnabled(False)
  2076.         self.instance_dir_button.setEnabled(False)
  2077.  
  2078.         layout.addWidget(QLabel("Folder docelowy instancji:"))
  2079.         layout.addWidget(self.use_custom_dir_check)
  2080.         layout.addLayout(instance_dir_layout)
  2081.  
  2082.         self.version_combo = QComboBox()
  2083.         layout.addWidget(QLabel("Wersja Minecrafta:"))
  2084.         layout.addWidget(self.version_combo)
  2085.  
  2086.         self.modloader_combo = QComboBox()
  2087.         layout.addWidget(QLabel("Modloader (dla wybranych wersji):"))
  2088.         layout.addWidget(self.modloader_combo)
  2089.  
  2090.         advanced_group = QWidget()
  2091.         advanced_layout = QVBoxLayout(advanced_group)
  2092.         advanced_layout.setContentsMargins(0, 0, 0, 0)
  2093.  
  2094.         self.ram_input = QLineEdit(self.launcher.settings.get("ram", DEFAULT_SETTINGS["ram"]))
  2095.         advanced_layout.addWidget(QLabel("Maksymalna pamięć RAM (np. 4G, 2048M):"))
  2096.         advanced_layout.addWidget(self.ram_input)
  2097.  
  2098.         self.jvm_args_input = QLineEdit(self.launcher.settings.get("jvm_args", DEFAULT_SETTINGS["jvm_args"]))
  2099.         advanced_layout.addWidget(QLabel("Dodatkowe argumenty JVM:"))
  2100.         advanced_layout.addWidget(self.jvm_args_input)
  2101.  
  2102.         self.java_combo = QComboBox()
  2103.         self.java_combo.addItem("Automatyczny wybór", userData="auto")
  2104.         sorted_java_versions = sorted(self.launcher.java_versions, key=lambda x: self.launcher.get_java_version_from_path(x[0]) or 0, reverse=True)
  2105.         for java_path, version in sorted_java_versions:
  2106.             major_v = self.launcher.get_java_version_from_path(java_path)
  2107.             self.java_combo.addItem(f"{version} (Java {major_v}) - {java_path}", userData=java_path)
  2108.  
  2109.         default_java_setting = self.launcher.settings.get("java_path")
  2110.         if default_java_setting and default_java_setting.lower() != 'auto':
  2111.              default_index = self.java_combo.findData(default_java_setting)
  2112.              if default_index != -1:
  2113.                   self.java_combo.setCurrentIndex(default_index)
  2114.              else:
  2115.                   custom_item_text = f"Zapisana ścieżka: {default_java_setting} (Nieznana wersja)"
  2116.                   self.java_combo.addItem(custom_item_text, userData=default_java_setting)
  2117.                   self.java_combo.setCurrentIndex(self.java_combo.count() - 1)
  2118.                   logging.warning(f"Zapisana ścieżka Javy w ustawieniach nie znaleziona wśród automatycznie wykrytych: {default_java_setting}. Dodano jako opcję niestandardową.")
  2119.  
  2120.         layout.addWidget(QLabel("Wersja Javy (zalecany 'Automatyczny wybór'):"))
  2121.         layout.addWidget(self.java_combo)
  2122.  
  2123.         layout.addWidget(advanced_group)
  2124.  
  2125.         button_layout = QHBoxLayout()
  2126.         create_button = QPushButton("Stwórz instancję")
  2127.         create_button.clicked.connect(self.check_and_accept)
  2128.         cancel_button = QPushButton("Anuluj")
  2129.         cancel_button.clicked.connect(self.reject)
  2130.         button_layout.addStretch(1)
  2131.         button_layout.addWidget(create_button)
  2132.         button_layout.addWidget(cancel_button)
  2133.         layout.addLayout(button_layout)
  2134.  
  2135.     def toggle_custom_dir_input(self, state):
  2136.         enabled = self.use_custom_dir_check.isChecked()
  2137.         self.instance_dir_input.setEnabled(enabled)
  2138.         self.instance_dir_button.setEnabled(enabled)
  2139.         if not enabled:
  2140.              self.instance_dir_input.setText(str(INSTANCES_DIR))
  2141.  
  2142.     def choose_instance_dir(self):
  2143.         current_dir = self.instance_dir_input.text()
  2144.         if not Path(current_dir).exists():
  2145.              current_dir = str(INSTANCES_DIR.parent)
  2146.  
  2147.         folder = QFileDialog.getExistingDirectory(self, "Wybierz folder docelowy dla instancji", current_dir)
  2148.         if folder:
  2149.             self.instance_dir_input.setText(folder)
  2150.  
  2151.     def populate_versions(self):
  2152.          self.version_combo.blockSignals(True)
  2153.          self.version_combo.clear()
  2154.          try:
  2155.              manifest = self.launcher.get_version_manifest()
  2156.              versions = sorted(manifest.get("versions", []), key=lambda x: x.get('releaseTime', '1970-01-01T00:00:00+00:00'), reverse=True)
  2157.  
  2158.              for version in versions:
  2159.                  self.version_combo.addItem(version["id"])
  2160.  
  2161.          except ConnectionError as e:
  2162.              QMessageBox.critical(self.parentWidget(), "Błąd połączenia", f"Nie udało się pobrać listy wersji gry. Sprawdź połączenie z internetem.\n{e}")
  2163.              self.version_combo.addItem("Błąd pobierania listy wersji")
  2164.              self.version_combo.setEnabled(False)
  2165.          except Exception as e:
  2166.              logging.error(f"Nieoczekiwany błąd podczas pobierania listy wersji: {e}")
  2167.              QMessageBox.critical(self.parentWidget(), "Błąd", f"Nie udało się pobrać listy wersji gry: {e}")
  2168.              self.version_combo.addItem("Błąd ładowania listy wersji")
  2169.              self.version_combo.setEnabled(False)
  2170.          finally:
  2171.               self.version_combo.blockSignals(False)
  2172.  
  2173.  
  2174.     def update_modloaders(self):
  2175.         version_id = self.version_combo.currentText()
  2176.         self.modloader_combo.clear()
  2177.         self.modloader_combo.addItem("Brak")
  2178.         if not version_id or version_id.startswith("Błąd"):
  2179.             self.modloader_combo.setEnabled(False)
  2180.             return
  2181.         else:
  2182.              self.modloader_combo.setEnabled(True)
  2183.  
  2184.         supported_modloaders = []
  2185.         for modloader in ["forge", "neoforge", "fabric", "quilt"]:
  2186.             if self.launcher.validate_modloader(modloader, version_id):
  2187.                 supported_modloaders.append(modloader.capitalize())
  2188.  
  2189.         if supported_modloaders:
  2190.              self.modloader_combo.addItems(supported_modloaders)
  2191.         elif re.match(r"^\d+w\d+[a-z]$", version_id):
  2192.              self.modloader_combo.addItem("Brak (Snapshot - brak oficjalnego wsparcia)")
  2193.  
  2194.     def check_and_accept(self):
  2195.         name = self.name_input.text().strip()
  2196.         if not name:
  2197.             QMessageBox.warning(self, "Brak nazwy", "Proszę podać nazwę instancji.")
  2198.             return
  2199.  
  2200.         version_id = self.version_combo.currentText()
  2201.         if not version_id or version_id.startswith("Błąd"):
  2202.              QMessageBox.warning(self, "Brak wersji", "Proszę wybrać poprawną wersję Minecrafta.")
  2203.              return
  2204.  
  2205.         ram_val = self.ram_input.text().strip().upper()
  2206.         if not re.match(r"^\d+[MG]$", ram_val):
  2207.              QMessageBox.warning(self, "Nieprawidłowy format RAM", "Proszę podać RAM w formacie np. '4G' lub '2048M'.")
  2208.              return
  2209.  
  2210.         selected_java_index = self.java_combo.currentIndex()
  2211.         if selected_java_index == -1:
  2212.              QMessageBox.warning(self, "Brak wyboru Javy", "Proszę wybrać wersję Javy lub 'Automatyczny wybór'.")
  2213.              return
  2214.         selected_java_path_data = self.java_combo.itemData(selected_java_index)
  2215.         selected_java_path = selected_java_path_data if selected_java_path_data is not None else "auto"
  2216.  
  2217.         if selected_java_path != 'auto':
  2218.              if not Path(selected_java_path).exists():
  2219.                   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'.")
  2220.                   return
  2221.              required_java = self.launcher.get_required_java_version(version_id)
  2222.              selected_java_major = self.launcher.get_java_version_from_path(selected_java_path)
  2223.              if selected_java_major is not None and selected_java_major < required_java:
  2224.                   reply = QMessageBox.question(self, "Niekompatybilna Java?",
  2225.                                                f"Wybrana wersja Javy ({selected_java_major}) może nie być kompatybilna z wersją Minecrafta {version_id} (wymaga {required_java}+). Czy chcesz kontynuować?",
  2226.                                                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  2227.                   if reply == QMessageBox.StandardButton.No:
  2228.                       return
  2229.  
  2230.         if self.use_custom_dir_check.isChecked():
  2231.              chosen_base_dir_str = self.instance_dir_input.text().strip()
  2232.              if not chosen_base_dir_str:
  2233.                   QMessageBox.warning(self, "Brak folderu docelowego", "Proszę wybrać folder docelowy dla instancji.")
  2234.                   return
  2235.  
  2236.         self.accept()
  2237.  
  2238.     def get_data(self):
  2239.         selected_java_index = self.java_combo.currentIndex()
  2240.         selected_java_path_data = self.java_combo.itemData(selected_java_index)
  2241.         java_path_setting_to_save = selected_java_path_data if selected_java_path_data is not None else "auto"
  2242.  
  2243.         base_instance_dir_input_value = self.instance_dir_input.text().strip() if self.use_custom_dir_check.isChecked() else None
  2244.         if self.use_custom_dir_check.isChecked() and not base_instance_dir_input_value:
  2245.              base_instance_dir_input_value = None
  2246.  
  2247.         return {
  2248.             "name": self.name_input.text().strip(),
  2249.             "version": self.version_combo.currentText(),
  2250.             "modloader": self.modloader_combo.currentText().lower() if self.modloader_combo.currentText() != "Brak" and "snapshot" not in self.modloader_combo.currentText().lower() else None,
  2251.             "ram": self.ram_input.text().strip().upper(),
  2252.             "java_path_setting": java_path_setting_to_save,
  2253.             "jvm_args_extra": self.jvm_args_input.text().strip(),
  2254.             "base_instance_dir_input": base_instance_dir_input_value,
  2255.         }
  2256.  
  2257. class ModBrowserDialog(QDialog):
  2258.     def __init__(self, launcher, version_id, instance_dir, parent=None):
  2259.         super().__init__(parent)
  2260.         self.launcher = launcher
  2261.         self.version_id = version_id
  2262.         self.instance_dir = instance_dir
  2263.         self.setWindowTitle(f"Przeglądarka modów dla {version_id}")
  2264.         self.setMinimumSize(800, 600)
  2265.         self.current_mod = None
  2266.         self.selected_compatible_file = None
  2267.         self.init_ui()
  2268.         self.mod_list.clear()
  2269.         self.reset_details()
  2270.  
  2271.     def init_ui(self):
  2272.         self.setStyleSheet(STYLESHEET)
  2273.         layout = QHBoxLayout(self)
  2274.         layout.setSpacing(10)
  2275.  
  2276.         left_panel = QWidget()
  2277.         left_layout = QVBoxLayout(left_panel)
  2278.         left_layout.setSpacing(5)
  2279.  
  2280.         self.search_input = QLineEdit()
  2281.         self.search_input.setPlaceholderText("Szukaj modów...")
  2282.         self.search_input.returnPressed.connect(self.search_mods)
  2283.         left_layout.addWidget(self.search_input)
  2284.  
  2285.         self.mod_list = QListWidget()
  2286.         self.mod_list.setIconSize(QSize(48, 48))
  2287.         self.mod_list.itemClicked.connect(self.show_mod_details)
  2288.         self.mod_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
  2289.         left_layout.addWidget(self.mod_list)
  2290.  
  2291.         layout.addWidget(left_panel, 1)
  2292.  
  2293.         right_panel_scroll = QScrollArea()
  2294.         right_panel_scroll.setWidgetResizable(True)
  2295.         right_panel_scroll.setMinimumWidth(300)
  2296.         right_panel_widget = QWidget()
  2297.         self.details_layout = QVBoxLayout(right_panel_widget)
  2298.         self.details_layout.setSpacing(10)
  2299.         self.details_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
  2300.         right_panel_scroll.setWidget(right_panel_widget)
  2301.  
  2302.         self.mod_icon = QLabel()
  2303.         self.mod_icon.setFixedSize(128, 128)
  2304.         self.mod_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
  2305.         self.mod_icon.setStyleSheet("border: 1px solid #ccc; background-color: #e0e0e0;")
  2306.         self.details_layout.addWidget(self.mod_icon)
  2307.  
  2308.         self.mod_name = QLabel("Wybierz mod z listy")
  2309.         self.mod_name.setStyleSheet("font-size: 18px; font-weight: bold; margin-top: 5px;")
  2310.         self.mod_name.setWordWrap(True)
  2311.         self.details_layout.addWidget(self.mod_name)
  2312.  
  2313.         self.mod_author = QLabel("Autor: Brak")
  2314.         self.details_layout.addWidget(self.mod_author)
  2315.  
  2316.         self.mod_downloads = QLabel("Pobrania: Brak danych")
  2317.         self.details_layout.addWidget(self.mod_downloads)
  2318.  
  2319.         self.mod_date = QLabel("Aktualizacja: Brak danych")
  2320.         self.details_layout.addWidget(self.mod_date)
  2321.  
  2322.         self.mod_version = QLabel("Kompatybilna wersja pliku: Szukam...")
  2323.         self.details_layout.addWidget(self.mod_version)
  2324.  
  2325.         self.mod_description_label = QLabel("Opis:")
  2326.         self.details_layout.addWidget(self.mod_description_label)
  2327.         self.mod_description = QTextEdit()
  2328.         self.mod_description.setReadOnly(True)
  2329.         self.mod_description.setMinimumHeight(150)
  2330.         self.mod_description.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
  2331.         self.details_layout.addWidget(self.mod_description)
  2332.  
  2333.         self.dependency_check = QCheckBox("Pobierz wymagane mody (zalecane)")
  2334.         self.dependency_check.setChecked(True)
  2335.         self.details_layout.addWidget(self.dependency_check)
  2336.  
  2337.         self.details_layout.addStretch(1)
  2338.  
  2339.         button_layout = QHBoxLayout()
  2340.         self.install_button = QPushButton("Zainstaluj")
  2341.         self.install_button.clicked.connect(self.install_mod)
  2342.         self.install_button.setEnabled(False)
  2343.         self.remove_button = QPushButton("Usuń")
  2344.         self.remove_button.setProperty("deleteButton", "true")
  2345.         self.remove_button.clicked.connect(self.remove_mod)
  2346.         self.remove_button.setEnabled(False)
  2347.         self.remove_button.setStyleSheet("background-color: #f44336;")
  2348.         self.remove_button.setStyleSheet(self.remove_button.styleSheet() + """
  2349.            QPushButton:hover { background-color: #d32f2f; }
  2350.            QPushButton:disabled { background-color: #cccccc; }
  2351.        """)
  2352.  
  2353.         button_layout.addWidget(self.install_button)
  2354.         button_layout.addWidget(self.remove_button)
  2355.         self.details_layout.addLayout(button_layout)
  2356.  
  2357.         close_button = QPushButton("Zamknij")
  2358.         close_button.clicked.connect(self.accept)
  2359.         self.details_layout.addWidget(close_button)
  2360.  
  2361.         layout.addWidget(right_panel_scroll, 2)
  2362.  
  2363.     def search_mods(self):
  2364.         query = self.search_input.text().strip()
  2365.         if not query:
  2366.             self.mod_list.clear()
  2367.             self.reset_details()
  2368.             self.mod_name.setText("Wprowadź frazę do wyszukiwania.")
  2369.             return
  2370.  
  2371.         logging.info(f"Wyszukiwanie modów: '{query}' dla wersji {self.version_id}")
  2372.         self.mod_list.clear()
  2373.         self.reset_details()
  2374.         self.mod_name.setText("Szukam modów...")
  2375.         self.setCursor(Qt.CursorShape.WaitCursor)
  2376.  
  2377.         try:
  2378.             mods = self.launcher.get_curseforge_mods(query, self.version_id)
  2379.             self.unsetCursor()
  2380.             if not mods:
  2381.                  self.mod_name.setText("Brak wyników.")
  2382.                  return
  2383.  
  2384.             self.mod_name.setText("Wybierz mod z listy")
  2385.  
  2386.             for mod in mods:
  2387.                 compatible_file = None
  2388.                 files = mod.get("latestFiles", [])
  2389.                 files.sort(key=lambda x: x.get('fileDate', '1970-01-01T00:00:00Z'), reverse=True)
  2390.                 for file in files:
  2391.                     if self.version_id in file.get("gameVersions", []):
  2392.                         compatible_file = file
  2393.                         break
  2394.  
  2395.                 item_text = f"{mod.get('name', 'Nazwa nieznana')}"
  2396.                 if not compatible_file:
  2397.                      item_text += " (Brak wersji dla tej gry)"
  2398.  
  2399.                 list_item = QListWidgetItem(item_text)
  2400.  
  2401.                 item_data = {
  2402.                     'mod': mod,
  2403.                     'compatible_file': compatible_file
  2404.                 }
  2405.                 list_item.setData(Qt.ItemDataRole.UserRole, item_data)
  2406.  
  2407.                 icon_url = mod.get("logo", {}).get("url")
  2408.                 if icon_url:
  2409.                      icon_file_extension = Path(icon_url).suffix or ".png"
  2410.                      icon_dest_path = MOD_ICONS_DIR / f"{mod['id']}{icon_file_extension}"
  2411.  
  2412.                      if icon_dest_path.exists():
  2413.                           list_item.setIcon(QIcon(str(icon_dest_path)))
  2414.                      else:
  2415.                           pass
  2416.  
  2417.                 self.mod_list.addItem(list_item)
  2418.  
  2419.         except (requests.exceptions.RequestException, PermissionError) as e:
  2420.              self.unsetCursor()
  2421.              QMessageBox.critical(self, "Błąd API CurseForge", f"Wystąpił błąd podczas wyszukiwania modów:\n{e}")
  2422.              logging.error(f"Błąd wyszukiwania modów: {e}")
  2423.              self.mod_name.setText("Błąd API CurseForge.")
  2424.         except Exception as e:
  2425.             self.unsetCursor()
  2426.             logging.error(f"Nieoczekiwany błąd podczas wyszukiwania modów: {e}")
  2427.             QMessageBox.critical(self, "Błąd wyszukiwania modów", f"Nie udało się wyszukać modów: {e}")
  2428.             self.mod_name.setText("Błąd wyszukiwania.")
  2429.  
  2430.  
  2431.     def show_mod_details(self, item):
  2432.         item_data = item.data(Qt.ItemDataRole.UserRole)
  2433.         mod = item_data.get('mod')
  2434.         compatible_file = item_data.get('compatible_file')
  2435.  
  2436.         if not mod:
  2437.             self.reset_details()
  2438.             return
  2439.  
  2440.         self.current_mod = mod
  2441.         self.selected_compatible_file = compatible_file
  2442.  
  2443.         self.mod_name.setText(mod.get("name", "Nazwa nieznana"))
  2444.         authors = mod.get("authors", [])
  2445.         self.mod_author.setText(f"Autor: {authors[0].get('name', 'Brak danych') if authors else 'Brak danych'}")
  2446.         self.mod_downloads.setText(f"Pobrania: {mod.get('downloadCount', 'Brak danych')}")
  2447.         try:
  2448.             date_modified_ts = mod.get('dateModified')
  2449.             if date_modified_ts is not None:
  2450.                  date_modified = datetime.fromtimestamp(date_modified_ts / 1000).strftime('%Y-%m-%d %H:%M')
  2451.                  self.mod_date.setText(f"Aktualizacja: {date_modified}")
  2452.             else:
  2453.                  self.mod_date.setText("Aktualizacja: Brak danych")
  2454.         except Exception as e:
  2455.             logging.warning(f"Błąd parsowania daty modyfikacji dla mod ID {mod.get('id')}: {e}")
  2456.             self.mod_date.setText("Aktualizacja: Nieprawidłowa data")
  2457.  
  2458.         if compatible_file:
  2459.              self.mod_version.setText(f"Kompatybilny plik: {compatible_file.get('fileName', 'Brak danych')}")
  2460.              self.install_button.setEnabled(True)
  2461.              mod_file_name = compatible_file.get("fileName")
  2462.              if mod_file_name:
  2463.                   mod_path = Path(self.instance_dir) / "mods" / mod_file_name
  2464.                   self.remove_button.setEnabled(mod_path.exists())
  2465.              else:
  2466.                   self.remove_button.setEnabled(False)
  2467.  
  2468.         else:
  2469.              self.mod_version.setText("Kompatybilny plik: Brak dla tej wersji")
  2470.              self.install_button.setEnabled(False)
  2471.              self.remove_button.setEnabled(False)
  2472.  
  2473.         description_text = mod.get("summary", "")
  2474.         self.mod_description.setHtml(description_text or "Brak opisu.")
  2475.  
  2476.         self.mod_icon.clear()
  2477.         icon_url = mod.get("logo", {}).get("url")
  2478.         if icon_url:
  2479.              icon_file_extension = Path(icon_url).suffix or ".png"
  2480.              icon_dest_path = MOD_ICONS_DIR / f"{mod['id']}{icon_file_extension}"
  2481.  
  2482.              if icon_dest_path.exists():
  2483.                  try:
  2484.                      pixmap = QPixmap(str(icon_dest_path)).scaled(128, 128, Qt.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
  2485.                      self.mod_icon.setPixmap(pixmap)
  2486.                  except Exception as e:
  2487.                      logging.warning(f"Błąd ładowania ikony z pliku {icon_dest_path}: {e}")
  2488.                      self.mod_icon.setText("Błąd ikony")
  2489.              else:
  2490.                  self.mod_icon.setText("Ładowanie ikony...")
  2491.  
  2492.         else:
  2493.             self.mod_icon.setText("Brak ikony")
  2494.  
  2495.     def reset_details(self):
  2496.         self.current_mod = None
  2497.         self.selected_compatible_file = None
  2498.         self.mod_icon.clear()
  2499.         self.mod_icon.setText("Ikona")
  2500.         self.mod_name.setText("Wybierz mod z listy")
  2501.         self.mod_author.setText("Autor: Brak")
  2502.         self.mod_downloads.setText("Pobrania: Brak danych")
  2503.         self.mod_date.setText("Aktualizacja: Brak danych")
  2504.         self.mod_version.setText("Kompatybilny plik: Brak danych")
  2505.         self.mod_description.setHtml("Wybierz mod z listy, aby zobaczyć szczegóły.")
  2506.         self.install_button.setEnabled(False)
  2507.         self.remove_button.setEnabled(False)
  2508.         self.dependency_check.setChecked(True)
  2509.  
  2510.     def install_mod(self):
  2511.         if not self.current_mod or not self.selected_compatible_file:
  2512.             QMessageBox.warning(self, "Błąd", "Proszę wybrać mod do instalacji i upewnić się, że jest dostępna kompatybilna wersja pliku.")
  2513.             return
  2514.  
  2515.         mod_id = self.current_mod.get("id")
  2516.         mod_name_display = self.current_mod.get("name", "Wybrany mod")
  2517.         download_deps = self.dependency_check.isChecked()
  2518.  
  2519.         reply = QMessageBox.question(self, "Potwierdzenie instalacji",
  2520.                                      f"Zainstalować mod '{mod_name_display}' (dla wersji {self.version_id})?",
  2521.                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  2522.         if reply == QMessageBox.StandardButton.No:
  2523.             return
  2524.  
  2525.         if self.launcher.current_download_thread or self.launcher.download_queue:
  2526.             QMessageBox.warning(self, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Poczekaj na jego zakończenie lub anuluj.")
  2527.             return
  2528.  
  2529.         try:
  2530.             logging.info(f"Rozpoczęcie instalacji moda '{mod_name_display}' (ID: {mod_id}) dla wersji {self.version_id}")
  2531.             self.launcher.download_queue.clear()
  2532.  
  2533.             visited_mods_during_install = set()
  2534.             total_queued = self.launcher._queue_curseforge_mod_files(
  2535.                 mod_id, self.version_id, self.instance_dir,
  2536.                 download_dependencies=download_deps,
  2537.                 visited_mods=visited_mods_during_install
  2538.             )
  2539.  
  2540.             if total_queued == 0:
  2541.                  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.")
  2542.                  logging.warning("Install mod: No files queued.")
  2543.                  self.show_mod_details(self.mod_list.currentItem())
  2544.                  return
  2545.  
  2546.             logging.info(f"Kolejka pobierania modów gotowa. Plików do pobrania: {total_queued}")
  2547.             self.launcher.progress_dialog = DownloadProgressDialog(self.launcher, self) # Pass launcher to dialog
  2548.             self.launcher.progress_dialog.set_total_files(total_queued)
  2549.             self.launcher.progress_dialog.cancel_signal.connect(self.launcher.cancel_downloads)
  2550.             self.launcher.progress_dialog.download_process_finished.connect(self._handle_mod_install_post_download)
  2551.  
  2552.             self._post_mod_install_data = {
  2553.                 "mod_name": mod_name_display,
  2554.                 "mod_id": mod_id,
  2555.                 "parent_dialog": self
  2556.             }
  2557.  
  2558.             self.launcher.process_download_queue()
  2559.             self.launcher.progress_dialog.exec()
  2560.  
  2561.         except (ValueError, requests.exceptions.RequestException, PermissionError, Exception) as e:
  2562.             logging.error(f"Błąd podczas przygotowania instalacji moda '{mod_name_display}': {e}")
  2563.             QMessageBox.critical(self, "Błąd instalacji moda", f"Nie udało się przygotować instalacji moda:\n{e}")
  2564.             self.launcher.download_queue.clear()
  2565.  
  2566.     def _handle_mod_install_post_download(self, success):
  2567.         if self.launcher.progress_dialog:
  2568.             post_data = self._post_mod_install_data
  2569.             QTimer.singleShot(0, self.launcher.progress_dialog.deleteLater)
  2570.             self.launcher.progress_dialog = None
  2571.  
  2572.             if post_data is None:
  2573.                   logging.error("Brak danych do konfiguracji po pobraniu moda. Nie mogę zakończyć instalacji.")
  2574.                   QMessageBox.critical(self, "Błąd instalacji moda", "Wystąpił wewnętrzny błąd po pobraniu moda. Spróbuj ponownie.")
  2575.                   return
  2576.  
  2577.             mod_name = post_data.get("mod_name", "Mod")
  2578.             mod_id = post_data.get("mod_id")
  2579.             parent_dialog = post_data.get("parent_dialog")
  2580.  
  2581.             self._post_mod_install_data = None
  2582.  
  2583.             if success:
  2584.                 logging.info(f"Mod '{mod_name}' zainstalowany pomyślnie.")
  2585.                 QMessageBox.information(parent_dialog, "Sukces", f"Mod '{mod_name}' zainstalowany pomyślnie!")
  2586.                 if mod_id is not None:
  2587.                      for i in range(self.mod_list.count()):
  2588.                           item = self.mod_list.item(i)
  2589.                           item_data = item.data(Qt.ItemDataRole.UserRole)
  2590.                           if item_data and item_data.get('mod', {}).get('id') == mod_id:
  2591.                                self.mod_list.setCurrentItem(item)
  2592.                                self.show_mod_details(item)
  2593.                                break
  2594.             else:
  2595.                 logging.warning(f"Instalacja moda '{mod_name}' anulowana lub zakończona z błędami.")
  2596.                 QMessageBox.warning(parent_dialog, "Instalacja anulowana", f"Instalacja moda '{mod_name}' została anulowana lub napotkała błędy.")
  2597.  
  2598.     def remove_mod(self):
  2599.         if not self.current_mod or not self.selected_compatible_file:
  2600.             QMessageBox.warning(self, "Błąd", "Proszę wybrać mod do usunięcia.")
  2601.             return
  2602.  
  2603.         mod_name_display = self.current_mod.get("name", "Wybrany mod")
  2604.         mod_file_name = self.selected_compatible_file.get("fileName")
  2605.  
  2606.         if not mod_file_name:
  2607.             QMessageBox.warning(self, "Błąd", "Nie można określić nazwy pliku moda do usunięcia.")
  2608.             return
  2609.  
  2610.         mod_path = Path(self.instance_dir) / "mods" / mod_file_name
  2611.         if not mod_path.exists():
  2612.              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.")
  2613.              self.remove_button.setEnabled(False)
  2614.              return
  2615.  
  2616.         reply = QMessageBox.question(self, "Potwierdzenie usunięcia",
  2617.                                      f"Usunąć mod '{mod_name_display}' ({mod_file_name})?",
  2618.                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  2619.         if reply == QMessageBox.StandardButton.No:
  2620.             return
  2621.  
  2622.         try:
  2623.             self.launcher.remove_mod(mod_file_name, self.instance_dir)
  2624.             QMessageBox.information(self, "Sukces", f"Usunięto mod: {mod_file_name}")
  2625.             self.remove_button.setEnabled(False)
  2626.         except FileNotFoundError:
  2627.              QMessageBox.warning(self, "Błąd usuwania", f"Plik moda nie znaleziono w katalogu: {mod_file_name}")
  2628.              self.remove_button.setEnabled(False)
  2629.         except IOError as e:
  2630.              QMessageBox.critical(self, "Błąd usuwania", f"Wystąpił błąd podczas usuwania pliku:\n{e}")
  2631.         except Exception as e:
  2632.             logging.error(f"Nieoczekiwany błąd podczas usuwania moda {mod_file_name}: {e}")
  2633.             QMessageBox.critical(self, "Błąd usuwania", f"Wystąpił nieoczekiwany błąd podczas usuwania moda:\n{e}")
  2634.  
  2635. class EditInstanceDialog(QDialog):
  2636.     def __init__(self, instance_dir, parent=None):
  2637.         super().__init__(parent)
  2638.         self.instance_dir = Path(instance_dir)
  2639.         self.setWindowTitle("Edytuj instancję")
  2640.         self.setFixedSize(400, 300)
  2641.         self.init_ui()
  2642.         self.load_settings()
  2643.  
  2644.     def init_ui(self):
  2645.         layout = QVBoxLayout()
  2646.  
  2647.         # RAM
  2648.         ram_label = QLabel("Maksymalna pamięć RAM:")
  2649.         self.ram_input = QComboBox()
  2650.         self.ram_input.addItems(["2G", "4G", "6G", "8G", "12G", "16G"])
  2651.         layout.addWidget(ram_label)
  2652.         layout.addWidget(self.ram_input)
  2653.  
  2654.         # Java path
  2655.         java_label = QLabel("Ścieżka do Javy (puste = automatyczne):")
  2656.         self.java_input = QLineEdit()
  2657.         java_browse = QPushButton("Przeglądaj")
  2658.         java_browse.clicked.connect(self.browse_java)
  2659.         java_layout = QHBoxLayout()
  2660.         java_layout.addWidget(self.java_input)
  2661.         java_layout.addWidget(java_browse)
  2662.         layout.addWidget(java_label)
  2663.         layout.addLayout(java_layout)
  2664.  
  2665.         # Rozdzielczość
  2666.         resolution_label = QLabel("Rozdzielczość (np. 1280x720):")
  2667.         self.resolution_input = QLineEdit()
  2668.         layout.addWidget(resolution_label)
  2669.         layout.addWidget(self.resolution_input)
  2670.  
  2671.         # Pełny ekran
  2672.         self.fullscreen_checkbox = QCheckBox("Pełny ekran")
  2673.         layout.addWidget(self.fullscreen_checkbox)
  2674.  
  2675.         # Przyciski
  2676.         buttons = QHBoxLayout()
  2677.         save_button = QPushButton("Zapisz")
  2678.         save_button.clicked.connect(self.save_settings)
  2679.         cancel_button = QPushButton("Anuluj")
  2680.         cancel_button.clicked.connect(self.reject)
  2681.         buttons.addWidget(save_button)
  2682.         buttons.addWidget(cancel_button)
  2683.         layout.addLayout(buttons)
  2684.  
  2685.         self.setLayout(layout)
  2686.  
  2687.     def browse_java(self):
  2688.         java_path, _ = QFileDialog.getOpenFileName(self, "Wybierz plik java.exe", "", "Pliki wykonywalne (*.exe);;Wszystkie pliki (*.*)")
  2689.         if java_path:
  2690.             self.java_input.setText(java_path)
  2691.  
  2692.     def load_settings(self):
  2693.         settings_path = self.instance_dir / "settings.json"
  2694.         if settings_path.exists():
  2695.             try:
  2696.                 with settings_path.open("r", encoding='utf-8') as f:
  2697.                     settings = json.load(f)
  2698.                 self.ram_input.setCurrentText(settings.get("ram", "4G"))
  2699.                 self.java_input.setText(settings.get("java_path", ""))
  2700.                 self.resolution_input.setText(settings.get("resolution", "1280x720"))
  2701.                 self.fullscreen_checkbox.setChecked(settings.get("fullscreen", False))
  2702.             except Exception as e:
  2703.                 logging.error(f"Błąd ładowania ustawień instancji {settings_path}: {e}")
  2704.  
  2705.     def save_settings(self):
  2706.         settings = {
  2707.             "version": self.load_settings_version(),
  2708.             "ram": self.ram_input.currentText(),
  2709.             "java_path": self.java_input.text(),
  2710.             "resolution": self.resolution_input.text(),
  2711.             "fullscreen": self.fullscreen_checkbox.isChecked(),
  2712.         }
  2713.         settings_path = self.instance_dir / "settings.json"
  2714.         try:
  2715.             with settings_path.open("w", encoding='utf-8') as f:
  2716.                 json.dump(settings, f, indent=4)
  2717.             logging.info(f"Zapisano ustawienia instancji: {settings_path}")
  2718.             self.accept()
  2719.         except Exception as e:
  2720.             logging.error(f"Błąd zapisu ustawień instancji {settings_path}: {e}")
  2721.             QMessageBox.critical(self, "Błąd", f"Nie udało się zapisać ustawień: {e}")
  2722.  
  2723.     def load_settings_version(self):
  2724.         settings_path = self.instance_dir / "settings.json"
  2725.         if settings_path.exists():
  2726.             try:
  2727.                 with settings_path.open("r", encoding='utf-8') as f:
  2728.                     settings = json.load(f)
  2729.                 return settings.get("version", "")
  2730.             except:
  2731.                 return ""
  2732.         return ""
  2733.  
  2734. class LauncherWindow(QMainWindow):
  2735.     def __init__(self):
  2736.         super().__init__()
  2737.         self.launcher = MinecraftLauncher()
  2738.         self.setWindowTitle("Paffcio's Minecraft Launcher")
  2739.         self.setGeometry(100, 100, 900, 650)
  2740.         self.selected_instance_dir = None
  2741.         self.init_ui()
  2742.         self.apply_theme()
  2743.         self.update_instance_tiles()
  2744.        
  2745.     def update_buttons_state(self):
  2746.         """
  2747.        Aktualizuje stan przycisków w zależności od wybranej instancji.
  2748.        """
  2749.         has_selection = bool(self.instance_list.selectedItems())
  2750.         has_valid_settings = False
  2751.         version_id = None
  2752.  
  2753.         if has_selection:
  2754.             current_item = self.instance_list.currentItem()
  2755.             instance_dir_path = current_item.data(Qt.ItemDataRole.UserRole)
  2756.             settings_path = Path(instance_dir_path) / "settings.json"
  2757.             if settings_path.exists():
  2758.                 try:
  2759.                     with settings_path.open("r", encoding='utf-8') as f:
  2760.                         settings = json.load(f)
  2761.                     version_id = settings.get("version")
  2762.                     has_valid_settings = bool(version_id)
  2763.                 except Exception as e:
  2764.                     logging.error(f"Błąd odczytu settings.json dla instancji {instance_dir_path}: {e}")
  2765.  
  2766.         self.play_button.setEnabled(has_selection and has_valid_settings)
  2767.         self.edit_instance_button.setEnabled(has_selection)
  2768.         self.mod_browser_button.setEnabled(has_selection and has_valid_settings)
  2769.         self.delete_instance_button.setEnabled(has_selection)
  2770.         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()}")
  2771.  
  2772.     def init_ui(self):
  2773.         # Główny widget i layout
  2774.         main_widget = QWidget()
  2775.         self.setCentralWidget(main_widget)
  2776.         main_layout = QVBoxLayout(main_widget)
  2777.         main_layout.setContentsMargins(10, 10, 10, 10)
  2778.         main_layout.setSpacing(10)
  2779.         logging.debug("Inicjalizacja głównego layoutu")
  2780.  
  2781.         # Layout na sidebar i główną zawartość
  2782.         content_layout = QHBoxLayout()
  2783.         content_layout.setSpacing(10)
  2784.  
  2785.         # Sidebar
  2786.         sidebar = QWidget()
  2787.         sidebar.setMinimumWidth(200)
  2788.         sidebar.setMaximumWidth(300)
  2789.         sidebar_layout = QVBoxLayout(sidebar)
  2790.         sidebar_layout.setContentsMargins(0, 0, 0, 0)
  2791.         sidebar_layout.setSpacing(5)
  2792.         logging.debug("Inicjalizacja sidebara")
  2793.  
  2794.         sidebar_layout.addWidget(QLabel("Twoje instancje:"))
  2795.         self.instance_list = QListWidget()
  2796.         self.instance_list.itemSelectionChanged.connect(self.handle_instance_selection_change)
  2797.         self.instance_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
  2798.         sidebar_layout.addWidget(self.instance_list)
  2799.         logging.debug("Dodano listę instancji")
  2800.  
  2801.         # Przyciski akcji instancji
  2802.         instance_actions_layout = QVBoxLayout()
  2803.         instance_actions_layout.setSpacing(5)
  2804.  
  2805.         self.play_button = QPushButton("Graj")
  2806.         self.play_button.clicked.connect(self.play_instance)
  2807.         self.play_button.setEnabled(False)
  2808.         instance_actions_layout.addWidget(self.play_button)
  2809.         logging.debug("Dodano przycisk Graj")
  2810.  
  2811.         self.edit_instance_button = QPushButton("Edytuj instancję")
  2812.         self.edit_instance_button.clicked.connect(self.edit_instance)
  2813.         self.edit_instance_button.setEnabled(False)
  2814.         self.edit_instance_button.setStyleSheet("background-color: #2196F3; color: white;")  # Tymczasowy styl dla widoczności
  2815.         instance_actions_layout.addWidget(self.edit_instance_button)
  2816.         logging.debug("Dodano przycisk Edytuj instancję")
  2817.  
  2818.         self.mod_browser_button = QPushButton("Przeglądaj mody")
  2819.         self.mod_browser_button.clicked.connect(self.open_mod_browser)
  2820.         self.mod_browser_button.setEnabled(False)
  2821.         instance_actions_layout.addWidget(self.mod_browser_button)
  2822.         logging.debug("Dodano przycisk Przeglądaj mody")
  2823.  
  2824.         self.delete_instance_button = QPushButton("Usuń instancję")
  2825.         self.delete_instance_button.setProperty("deleteButton", "true")
  2826.         self.delete_instance_button.clicked.connect(self.delete_instance)
  2827.         self.delete_instance_button.setEnabled(False)
  2828.         instance_actions_layout.addWidget(self.delete_instance_button)
  2829.         logging.debug("Dodano przycisk Usuń instancję")
  2830.  
  2831.         sidebar_layout.addLayout(instance_actions_layout)
  2832.         content_layout.addWidget(sidebar, 1)
  2833.         logging.debug("Dodano sidebar do content_layout")
  2834.  
  2835.         # Główna zawartość
  2836.         main_content_area = QWidget()
  2837.         main_content_layout = QVBoxLayout(main_content_area)
  2838.         main_content_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
  2839.         main_content_layout.addStretch(1)
  2840.         self.main_info_label = QLabel("Wybierz instancję z listy po lewej lub stwórz nową instancję (Plik -> Nowa instancja...).")
  2841.         self.main_info_label.setWordWrap(True)
  2842.         self.main_info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
  2843.         main_content_layout.addWidget(self.main_info_label)
  2844.         main_content_layout.addStretch(2)
  2845.         content_layout.addWidget(main_content_area, 3)
  2846.         main_layout.addLayout(content_layout)
  2847.         logging.debug("Dodano główną zawartość")
  2848.  
  2849.         # Status bar
  2850.         self.status_label = QLabel("Gotowy.")
  2851.         self.status_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
  2852.         main_layout.addWidget(self.status_label)
  2853.         logging.debug("Dodano status bar")
  2854.  
  2855.         # Menu
  2856.         menubar = self.menuBar()
  2857.         file_menu = menubar.addMenu("Plik")
  2858.         file_menu.addAction("Nowa instancja...", self.create_instance)
  2859.         file_menu.addSeparator()
  2860.         file_menu.addAction("Importuj instancję...", self.import_instance)
  2861.         file_menu.addAction("Eksportuj wybraną instancję...", self.export_instance)
  2862.         file_menu.addSeparator()
  2863.         file_menu.addAction("Zamknij", self.close)
  2864.  
  2865.         settings_menu = menubar.addMenu("Ustawienia")
  2866.         settings_menu.addAction("Ustawienia launchera...", self.open_settings)
  2867.  
  2868.         accounts_menu = menubar.addMenu("Konta")
  2869.         accounts_menu.addAction("Ustaw nazwę konta offline...", self.set_offline_account)
  2870.         logging.debug("Dodano menu")
  2871.        
  2872.     def edit_instance(self):
  2873.         """
  2874.        Otwiera okno edycji wybranej instancji.
  2875.        """
  2876.         selected_items = self.instance_list.selectedItems()
  2877.         if not selected_items:
  2878.             logging.warning("Próba edycji instancji bez wybrania instancji.")
  2879.             return
  2880.         instance_name = selected_items[0].text()
  2881.         instance_dir = Path(self.launcher.instances_dir) / instance_name
  2882.         dialog = EditInstanceDialog(instance_dir, self)
  2883.         dialog.exec_()
  2884.         logging.info(f"Otwarto okno edycji dla instancji: {instance_name}")
  2885.  
  2886.     def apply_theme(self):
  2887.         theme = self.launcher.settings.get("theme", "Light")
  2888.         if theme == "Light":
  2889.             self.setStyleSheet(STYLESHEET)
  2890.         else:
  2891.             dark_stylesheet = STYLESHEET + """
  2892.            QMainWindow, QDialog, QWidget {
  2893.                background-color: #2e2e2e;
  2894.                color: #cccccc;
  2895.            }
  2896.            QLabel {
  2897.                color: #cccccc;
  2898.            }
  2899.            QListWidget {
  2900.                background-color: #3a3a3a;
  2901.                color: #cccccc;
  2902.                border: 1px solid #555555;
  2903.            }
  2904.            QListWidget::item:selected {
  2905.                background-color: #5a5a5a;
  2906.                color: #ffffff;
  2907.            }
  2908.            QLineEdit, QComboBox, QTextEdit {
  2909.                background-color: #4a4a4a;
  2910.                color: #cccccc;
  2911.                border: 1px solid #666666;
  2912.            }
  2913.            QTextEdit {
  2914.                background-color: #3a3a3a;
  2915.                border: 1px solid #555555;
  2916.            }
  2917.            QPushButton {
  2918.                background-color: #4CAF50;
  2919.                color: white;
  2920.            }
  2921.            QPushButton:hover {
  2922.                background-color: #45a049;
  2923.            }
  2924.            QPushButton:disabled {
  2925.                background-color: #555555;
  2926.                color: #aaaaaa;
  2927.            }
  2928.            QPushButton[deleteButton="true"] {
  2929.                background-color: #c62828;
  2930.            }
  2931.            QPushButton[deleteButton="true"]:hover {
  2932.                background-color: #d32f2f;
  2933.            }
  2934.            QPushButton[deleteButton="true"]:disabled {
  2935.                background-color: #555555;
  2936.            }
  2937.            QProgressBar {
  2938.                background-color: #555555;
  2939.                border: 1px solid #666666;
  2940.            }
  2941.            QProgressBar::chunk {
  2942.                background-color: #4CAF50;
  2943.            }
  2944.            QScrollArea {
  2945.                border: none;
  2946.            }
  2947.            """
  2948.             self.setStyleSheet(dark_stylesheet)
  2949.        
  2950.         # Ustaw atrybut deleteButton dla przycisków usuwania
  2951.         self.delete_instance_button.setProperty("deleteButton", "true")
  2952.         self.delete_instance_button.style().unpolish(self.delete_instance_button)
  2953.         self.delete_instance_button.style().polish(self.delete_instance_button)
  2954.  
  2955.     def update_instance_tiles(self):
  2956.         """
  2957.        Odświeża listę instancji w UI.
  2958.        """
  2959.         logging.info("Odświeżanie listy instancji...")
  2960.         current_selection_path = self.selected_instance_dir
  2961.         self.instance_list.clear()
  2962.         self.selected_instance_dir = None
  2963.  
  2964.         instances = self.launcher.get_instance_list()
  2965.  
  2966.         if not instances:
  2967.             self.status_label.setText("Brak instancji. Stwórz nową (Plik -> Nowa instancja...).")
  2968.             self.main_info_label.setText("Brak instancji. Stwórz nową instancję (Plik -> Nowa instancja...).")
  2969.             self.update_buttons_state()
  2970.             return
  2971.  
  2972.         found_selected_index = -1
  2973.         for i, (name, path) in enumerate(instances):
  2974.             item = QListWidgetItem(name)
  2975.             item.setData(Qt.ItemDataRole.UserRole, path)
  2976.             self.instance_list.addItem(item)
  2977.             if path == current_selection_path:
  2978.                 found_selected_index = i
  2979.  
  2980.         logging.info(f"Znaleziono {len(instances)} instancji.")
  2981.         self.status_label.setText(f"Znaleziono {len(instances)} instancji.")
  2982.         self.main_info_label.setText("Wybierz instancję z listy po lewej lub stwórz nową instancję (Plik -> Nowa instancja...).")
  2983.  
  2984.         if found_selected_index != -1:
  2985.             self.instance_list.setCurrentRow(found_selected_index)
  2986.         else:
  2987.             if self.instance_list.count() > 0:
  2988.                 self.instance_list.setCurrentRow(0)
  2989.             else:
  2990.                 self.update_buttons_state()
  2991.  
  2992.     def handle_instance_selection_change(self):
  2993.         """
  2994.        Obsługuje zmianę wybranej instancji w liście.
  2995.        """
  2996.         current_item = self.instance_list.currentItem()
  2997.         if current_item:
  2998.             self.load_instance(current_item)
  2999.         else:
  3000.             self.selected_instance_dir = None
  3001.             self.update_buttons_state()
  3002.             self.status_label.setText("Gotowy.")
  3003.  
  3004.     def create_instance(self):
  3005.         if self.launcher.current_download_thread or self.launcher.download_queue:
  3006.              QMessageBox.warning(self, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Poczekaj na jego zakończenie lub anuluj.")
  3007.              return
  3008.  
  3009.         dialog = CreateInstanceDialog(self.launcher, self)
  3010.         if dialog.exec():
  3011.             data = dialog.get_data()
  3012.             try:
  3013.                 self.launcher.create_instance(
  3014.                     name=data["name"],
  3015.                     version_id=data["version"],
  3016.                     modloader=data["modloader"],
  3017.                     ram=data["ram"],
  3018.                     java_path_setting=data["java_path_setting"],
  3019.                     jvm_args_extra=data["jvm_args_extra"],
  3020.                     base_instance_dir_input=data["base_instance_dir_input"],
  3021.                     parent_window=self
  3022.                 )
  3023.  
  3024.             except (ValueError, FileExistsError, FileNotFoundError, ConnectionError, PermissionError, Exception) as e:
  3025.                 error_title = "Błąd tworzenia instancji"
  3026.                 if isinstance(e, FileExistsError):
  3027.                      error_title = "Katalog instancji już istnieje"
  3028.                 elif isinstance(e, FileNotFoundError):
  3029.                      error_title = "Wymagany plik/folder nie znaleziono"
  3030.                 elif isinstance(e, ConnectionError):
  3031.                      error_title = "Błąd połączenia sieciowego"
  3032.                 elif isinstance(e, PermissionError):
  3033.                      error_title = "Błąd uprawnień (klucz API?)"
  3034.  
  3035.                 QMessageBox.critical(self, error_title, f"Nie udało się przygotować instancji:\n{e}")
  3036.                 self.update_instance_tiles()
  3037.  
  3038.     def import_instance(self):
  3039.         file, _ = QFileDialog.getOpenFileName(self, "Importuj instancję", "", "Archiwa ZIP (*.zip);;Wszystkie pliki (*)")
  3040.         if file:
  3041.             if self.launcher.current_download_thread or self.launcher.download_queue:
  3042.                  QMessageBox.warning(self, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Poczekaj na jego zakończenie lub anuluj.")
  3043.                  return
  3044.  
  3045.             try:
  3046.                 imported_dir = self.launcher.import_instance(file)
  3047.                 QMessageBox.information(self, "Sukces", f"Instancja zaimportowana pomyślnie do:\n{imported_dir}")
  3048.                 self.update_instance_tiles()
  3049.             except (FileNotFoundError, zipfile.BadZipFile, ValueError, Exception) as e:
  3050.                  error_title = "Błąd importu instancji"
  3051.                  if isinstance(e, FileNotFoundError):
  3052.                      error_title = "Plik nie znaleziono"
  3053.                  elif isinstance(e, zipfile.BadZipFile):
  3054.                      error_title = "Nieprawidłowy plik ZIP"
  3055.                  QMessageBox.critical(self, error_title, f"Nie udało się zaimportować instancji:\n{e}")
  3056.  
  3057.     def export_instance(self):
  3058.         current_item = self.instance_list.currentItem()
  3059.         if not current_item or not self.selected_instance_dir:
  3060.             QMessageBox.warning(self, "Brak wybranej instancji", "Proszę wybrać instancję do eksportu.")
  3061.             return
  3062.  
  3063.         instance_name = current_item.text()
  3064.         instance_dir_path = self.selected_instance_dir
  3065.  
  3066.         if not Path(instance_dir_path).exists():
  3067.              self.update_instance_tiles()
  3068.              return
  3069.  
  3070.         default_filename = f"{instance_name}.zip"
  3071.         start_dir = str(Path(instance_dir_path).parent)
  3072.         if not Path(start_dir).exists():
  3073.              start_dir = str(Path.home())
  3074.  
  3075.         file, _ = QFileDialog.getSaveFileName(self, f"Eksportuj instancję '{instance_name}'", os.path.join(start_dir, default_filename), "Archiwa ZIP (*.zip);;Wszystkie pliki (*)")
  3076.         if file:
  3077.             if not file.lower().endswith('.zip'):
  3078.                  file += '.zip'
  3079.             try:
  3080.                 self.launcher.export_instance(instance_dir_path, file)
  3081.                 QMessageBox.information(self, "Sukces", f"Instancja '{instance_name}' wyeksportowana pomyślnie do:\n{file}")
  3082.             except (FileNotFoundError, IOError, Exception) as e:
  3083.                  error_title = "Błąd eksportu instancji"
  3084.                  if isinstance(e, FileNotFoundError):
  3085.                       error_title = "Katalog instancji nie znaleziono"
  3086.                  elif isinstance(e, IOError):
  3087.                       error_title = "Błąd zapisu pliku"
  3088.  
  3089.                  QMessageBox.critical(self, error_title, f"Nie udało się wyeksportować instancji '{instance_name}':\n{e}")
  3090.  
  3091.     def delete_instance(self):
  3092.         current_item = self.instance_list.currentItem()
  3093.         if not current_item or not self.selected_instance_dir:
  3094.             QMessageBox.warning(self, "Brak wybranej instancji", "Proszę wybrać instancję do usunięcia.")
  3095.             self.delete_instance_button.setEnabled(False)
  3096.             return
  3097.  
  3098.         instance_name = current_item.text()
  3099.         instance_dir_path = self.selected_instance_dir
  3100.         instance_dir = Path(instance_dir_path)
  3101.  
  3102.         if not instance_dir.exists():
  3103.              QMessageBox.warning(self, "Błąd", "Katalog instancji już nie istnieje. Odświeżam listę.")
  3104.              self.update_instance_tiles()
  3105.              return
  3106.  
  3107.         reply = QMessageBox.question(self, "Potwierdzenie usunięcia",
  3108.                                      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}",
  3109.                                      QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
  3110.                                      QMessageBox.StandardButton.No)
  3111.  
  3112.         if reply == QMessageBox.StandardButton.Yes:
  3113.             try:
  3114.                 logging.info(f"Usuwanie instancji: {instance_dir_path}")
  3115.                 shutil.rmtree(instance_dir)
  3116.                 logging.info("Instancja usunięta pomyślnie.")
  3117.                 QMessageBox.information(self, "Sukces", f"Instancja '{instance_name}' została usunięta.")
  3118.                 self.update_instance_tiles()
  3119.             except Exception as e:
  3120.                 logging.error(f"Błąd podczas usuwania instancji {instance_dir_path}: {e}")
  3121.                 QMessageBox.critical(self, "Błąd usuwania instancji", f"Nie udało się usunąć instancji '{instance_name}':\n{e}")
  3122.  
  3123.  
  3124.     def load_instance(self, item):
  3125.         instance_name = item.text()
  3126.         instance_dir_path = item.data(Qt.ItemDataRole.UserRole)
  3127.         logging.info(f"Wybrano instancję: '{instance_name}' w katalogu {instance_dir_path}")
  3128.  
  3129.         instance_dir = Path(instance_dir_path)
  3130.         if not instance_dir.exists():
  3131.             logging.error(f"Katalog instancji nie istnieje: {instance_dir_path}")
  3132.             self.instance_list.takeItem(self.instance_list.row(item))
  3133.             self.handle_instance_selection_change()
  3134.             return
  3135.  
  3136.         self.selected_instance_dir = instance_dir_path
  3137.  
  3138.         settings_path = instance_dir / "settings.json"
  3139.         has_settings = False
  3140.         version_id = None
  3141.  
  3142.         if settings_path.exists():
  3143.              try:
  3144.                  with settings_path.open("r", encoding='utf-8') as f:
  3145.                      settings = json.load(f)
  3146.                  version_id = settings.get("version")
  3147.                  has_settings = True
  3148.              except json.JSONDecodeError as e:
  3149.                  logging.error(f"Błąd odczytu settings.json dla instancji {instance_name}: {e}")
  3150.                  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.")
  3151.              except Exception as e:
  3152.                  logging.error(f"Nieoczekiwany błąd podczas ładowania settings.json dla instancji {instance_name}: {e}")
  3153.                  QMessageBox.critical(self, "Błąd ładowania instancji", f"Wystąpił nieoczekiwany błąd podczas odczytu ustawień instancji '{instance_name}'.")
  3154.  
  3155.         self.play_button.setEnabled(has_settings and version_id is not None)
  3156.         self.mod_browser_button.setEnabled(has_settings and version_id is not None)
  3157.         self.delete_instance_button.setEnabled(True)
  3158.  
  3159.     def play_instance(self):
  3160.         current_item = self.instance_list.currentItem()
  3161.         instance_dir_path = self.selected_instance_dir
  3162.  
  3163.         if not current_item or not instance_dir_path:
  3164.             QMessageBox.warning(self, "Brak wybranej instancji", "Proszę wybrać instancję do uruchomienia.")
  3165.             self.play_button.setEnabled(False)
  3166.             return
  3167.  
  3168.         instance_name = current_item.text()
  3169.  
  3170.         if not Path(instance_dir_path).exists():
  3171.              QMessageBox.critical(self, "Błąd uruchamiania", "Katalog wybranej instancji nie istnieje! Odświeżam listę.")
  3172.              self.update_instance_tiles()
  3173.              return
  3174.  
  3175.         if self.launcher.current_download_thread or self.launcher.download_queue:
  3176.             QMessageBox.warning(self, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Proszę poczekać na jego zakończenie przed uruchomieniem gry.")
  3177.             return
  3178.  
  3179.         username = self.launcher.settings.get("default_account", DEFAULT_SETTINGS["default_account"])
  3180.         if not username or not username.strip():
  3181.              username, ok = QInputDialog.getText(self, "Nazwa gracza offline", "Wprowadź domyślną nazwę użytkownika offline:", text="Player")
  3182.              if not ok or not username.strip():
  3183.                   QMessageBox.warning(self, "Anulowano", "Nazwa gracza jest wymagana do uruchomienia gry offline.")
  3184.                   self.status_label.setText("Uruchomienie anulowane (brak nazwy gracza).")
  3185.                   return
  3186.              username = username.strip()
  3187.  
  3188.         try:
  3189.             self.status_label.setText(f"Uruchamiam instancję: {instance_name}...")
  3190.             self.launcher.launch_game(instance_dir_path, username)
  3191.             self.status_label.setText(f"Uruchomiono instancję: {instance_name}")
  3192.  
  3193.         except (FileNotFoundError, ValueError, RuntimeError, TimeoutError, Exception) as e:
  3194.             error_title = "Błąd uruchamiania"
  3195.             if isinstance(e, FileNotFoundError):
  3196.                  error_title = "Brak wymaganych plików"
  3197.             elif isinstance(e, ValueError):
  3198.                  error_title = "Błąd konfiguracji instancji"
  3199.             elif isinstance(e, TimeoutError):
  3200.                  error_title = "Przekroczono czas oczekiwania"
  3201.  
  3202.             logging.error(f"Błąd podczas uruchamiania instancji {instance_name}: {e}")
  3203.             QMessageBox.critical(self, error_title, f"Nie udało się uruchomić gry:\n{e}\nSprawdź logi launchera.")
  3204.             self.status_label.setText(f"Błąd uruchamiania instancji {instance_name}.")
  3205.  
  3206.     def open_mod_browser(self):
  3207.         current_item = self.instance_list.currentItem()
  3208.         instance_dir_path = self.selected_instance_dir
  3209.  
  3210.         if not current_item or not instance_dir_path:
  3211.             QMessageBox.warning(self, "Brak wybranej instancji", "Proszę wybrać instancję, dla której chcesz przeglądać mody.")
  3212.             self.mod_browser_button.setEnabled(False)
  3213.             return
  3214.  
  3215.         instance_name = current_item.text()
  3216.  
  3217.         if not Path(instance_dir_path).exists():
  3218.              QMessageBox.critical(self, "Błąd przeglądania modów", "Katalog wybranej instancji nie istnieje! Odświeżam listę.")
  3219.              self.update_instance_tiles()
  3220.              return
  3221.  
  3222.         settings_path = Path(instance_dir_path) / "settings.json"
  3223.         if not settings_path.exists():
  3224.              QMessageBox.warning(self, "Ustawienia instancji", f"Brak pliku settings.json dla instancji '{instance_name}'. Nie można przeglądać modów.")
  3225.              self.mod_browser_button.setEnabled(False)
  3226.              return
  3227.  
  3228.         try:
  3229.              with settings_path.open("r", encoding='utf-8') as f:
  3230.                  settings = json.load(f)
  3231.              version_id = settings.get("version")
  3232.  
  3233.              if not version_id:
  3234.                  QMessageBox.warning(self, "Wersja nieznana", f"Wersja gry dla instancji '{instance_name}' nie została poprawnie skonfigurowana. Nie można przeglądać modów.")
  3235.                  self.mod_browser_button.setEnabled(False)
  3236.                  return
  3237.  
  3238.              if self.launcher.current_download_thread or self.launcher.download_queue:
  3239.                   QMessageBox.warning(self, "Pobieranie aktywne", "Inny proces pobierania jest aktywny. Proszę poczekać na jego zakończenie.")
  3240.                   return
  3241.  
  3242.              dialog = ModBrowserDialog(self.launcher, version_id, instance_dir_path, self)
  3243.              dialog.exec()
  3244.  
  3245.         except json.JSONDecodeError as e:
  3246.              logging.error(f"Błąd odczytu settings.json dla instancji {instance_name}: {e}")
  3247.              QMessageBox.critical(self, "Błąd ładowania instancji", f"Nie udało się odczytać ustawień instancji '{instance_name}'.")
  3248.         except Exception as e:
  3249.             logging.error(f"Nieoczekiwany błąd podczas otwierania przeglądarki modów dla instancji {instance_name}: {e}")
  3250.             QMessageBox.critical(self, "Błąd przeglądania modów", f"Wystąpił nieoczekiwany błąd: {e}")
  3251.  
  3252.     def set_offline_account(self):
  3253.         current_username = self.launcher.settings.get("default_account", "")
  3254.         username, ok = QInputDialog.getText(self, "Ustaw nazwę konta offline", "Wprowadź domyślną nazwę użytkownika offline:", text=current_username)
  3255.         if ok and username:
  3256.             username = username.strip()
  3257.             if username:
  3258.                 self.launcher.settings["default_account"] = username
  3259.                 self.launcher.save_settings()
  3260.                 QMessageBox.information(self, "Ustawiono konto", f"Domyślne konto offline ustawione na: '{username}'.")
  3261.                 logging.info(f"Ustawiono domyślne konto offline: {username}")
  3262.             else:
  3263.                 self.launcher.settings["default_account"] = ""
  3264.                 self.launcher.save_settings()
  3265.                 QMessageBox.information(self, "Ustawiono konto", "Domyślne konto offline zostało zresetowane. Nazwa będzie pytana przy uruchomieniu lub użyta domyślna 'Player'.")
  3266.                 logging.info("Domyślne konto offline zresetowane.")
  3267.  
  3268.     def open_settings(self):
  3269.         dialog = QDialog(self)
  3270.         dialog.setWindowTitle("Ustawienia launchera")
  3271.         dialog.setMinimumWidth(400)
  3272.         layout = QVBoxLayout(dialog)
  3273.         layout.setSpacing(10)
  3274.  
  3275.         layout.addWidget(QLabel("Motyw interfejsu:"))
  3276.         theme_combo = QComboBox()
  3277.         theme_combo.addItems(["Light", "Night"])
  3278.         theme_combo.setCurrentText(self.launcher.settings.get("theme", "Light"))
  3279.         layout.addWidget(theme_combo)
  3280.  
  3281.         layout.addWidget(QLabel("Domyślna wersja Javy dla nowych instancji:"))
  3282.         java_combo = QComboBox()
  3283.         java_combo.addItem("Automatyczny wybór", userData="auto")
  3284.         sorted_java_versions = sorted(self.launcher.java_versions, key=lambda x: self.launcher.get_java_version_from_path(x[0]) or 0, reverse=True)
  3285.         for java_path, version in sorted_java_versions:
  3286.             major_v = self.launcher.get_java_version_from_path(java_path)
  3287.             java_combo.addItem(f"{version} (Java {major_v}) - {java_path}", userData=java_path)
  3288.  
  3289.         current_java_setting = self.launcher.settings.get("java_path", "auto")
  3290.         if current_java_setting.lower() == 'auto':
  3291.              java_combo.setCurrentText("Automatyczny wybór")
  3292.         else:
  3293.              found_index = java_combo.findData(current_java_setting)
  3294.              if found_index != -1:
  3295.                   java_combo.setCurrentIndex(found_index)
  3296.              else:
  3297.                   custom_item_text = f"Zapisana ścieżka: {current_java_setting} (Nieznana wersja)"
  3298.                   java_combo.addItem(custom_item_text, userData=current_java_setting)
  3299.                   java_combo.setCurrentIndex(java_combo.count() - 1)
  3300.                   logging.warning(f"Zapisana ścieżka Javy w ustawieniach nie znaleziona wśród automatycznie wykrytych: {current_java_setting}. Dodano jako opcję niestandardową.")
  3301.  
  3302.         layout.addWidget(java_combo)
  3303.  
  3304.         layout.addWidget(QLabel("Domyślna pamięć RAM (np. 4G, 2048M):"))
  3305.         ram_input = QLineEdit(self.launcher.settings.get("ram", DEFAULT_SETTINGS["ram"]))
  3306.         layout.addWidget(ram_input)
  3307.  
  3308.         layout.addWidget(QLabel("Domyślne dodatkowe argumenty JVM:"))
  3309.         jvm_args_input = QLineEdit(self.launcher.settings.get("jvm_args", DEFAULT_SETTINGS["jvm_args"]))
  3310.         layout.addWidget(jvm_args_input)
  3311.  
  3312.         fullscreen_check = QCheckBox("Domyślnie pełny ekran")
  3313.         fullscreen_check.setChecked(self.launcher.settings.get("fullscreen", DEFAULT_SETTINGS["fullscreen"]))
  3314.         layout.addWidget(fullscreen_check)
  3315.  
  3316.         layout.addWidget(QLabel("Domyślna rozdzielczość (np. 1280x720):"))
  3317.         resolution_input = QLineEdit(self.launcher.settings.get("resolution", DEFAULT_SETTINGS["resolution"]))
  3318.         layout.addWidget(resolution_input)
  3319.  
  3320.         current_account_label = QLabel(f"Domyślne konto offline: {self.launcher.settings.get('default_account', 'Brak')}")
  3321.         layout.addWidget(current_account_label)
  3322.  
  3323.         button_layout = QHBoxLayout()
  3324.         save_button = QPushButton("Zapisz ustawienia")
  3325.         save_button.clicked.connect(dialog.accept)
  3326.  
  3327.         cancel_button = QPushButton("Anuluj")
  3328.         cancel_button.clicked.connect(dialog.reject)
  3329.  
  3330.         button_layout.addStretch(1)
  3331.         button_layout.addWidget(save_button)
  3332.         button_layout.addWidget(cancel_button)
  3333.         layout.addLayout(button_layout)
  3334.  
  3335.         if dialog.exec():
  3336.             selected_theme = theme_combo.currentText()
  3337.  
  3338.             selected_java_index = java_combo.currentIndex()
  3339.             selected_java_path_data = java_combo.itemData(selected_java_index)
  3340.             selected_java_path_to_save = selected_java_path_data if selected_java_path_data is not None else "auto"
  3341.  
  3342.             selected_ram = ram_input.text().strip().upper()
  3343.             selected_jvm_args = jvm_args_input.text().strip()
  3344.             selected_fullscreen = fullscreen_check.isChecked()
  3345.             selected_resolution = resolution_input.text().strip()
  3346.  
  3347.             if not re.match(r"^\d+[MG]$", selected_ram):
  3348.                  QMessageBox.warning(dialog, "Nieprawidłowy format RAM", "Nieprawidłowy format pamięci RAM. Ustawienia nie zostały zapisane.")
  3349.                  return
  3350.  
  3351.             if not re.match(r"^\d+x\d+$", selected_resolution):
  3352.                 QMessageBox.warning(dialog, "Nieprawidłowy format rozdzielczości", "Nieprawidłowy format rozdzielczości. Ustawienia nie zostały zapisane.")
  3353.                 return
  3354.  
  3355.             if selected_java_path_to_save != 'auto' and not Path(selected_java_path_to_save).exists():
  3356.                  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.")
  3357.                  return
  3358.  
  3359.             self.launcher.settings["theme"] = selected_theme
  3360.             self.launcher.settings["java_path"] = selected_java_path_to_save
  3361.             self.launcher.settings["ram"] = selected_ram
  3362.             self.launcher.settings["jvm_args"] = selected_jvm_args
  3363.             self.launcher.settings["fullscreen"] = selected_fullscreen
  3364.             self.launcher.settings["resolution"] = selected_resolution
  3365.  
  3366.             self.launcher.save_settings()
  3367.  
  3368.             self.apply_theme()
  3369.             logging.info("Ustawienia launchera zaktualizowane.")
  3370.             QMessageBox.information(self, "Sukces", "Ustawienia zostały zapisane.")
  3371.  
  3372.     def closeEvent(self, event):
  3373.         if self.launcher.progress_dialog and self.launcher.progress_dialog.isVisible():
  3374.             reply = QMessageBox.question(self, "Zamknąć?",
  3375.                                          "Pobieranie wciąż trwa. Czy na pewno chcesz zamknąć launcher i anulować pobieranie?",
  3376.                                          QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
  3377.             if reply == QMessageBox.StandardButton.Yes:
  3378.                 self.launcher.progress_dialog.cancel_downloads()
  3379.                 event.accept()
  3380.             else:
  3381.                 event.ignore()
  3382.         else:
  3383.             event.accept()
  3384.  
  3385. if __name__ == "__main__":
  3386.     signal.signal(signal.SIGINT, signal.SIG_DFL)
  3387.  
  3388.     app = QApplication(sys.argv)
  3389.     window = LauncherWindow()
  3390.     window.show()
  3391.     sys.exit(app.exec())
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement