Advertisement
PaffcioStudio

Untitled

Apr 29th, 2025
267
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 191.03 KB | None | 0 0
  1. import subprocess
  2. import sys
  3. import importlib.metadata
  4. import tkinter as tk
  5. from tkinter import ttk, messagebox, scrolledtext, filedialog, simpledialog
  6. import requests
  7. import os
  8. import json
  9. import threading
  10. import re
  11. import hashlib
  12. import pyperclip
  13. import zipfile
  14. import tarfile  # Added for .tar.gz archives (Modrinth mods)
  15. from datetime import datetime
  16. import platform
  17. import webbrowser
  18. from packaging import version as pkg_version
  19. import humanize
  20. from colorama import init, Fore, Style
  21. from PIL import Image, ImageTk
  22. import time # For tooltips
  23.  
  24. # Inicjalizacja colorama
  25. init(autoreset=True)
  26.  
  27. # --- Utility: Tooltip ---
  28. class Tooltip:
  29.     def __init__(self, widget, text):
  30.         self.widget = widget
  31.         self.text = text
  32.         self.tooltip_window = None
  33.         self.widget.bind("<Enter>", self.show_tooltip)
  34.         self.widget.bind("<Leave>", self.hide_tooltip)
  35.         self.id = None
  36.  
  37.     def show_tooltip(self, event=None):
  38.         self.id = self.widget.after(500, self._show) # Delay tooltip appearance
  39.  
  40.     def hide_tooltip(self, event=None):
  41.         if self.id:
  42.             self.widget.after_cancel(self.id)
  43.         self._hide()
  44.  
  45.     def _show(self):
  46.         if self.tooltip_window:
  47.             return
  48.         x, y, cx, cy = self.widget.bbox("insert")
  49.         x += self.widget.winfo_rootx() + 25
  50.         y += self.widget.winfo_rooty() + 20
  51.  
  52.         self.tooltip_window = tk.Toplevel(self.widget)
  53.         self.tooltip_window.wm_overrideredirect(True) # Hide window borders
  54.         self.tooltip_window.wm_geometry(f"+{x}+{y}")
  55.  
  56.         label = tk.Label(self.tooltip_window, text=self.text, background="#ffffc0", relief="solid", borderwidth=1, font=("tahoma", "8", "normal"))
  57.         label.pack()
  58.  
  59.     def _hide(self):
  60.         if self.tooltip_window:
  61.             self.tooltip_window.destroy()
  62.         self.tooltip_window = None
  63. # --- End Tooltip ---
  64.  
  65. # Instalacja zależności (przeniesione na początek skryptu, ale po importach)
  66. def install_requirements():
  67.     required_libraries = ['requests', 'pyperclip', 'packaging', 'humanize', 'colorama', 'pillow']
  68.     print(f"{Fore.CYAN}Sprawdzanie zależności...{Style.RESET_ALL}")
  69.     for library in required_libraries:
  70.         try:
  71.             importlib.metadata.version(library)
  72.             print(f"{Fore.GREEN}[OK] {library} już zainstalowane.")
  73.         except importlib.metadata.PackageNotFoundError:
  74.             try:
  75.                 print(f"{Fore.YELLOW}[INFO] Instaluję {library}...")
  76.                 subprocess.check_call([sys.executable, "-m", "pip", "install", library], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  77.                 print(f"{Fore.CYAN}[INFO] {library} zainstalowane.")
  78.             except subprocess.CalledProcessError as e:
  79.                  print(f"{Fore.RED}[ERROR] Nie udało się zainstalować {library}. Błąd: {e.stderr.decode().strip()}")
  80.             except Exception as e:
  81.                  print(f"{Fore.RED}[ERROR] Nieznany błąd podczas instalacji {library}: {e}")
  82.     print(f"{Fore.CYAN}Zakończono sprawdzanie zależności.{Style.RESET_ALL}")
  83.  
  84. install_requirements()
  85.  
  86. # Ścieżki
  87. BASE_DIR = os.path.join(os.getcwd(), "minecraft_launcher_data") # Changed base dir to avoid conflicts
  88. CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
  89. ASSETS_DIR = os.path.join(BASE_DIR, "assets")
  90. LIBRARIES_DIR = os.path.join(BASE_DIR, "libraries")
  91. NATIVES_DIR = os.path.join(BASE_DIR, "natives")
  92. LOGS_DIR = os.path.join(BASE_DIR, "logs")
  93. JAVA_DIR = os.path.join(BASE_DIR, "java")
  94. ICONS_DIR = os.path.join(BASE_DIR, "icons")
  95. # Ensure icons directory exists if we plan to use custom icons
  96. os.makedirs(ICONS_DIR, exist_ok=True)
  97.  
  98. # Kolory i styl (Ulepszone nazewnictwo)
  99. PRIMARY_BG = "#1a1a1a"
  100. SECONDARY_BG = "#2a2a2a"
  101. TERTIARY_BG = "#3a3a3a"
  102. PRIMARY_FG = "#ffffff"
  103. ACCENT_COLOR = "#2a9fd6" # Blue
  104. SUCCESS_COLOR = "#5cb85c" # Green
  105. ERROR_COLOR = "#d9534f" # Red
  106. WARNING_COLOR = "#f0ad4e" # Yellow
  107. INFO_COLOR = "#337ab7" # Light Blue
  108.  
  109. CONSOLE_BG = "#0d0d0d"
  110. CONSOLE_FG_DEFAULT = "#cccccc"
  111. CONSOLE_FG_INFO = INFO_COLOR
  112. CONSOLE_FG_SUCCESS = SUCCESS_COLOR
  113. CONSOLE_FG_WARNING = WARNING_COLOR
  114. CONSOLE_FG_ERROR = ERROR_COLOR
  115.  
  116. BUTTON_BG = TERTIARY_BG
  117. BUTTON_HOVER = SECONDARY_BG
  118. SIDEBAR_BG = "#222222"
  119. ACTIVE_TAB_COLOR = ACCENT_COLOR # Active tab is the accent color
  120. HOVER_TAB_COLOR = "#444444" # Slightly lighter hover
  121.  
  122. # Globalne zmienne
  123. # Initialize variables before potentially loading config
  124. pending_instance_settings = {}
  125. pending_version = ""
  126. download_thread = None
  127. download_active = False
  128. global_progress_bar = None
  129. global_status_label = None
  130. instances = {}
  131. java_versions_cache = {}
  132. console = None
  133. username_var = tk.StringVar(value="Player")
  134. memory_var = tk.StringVar(value="2")
  135. shared_assets_var = tk.BooleanVar(value=True)
  136. shared_libraries_var = tk.BooleanVar(value=True)
  137. shared_natives_var = tk.BooleanVar(value=True)
  138. snapshots_var = tk.BooleanVar(value=True)
  139. releases_var = tk.BooleanVar(value=True)
  140. alpha_var = tk.BooleanVar(value=False)
  141. beta_var = tk.BooleanVar(value=False)
  142. current_tab = tk.StringVar(value="Instancje")
  143. selected_modrinth_instance_var = tk.StringVar()
  144.  
  145.  
  146. # Funkcje narzędziowe
  147. def log_to_console(console_widget, message, level="INFO"):
  148.     timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  149.     log_message = f"[{timestamp}] {level}: {message}\n"
  150.     # Log to file always
  151.     os.makedirs(LOGS_DIR, exist_ok=True)
  152.     try:
  153.         with open(os.path.join(LOGS_DIR, "launcher.log"), "a", encoding="utf-8") as f:
  154.             f.write(log_message)
  155.     except Exception as e:
  156.         print(f"Failed to write to launcher.log: {e}") # Fallback print
  157.  
  158.     # Log to console widget if available
  159.     if console_widget:
  160.         try:
  161.             console_widget.config(state="normal")
  162.             tag = level.lower()
  163.             if tag not in console_widget.tag_names():
  164.                 color = CONSOLE_FG_DEFAULT
  165.                 if level == "ERROR": color = CONSOLE_FG_ERROR
  166.                 elif level == "WARNING": color = CONSOLE_FG_WARNING
  167.                 elif level == "SUCCESS": color = CONSOLE_FG_SUCCESS
  168.                 elif level == "INFO": color = CONSOLE_FG_INFO
  169.                 console_widget.tag_config(tag, foreground=color)
  170.  
  171.             console_widget.insert(tk.END, log_message, tag)
  172.             console_widget.see(tk.END)
  173.             console_widget.config(state="disabled")
  174.         except Exception as e:
  175.              # This shouldn't happen if console_widget is valid, but as a fallback
  176.             print(f"Failed to write to console widget: {e}")
  177.  
  178.  
  179. def verify_sha1(file_path, expected_sha1):
  180.    if not os.path.exists(file_path):
  181.        return False
  182.    sha1 = hashlib.sha1()
  183.    try:
  184.        with open(file_path, "rb") as f:
  185.            # Read in chunks to handle large files
  186.            while chunk := f.read(4096):
  187.                sha1.update(chunk)
  188.        return sha1.hexdigest() == expected_sha1
  189.    except Exception as e:
  190.        log_to_console(console, f"Error verifying SHA1 for {file_path}: {e}", "ERROR")
  191.        return False
  192.  
  193. def save_config():
  194.    config = {
  195.        "default_settings": {
  196.            "username": username_var.get(),
  197.            "memory": memory_var.get(),
  198.            "shared_assets": shared_assets_var.get(),
  199.            "shared_libraries": shared_libraries_var.get(),
  200.            "shared_natives": shared_natives_var.get()
  201.        },
  202.        "version_filters": {
  203.            "snapshots": snapshots_var.get(),
  204.            "releases": releases_var.get(),
  205.            "alpha": alpha_var.get(),
  206.            "beta": beta_var.get()
  207.        },
  208.        "instances": instances,
  209.        "java_versions": java_versions_cache # Cache found/downloaded java paths
  210.    }
  211.    os.makedirs(BASE_DIR, exist_ok=True)
  212.    try:
  213.        with open(CONFIG_FILE, "w", encoding="utf-8") as f:
  214.            json.dump(config, f, indent=4)
  215.        log_to_console(console, "Konfiguracja zapisana.", "INFO")
  216.    except Exception as e:
  217.        log_to_console(console, f"Błąd podczas zapisywania konfiguracji: {e}", "ERROR")
  218.  
  219.  
  220. def load_config():
  221.    global instances, java_versions_cache
  222.    # Variables are initialized globally before this function is called.
  223.  
  224.    if os.path.exists(CONFIG_FILE):
  225.        try:
  226.            with open(CONFIG_FILE, "r", encoding="utf-8") as f:
  227.                config = json.load(f)
  228.  
  229.                if "default_settings" in config:
  230.                    default_settings = config["default_settings"]
  231.                    username_var.set(default_settings.get("username", "Player"))
  232.                    memory_var.set(default_settings.get("memory", "2"))
  233.                    shared_assets_var.set(default_settings.get("shared_assets", True))
  234.                    shared_libraries_var.set(default_settings.get("shared_libraries", True))
  235.                    shared_natives_var.set(default_settings.get("shared_natives", True))
  236.  
  237.                if "version_filters" in config:
  238.                    version_filters = config["version_filters"]
  239.                    snapshots_var.set(version_filters.get("snapshots", True))
  240.                    releases_var.set(version_filters.get("releases", True))
  241.                    alpha_var.set(version_filters.get("alpha", False))
  242.                    beta_var.set(version_filters.get("beta", False))
  243.  
  244.                # Load instances, ensuring required keys exist with defaults
  245.                instances = config.get("instances", {})
  246.                for version, data in instances.items():
  247.                     data.setdefault("settings", {})
  248.                     data["settings"].setdefault("username", username_var.get())
  249.                     data["settings"].setdefault("memory", memory_var.get())
  250.                     data["settings"].setdefault("shared_assets", shared_assets_var.get())
  251.                     data["settings"].setdefault("shared_libraries", shared_libraries_var.get())
  252.                     data["settings"].setdefault("shared_natives", shared_natives_var.get())
  253.                     data["settings"].setdefault("loader_type", "vanilla") # Default to vanilla
  254.                     data["settings"].setdefault("loader_version", "")
  255.                     data["settings"].setdefault("server_ip", "")
  256.                     data["settings"].setdefault("server_port", "")
  257.                     data.setdefault("java_path", "")
  258.                     data.setdefault("java_version", "")
  259.                     data.setdefault("required_java", "1.8")
  260.                     data.setdefault("ready", False) # Assume not ready until verified or downloaded
  261.                     data.setdefault("timestamp", datetime.now().isoformat()) # Add timestamp if missing
  262.  
  263.  
  264.                java_versions_cache = config.get("java_versions", {})
  265.  
  266.                log_to_console(console, "Konfiguracja wczytana.", "INFO")
  267.                return instances, java_versions_cache
  268.        except Exception as e:
  269.            log_to_console(console, f"Błąd wczytywania konfiguracji. Resetowanie do domyślnych: {e}", "ERROR")
  270.            # Reset to default values in case of error
  271.            instances = {}
  272.            java_versions_cache = {}
  273.            # Variables like username_var etc. are already set to defaults
  274.            return instances, java_versions_cache
  275.    else:
  276.        log_to_console(console, "Plik konfiguracji nie istnieje. Użyto domyślnych ustawień.", "INFO")
  277.        # Default values if file doesn't exist
  278.         instances = {}
  279.         java_versions_cache = {}
  280.         return instances, java_versions_cache
  281.  
  282. def get_versions():
  283.     try:
  284.         url = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
  285.         resp = requests.get(url, timeout=10)
  286.         resp.raise_for_status()
  287.         manifest = resp.json()
  288.         versions = []
  289.         allowed_types = []
  290.         if snapshots_var.get():
  291.             allowed_types.append("snapshot")
  292.         if releases_var.get():
  293.             allowed_types.append("release")
  294.         if alpha_var.get():
  295.             allowed_types.append("old_alpha")
  296.         if beta_var.get():
  297.             allowed_types.append("old_beta")
  298.         for v in manifest["versions"]:
  299.             if v["type"] in allowed_types:
  300.                 versions.append(v["id"])
  301.         log_to_console(console, f"Pobrano {len(versions)} wersji (filtry: {', '.join(allowed_types)})", "INFO")
  302.         # Sort by parsing version string, releases first, then snapshots, then old
  303.         def version_sort_key(v_id):
  304.             v_info = next((item for item in manifest['versions'] if item['id'] == v_id), None)
  305.             if v_info:
  306.                 v_type = v_info['type']
  307.                 # Assign a numeric value to types for sorting: release > snapshot > beta > alpha
  308.                 type_order = {'release': 4, 'snapshot': 3, 'old_beta': 2, 'old_alpha': 1}
  309.                 type_priority = type_order.get(v_type, 0)
  310.                 try:
  311.                     # Use packaging.version for robust version comparison
  312.                     parsed_version = pkg_version.parse(v_id.split('-')[0]) # Parse core version part
  313.                     return (-type_priority, -parsed_version.release[0] if parsed_version.release else 0, -parsed_version.public[0] if parsed_version.public else 0, v_id) # Sort descending by type priority, then version parts, then id
  314.                 except:
  315.                     # Handle invalid versions by putting them last
  316.                     return (-type_priority, -float('inf'), v_id)
  317.             return (0, v_id) # Default for unknown versions
  318.  
  319.         return sorted(versions, key=version_sort_key, reverse=True) # Reverse again to get newest first within categories
  320.     except Exception as e:
  321.         log_to_console(console, f"Nie udało się pobrać listy wersji: {e}", "ERROR")
  322.         return []
  323.  
  324. def get_version_info(version):
  325.     try:
  326.         url = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
  327.         resp = requests.get(url, timeout=10)
  328.         resp.raise_for_status()
  329.         manifest = resp.json()
  330.         for v in manifest["versions"]:
  331.             if v["id"] == version:
  332.                 version_url = v["url"]
  333.                 resp_info = requests.get(version_url, timeout=10)
  334.                 resp_info.raise_for_status()
  335.                 return resp_info.json()
  336.         log_to_console(console, f"Nie znaleziono wersji {version} w manifeście.", "WARNING")
  337.         return None
  338.     except Exception as e:
  339.         log_to_console(console, f"Nie udało się pobrać info o wersji {version}: {e}", "ERROR")
  340.         return None
  341.  
  342. def download_java(java_version, console_widget):
  343.     try:
  344.         system = platform.system().lower()
  345.         arch = "x64"
  346.         base_url = "https://api.adoptium.net/v3/binary/latest/"
  347.         # Map Minecraft required version strings to Adoptium feature versions
  348.         java_map = {
  349.             "1.8": "8",
  350.             "9": "9",
  351.             "10": "10",
  352.             "11": "11",
  353.             "12": "12",
  354.             "13": "13",
  355.             "14": "14",
  356.             "15": "15",
  357.             "16": "16",
  358.             "17": "17",
  359.             "18": "18",
  360.             "19": "19",
  361.             "20": "20",
  362.             "21": "21",
  363.             "22": "22"
  364.         }
  365.         feature_version = java_map.get(str(java_version), str(java_version)) # Use string keys for map lookup
  366.  
  367.         # Adoptium URL structure: /feature_version/release_type/os/arch/image_type/jvm_impl/heap_size/vendor
  368.         # release_type: ga (General Availability)
  369.         # os: windows, mac, linux, etc.
  370.         # arch: x64, arm, etc.
  371.         # image_type: jdk, jre
  372.         # jvm_impl: hotspot (OpenJDK's default)
  373.        # heap_size: normal (default)
  374.        # vendor: eclipse (Temurin)
  375.        url = f"{base_url}{feature_version}/ga/{system}/{arch}/jdk/hotspot/normal/eclipse"
  376.        log_to_console(console_widget, f"Szukam Javy {feature_version} (Adoptium) dla {system}/{arch}...", "INFO")
  377.  
  378.        resp_metadata = requests.get(url, timeout=10)
  379.        resp_metadata.raise_for_status()
  380.        metadata = resp_metadata.json()
  381.        if not metadata:
  382.            log_to_console(console_widget, f"Nie znaleziono dostępnych pakietów Javy {feature_version} dla {system}/{arch} na Adoptium.", "ERROR")
  383.            return None, None
  384.  
  385.        # Find a suitable package (e.g., with a .zip or .tar.gz link)
  386.        download_link = None
  387.        for package in metadata:
  388.             if package['binary']['os'] == system and package['binary']['architecture'] == arch and package['binary']['image_type'] == 'jdk':
  389.                 download_link = package['binary']['package']['link']
  390.                 expected_size = package['binary']['package'].get('size', 0)
  391.                 expected_sha256 = package['binary']['package'].get('checksum', None) # Adoptium uses SHA256
  392.                 break # Take the first suitable one
  393.  
  394.        if not download_link:
  395.             log_to_console(console_widget, f"Nie znaleziono linku do pobrania pakietu Javy {feature_version} dla {system}/{arch}.", "ERROR")
  396.             return None, None
  397.  
  398.        file_extension = ".zip" if system == "windows" else ".tar.gz"
  399.        java_target_dir = os.path.join(JAVA_DIR, f"jdk-{feature_version}-{system}-{arch}")
  400.        os.makedirs(java_target_dir, exist_ok=True)
  401.        archive_path = os.path.join(java_target_dir, f"jdk-{feature_version}{file_extension}")
  402.  
  403.        log_to_console(console_widget, f"Pobieranie Javy {feature_version} z {download_link}", "INFO")
  404.  
  405.        resp_file = requests.get(download_link, stream=True, timeout=30) # Increased timeout
  406.        resp_file.raise_for_status()
  407.        total_size = int(resp_file.headers.get('content-length', 0)) or expected_size
  408.  
  409.        downloaded_size = 0
  410.        with open(archive_path, "wb") as f:
  411.            for chunk in resp_file.iter_content(chunk_size=8192):
  412.                if chunk:
  413.                    f.write(chunk)
  414.                    downloaded_size += len(chunk)
  415.                    if total_size > 0 and global_progress_bar:
  416.                        update_progress(global_progress_bar, global_status_label,
  417.                                      (downloaded_size/total_size)*100, total_size-downloaded_size, f"Java {feature_version}", downloaded_size, total_size) # Pass bytes directly
  418.  
  419.        # Verification (SHA256 for Adoptium) - Need sha256 function if not using sha1
  420.        # For simplicity here, we'll skip sha256 verification for now or rely on sha1 if available (less common for modern downloads)
  421.         # if expected_sha256 and not verify_sha256(archive_path, expected_sha256):
  422.         #     log_to_console(console_widget, f"Błąd SHA256 dla archiwum Javy!", "ERROR")
  423.         #     os.remove(archive_path) # Clean up
  424.         #     return None, None
  425.         # Note: verify_sha1 exists, but Adoptium uses sha256. Implementing sha256 needed for proper verification.
  426.  
  427.         log_to_console(console_widget, f"Rozpakowywanie Javy do {java_target_dir}", "INFO")
  428.         extracted_folder = None
  429.         try:
  430.             if system == "windows":
  431.                 with zipfile.ZipFile(archive_path, 'r') as zip_ref:
  432.                     zip_ref.extractall(java_target_dir)
  433.                     # Find the actual JDK folder inside the zip (usually one level deep)
  434.                     extracted_folder = next((os.path.join(java_target_dir, name) for name in zip_ref.namelist() if name.endswith('/bin/') or name.endswith('\\bin\\')), None)
  435.                     if extracted_folder:
  436.                         extracted_folder = os.path.dirname(extracted_folder) # Go up one level to the JDK root
  437.             else: # macOS/Linux
  438.                  with tarfile.open(archive_path, 'r:gz') as tar_ref:
  439.                      tar_ref.extractall(java_target_dir)
  440.                      # Find the actual JDK folder inside the tar.gz
  441.                      extracted_folder = next((os.path.join(java_target_dir, name) for name in tar_ref.getnames() if name.endswith('/bin/') or name.endswith('\\bin\\')), None)
  442.                      if extracted_folder:
  443.                         extracted_folder = os.path.dirname(extracted_folder) # Go up one level to the JDK root
  444.  
  445.             os.remove(archive_path) # Clean up archive
  446.             log_to_console(console_widget, "Archiwum Javy rozpakowane.", "INFO")
  447.  
  448.         except Exception as e:
  449.             log_to_console(console_widget, f"Błąd rozpakowywania archiwum Javy: {e}", "ERROR")
  450.             if os.path.exists(archive_path):
  451.                 os.remove(archive_path)
  452.             return None, None
  453.  
  454.  
  455.         # Find java executable within extracted folder
  456.         if extracted_folder:
  457.             java_exec_name = "java.exe" if system == "windows" else "java"
  458.             # Search for java executable recursively in case the inner structure is complex
  459.             for root, _, files in os.walk(extracted_folder):
  460.                 if java_exec_name in files:
  461.                     java_path = os.path.join(root, java_exec_name)
  462.                     version = get_java_version(java_path) # Use the correct console widget
  463.                     if version:
  464.                         log_to_console(console_widget, f"Pobrano i zainstalowano Javę: {java_path} (wersja: {version})", "SUCCESS")
  465.                         # Add to cache
  466.                         java_versions_cache[version] = java_path
  467.                         save_config()
  468.                         return java_path, version
  469.             log_to_console(console_widget, "Nie znaleziono java(.exe) w pobranym i rozpakowanym folderze!", "ERROR")
  470.             return None, None
  471.         else:
  472.             log_to_console(console_widget, "Nie udało się znaleźć ścieżki JDK po rozpakowaniu.", "ERROR")
  473.             return None, None
  474.  
  475.     except requests.exceptions.RequestException as e:
  476.         log_to_console(console_widget, f"Błąd sieci podczas pobierania Javy {java_version}: {e}", "ERROR")
  477.         return None, None
  478.     except Exception as e:
  479.         log_to_console(console_widget, f"Ogólny błąd pobierania/instalacji Javy {java_version}: {e}", "ERROR")
  480.         return None, None
  481.  
  482. def find_java(required_version=None):
  483.     possible_paths = []
  484.     system = platform.system()
  485.     java_exec_name = "java.exe" if system == "Windows" else "java"
  486.     log_to_console(console, f"Szukam Javy {required_version or 'dowolnej'} ({java_exec_name})...", "INFO")
  487.  
  488.     # Check cache first
  489.     for ver, path in java_versions_cache.items():
  490.         if os.path.exists(path) and get_java_version(path): # Verify path still exists and is valid java
  491.              if (not required_version or check_java_version(ver, required_version)):
  492.                   possible_paths.append((path, ver, "Cache"))
  493.                   log_to_console(console, f"Znaleziono Javę w cache: {path} (wersja: {ver})", "INFO")
  494.  
  495.     # Check custom JAVA_DIR installations
  496.     if os.path.exists(JAVA_DIR):
  497.         for java_folder in os.listdir(JAVA_DIR):
  498.             java_path = os.path.join(JAVA_DIR, java_folder) # Point to the root of the JDK folder
  499.             # Search for java executable inside this folder structure
  500.             found_exec = None
  501.             for root, _, files in os.walk(java_path):
  502.                 if java_exec_name in files:
  503.                      found_exec = os.path.join(root, java_exec_name)
  504.                      break # Found the executable
  505.  
  506.             if found_exec:
  507.                 version = get_java_version(found_exec)
  508.                 if version and (not required_version or check_java_version(version, required_version)):
  509.                     if (found_exec, version, "Pobrana") not in possible_paths: # Avoid duplicates
  510.                          possible_paths.append((found_exec, version, "Pobrana"))
  511.                          log_to_console(console, f"Znaleziono Javę w {JAVA_DIR}: {found_exec} (wersja: {version})", "INFO")
  512.  
  513.     # Check standard system locations
  514.     java_home = os.environ.get("JAVA_HOME")
  515.     if java_home:
  516.         java_path = os.path.join(java_home, "bin", java_exec_name)
  517.         if os.path.exists(java_path):
  518.             version = get_java_version(java_path)
  519.             if version and (not required_version or check_java_version(version, required_version)):
  520.                 if (java_path, version, "JAVA_HOME") not in possible_paths:
  521.                     possible_paths.append((java_path, version, "JAVA_HOME"))
  522.                     log_to_console(console, f"Znaleziono Javę w JAVA_HOME: {java_path} (wersja: {version})", "INFO")
  523.  
  524.     # Check PATH
  525.     try:
  526.         # Use 'where' on Windows, 'which' on Unix-like
  527.         command = ["where", java_exec_name] if system == "Windows" else ["which", java_exec_name]
  528.         out = subprocess.check_output(command, stderr=subprocess.DEVNULL).decode().strip()
  529.         for line in out.splitlines():
  530.             line = line.strip()
  531.             if os.path.exists(line):
  532.                 version = get_java_version(line)
  533.                 if version and (not required_version or check_java_version(version, required_version)):
  534.                     if (line, version, "PATH") not in possible_paths:
  535.                         possible_paths.append((line, version, "PATH"))
  536.                         log_to_console(console, f"Znaleziono Javę w PATH: {line} (wersja: {version})", "INFO")
  537.     except:
  538.         pass # 'where' or 'which' command not found or java not in PATH
  539.  
  540.     # Specific Windows paths (less reliable, but can catch some installs)
  541.     if system == "Windows":
  542.          for base in [os.environ.get('ProgramFiles'), os.environ.get('ProgramFiles(x86)'), "C:\\Program Files\\Java", "C:\\Program Files (x86)\\Java"]:
  543.              if base and os.path.isdir(base):
  544.                  java_root = os.path.join(base, "Java")
  545.                  if os.path.isdir(java_root):
  546.                      for item in os.listdir(java_root):
  547.                          java_path = os.path.join(java_root, item, "bin", java_exec_name)
  548.                          if os.path.exists(java_path):
  549.                              version = get_java_version(java_path)
  550.                              if version and (not required_version or check_java_version(version, required_version)):
  551.                                  if (java_path, version, "Program Files") not in possible_paths:
  552.                                      possible_paths.append((java_path, version, "Program Files"))
  553.                                      log_to_console(console, f"Znaleziono Javę w {java_root}: {java_path} (wersja: {version})", "INFO")
  554.  
  555.  
  556.     # Ensure paths are unique (based on path itself) and prefer cached/downloaded ones
  557.     unique_paths = {}
  558.     for path, ver, source in possible_paths:
  559.         if path not in unique_paths:
  560.             unique_paths[path] = (path, ver, source)
  561.         else:
  562.              # Prefer 'Pobrana' or 'Cache' over others if duplicates exist
  563.              existing_source = unique_paths[path][2]
  564.              if source in ["Cache", "Pobrana"] and existing_source not in ["Cache", "Pobrana"]:
  565.                  unique_paths[path] = (path, ver, source)
  566.  
  567.     # Convert back to list, maintaining preference order if possible
  568.     # Simple sort order preference: Cache > Pobrana > JAVA_HOME > PATH > Others
  569.     source_order = {"Cache": 0, "Pobrana": 1, "JAVA_HOME": 2, "PATH": 3}
  570.     sorted_unique_paths = sorted(unique_paths.values(), key=lambda item: (source_order.get(item[2], 99), item[0]))
  571.  
  572.  
  573.     if not sorted_unique_paths:
  574.         log_to_console(console, f"Nie znaleziono Javy {required_version if required_version else ''} w 64-bitowej wersji!", "WARNING")
  575.     else:
  576.         log_to_console(console, f"Zakończono wyszukiwanie Javy. Znaleziono {len(sorted_unique_paths)} pasujących ścieżek.", "INFO")
  577.  
  578.     # Return list of (path, version) tuples
  579.     return [(p, v) for p, v, _ in sorted_unique_paths]
  580.  
  581.  
  582. def get_java_version(java_path):
  583.     if not os.path.exists(java_path):
  584.         return None
  585.     try:
  586.         # Use 'java -version' which outputs to stderr
  587.         result = subprocess.run([java_path, "-version"], capture_output=True, text=True, check=True, encoding='utf-8', errors='ignore')
  588.         version_line = result.stderr.split('\n')[0].strip()
  589.         version_match = re.search(r'version "([^"]+)"', version_line)
  590.         if version_match:
  591.             version = version_match.group(1)
  592.             # Check if it's a 64-Bit JVM
  593.            is_64bit = "64-Bit" in result.stderr or "64-bit" in result.stderr or "x86_64" in result.stderr.lower()
  594.  
  595.            # Use only the main version part for key comparisons (e.g., 1.8, 16, 17, 21)
  596.            # Handle different version formats (e.g., "1.8.0_301", "17.0.1", "9")
  597.            major_version_match = re.match(r'(\d+\.\d+|\d+)', version)
  598.            if major_version_match:
  599.                 simple_version = major_version_match.group(1)
  600.                 # Special case: 1.8 is commonly referred to as 8
  601.                 if simple_version.startswith("1.8"):
  602.                     simple_version = "1.8"
  603.                 elif "." in simple_version: # For >= 9, just the first number is often enough
  604.                     simple_version = simple_version.split('.')[0]
  605.                 # Store the full version but key it by the simple version in cache if needed
  606.                 if is_64bit:
  607.                      java_versions_cache[version] = java_path # Cache full version string to path
  608.                      return version # Return full version for display
  609.                 else:
  610.                      log_to_console(console, f"Java found at {java_path} is not 64-bit. Skipping.", "WARNING")
  611.                      return None
  612.            else:
  613.                 log_to_console(console, f"Could not parse version from '{version_line}' for Java at {java_path}.", "WARNING")
  614.                 return None
  615.        else:
  616.             log_to_console(console, f"Could not find version string in output for Java at {java_path}. Output: {result.stderr.strip()}", "WARNING")
  617.             return None
  618.    except FileNotFoundError:
  619.        log_to_console(console, f"Java executable not found at {java_path}", "ERROR")
  620.        return None
  621.    except subprocess.CalledProcessError as e:
  622.        log_to_console(console, f"Error running java -version for {java_path}: {e.stderr.strip()}", "ERROR")
  623.        return None
  624.    except Exception as e:
  625.        log_to_console(console, f"Unexpected error checking version for {java_path}: {e}", "ERROR")
  626.    return None
  627.  
  628. def check_java_version(installed_version_str, required_version_str):
  629.    """
  630.    Checks if the installed Java version meets the required minimum version.
  631.    Handles versions like "1.8", "9", "16", "17", "21".
  632.    """
  633.    try:
  634.        # Normalize required version for comparison (e.g., "1.8" -> "8")
  635.        required_normalized = required_version_str
  636.        if required_version_str == "1.8":
  637.             required_normalized = "8"
  638.        elif "." in required_version_str:
  639.             required_normalized = required_version_str.split('.')[0] # Use major version for comparison
  640.  
  641.        # Extract major version from installed string (e.g., "1.8.0_301" -> "1.8", "17.0.1" -> "17")
  642.        installed_major_match = re.match(r'(\d+\.\d+|\d+)', installed_version_str)
  643.        if not installed_major_match:
  644.            log_to_console(console, f"Nie można sparsować wersji zainstalowanej Javy: {installed_version_str}", "WARNING")
  645.            return False
  646.        installed_simple = installed_major_match.group(1)
  647.        # Special case: 1.8 comparison
  648.        if required_version_str == "1.8":
  649.             return installed_simple.startswith("1.8.") # Java 8 needs 1.8.x
  650.        # For >= 9, compare as integers if possible
  651.        try:
  652.             installed_major_int = int(installed_simple.split('.')[0])
  653.             required_major_int = int(required_normalized)
  654.             return installed_major_int >= required_major_int
  655.        except ValueError:
  656.             # Fallback to string comparison if not simple integers
  657.             log_to_console(console, f"Porównanie wersji Javy jako stringi: '{installed_version_str}' vs '{required_version_str}'", "INFO")
  658.             return installed_version_str.startswith(required_version_str) # Simple prefix check
  659.  
  660.    except Exception as e:
  661.        log_to_console(console, f"Błąd podczas porównania wersji Javy: zainstalowana='{installed_version_str}', wymagana='{required_version_str}' - {e}", "ERROR")
  662.        return False
  663.  
  664.  
  665. def is_new_launcher(version):
  666.    """Checks if the version uses the new launcher arguments (>= 1.6)."""
  667.    try:
  668.        ver = pkg_version.parse(version)
  669.        # New launcher arguments were introduced around 1.6
  670.        return ver >= pkg_version.parse("1.6")
  671.    except pkg_version.InvalidVersion:
  672.        # Assume newer arguments for unparseable versions
  673.        return True
  674.  
  675. def get_required_java(version, version_info):
  676.    """Determines the required major Java version for a given MC version."""
  677.    if version_info and "javaVersion" in version_info:
  678.        major_version_info = version_info["javaVersion"].get("majorVersion")
  679.        if major_version_info:
  680.             return str(major_version_info)
  681.        # Fallback to component version if majorVersion is missing but component is present
  682.        component_version = version_info["javaVersion"].get("component")
  683.        if component_version:
  684.             major_match = re.match(r'jre([\d]+)', component_version)
  685.             if major_match:
  686.                  return major_match.group(1)
  687.  
  688.    # Fallback based on known version ranges
  689.    try:
  690.        ver = pkg_version.parse(version)
  691.        if ver >= pkg_version.parse("1.20.5"): # Placeholder, check actual version manifests
  692.            return "21" # MC 1.20.5+ requires Java 21
  693.        if ver >= pkg_version.parse("1.18"):
  694.            return "17" # MC 1.18+ requires Java 17 (1.17 also uses 17 technically)
  695.        if ver >= pkg_version.parse("1.17"):
  696.            return "16" # MC 1.17 requires Java 16 (often works with 17 too)
  697.        # MC versions 1.6 to 1.16 require Java 8
  698.        if ver >= pkg_version.parse("1.6"):
  699.            return "1.8"
  700.  
  701.        # Older versions might use Java 6 or 7, but Java 8 is often compatible
  702.        log_to_console(console, f"Using default Java 8 for older/unknown version {version}", "INFO")
  703.        return "1.8" # Default for old/unparsed versions
  704.    except pkg_version.InvalidVersion:
  705.        log_to_console(console, f"Invalid version string '{version}', using default Java 8.", "WARNING")
  706.        return "1.8"  # Default for invalid versions
  707.  
  708. def download_file(url, path, progress_callback=None, expected_sha1=None, console_widget=None, description="plik"):
  709.    os.makedirs(os.path.dirname(path), exist_ok=True)
  710.    try:
  711.        log_to_console(console_widget, f"Pobieranie {description}: {url} do {path}", "INFO")
  712.        resp = requests.get(url, stream=True, timeout=30) # Increased timeout
  713.        resp.raise_for_status()
  714.        total_size = int(resp.headers.get('content-length', 0)) or 1 # Avoid division by zero if size is unknown
  715.        downloaded_size = 0
  716.        with open(path, "wb") as f:
  717.            for chunk in resp.iter_content(chunk_size=8192):
  718.                if chunk:
  719.                    f.write(chunk)
  720.                    downloaded_size += len(chunk)
  721.                    if progress_callback:
  722.                        progress_callback(downloaded_size, total_size)
  723.        log_to_console(console_widget, f"Pobrano {description}: {os.path.basename(path)}", "INFO")
  724.  
  725.        if expected_sha1 and not verify_sha1(path, expected_sha1):
  726.            log_to_console(console_widget, f"Błąd SHA1 dla {description} {path}", "ERROR")
  727.            # Optionally delete the corrupted file
  728.            # os.remove(path)
  729.            return False
  730.        return True
  731.    except requests.exceptions.RequestException as e:
  732.        log_to_console(console_widget, f"Błąd sieci podczas pobierania {description} {path}: {e}", "ERROR")
  733.        return False
  734.    except Exception as e:
  735.        log_to_console(console_widget, f"Ogólny błąd podczas pobierania {description} {path}: {e}", "ERROR")
  736.        return False
  737.  
  738.  
  739. def download_libraries(libs_info, version_dir, shared_libraries, console_widget, progress_callback):
  740.    libraries_dir = LIBRARIES_DIR if shared_libraries else os.path.join(version_dir, "libraries")
  741.    os.makedirs(libraries_dir, exist_ok=True)
  742.  
  743.    libs_to_download = []
  744.    total_libs_size = 0
  745.    for lib in libs_info:
  746.        # Check for rules allowing/denying the library based on OS (simplistic check)
  747.        is_allowed = True
  748.        if "rules" in lib:
  749.            is_allowed = False # Start assuming denied unless explicitly allowed
  750.            for rule in lib["rules"]:
  751.                 action = rule.get("action", "allow")
  752.                 os_info = rule.get("os")
  753.                 if os_info:
  754.                     current_os = platform.system().lower()
  755.                     if os_info.get("name") == current_os:
  756.                         if action == "allow":
  757.                              is_allowed = True
  758.                              break # Found an allowing rule for this OS
  759.                         elif action == "deny":
  760.                              is_allowed = False # Found a denying rule, stop checking
  761.                              break
  762.                 else: # Rule applies to all OS if no 'os' is specified
  763.                     if action == "allow":
  764.                         is_allowed = True
  765.                         break
  766.                     elif action == "deny":
  767.                          is_allowed = False
  768.                          break
  769.  
  770.        if is_allowed and "downloads" in lib and "artifact" in lib["downloads"]:
  771.            artifact = lib["downloads"]["artifact"]
  772.            lib_path = artifact["path"].replace("/", os.sep)
  773.            full_lib_path = os.path.join(libraries_dir, lib_path)
  774.            if not os.path.exists(full_lib_path) or not verify_sha1(full_lib_path, artifact["sha1"]):
  775.                libs_to_download.append((lib, artifact))
  776.                total_libs_size += artifact.get("size", 0)
  777.  
  778.    log_to_console(console_widget, f"Pobieranie {len(libs_to_download)} bibliotek (łączny rozmiar: {humanize.naturalsize(total_libs_size)})", "INFO")
  779.    downloaded_libs_size = 0
  780.    total_libs = len(libs_to_download)
  781.  
  782.    for i, (lib, artifact) in enumerate(libs_to_download):
  783.        lib_path_rel = artifact["path"].replace("/", os.sep) # Relative path within libraries dir
  784.        full_lib_path = os.path.join(libraries_dir, lib_path_rel)
  785.        lib_url = artifact["url"]
  786.        lib_sha1 = artifact["sha1"]
  787.        lib_size = artifact.get("size", 0)
  788.  
  789.        if not download_file(
  790.            lib_url, full_lib_path,
  791.            lambda d, t: progress_callback(downloaded_libs_size + d, total_libs_size, "biblioteki", i + (d / t) if t > 0 else i, total_libs),
  792.            expected_sha1=lib_sha1, console_widget=console_widget, description=f"biblioteka {os.path.basename(lib_path_rel)}"
  793.        ):
  794.             # If a library download fails, it might be critical. Report error but continue trying others.
  795.             log_to_console(console_widget, f"Nie udało się pobrać lub zweryfikować biblioteki {lib_path_rel}", "ERROR")
  796.             # Decide whether to fail completely or just mark instance as not ready
  797.             # For now, just log error and continue
  798.        else:
  799.            downloaded_libs_size += lib_size # Only add size if download was successful
  800.  
  801.    # Final progress update for this stage
  802.    progress_callback(total_libs_size, total_libs_size, "biblioteki", total_libs, total_libs)
  803.    log_to_console(console_widget, "Pobieranie bibliotek zakończone.", "INFO")
  804.    return True # Assuming we don't stop on individual library failures
  805.  
  806.  
  807. def download_natives(libs_info, version_dir, shared_natives, console_widget, progress_callback):
  808.     system = platform.system().lower()
  809.     arch = platform.architecture()[0] # '32bit' or '64bit'
  810.     natives_dir = NATIVES_DIR if shared_natives else os.path.join(version_dir, "natives")
  811.     os.makedirs(natives_dir, exist_ok=True)
  812.  
  813.     natives_classifier = f"natives-{system}" # e.g., natives-windows
  814.     # Some classifiers might include architecture, check manifest details if needed
  815.     # e.g., natives-windows-64
  816.  
  817.     natives_to_download = []
  818.     total_size = 0
  819.  
  820.     for lib in libs_info:
  821.         # Apply rules same as libraries
  822.         is_allowed = True
  823.         if "rules" in lib:
  824.             is_allowed = False
  825.             for rule in lib["rules"]:
  826.                  action = rule.get("action", "allow")
  827.                  os_info = rule.get("os")
  828.                  if os_info:
  829.                      current_os = platform.system().lower()
  830.                      if os_info.get("name") == current_os:
  831.                          # Basic architecture check if present in rule os info
  832.                          rule_arch = os_info.get("arch")
  833.                          if rule_arch and rule_arch != arch.replace("bit", ""):
  834.                              continue # Rule doesn't match current architecture
  835.  
  836.                         if action == "allow":
  837.                              is_allowed = True
  838.                              break
  839.                         elif action == "deny":
  840.                              is_allowed = False
  841.                              break
  842.                 else:
  843.                     if action == "allow":
  844.                         is_allowed = True
  845.                         break
  846.                     elif action == "deny":
  847.                          is_allowed = False
  848.                          break
  849.  
  850.        if is_allowed and "downloads" in lib and "classifiers" in lib["downloads"]:
  851.            classifiers = lib["downloads"]["classifiers"]
  852.            # Find the most specific classifier that matches the system and architecture
  853.            matching_classifier = None
  854.            # Prioritize architecture specific if available
  855.            if f"{natives_classifier}-{arch.replace('bit','')}" in classifiers:
  856.                 matching_classifier = classifiers[f"{natives_classifier}-{arch.replace('bit','')}"]
  857.            elif natives_classifier in classifiers:
  858.                 matching_classifier = classifiers[natives_classifier]
  859.  
  860.            if matching_classifier:
  861.                 natives_to_download.append(matching_classifier)
  862.                 total_size += matching_classifier.get("size", 0)
  863.  
  864.    log_to_console(console_widget, f"Pobieranie {len(natives_to_download)} natywnych bibliotek (łączny rozmiar: {humanize.naturalsize(total_size)})", "INFO")
  865.    downloaded_size = 0
  866.    total_natives = len(natives_to_download)
  867.  
  868.    temp_native_dir = os.path.join(version_dir, "temp_natives_extract") # Temp dir for extraction
  869.  
  870.    for i, artifact in enumerate(natives_to_download):
  871.        native_url = artifact["url"]
  872.        native_sha1 = artifact["sha1"]
  873.        native_size = artifact.get("size", 0)
  874.        # Download natives as temp files
  875.        native_temp_path = os.path.join(version_dir, f"temp_native_{i}.jar") # Or .zip, depending on content-type/url
  876.  
  877.        if not download_file(
  878.            native_url, native_temp_path,
  879.            lambda d, t: progress_callback(downloaded_size + d, total_size, "natives", i + (d/t) if t > 0 else i, total_natives),
  880.            expected_sha1=native_sha1, console_widget=console_widget, description=f"natywna biblioteka {os.path.basename(native_url)}"
  881.        ):
  882.            log_to_console(console_widget, f"Nie udało się pobrać lub zweryfikować natywnej biblioteki {native_url}", "ERROR")
  883.            # Decide whether to fail completely or just mark instance as not ready
  884.            # For now, just log error and continue
  885.            continue # Skip extraction for this failed download
  886.  
  887.        # Extract contents of the native JAR/ZIP
  888.        os.makedirs(temp_native_dir, exist_ok=True)
  889.        try:
  890.            with zipfile.ZipFile(native_temp_path, 'r') as zip_ref:
  891.                # Extract only files, ignoring directories and META-INF (as per Mojang launcher)
  892.                for file_info in zip_ref.infolist():
  893.                    if not file_info.is_dir() and not file_info.filename.startswith('META-INF/'):
  894.                         target_path = os.path.join(natives_dir, os.path.basename(file_info.filename)) # Extract directly to natives_dir
  895.                         # Avoid extracting duplicates if multiple natives archives have the same file
  896.                         if not os.path.exists(target_path):
  897.                              zip_ref.extract(file_info, path=natives_dir) # Extract directly
  898.                         else:
  899.                              log_to_console(console_widget, f"Plik {os.path.basename(file_info.filename)} już istnieje w {natives_dir}. Pomijam.", "INFO")
  900.  
  901.            os.remove(native_temp_path) # Clean up temp file
  902.            downloaded_size += native_size # Only add size if download was successful and extraction attempted
  903.        except Exception as e:
  904.            log_to_console(console_widget, f"Błąd rozpakowywania natywnej biblioteki {os.path.basename(native_temp_path)}: {e}", "ERROR")
  905.            if os.path.exists(native_temp_path):
  906.                 os.remove(native_temp_path)
  907.            continue # Continue with next native
  908.  
  909.    # Clean up temp native directory if it was created and is empty
  910.    if os.path.exists(temp_native_dir) and not os.listdir(temp_native_dir):
  911.        os.rmdir(temp_native_dir)
  912.    elif os.path.exists(temp_native_dir):
  913.         log_to_console(console_widget, f"Nie udało się usunąć tymczasowego katalogu natywnego: {temp_native_dir}. Może zawierać pliki.", "WARNING")
  914.  
  915.  
  916.    # Final progress update for this stage
  917.    progress_callback(total_size, total_size, "natives", total_natives, total_natives)
  918.    log_to_console(console_widget, "Pobieranie i rozpakowywanie natywnych bibliotek zakończone.", "INFO")
  919.    # We return True even if some failed, the verify_instance function will check integrity
  920.    return True
  921.  
  922.  
  923. def download_assets(version_info, version_dir, shared_assets, console_widget, progress_callback):
  924.    try:
  925.        asset_index = version_info.get("assetIndex", {})
  926.        asset_url = asset_index.get("url")
  927.        asset_id = asset_index.get("id")
  928.        if not asset_url:
  929.            log_to_console(console_widget, "Brak assetIndex dla tej wersji.", "WARNING")
  930.            return True # Not critical, some old versions might not have assets this way
  931.  
  932.        resp = requests.get(asset_url, timeout=10)
  933.        resp.raise_for_status()
  934.        assets_data = resp.json()
  935.  
  936.        assets_base_dir = ASSETS_DIR if shared_assets else os.path.join(version_dir, "assets")
  937.        objects_dir = os.path.join(assets_base_dir, "objects")
  938.        indexes_dir = os.path.join(assets_base_dir, "indexes")
  939.        os.makedirs(objects_dir, exist_ok=True)
  940.        os.makedirs(indexes_dir, exist_ok=True)
  941.  
  942.        index_file = os.path.join(indexes_dir, f"{asset_id}.json")
  943.        # Save index file even if assets are shared, as it's version-specific
  944.         log_to_console(console_widget, f"Zapisywanie indeksu assetów: {index_file}", "INFO")
  945.         try:
  946.              with open(index_file, "w", encoding="utf-8") as f:
  947.                  json.dump(assets_data, f, indent=4)
  948.         except Exception as e:
  949.              log_to_console(console_widget, f"Błąd zapisu indeksu assetów {index_file}: {e}", "WARNING")
  950.  
  951.         assets_to_download = []
  952.         total_assets_size = 0
  953.  
  954.         for asset_name, asset_info in assets_data.get("objects", {}).items():
  955.             asset_hash = asset_info["hash"]
  956.             asset_size = asset_info["size"]
  957.             asset_subpath = f"{asset_hash[:2]}/{asset_hash}"
  958.             asset_path = os.path.join(objects_dir, asset_subpath)
  959.             # Check if asset exists and is valid (optional but good practice)
  960.             if not os.path.exists(asset_path) or not verify_sha1(asset_path, asset_hash):
  961.                  assets_to_download.append((asset_name, asset_info))
  962.                  total_assets_size += asset_size
  963.  
  964.         log_to_console(console_widget, f"Pobieranie {len(assets_to_download)} assetów (łączny rozmiar: {humanize.naturalsize(total_assets_size)})", "INFO")
  965.         downloaded_size = 0
  966.         total_assets = len(assets_to_download)
  967.  
  968.         for i, (asset_name, asset_info) in enumerate(assets_to_download):
  969.             asset_hash = asset_info["hash"]
  970.             asset_size = asset_info["size"]
  971.             asset_subpath = f"{asset_hash[:2]}/{asset_hash}"
  972.             asset_path = os.path.join(objects_dir, asset_subpath)
  973.             asset_url = f"https://resources.download.minecraft.net/{asset_subpath}"
  974.  
  975.             if not download_file(
  976.                 asset_url, asset_path,
  977.                 lambda d, t: progress_callback(downloaded_size + d, total_assets_size, "assets", i + (d / t) if t > 0 else i, total_assets),
  978.                 expected_sha1=asset_hash, console_widget=console_widget, description=f"asset {asset_name}"
  979.             ):
  980.                 log_to_console(console_widget, f"Nie udało się pobrać lub zweryfikować assetu {asset_name}", "ERROR")
  981.                 # Continue despite failure, verification will catch missing/corrupt files
  982.             else:
  983.                  downloaded_size += asset_size # Only add size if download was successful
  984.  
  985.         # Final progress update
  986.         progress_callback(total_assets_size, total_assets_size, "assets", total_assets, total_assets)
  987.         log_to_console(console_widget, "Pobieranie assetów zakończone.", "INFO")
  988.         return True # Verification function will check if all required assets are present later
  989.     except requests.exceptions.RequestException as e:
  990.          log_to_console(console_widget, f"Błąd sieci podczas pobierania assetów: {e}", "ERROR")
  991.          return False
  992.     except Exception as e:
  993.         log_to_console(console_widget, f"Ogólny błąd pobierania assetów: {e}", "ERROR")
  994.         return False
  995.  
  996. def verify_instance(version, console_widget=None):
  997.     """
  998.    Verifies if an instance is complete based on its version manifest.
  999.    Can optionally take a console widget to log progress.
  1000.    Returns True if complete and valid, False otherwise.
  1001.    """
  1002.     instance = instances.get(version)
  1003.     if not instance:
  1004.         log_to_console(console_widget, f"Instancja {version} nie istnieje w konfiguracji!", "ERROR")
  1005.         return False
  1006.  
  1007.     version_dir = instance["path"]
  1008.     settings = instance.get("settings", {}) # Use .get for safety
  1009.     info = get_version_info(version)
  1010.     if not info:
  1011.         log_to_console(console_widget, f"Nie udało się pobrać manifestu wersji {version}. Nie można zweryfikować.", "ERROR")
  1012.         instance["ready"] = False
  1013.         save_config()
  1014.         return False
  1015.  
  1016.     log_to_console(console_widget, f"Weryfikacja instancji {version}...", "INFO")
  1017.     all_valid = True
  1018.     missing_files = []
  1019.     corrupt_files = []
  1020.  
  1021.     # 1. Verify client.jar
  1022.     client_path = os.path.join(version_dir, f"{version}.jar")
  1023.     client_download_info = info.get("downloads", {}).get("client")
  1024.     if client_download_info:
  1025.         client_sha1 = client_download_info.get("sha1")
  1026.         if not os.path.exists(client_path):
  1027.             log_to_console(console_widget, f"Brak client.jar dla {version}", "WARNING")
  1028.             missing_files.append(f"client.jar ({version})")
  1029.             all_valid = False
  1030.         elif client_sha1 and not verify_sha1(client_path, client_sha1):
  1031.             log_to_console(console_widget, f"Błąd SHA1 dla client.jar ({version})", "WARNING")
  1032.             corrupt_files.append(f"client.jar ({version})")
  1033.             all_valid = False
  1034.         else:
  1035.             log_to_console(console_widget, f"client.jar OK", "INFO")
  1036.     else:
  1037.          log_to_console(console_widget, f"Brak informacji o pobieraniu client.jar w manifeście wersji {version}.", "WARNING")
  1038.          # Cannot verify, assume missing for older versions or incomplete manifests
  1039.          missing_files.append(f"client.jar ({version}) - Brak info w manifeście?")
  1040.          all_valid = False
  1041.  
  1042.  
  1043.     # 2. Verify libraries
  1044.     libraries_dir = LIBRARIES_DIR if settings.get("shared_libraries", True) else os.path.join(version_dir, "libraries")
  1045.     for lib in info.get("libraries", []):
  1046.         # Apply rules just like in download
  1047.         is_required = True
  1048.         if "rules" in lib:
  1049.             is_required = False # Start assuming denied unless explicitly allowed
  1050.             for rule in lib["rules"]:
  1051.                  action = rule.get("action", "allow")
  1052.                  os_info = rule.get("os")
  1053.                  if os_info:
  1054.                      current_os = platform.system().lower()
  1055.                      if os_info.get("name") == current_os:
  1056.                          rule_arch = os_info.get("arch")
  1057.                          current_arch = platform.architecture()[0].replace("bit", "")
  1058.                          if rule_arch and rule_arch != current_arch:
  1059.                              continue
  1060.  
  1061.                          if action == "allow":
  1062.                               is_required = True
  1063.                               break
  1064.                          elif action == "deny":
  1065.                               is_required = False
  1066.                               break
  1067.                  else: # Rule applies to all OS if no 'os' is specified
  1068.                      if action == "allow":
  1069.                          is_required = True
  1070.                          break
  1071.                      elif action == "deny":
  1072.                           is_required = False
  1073.                           break
  1074.  
  1075.  
  1076.         if is_required and "downloads" in lib and "artifact" in lib["downloads"]:
  1077.             artifact = lib["downloads"]["artifact"]
  1078.             lib_path_rel = artifact["path"].replace("/", os.sep)
  1079.             full_lib_path = os.path.join(libraries_dir, lib_path_rel)
  1080.             lib_sha1 = artifact["sha1"]
  1081.             if not os.path.exists(full_lib_path):
  1082.                 log_to_console(console_widget, f"Brak wymaganej biblioteki {lib_path_rel}", "WARNING")
  1083.                 missing_files.append(f"biblioteka {lib_path_rel}")
  1084.                 all_valid = False
  1085.             elif not verify_sha1(full_lib_path, lib_sha1):
  1086.                 log_to_console(console_widget, f"Błąd SHA1 dla biblioteki {lib_path_rel}", "WARNING")
  1087.                 corrupt_files.append(f"biblioteka {lib_path_rel}")
  1088.                 all_valid = False
  1089.             # else:
  1090.                 # log_to_console(console_widget, f"Biblioteka {lib_path_rel} OK", "INFO") # Too verbose
  1091.  
  1092.     # 3. Verify assets (checks index and potentially some objects if shared)
  1093.     assets_base_dir = ASSETS_DIR if settings.get("shared_assets", True) else os.path.join(version_dir, "assets")
  1094.     asset_index_info = info.get("assetIndex", {})
  1095.     asset_id = asset_index_info.get("id", version) # Default asset index id to version if missing
  1096.     index_file_path = os.path.join(assets_base_dir, "indexes", f"{asset_id}.json")
  1097.     objects_dir = os.path.join(assets_base_dir, "objects")
  1098.  
  1099.     if not os.path.exists(index_file_path):
  1100.          log_to_console(console_widget, f"Brak pliku indeksu assetów: {index_file_path}", "WARNING")
  1101.          missing_files.append(f"indeks assetów ({asset_id})")
  1102.          all_valid = False
  1103.     elif asset_index_info.get("sha1") and not verify_sha1(index_file_path, asset_index_info["sha1"]):
  1104.          log_to_console(console_widget, f"Błąd SHA1 dla indeksu assetów {index_file_path}", "WARNING")
  1105.          corrupt_files.append(f"indeks assetów ({asset_id})")
  1106.          all_valid = False
  1107.     else:
  1108.          log_to_console(console_widget, f"Indeks assetów OK: {index_file_path}", "INFO")
  1109.          # If index is okay, check some random assets or a critical few
  1110.          # Full asset verification is too slow. Rely on download process to get them.
  1111.          # Just check if the objects directory exists and has content if shared assets are used.
  1112.          if settings.get("shared_assets", True) and not os.path.exists(objects_dir) or (os.path.exists(objects_dir) and not os.listdir(objects_dir)):
  1113.               log_to_console(console_widget, f"Współdzielony folder assetów ({objects_dir}) wydaje się pusty.", "WARNING")
  1114.               # This isn't necessarily an error if no assets were needed yet, but worth a warning.
  1115.              pass # Don't set all_valid = False just for this warning
  1116.  
  1117.     # 4. Verify natives (basic check if folder exists and has content if not shared)
  1118.     natives_dir = NATIVES_DIR if settings.get("shared_natives", True) else os.path.join(version_dir, "natives")
  1119.     if not os.path.exists(natives_dir) or (os.path.exists(natives_dir) and not os.listdir(natives_dir)):
  1120.          log_to_console(console_widget, f"Folder natywnych bibliotek ({natives_dir}) wydaje się pusty.", "WARNING")
  1121.          # Natywne są krytyczne do uruchomienia.
  1122.          missing_files.append("natywne biblioteki")
  1123.          all_valid = False
  1124.     else:
  1125.          log_to_console(console_widget, f"Folder natywnych bibliotek ({natives_dir}) wygląda OK.", "INFO")
  1126.  
  1127.  
  1128.     # 5. Verify Java Path
  1129.     java_path = instance.get("java_path")
  1130.     required_java_version = instance.get("required_java")
  1131.     if not java_path or not os.path.exists(java_path):
  1132.          log_to_console(console_widget, f"Brak ścieżki Javy lub plik nie istnieje: {java_path}", "ERROR")
  1133.          all_valid = False
  1134.          # Try to find a suitable Java if the saved one is missing
  1135.          log_to_console(console_widget, f"Próbuję znaleźć pasującą Javę {required_java_version}", "INFO")
  1136.          found_javas = find_java(required_java_version)
  1137.          if found_javas:
  1138.              new_java_path, new_java_version = found_javas[0]
  1139.              log_to_console(console_widget, f"Znaleziono alternatywną Javę: {new_java_path} (wersja: {new_java_version}). Zapisuję w konfiguracji.", "INFO")
  1140.              instance["java_path"] = new_java_path
  1141.              instance["java_version"] = new_java_version
  1142.              # all_valid remains False because the original config had an issue,
  1143.              # but the instance data is updated for the next launch attempt.
  1144.          else:
  1145.               log_to_console(console_widget, f"Nie znaleziono żadnej pasującej Javy {required_java_version}. Nie można uruchomić.", "ERROR")
  1146.  
  1147.     elif required_java_version:
  1148.          actual_java_version = get_java_version(java_path)
  1149.          if not actual_java_version or not check_java_version(actual_java_version, required_java_version):
  1150.               log_to_console(console_widget, f"Wybrana Java ({java_path}, wersja: {actual_java_version}) nie spełnia wymagań wersji {required_java_version}.", "ERROR")
  1151.               all_valid = False
  1152.          else:
  1153.               log_to_console(console_widget, f"Wybrana Java ({java_path}, wersja: {actual_java_version}) spełnia wymagania wersji {required_java_version}.", "INFO")
  1154.  
  1155.  
  1156.     # 6. Check and regenerate start.bat
  1157.     # We regenerate it every time during verification to ensure it's up-to-date
  1158.    # with current settings (like username, memory, java path, loader)
  1159.    log_to_console(console_widget, f"Generowanie/regenerowanie start.bat dla {version}...", "INFO")
  1160.    try:
  1161.        regenerate_start_bat(version, instance, info, console_widget)
  1162.        log_to_console(console_widget, f"start.bat dla {version} zregenerowany.", "SUCCESS")
  1163.    except Exception as e:
  1164.        log_to_console(console_widget, f"Błąd podczas regeneracji start.bat dla {version}: {e}", "ERROR")
  1165.        # Failure to regenerate start.bat is critical
  1166.        all_valid = False
  1167.  
  1168.  
  1169.    # Final status update
  1170.    if all_valid:
  1171.        log_to_console(console_widget, f"Instancja {version} zweryfikowana poprawnie. Jest gotowa!", "SUCCESS")
  1172.        instance["ready"] = True
  1173.        message = f"Instancja {version} jest kompletna i gotowa do gry!"
  1174.    else:
  1175.        log_to_console(console_widget, f"Weryfikacja instancji {version} zakończona z błędami.", "WARNING")
  1176.        instance["ready"] = False
  1177.        message = f"Instancja {version} ma problemy:\n"
  1178.        if missing_files:
  1179.             message += "\nBrakuje plików:\n" + "\n".join(missing_files)
  1180.        if corrupt_files:
  1181.             message += "\nUszkodzone pliki:\n" + "\n".join(corrupt_files)
  1182.        if not instance.get("java_path") or not os.path.exists(instance["java_path"]):
  1183.             message += f"\nNie znaleziono działającej Javy ({instance.get('java_path')})."
  1184.        elif required_java_version and (not instance.get("java_version") or not check_java_version(instance["java_version"], required_java_version)):
  1185.              message += f"\nWybrana Java ({instance.get('java_path')}, wersja {instance.get('java_version')}) nie spełnia wymagań ({required_java_version})."
  1186.  
  1187.        message += "\n\nSpróbuj pobrać instancję ponownie lub edytować ustawienia Javy."
  1188.  
  1189.    save_config() # Save changes to instance data (java path, ready status)
  1190.  
  1191.    # Provide feedback to user
  1192.    if all_valid:
  1193.        messagebox.showinfo("Weryfikacja zakończona", message)
  1194.    else:
  1195.        messagebox.showwarning("Weryfikacja zakończona", message)
  1196.  
  1197.    refresh_instances() # Update the GUI list
  1198.    return all_valid
  1199.  
  1200.  
  1201. def regenerate_start_bat(version, instance_data, version_info, console_widget=None):
  1202.    """Generates or regenerates the start.bat file for an instance."""
  1203.    if not version_info:
  1204.         log_to_console(console_widget, f"Nie można zregenerować start.bat dla {version}: brak info o wersji.", "ERROR")
  1205.         raise ValueError("Brak informacji o wersji.") # Raise error to indicate failure
  1206.  
  1207.    version_dir = instance_data["path"]
  1208.    settings = instance_data.get("settings", {})
  1209.    java_path = instance_data.get("java_path")
  1210.    loader_type = settings.get("loader_type", "vanilla")
  1211.    server_ip = settings.get("server_ip", "")
  1212.    server_port = settings.get("server_port", "")
  1213.  
  1214.  
  1215.    if not java_path or not os.path.exists(java_path):
  1216.        log_to_console(console_widget, f"Nie można zregenerować start.bat: brak ścieżki Javy lub plik nie istnieje: {java_path}", "ERROR")
  1217.        # We could try to find Java here, but verify_instance already does this.
  1218.        # Just raise the error to signal failure.
  1219.        raise FileNotFoundError(f"Java executable not found at {java_path}")
  1220.  
  1221.    bat_path = os.path.join(version_dir, "start.bat")
  1222.    libraries_dir = LIBRARIES_DIR if settings.get("shared_libraries", True) else os.path.join(version_dir, "libraries")
  1223.    assets_path = ASSETS_DIR if settings.get("shared_assets", True) else os.path.join(version_dir, "assets")
  1224.  
  1225.    # Build the classpath
  1226.    # Start with client JAR
  1227.    classpath_entries = [f'"{os.path.join(version_dir, f"{version}.jar")}"']
  1228.  
  1229.    # Add libraries based on manifest rules
  1230.    for lib in version_info.get("libraries", []):
  1231.         is_required = True
  1232.         # Apply rules similar to download/verify
  1233.         if "rules" in lib:
  1234.            is_required = False
  1235.            for rule in lib["rules"]:
  1236.                 action = rule.get("action", "allow")
  1237.                 os_info = rule.get("os")
  1238.                 if os_info:
  1239.                     current_os = platform.system().lower()
  1240.                     if os_info.get("name") == current_os:
  1241.                         rule_arch = os_info.get("arch")
  1242.                         current_arch = platform.architecture()[0].replace("bit", "")
  1243.                         if rule_arch and rule_arch != current_arch:
  1244.                             continue
  1245.                         if action == "allow":
  1246.                              is_required = True
  1247.                              break
  1248.                         elif action == "deny":
  1249.                              is_required = False
  1250.                              break
  1251.                 else:
  1252.                     if action == "allow":
  1253.                         is_required = True
  1254.                         break
  1255.                     elif action == "deny":
  1256.                          is_required = False
  1257.                          break
  1258.  
  1259.         if is_required and "downloads" in lib and "artifact" in lib["downloads"]:
  1260.            artifact = lib["downloads"]["artifact"]
  1261.            lib_path_rel = artifact["path"].replace("/", os.sep)
  1262.            full_lib_path = os.path.join(libraries_dir, lib_path_rel)
  1263.            if os.path.exists(full_lib_path): # Only add if the file exists
  1264.                 classpath_entries.append(f'"{full_lib_path}"')
  1265.            else:
  1266.                 log_to_console(console_widget, f"Biblioteka '{lib_path_rel}' brakuje, nie dodano do class ścieżki.", "WARNING")
  1267.  
  1268.  
  1269.    classpath = ';'.join(classpath_entries) # Use ';' for Windows classpath
  1270.  
  1271.    # Determine main class and arguments based on loader type
  1272.    mc_args = version_info.get("minecraftArguments") # Old style args
  1273.    arguments = version_info.get("arguments", {}) # New style args
  1274.    main_class = version_info.get("mainClass")
  1275.  
  1276.    jvm_args = []
  1277.    game_args = []
  1278.  
  1279.    if arguments: # New style arguments (1.13+)
  1280.        log_to_console(console_widget, f"Używam nowych argumentów uruchamiania (version_info['arguments']).", "INFO")
  1281.        # Parse JVM arguments
  1282.        jvm_arg_list = arguments.get("jvm", [])
  1283.        for arg in jvm_arg_list:
  1284.            if isinstance(arg, str):
  1285.                jvm_args.append(arg)
  1286.            elif isinstance(arg, dict):
  1287.                # Handle rules within arguments (simplified - just check for os rule)
  1288.                is_allowed = True
  1289.                if "rules" in arg:
  1290.                    is_allowed = False
  1291.                    for rule in arg["rules"]:
  1292.                         action = rule.get("action", "allow")
  1293.                         os_info = rule.get("os")
  1294.                         if os_info:
  1295.                             current_os = platform.system().lower()
  1296.                             if os_info.get("name") == current_os:
  1297.                                 if action == "allow":
  1298.                                     is_allowed = True
  1299.                                     break
  1300.                                 elif action == "deny":
  1301.                                     is_allowed = False
  1302.                                     break
  1303.                         else: # Rule applies to all OS
  1304.                              if action == "allow":
  1305.                                  is_allowed = True
  1306.                                  break
  1307.                              elif action == "deny":
  1308.                                  is_allowed = False
  1309.                                  break
  1310.                if is_allowed and "value" in arg:
  1311.                    value = arg["value"]
  1312.                    if isinstance(value, list): # Handle multi-value args like system properties
  1313.                        jvm_args.extend(value)
  1314.                    else:
  1315.                        jvm_args.append(value)
  1316.  
  1317.        # Parse game arguments
  1318.        game_arg_list = arguments.get("game", [])
  1319.        for arg in game_arg_list:
  1320.            if isinstance(arg, str):
  1321.                game_args.append(arg)
  1322.            elif isinstance(arg, dict):
  1323.                is_allowed = True # Simplified rule checking
  1324.                if "rules" in arg:
  1325.                    is_allowed = False
  1326.                    for rule in arg["rules"]:
  1327.                        action = rule.get("action", "allow")
  1328.                        if action == "allow" and ("features" not in rule or all(settings.get(f, False) for f in rule["features"])): # Basic feature check (not fully implemented)
  1329.                             is_allowed = True
  1330.                             break
  1331.                        elif action == "deny" and ("features" not in rule or all(settings.get(f, False) for f in rule["features"])):
  1332.                             is_allowed = False
  1333.                             break
  1334.  
  1335.                if is_allowed and "value" in arg:
  1336.                    value = arg["value"]
  1337.                    if isinstance(value, list):
  1338.                        game_args.extend(value)
  1339.                    else:
  1340.                        game_args.append(value)
  1341.  
  1342.        # Replace placeholders (basic implementation)
  1343.        replacements = {
  1344.            "${auth_player_name}": settings.get("username", "Player"),
  1345.            "${version_name}": version,
  1346.            "${game_directory}": ".", # Relative to the instance folder
  1347.            "${assets_root}": f'"{assets_path}"',
  1348.            "${assets_index}": version_info.get("assetIndex", {}).get("id", version),
  1349.            "${auth_uuid}": "0", # Dummy UUID for offline mode
  1350.            "${auth_access_token}": "null", # Dummy token for offline mode
  1351.            "${user_type}": "legacy", # or "mojang" depending on version/auth
  1352.            "${version_type}": version_info.get("type", "release"),
  1353.            # Add other common placeholders as needed
  1354.            "${natives_directory}": f'"{natives_path}"' # Added for newer JVM args
  1355.        }
  1356.        # Apply replacements to game args first (as they are more user-facing)
  1357.        processed_game_args = []
  1358.        for arg in game_args:
  1359.            processed_arg = arg
  1360.            for placeholder, value in replacements.items():
  1361.                processed_arg = processed_arg.replace(placeholder, str(value))
  1362.            processed_game_args.append(processed_arg)
  1363.  
  1364.        # Apply replacements to JVM args
  1365.        processed_jvm_args = []
  1366.        for arg in jvm_args:
  1367.            processed_arg = arg
  1368.            for placeholder, value in replacements.items():
  1369.                processed_arg = processed_arg.replace(placeholder, str(value))
  1370.            processed_jvm_args.append(processed_arg)
  1371.  
  1372.        final_game_args = processed_game_args
  1373.        final_jvm_args = processed_jvm_args
  1374.  
  1375.  
  1376.    elif mc_args: # Old style arguments (pre-1.13) - simpler placeholder replacement
  1377.        log_to_console(console_widget, f"Używam starych argumentów uruchamiania (version_info['minecraftArguments']).", "INFO")
  1378.        # Split old args string and replace placeholders
  1379.        arg_string = mc_args
  1380.        replacements = {
  1381.            "${auth_player_name}": settings.get("username", "Player"),
  1382.            "${version_name}": version,
  1383.            "${game_directory}": ".",
  1384.            "${assets_root}": f'"{assets_path}"',
  1385.            "${assets_index}": version_info.get("assetIndex", {}).get("id", version),
  1386.            "${auth_uuid}": "0",
  1387.            "${auth_access_token}": "null",
  1388.            "${user_type}": "legacy",
  1389.            "${version_type}": version_info.get("type", "release"),
  1390.            # Old format doesn't usually include natives dir in args, uses -Djava.library.path
  1391.         }
  1392.         # Split, replace, and join
  1393.         old_game_args = arg_string.split()
  1394.         final_game_args = []
  1395.         for arg in old_game_args:
  1396.             processed_arg = arg
  1397.             for placeholder, value in replacements.items():
  1398.                 processed_arg = processed_arg.replace(placeholder, str(value))
  1399.             final_game_args.append(processed_arg)
  1400.  
  1401.         # Old versions typically used default JVM args or few custom ones
  1402.         # Set some common default JVM args
  1403.         final_jvm_args = [
  1404.              f'-Djava.library.path="{natives_path}"', # Specify natives path
  1405.              '-Dorg.lwjgl.util.Debug=true' # Optional LWJGL debug
  1406.         ]
  1407.         # Add memory argument
  1408.         final_jvm_args.insert(0, f'-Xmx{settings.get("memory", "2")}G')
  1409.  
  1410.  
  1411.     else:
  1412.         log_to_console(console_widget, f"Brak argumentów uruchamiania w manifeście wersji {version}.", "WARNING")
  1413.         # Fallback to a basic set of arguments if manifest is weird
  1414.         main_class = "net.minecraft.client.main.Main" # Assume vanilla main class
  1415.         final_jvm_args = [
  1416.              f'-Xmx{settings.get("memory", "2")}G',
  1417.              f'-Djava.library.path="{natives_path}"'
  1418.         ]
  1419.         final_game_args = [
  1420.              f'--username', settings.get("username", "Player"),
  1421.              f'--version', version,
  1422.              f'--gameDir', '.',
  1423.              f'--assetsDir', f'"{assets_path}"',
  1424.              f'--assetIndex', version_info.get("assetIndex", {}).get("id", version),
  1425.              f'--accessToken', 'null',
  1426.              f'--uuid', '0',
  1427.              f'--userType', 'legacy'
  1428.         ]
  1429.         log_to_console(console_widget, f"Generowanie start.bat z domyślnymi argumentami.", "INFO")
  1430.  
  1431.  
  1432.     # Adjust main class and add loader args if a mod loader is selected
  1433.     loader_main_class = None
  1434.     loader_extra_jvm_args = []
  1435.     loader_extra_game_args = []
  1436.     loader_extra_classpath_entries = []
  1437.  
  1438.     if loader_type and loader_type != "vanilla":
  1439.         log_to_console(console_widget, f"Instancja używa mod loadera: {loader_type}", "INFO")
  1440.         if loader_type == "fabric":
  1441.             # Fabric's main class and arguments vary slightly by version,
  1442.            # but net.fabricmc.loader.launch.knot.KnotClient is common for newer Fabric.
  1443.            # The fabric-loader-*.jar needs to be on the classpath *before* the client.jar.
  1444.            # The installer places necessary files (including fabric-loader-*.jar) into the libraries folder.
  1445.            loader_main_class = "net.fabricmc.loader.launch.knot.KnotClient" # Common for modern Fabric
  1446.            # Need to find the Fabric loader JAR in libraries and add it to classpath
  1447.            fabric_loader_jar = None
  1448.            # Search for a JAR matching a pattern, e.g., 'fabric-loader-*'
  1449.            try:
  1450.                for root, _, files in os.walk(libraries_dir):
  1451.                    for fname in files:
  1452.                        if fname.startswith("fabric-loader-") and fname.endswith(".jar"):
  1453.                            fabric_loader_jar = os.path.join(root, fname)
  1454.                            break
  1455.                    if fabric_loader_jar: break
  1456.            except Exception as e:
  1457.                log_to_console(console_widget, f"Błąd szukania Fabric loader JAR: {e}", "ERROR")
  1458.  
  1459.            if fabric_loader_jar and os.path.exists(fabric_loader_jar):
  1460.                 loader_extra_classpath_entries.append(f'"{fabric_loader_jar}"')
  1461.                 log_to_console(console_widget, f"Dodano Fabric loader JAR do class ścieżki: {fabric_loader_jar}", "INFO")
  1462.            else:
  1463.                 log_to_console(console_widget, "Nie znaleziono Fabric loader JAR w folderze bibliotek. Upewnij się, że Fabric jest zainstalowany.", "WARNING")
  1464.                 # Decide if this should be a fatal error or just a warning
  1465.                 # For now, let's allow generation but log warning.
  1466.  
  1467.         elif loader_type == "forge":
  1468.              # Forge's main class is typically cpw.mods.modlauncher.Launcher for newer versions.
  1469.             # Similar to Fabric, its main JAR needs to be on the classpath, often after libraries but before client.jar.
  1470.             # Installer places files in libraries and a 'forge-*.jar' or similar.
  1471.             loader_main_class = "cpw.mods.modlauncher.Launcher" # Common for modern Forge
  1472.             forge_launcher_jar = None
  1473.             # Search for forge launcher JAR
  1474.             try:
  1475.                 # Forge might place it directly in the instance root or libraries
  1476.                 candidate_paths = [
  1477.                     os.path.join(version_dir, f"forge-{version}-{settings.get('loader_version', '')}-universal.jar"), # Example name
  1478.                     os.path.join(version_dir, f"forge-{version}-{settings.get('loader_version', '')}-launcher.jar"), # Example name
  1479.                 ]
  1480.                 # Also search libraries dir more generally
  1481.                 for root, _, files in os.walk(libraries_dir):
  1482.                     for fname in files:
  1483.                         if fname.startswith("forge-") and "universal" in fname and fname.endswith(".jar"):
  1484.                              candidate_paths.append(os.path.join(root, fname))
  1485.                              break # Found one, assume it's the right one for now
  1486.                      if forge_launcher_jar: break
  1487.  
  1488.                  for p in candidate_paths:
  1489.                      if os.path.exists(p):
  1490.                          forge_launcher_jar = p
  1491.                          break
  1492.  
  1493.              except Exception as e:
  1494.                  log_to_console(console_widget, f"Błąd szukania Forge launcher JAR: {e}", "ERROR")
  1495.  
  1496.              if forge_launcher_jar and os.path.exists(forge_launcher_jar):
  1497.                   # Forge's structure and required classpath entries can be complex.
  1498.                  # Often the installer creates a JSON profile (like a mini version_info) in the instance folder
  1499.                  # that contains the exact classpath and main class.
  1500.                  # A robust launcher would parse this.
  1501.                  # For this example, we'll just add the *presumed* main JAR and hope the installer handled the rest of the classpath.
  1502.                   loader_extra_classpath_entries.append(f'"{forge_launcher_jar}"')
  1503.                   log_to_console(console_widget, f"Dodano Forge launcher JAR do class ścieżki: {forge_launcher_jar}", "INFO")
  1504.  
  1505.                   # Forge might also need specific game arguments like --launchwrapper.tweaker
  1506.                   # These are typically found in the installer-generated JSON profile.
  1507.                   # Adding basic placeholder arguments for now.
  1508.                   # loader_extra_game_args.extend(["--launchwrapper.tweaker", "cpw.mods.fml.common.launcher.FMLTweaker"]) # Example for older Forge
  1509.                   pass # Placeholder for potential Forge args parsing
  1510.  
  1511.              else:
  1512.                   log_to_console(console_widget, "Nie znaleziono Forge launcher JAR. Upewnij się, że Forge jest zainstalowany.", "WARNING")
  1513.                   # Decide if this is fatal... likely is for Forge.
  1514.  
  1515.         # Add other loaders (NeoForge, etc.) here...
  1516.         # elif loader_type == "neoforge":
  1517.         #    ...
  1518.  
  1519.         # Prepend loader classpath entries if any
  1520.         if loader_extra_classpath_entries:
  1521.              classpath = ';'.join(loader_extra_classpath_entries) + ';' + classpath
  1522.  
  1523.         # Use loader's main class if found, otherwise fallback to vanilla
  1524.        if loader_main_class:
  1525.             main_class = loader_main_class
  1526.        elif loader_type != "vanilla":
  1527.             log_to_console(console_widget, f"Nie znaleziono głównej klasy dla loadera '{loader_type}'. Używam domyślnej vanilla.", "ERROR")
  1528.             main_class = version_info.get("mainClass", "net.minecraft.client.main.Main") # Fallback
  1529.  
  1530.  
  1531.    # Add server arguments if specified
  1532.    if server_ip:
  1533.        loader_extra_game_args.extend(["--server", server_ip])
  1534.        if server_port:
  1535.            loader_extra_game_args.extend(["--port", server_port])
  1536.  
  1537.    # Combine all game args
  1538.    final_game_args_combined = final_game_args + loader_extra_game_args
  1539.  
  1540.    # Build the final command string
  1541.    # Use cmd /c start "" ... for Windows to run in a new window, or just the command
  1542.    # Using just the command is simpler for Popen/subprocess
  1543.    command_parts = []
  1544.    command_parts.append(f'"{java_path}"') # Java executable in quotes
  1545.    command_parts.extend(final_jvm_args) # JVM args
  1546.    command_parts.extend([f'-cp', f'"{classpath}"']) # Classpath
  1547.    command_parts.append(main_class) # Main class
  1548.    command_parts.extend(final_game_args_combined) # Game args
  1549.  
  1550.    # Construct the final bat content
  1551.    # Escape special characters for .bat? No, python subprocess handles quotes well.
  1552.    # Need to quote paths with spaces. Already doing this.
  1553.    bat_content = f'@echo off\n'
  1554.    bat_content += f'title Minecraft {version} ({loader_type})\n'
  1555.    # Use PUSHD and POPD to ensure relative paths work correctly if batch file is run from elsewhere
  1556.    bat_content += f'PUSHD "{version_dir}"\n'
  1557.    bat_content += f'"{java_path}" ' # Java executable path
  1558.    bat_content += ' '.join([f'"{arg}"' if ' ' in arg and not arg.startswith('"') else arg for arg in final_jvm_args]) + ' ' # Quoted JVM args
  1559.    bat_content += f'-cp "{classpath}" ' # Classpath
  1560.    bat_content += main_class + ' ' # Main class
  1561.    bat_content += ' '.join([f'"{arg}"' if ' ' in arg and not arg.startswith('"') else arg for arg in final_game_args_combined]) # Quoted game args
  1562.    bat_content += '\n'
  1563.    bat_content += f'POPD\n' # Return to original directory
  1564.    bat_content += f'pause\n' # Keep window open after execution
  1565.  
  1566.    try:
  1567.        with open(bat_path, "w", encoding="utf-8") as f:
  1568.            f.write(bat_content)
  1569.        log_to_console(console_widget, f"Plik {bat_path} został utworzony/zaktualizowany.", "SUCCESS")
  1570.    except Exception as e:
  1571.        log_to_console(console_widget, f"Błąd zapisu pliku start.bat {bat_path}: {e}", "ERROR")
  1572.        raise # Re-raise to indicate failure in generation
  1573.  
  1574.  
  1575. def update_progress(progress_bar, status_label, current_bytes, total_bytes, stage, current_item, total_items):
  1576.    """Updates the progress bar and status label."""
  1577.    if progress_bar and status_label:
  1578.        try:
  1579.            progress = (current_bytes / total_bytes) * 100 if total_bytes > 0 else 0
  1580.            progress_bar["value"] = progress
  1581.            remaining_bytes = max(0, total_bytes - current_bytes) # Ensure not negative
  1582.            remaining_str = humanize.naturalsize(remaining_bytes) if remaining_bytes > 0 else "0 B"
  1583.            total_str = humanize.naturalsize(total_bytes) if total_bytes > 0 else "Nieznany rozmiar"
  1584.  
  1585.            status_label.config(text=f"[{stage}] Postęp: {progress:.1f}% | Pobrano: {humanize.naturalsize(current_bytes)} / {total_str} | {current_item}/{total_items}")
  1586.            root.update_idletasks() # Update GUI immediately
  1587.        except Exception as e:
  1588.            # This can happen if widgets are destroyed during download
  1589.            print(f"Error updating progress bar: {e}")
  1590.            pass
  1591.  
  1592.  
  1593. def download_version(version, instance_settings, console_widget):
  1594.    global download_active
  1595.    download_active = True
  1596.    instance_created = False # Flag to know if we successfully created the instance entry
  1597.  
  1598.    try:
  1599.        log_to_console(console_widget, f"Rozpoczynam pobieranie instancji: {version}", "INFO")
  1600.        global_status_label.config(text="Pobieranie manifestu wersji...")
  1601.        root.update_idletasks()
  1602.  
  1603.        info = get_version_info(version)
  1604.        if not info:
  1605.            log_to_console(console_widget, f"Nie udało się pobrać manifestu dla wersji {version}. Anulowano.", "ERROR")
  1606.            messagebox.showerror("Błąd pobierania", f"Nie udało się pobrać manifestu dla wersji {version}.")
  1607.            return # Exit thread
  1608.  
  1609.        # Determine required Java version early
  1610.        java_major_version = get_required_java(version, info)
  1611.        log_to_console(console_widget, f"Wymagana wersja Javy: {java_major_version}", "INFO")
  1612.  
  1613.        # Find or download Java
  1614.        global_status_label.config(text="Weryfikacja/pobieranie Javy...")
  1615.        root.update_idletasks()
  1616.        java_path_setting = instance_settings.get("java_path") # User specified path if any
  1617.  
  1618.        if java_path_setting and os.path.exists(java_path_setting) and get_java_version(java_path_setting):
  1619.             # Use user-specified Java if valid
  1620.             java_path = java_path_setting
  1621.             java_version = get_java_version(java_path)
  1622.             log_to_console(console_widget, f"Używam wybranej Javy: {java_path} (wersja {java_version})", "INFO")
  1623.        else:
  1624.            # Find or download the required version
  1625.            java_paths = find_java(java_major_version)
  1626.            if not java_paths:
  1627.                log_to_console(console_widget, f"Nie znaleziono zainstalowanej Javy {java_major_version}.", "WARNING")
  1628.                # Ask user if they want to download
  1629.                if messagebox.askyesno("Brak Javy", f"Nie znaleziono 64-bitowej Javy {java_major_version}. Czy chcesz pobrać ją automatycznie?"):
  1630.                    global_status_label.config(text=f"Pobieranie Javy {java_major_version}...")
  1631.                    root.update_idletasks()
  1632.                    java_path, java_version = download_java(java_major_version, console_widget)
  1633.                    if not java_path:
  1634.                        log_to_console(console_widget, f"Nie udało się pobrać Javy {java_major_version}!", "ERROR")
  1635.                        messagebox.showerror("Błąd pobierania", f"Nie udało się pobrać Javy {java_major_version}. Anulowano instalację.")
  1636.                        return # Exit thread
  1637.                    log_to_console(console_widget, f"Pobrano i zainstalowano Javę: {java_path} (wersja: {java_version})", "SUCCESS")
  1638.                else:
  1639.                    log_to_console(console_widget, f"Pobieranie Javy {java_major_version} anulowane przez użytkownika.", "WARNING")
  1640.                    messagebox.showerror("Brak Javy", f"Nie wybrano/pobrano Javy {java_major_version}. Nie można zainstalować instancji.")
  1641.                    return # Exit thread
  1642.            else:
  1643.                # Use the first suitable Java found
  1644.                java_path, java_version = java_paths[0]
  1645.                log_to_console(console_widget, f"Używam znalezionej Javy: {java_path} (wersja {java_version})", "INFO")
  1646.  
  1647.  
  1648.        # Prepare instance directory structure
  1649.        version_dir = os.path.join(BASE_DIR, "instances", version)
  1650.        os.makedirs(version_dir, exist_ok=True)
  1651.        # Sub-directories like 'mods', 'config', 'saves' can be created here too
  1652.        os.makedirs(os.path.join(version_dir, "mods"), exist_ok=True)
  1653.        os.makedirs(os.path.join(version_dir, "config"), exist_ok=True)
  1654.        os.makedirs(os.path.join(version_dir, "saves"), exist_ok=True)
  1655.        os.makedirs(os.path.join(version_dir, "resourcepacks"), exist_ok=True)
  1656.  
  1657.  
  1658.        # Add instance to global list and save config early
  1659.        # Mark as not ready initially
  1660.        instances[version] = {
  1661.            "path": version_dir,
  1662.            "java_path": java_path,
  1663.            "java_version": java_version,
  1664.            "required_java": java_major_version,
  1665.            "settings": instance_settings,
  1666.            "ready": False, # Not ready until all parts are verified
  1667.            "timestamp": datetime.now().isoformat()
  1668.        }
  1669.        save_config()
  1670.        instance_created = True # Instance entry is now in config
  1671.  
  1672.        # --- Download Steps ---
  1673.        # Total progress tracking needs the sum of sizes for all steps.
  1674.        # Getting total sizes requires pre-fetching some info (assets, libraries).
  1675.        # A simpler approach for progress is to treat each major stage as a step (e.g., 5 steps).
  1676.        # Or, refine the update_progress to take current bytes / total bytes for the *current file*,
  1677.        # and overall stage progress (e.g., 1/5 stages complete). Let's use the latter.
  1678.  
  1679.         # Step 1: Client.jar
  1680.         total_stages = 5 # Client, Libraries, Natives, Assets, start.bat
  1681.         global_status_label.config(text="[1/5] Pobieranie client.jar...")
  1682.         root.update_idletasks()
  1683.         client_download_info = info.get("downloads", {}).get("client")
  1684.         if not client_download_info:
  1685.              log_to_console(console_widget, "Brak informacji o pobieraniu client.jar.", "ERROR")
  1686.              messagebox.showerror("Błąd", "Brak informacji o pobieraniu client.jar w manifeście.")
  1687.              # Mark instance as not ready and return
  1688.              instances[version]["ready"] = False
  1689.              save_config()
  1690.              return
  1691.  
  1692.         client_url = client_download_info.get("url")
  1693.         client_sha1 = client_download_info.get("sha1")
  1694.         client_size = client_download_info.get("size", 0)
  1695.         client_path = os.path.join(version_dir, f"{version}.jar")
  1696.  
  1697.         if not os.path.exists(client_path) or not verify_sha1(client_path, client_sha1):
  1698.             if not download_file(
  1699.                 client_url, client_path,
  1700.                 lambda d, t: update_progress(global_progress_bar, global_status_label, d, t, "client.jar", 1, 1),
  1701.                 expected_sha1=client_sha1, console_widget=console_widget, description="client.jar"
  1702.             ):
  1703.                 log_to_console(console_widget, "Pobieranie client.jar nie powiodło się.", "ERROR")
  1704.                 # Mark instance as not ready and return
  1705.                 instances[version]["ready"] = False
  1706.                 save_config()
  1707.                 messagebox.showerror("Błąd pobierania", "Pobieranie client.jar nie powiodło się.")
  1708.                 return
  1709.         else:
  1710.              log_to_console(console_widget, "client.jar już istnieje i jest poprawny. Pomijam pobieranie.", "INFO")
  1711.              update_progress(global_progress_bar, global_status_label, client_size, client_size, "client.jar", 1, 1) # Update progress visually
  1712.  
  1713.         # Step 2: Libraries
  1714.         global_status_label.config(text="[2/5] Pobieranie bibliotek...")
  1715.         root.update_idletasks()
  1716.         # Pass a sub-progress callback that maps library progress to overall stage 2 progress
  1717.         lib_progress_callback = lambda current_b, total_b, stage_name, current_i, total_i: \
  1718.             update_progress(global_progress_bar, global_status_label, current_b, total_b, f"[2/5] {stage_name}", current_i, total_i)
  1719.  
  1720.         if not download_libraries(info.get("libraries", []), version_dir, instance_settings.get("shared_libraries", True), console_widget, lib_progress_callback):
  1721.             log_to_console(console_widget, "Pobieranie bibliotek zakończone z błędami.", "WARNING")
  1722.             # Continue, verification will catch missing libs
  1723.  
  1724.         # Step 3: Natives
  1725.         global_status_label.config(text="[3/5] Pobieranie natywnych bibliotek...")
  1726.         root.update_idletasks()
  1727.         native_progress_callback = lambda current_b, total_b, stage_name, current_i, total_i: \
  1728.              update_progress(global_progress_bar, global_status_label, current_b, total_b, f"[3/5] {stage_name}", current_i, total_i)
  1729.  
  1730.         if not download_natives(info.get("libraries", []), version_dir, instance_settings.get("shared_natives", True), console_widget, native_progress_callback):
  1731.             log_to_console(console_widget, "Pobieranie natywnych bibliotek zakończone z błędami.", "WARNING")
  1732.             # Continue, verification will catch missing natives
  1733.  
  1734.         # Step 4: Assets
  1735.         global_status_label.config(text="[4/5] Pobieranie assetów...")
  1736.         root.update_idletasks()
  1737.         asset_progress_callback = lambda current_b, total_b, stage_name, current_i, total_i: \
  1738.              update_progress(global_progress_bar, global_status_label, current_b, total_b, f"[4/5] {stage_name}", current_i, total_i)
  1739.  
  1740.         if not download_assets(info, version_dir, instance_settings.get("shared_assets", True), console_widget, asset_progress_callback):
  1741.              log_to_console(console_widget, "Pobieranie assetów zakończone z błędami.", "WARNING")
  1742.              # Continue, verification will catch missing assets
  1743.  
  1744.  
  1745.         # Step 5: Generate start.bat
  1746.         global_status_label.config(text="[5/5] Generowanie start.bat...")
  1747.         root.update_idletasks()
  1748.         try:
  1749.             regenerate_start_bat(version, instances[version], info, console_widget)
  1750.             update_progress(global_progress_bar, global_status_label, 1, 1, "start.bat", 1, 1) # Indicate completion for this small step
  1751.         except Exception as e:
  1752.              log_to_console(console_widget, f"Nie udało się zregenerować start.bat: {e}. Instancja może nie działać poprawnie.", "ERROR")
  1753.              # This is a critical error, instance is not ready
  1754.              instances[version]["ready"] = False
  1755.              save_config()
  1756.              messagebox.showerror("Błąd", "Nie udało się wygenerować pliku start.bat. Sprawdź logi konsoli.")
  1757.              return
  1758.  
  1759.  
  1760.         # --- Finalization ---
  1761.         log_to_console(console_widget, f"Pobieranie dla instancji {version} zakończone. Weryfikuję pliki...", "INFO")
  1762.         global_status_label.config(text="Weryfikacja instancji...")
  1763.         root.update_idletasks()
  1764.  
  1765.         # Run a final verification
  1766.         if verify_instance(version, console_widget):
  1767.              log_to_console(console_widget, f"Instancja {version} pobrana i zweryfikowana pomyślnie!", "SUCCESS")
  1768.              instances[version]["ready"] = True
  1769.              messagebox.showinfo("Sukces", f"Instancja {version} gotowa do uruchomienia!")
  1770.         else:
  1771.              log_to_console(console_widget, f"Instancja {version} pobrana, ale weryfikacja zakończyła się błędami. Sprawdź logi.", "WARNING")
  1772.              instances[version]["ready"] = False
  1773.              messagebox.showwarning("Uwaga", f"Instancja {version} została pobrana, ale wystąpiły błędy weryfikacji. Sprawdź logi konsoli.")
  1774.  
  1775.         save_config() # Save final instance state
  1776.  
  1777.     except requests.exceptions.RequestException as e:
  1778.         log_to_console(console_widget, f"Błąd sieci podczas pobierania instancji {version}: {e}", "ERROR")
  1779.         if instance_created:
  1780.              instances[version]["ready"] = False # Mark as not ready if failed after creation
  1781.              save_config()
  1782.         messagebox.showerror("Błąd sieci", f"Wystąpił błąd sieci podczas pobierania instancji {version}: {e}")
  1783.  
  1784.     except Exception as e:
  1785.         log_to_console(console_widget, f"Ogólny błąd podczas pobierania instancji {version}: {e}", "ERROR")
  1786.         if instance_created:
  1787.              instances[version]["ready"] = False # Mark as not ready if failed after creation
  1788.              save_config()
  1789.         messagebox.showerror("Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd podczas pobierania instancji {version}: {e}")
  1790.  
  1791.     finally:
  1792.         # Ensure progress bar and status are reset regardless of success/failure
  1793.         if global_progress_bar:
  1794.             global_progress_bar["value"] = 0
  1795.         if global_status_label:
  1796.             global_status_label.config(text="Gotowe do działania!")
  1797.         refresh_instances() # Update list regardless
  1798.         download_active = False
  1799.  
  1800.  
  1801. def run_game(version):
  1802.     instance = instances.get(version)
  1803.     if not instance:
  1804.         messagebox.showerror("Błąd", f"Instancja {version} nie istnieje!")
  1805.         return
  1806.     if not instance.get("ready"):
  1807.          if messagebox.askyesno("Instancja niegotowa", f"Instancja {version} nie jest oznaczona jako gotowa. Spróbować ją zweryfikować przed uruchomieniem?"):
  1808.               # Verify first on main thread (or show console and verify there)
  1809.               # For simplicity, let's just warn and let the user verify manually via context menu
  1810.              # Alternatively, start a verification thread and then run if successful.
  1811.              # Let's just warn for now.
  1812.               messagebox.showwarning("Uwaga", f"Instancja {version} nie jest oznaczona jako gotowa. Może nie działać poprawnie.")
  1813.               # Proceed with attempting to run anyway
  1814.  
  1815.     bat_path = os.path.join(instance["path"], "start.bat")
  1816.     if os.path.exists(bat_path):
  1817.         try:
  1818.             # Use subprocess.Popen to run without waiting and in a new window
  1819.             # shell=True is needed for .bat files
  1820.             log_to_console(console, f"Uruchamiam Minecraft {version} z '{bat_path}' w katalogu '{instance['path']}'", "INFO")
  1821.             subprocess.Popen(bat_path, cwd=instance["path"], shell=True)
  1822.             log_to_console(console, f"Uruchomiono Minecraft {version}", "SUCCESS")
  1823.         except Exception as e:
  1824.             log_to_console(console, f"Nie udało się uruchomić gry: {e}", "ERROR")
  1825.             messagebox.showerror("Błąd uruchamiania", f"Nie udało się uruchomić gry: {e}")
  1826.     else:
  1827.         log_to_console(console, f"Brak pliku start.bat dla instancji {version}. Spróbuj zweryfikować lub pobrać instancję ponownie.", "ERROR")
  1828.         messagebox.showerror("Błąd uruchamiania", f"Brak pliku start.bat dla {version}! Spróbuj zweryfikować lub pobrać instancję.")
  1829.  
  1830.  
  1831. def delete_instance(version):
  1832.     if messagebox.askyesno("Potwierdź usunięcie", f"Na pewno usunąć instancję {version}?\n\nSpowoduje to usunięcie folderu:\n{instances[version]['path']}"):
  1833.         instance = instances.get(version)
  1834.         if instance:
  1835.             import shutil
  1836.             instance_path = instance["path"]
  1837.             try:
  1838.                 if os.path.exists(instance_path):
  1839.                     shutil.rmtree(instance_path)
  1840.                     log_to_console(console, f"Folder instancji {version} usunięty: {instance_path}", "INFO")
  1841.                 del instances[version]
  1842.                 save_config()
  1843.                 log_to_console(console, f"Instancja {version} usunięta z konfiguracji.", "SUCCESS")
  1844.                 refresh_instances()
  1845.             except Exception as e:
  1846.                 log_to_console(console, f"Błąd usuwania instancji {version} w {instance_path}: {e}", "ERROR")
  1847.                 messagebox.showerror("Błąd usuwania", f"Nie udało się usunąć instancji {version}: {e}\n\nSpróbuj usunąć folder ręcznie.")
  1848.  
  1849. def open_instance_folder(version):
  1850.     instance = instances.get(version)
  1851.     if not instance:
  1852.         messagebox.showwarning("Błąd", "Wybierz instancję.")
  1853.         return
  1854.  
  1855.     instance_path = instance["path"]
  1856.     if os.path.exists(instance_path):
  1857.         try:
  1858.             system = platform.system()
  1859.             if system == "Windows":
  1860.                 os.startfile(instance_path)
  1861.             elif system == "Darwin": # macOS
  1862.                 subprocess.Popen(["open", instance_path])
  1863.             else: # Linux and others
  1864.                 subprocess.Popen(["xdg-open", instance_path])
  1865.             log_to_console(console, f"Otwieram folder instancji: {instance_path}", "INFO")
  1866.         except FileNotFoundError:
  1867.              messagebox.showerror("Błąd", f"Nie znaleziono programu do otwarcia folderu.")
  1868.              log_to_console(console, f"Nie znaleziono programu do otwarcia folderu {instance_path}", "ERROR")
  1869.         except Exception as e:
  1870.             messagebox.showerror("Błąd", f"Nie udało się otworzyć folderu: {e}")
  1871.             log_to_console(console, f"Nie udało się otworzyć folderu {instance_path}: {e}", "ERROR")
  1872.     else:
  1873.         messagebox.showwarning("Uwaga", f"Folder instancji nie istnieje:\n{instance_path}")
  1874.         log_to_console(console, f"Próba otwarcia nieistniejącego folderu instancji: {instance_path}", "WARNING")
  1875.  
  1876.  
  1877. def copy_console_content(console_widget):
  1878.     if not console_widget: return
  1879.     try:
  1880.         console_widget.config(state="normal")
  1881.         content = console_widget.get("1.0", tk.END).strip()
  1882.         pyperclip.copy(content)
  1883.         console_widget.config(state="disabled")
  1884.         messagebox.showinfo("Skopiowano", "Zawartość konsoli skopiowana do schowka.")
  1885.         log_to_console(console_widget, "Zawartość konsoli skopiowana do schowka.", "INFO")
  1886.     except pyperclip.PyperclipException as e:
  1887.         messagebox.showerror("Błąd", f"Nie udało się skopiować do schowka: {e}")
  1888.         log_to_console(console_widget, f"Błąd kopiowania do schowka: {e}", "ERROR")
  1889.     except Exception as e:
  1890.          messagebox.showerror("Błąd", f"Nieoczekiwany błąd podczas kopiowania: {e}")
  1891.          log_to_console(console_widget, f"Nieoczekiwany błąd podczas kopiowania: {e}", "ERROR")
  1892.  
  1893.  
  1894. def clear_console_content(console_widget):
  1895.     if not console_widget: return
  1896.     if messagebox.askyesno("Potwierdź", "Na pewno wyczyścić zawartość konsoli?"):
  1897.         try:
  1898.             console_widget.config(state="normal")
  1899.             console_widget.delete("1.0", tk.END)
  1900.             console_widget.config(state="disabled")
  1901.             log_to_console(console_widget, "Konsola została wyczyszczona.", "INFO") # This log won't appear unless added before clear
  1902.        except Exception as e:
  1903.             log_to_console(console_widget, f"Błąd czyszczenia konsoli: {e}", "ERROR")
  1904.  
  1905.  
  1906. def import_config():
  1907.    file_path = filedialog.askopenfilename(
  1908.        title="Wybierz plik konfiguracji",
  1909.        filetypes=(("JSON files", "*.json"), ("All files", "*.*"))
  1910.    )
  1911.    if not file_path:
  1912.        return
  1913.  
  1914.    try:
  1915.        with open(file_path, "r", encoding="utf-8") as f:
  1916.            imported_config = json.load(f)
  1917.  
  1918.        # Merge/overwrite current configuration
  1919.        # Ask user how to handle existing instances? For simplicity, just overwrite if key exists.
  1920.        if messagebox.askyesno("Import konfiguracji", "Czy chcesz zastąpić obecne instancje i ustawienia importowaną konfiguracją?\n(Zaleca się wykonanie eksportu przed importem!)"):
  1921.             global instances, java_versions_cache
  1922.             # Reset to imported state
  1923.             instances = imported_config.get("instances", {})
  1924.             java_versions_cache = imported_config.get("java_versions", {})
  1925.  
  1926.             # Update default settings variables (optional, could just load them next time)
  1927.             default_settings = imported_config.get("default_settings", {})
  1928.             username_var.set(default_settings.get("username", "Player"))
  1929.             memory_var.set(default_settings.get("memory", "2"))
  1930.             shared_assets_var.set(default_settings.get("shared_assets", True))
  1931.             shared_libraries_var.set(default_settings.get("shared_libraries", True))
  1932.             shared_natives_var.set(default_settings.get("shared_natives", True))
  1933.  
  1934.             # Update version filters variables
  1935.             version_filters = imported_config.get("version_filters", {})
  1936.             snapshots_var.set(version_filters.get("snapshots", True))
  1937.             releases_var.set(version_filters.get("releases", True))
  1938.             alpha_var.set(version_filters.get("alpha", False))
  1939.             beta_var.set(version_filters.get("beta", False))
  1940.  
  1941.  
  1942.             save_config() # Save the merged config
  1943.             log_to_console(console, f"Konfiguracja wczytana z {file_path}.", "SUCCESS")
  1944.             messagebox.showinfo("Sukces", "Konfiguracja została zaimportowana.")
  1945.             refresh_instances() # Refresh GUI based on new data
  1946.        else:
  1947.             log_to_console(console, "Import konfiguracji anulowany przez użytkownika.", "INFO")
  1948.  
  1949.    except FileNotFoundError:
  1950.        messagebox.showerror("Błąd importu", "Wybrany plik nie istnieje.")
  1951.        log_to_console(console, f"Błąd importu: Plik nie znaleziono {file_path}", "ERROR")
  1952.    except json.JSONDecodeError:
  1953.        messagebox.showerror("Błąd importu", "Nieprawidłowy format pliku JSON.")
  1954.        log_to_console(console, f"Błąd importu: Nieprawidłowy format JSON w {file_path}", "ERROR")
  1955.    except Exception as e:
  1956.        messagebox.showerror("Błąd importu", f"Nieoczekiwany błąd podczas importu: {e}")
  1957.        log_to_console(console, f"Nieoczekiwany błąd podczas importu z {file_path}: {e}", "ERROR")
  1958.  
  1959.  
  1960. def export_config():
  1961.    file_path = filedialog.asksaveasfilename(
  1962.        title="Zapisz plik konfiguracji",
  1963.        defaultextension=".json",
  1964.        filetypes=(("JSON files", "*.json"), ("All files", "*.*"))
  1965.    )
  1966.    if not file_path:
  1967.        return
  1968.  
  1969.    try:
  1970.        # Get current configuration structure
  1971.        config = {
  1972.            "default_settings": {
  1973.                "username": username_var.get(),
  1974.                "memory": memory_var.get(),
  1975.                "shared_assets": shared_assets_var.get(),
  1976.                "shared_libraries": shared_libraries_var.get(),
  1977.                "shared_natives": shared_natives_var.get()
  1978.            },
  1979.            "version_filters": {
  1980.                "snapshots": snapshots_var.get(),
  1981.                "releases": releases_var.get(),
  1982.                "alpha": alpha_var.get(),
  1983.                "beta": beta_var.get()
  1984.            },
  1985.            "instances": instances,
  1986.            "java_versions": java_versions_cache
  1987.        }
  1988.  
  1989.        with open(file_path, "w", encoding="utf-8") as f:
  1990.            json.dump(config, f, indent=4)
  1991.  
  1992.        log_to_console(console, f"Konfiguracja wyeksportowana do {file_path}.", "SUCCESS")
  1993.        messagebox.showinfo("Sukces", f"Konfiguracja została wyeksportowana do:\n{file_path}")
  1994.  
  1995.    except Exception as e:
  1996.        messagebox.showerror("Błąd eksportu", f"Nie udało się wyeksportować konfiguracji: {e}")
  1997.        log_to_console(console, f"Błąd eksportu do {file_path}: {e}", "ERROR")
  1998.  
  1999.  
  2000. # --- GUI Functions ---
  2001. def switch_tab(tab_name):
  2002.    global download_active
  2003.    # Check if a download is active before switching away from Download tab
  2004.    if download_active and current_tab.get() == "Pobieranie" and tab_name != "Pobieranie":
  2005.        if not messagebox.askyesno("Ostrzeżenie", "Pobieranie w toku! Zmiana zakładki przerwie proces. Kontynuować?"):
  2006.            return
  2007.        log_to_console(console, "Pobieranie przerwane przez użytkownika (zmiana zakładki).", "WARNING")
  2008.        download_active = False # Stop the download flag
  2009.        # The download thread will likely finish its current operation or encounter an error and exit
  2010.  
  2011.    current_tab.set(tab_name)
  2012.    for name, btn in tab_buttons.items():
  2013.        if name == tab_name:
  2014.            btn.config(bg=ACTIVE_TAB_COLOR, relief="sunken")
  2015.        else:
  2016.            btn.config(bg=SIDEBAR_BG, relief="flat")
  2017.        # Re-bind hover effects only for non-active tabs
  2018.        if name != tab_name:
  2019.             btn.bind("<Enter>", lambda e, b=btn: b.config(bg=HOVER_TAB_COLOR))
  2020.             btn.bind("<Leave>", lambda e, b=btn: b.config(bg=SIDEBAR_BG))
  2021.        else:
  2022.             btn.unbind("<Enter>")
  2023.             btn.unbind("<Leave>")
  2024.  
  2025.  
  2026.    # Clear current content frame
  2027.    for widget in content_frame.winfo_children():
  2028.        widget.destroy()
  2029.  
  2030.    # Show content for the selected tab
  2031.    if tab_name == "Instancje":
  2032.        show_instances()
  2033.    elif tab_name == "Pobieranie":
  2034.        show_download()
  2035.    elif tab_name == "Ustawienia":
  2036.        show_settings()
  2037.    elif tab_name == "Konsola":
  2038.        show_console()
  2039.    elif tab_name == "Modrinth":
  2040.        show_modrinth_browser()
  2041.    elif tab_name == "Narzędzia":
  2042.        show_tools() # Import/Export and other tools
  2043.  
  2044.  
  2045. def show_instances():
  2046.    frame = ttk.Frame(content_frame, padding="10")
  2047.    frame.pack(fill="both", expand=True)
  2048.  
  2049.    header_frame = ttk.Frame(frame)
  2050.    header_frame.pack(fill="x", pady=(0, 10))
  2051.    ttk.Label(header_frame, text="Twoje instancje Minecraft", style="TLabel", font=("Segoe UI", 14, "bold")).pack(side="left")
  2052.  
  2053.    btn_frame = ttk.Frame(header_frame)
  2054.    btn_frame.pack(side="right")
  2055.  
  2056.    # Ensure global_progress_bar and global_status_label are visible if needed elsewhere
  2057.    # They are packed at the bottom of the root window, so they are always visible.
  2058.  
  2059.    columns = ("version", "loader", "java", "status", "date", "path") # Added loader and path
  2060.    tree = ttk.Treeview(
  2061.        frame, columns=columns, show="headings", selectmode="browse",
  2062.        style="Treeview"
  2063.    )
  2064.  
  2065.    tree.heading("version", text="Wersja", anchor="w")
  2066.    tree.heading("loader", text="Loader", anchor="w")
  2067.    tree.heading("java", text="Java", anchor="w")
  2068.    tree.heading("status", text="Status", anchor="w")
  2069.    tree.heading("date", text="Utworzona", anchor="w")
  2070.    tree.heading("path", text="Ścieżka", anchor="w") # Hidden by default
  2071.  
  2072.    tree.column("version", width=100, anchor="w", stretch=tk.NO)
  2073.    tree.column("loader", width=80, anchor="w", stretch=tk.NO)
  2074.    tree.column("java", width=100, anchor="w", stretch=tk.NO)
  2075.    tree.column("status", width=80, anchor="w", stretch=tk.NO)
  2076.    tree.column("date", width=120, anchor="w", stretch=tk.NO)
  2077.    tree.column("path", width=250, anchor="w") # Default width, can be expanded
  2078.  
  2079.    # Hide the 'path' column
  2080.    tree.column("path", width=0, stretch=tk.NO)
  2081.    tree.heading("path", text="") # Clear header text when hidden
  2082.  
  2083.    scrollbar = ttk.Scrollbar(frame, orient="vertical", command=tree.yview)
  2084.    scrollbar.pack(side="right", fill="y")
  2085.    tree.configure(yscrollcommand=scrollbar.set)
  2086.    tree.pack(side="left", fill="both", expand=True)
  2087.  
  2088.    # Populate the treeview
  2089.    populate_instances_treeview(tree)
  2090.  
  2091.    # Context Menu (Right-click)
  2092.    instance_context_menu = tk.Menu(root, tearoff=0)
  2093.    instance_context_menu.add_command(label="Uruchom", command=lambda: run_selected_instance(tree))
  2094.    instance_context_menu.add_command(label="Edytuj", command=lambda: edit_selected_instance(tree))
  2095.    instance_context_menu.add_command(label="Weryfikuj", command=lambda: verify_selected_instance(tree))
  2096.    instance_context_menu.add_command(label="Otwórz folder", command=lambda: open_selected_instance_folder(tree))
  2097.    instance_context_menu.add_separator()
  2098.    instance_context_menu.add_command(label="Zmień nazwę", command=lambda: rename_selected_instance(tree))
  2099.    instance_context_menu.add_command(label="Duplikuj", command=lambda: duplicate_selected_instance(tree))
  2100.    instance_context_menu.add_command(label="Eksportuj", command=lambda: export_selected_instance(tree))
  2101.    instance_context_menu.add_separator()
  2102.    instance_context_menu.add_command(label="Usuń", command=lambda: delete_selected_instance(tree))
  2103.  
  2104.  
  2105.    def show_context_menu(event):
  2106.        selected_item = tree.focus()
  2107.        if selected_item:
  2108.            try:
  2109.                instance_context_menu.tk_popup(event.x_root, event.y_root)
  2110.            finally:
  2111.                instance_context_menu.grab_release()
  2112.  
  2113.    tree.bind("<Button-3>", show_context_menu) # Bind right-click
  2114.  
  2115.  
  2116.    # Action Buttons (below the treeview)
  2117.    action_frame = ttk.Frame(frame)
  2118.    action_frame.pack(fill="x", pady=(10, 0))
  2119.  
  2120.    ttk.Button(action_frame, text="Nowa instancja", command=create_instance, style="TButton", width=18).pack(side="left", padx=5)
  2121.    ttk.Button(action_frame, text="Uruchom Wybraną", command=lambda: run_selected_instance(tree), style="TButton", width=18).pack(side="left", padx=5)
  2122.    ttk.Button(action_frame, text="Edytuj Wybraną", command=lambda: edit_selected_instance(tree), style="TButton", width=18).pack(side="left", padx=5)
  2123.    ttk.Button(action_frame, text="Usuń Wybraną", command=lambda: delete_selected_instance(tree), style="TButton", width=18).pack(side="right", padx=5)
  2124.    ttk.Button(action_frame, text="Weryfikuj Wybraną", command=lambda: verify_selected_instance(tree), style="TButton", width=18).pack(side="right", padx=5)
  2125.  
  2126.  
  2127.    ttk.Label(frame, text="Wybierz instancję z listy, kliknij prawym przyciskiem lub użyj przycisków poniżej.", style="TLabel", font=("Segoe UI", 9)).pack(fill="x", pady=(10, 0))
  2128.  
  2129. def populate_instances_treeview(tree):
  2130.    """Clears and repopulates the instance treeview."""
  2131.    # Clear existing items
  2132.    for item in tree.get_children():
  2133.        tree.delete(item)
  2134.  
  2135.    # Sort instances by version (newest first)
  2136.    # Get list of versions, sort using pkg_version.parse if possible
  2137.    sorted_versions = sorted(instances.keys(), key=lambda v: pkg_version.parse(v) if v[0].isdigit() else v, reverse=True)
  2138.  
  2139.    # Add instances to treeview
  2140.    for version in sorted_versions:
  2141.        data = instances[version] # Get data using the sorted version key
  2142.        instance_settings = data.get("settings", {})
  2143.        tree.insert("", "end", iid=version, values=(
  2144.            version,
  2145.            instance_settings.get("loader_type", "vanilla").capitalize(),
  2146.            data.get("java_version", "?"),
  2147.            "Gotowe" if data.get("ready", False) else "Błąd/Niegotowe",
  2148.            datetime.fromisoformat(data["timestamp"]).strftime("%Y-%m-%d %H:%M") if "timestamp" in data else "Nieznana",
  2149.            data["path"]
  2150.        ))
  2151.  
  2152. def refresh_instances():
  2153.    """Refreshes the instances tab GUI."""
  2154.    # Check if the current tab is 'Instancje' before refreshing the GUI
  2155.    if current_tab.get() == "Instancje":
  2156.        for widget in content_frame.winfo_children():
  2157.            widget.destroy()
  2158.        show_instances()
  2159.    # If not on the instances tab, just update the data and rely on switching back to refresh
  2160.  
  2161.  
  2162. def get_selected_instance_version(tree):
  2163.    """Gets the version of the currently selected item in the treeview."""
  2164.    selected_item = tree.focus()
  2165.    if selected_item:
  2166.        # Assuming the version is stored as the iid or the first value
  2167.        return tree.item(selected_item, "iid") # Using iid is safer if values order changes
  2168.        # return tree.item(selected_item)["values"][0] # Alternative if version is always the first column
  2169.    return None # Nothing selected
  2170.  
  2171. def run_selected_instance(tree):
  2172.    version = get_selected_instance_version(tree)
  2173.    if version:
  2174.        run_game(version)
  2175.    else:
  2176.        messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją uruchomić.")
  2177.  
  2178. def edit_selected_instance(tree):
  2179.    version = get_selected_instance_version(tree)
  2180.    if version:
  2181.        edit_instance(version)
  2182.    else:
  2183.        messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją edytować.")
  2184.  
  2185. def verify_selected_instance(tree):
  2186.    version = get_selected_instance_version(tree)
  2187.    if version:
  2188.        # Verification can take time, run in a thread and show console
  2189.        switch_tab("Konsola") # Switch to console tab to see progress
  2190.        threading.Thread(target=verify_instance, args=(version, console), daemon=True).start()
  2191.    else:
  2192.        messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją zweryfikować.")
  2193.  
  2194. def delete_selected_instance(tree):
  2195.    version = get_selected_instance_version(tree)
  2196.    if version:
  2197.        delete_instance(version)
  2198.    else:
  2199.        messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją usunąć.")
  2200.  
  2201. def open_selected_instance_folder(tree):
  2202.    version = get_selected_instance_version(tree)
  2203.    if version:
  2204.        open_instance_folder(version)
  2205.    else:
  2206.        messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby otworzyć jej folder.")
  2207.  
  2208. def rename_selected_instance(tree):
  2209.    version = get_selected_instance_version(tree)
  2210.    if not version:
  2211.        messagebox.showwarning("Uwaga", "Wybierz instancję do zmiany nazwy.")
  2212.        return
  2213.    # NOTE: Minecraft instances are fundamentally tied to their version ID as the folder name.
  2214.    # Renaming here would only change the display name *in the launcher*.
  2215.    # This is a complex feature as it clashes with the folder structure.
  2216.    # For now, let's add a note field instead of renaming.
  2217.  
  2218.     messagebox.showinfo("Funkcja niedostępna", "Zmiana nazwy instancji nie jest obecnie obsługiwana, ponieważ nazwa jest powiązana z wersją Minecrafta.")
  2219.     # Implement adding a note instead if desired
  2220.     # add_instance_note(version)
  2221.  
  2222.  
  2223. def duplicate_selected_instance(tree):
  2224.     version = get_selected_instance_version(tree)
  2225.     if not version:
  2226.         messagebox.showwarning("Uwaga", "Wybierz instancję do zduplikowania.")
  2227.         return
  2228.  
  2229.     messagebox.showinfo("Funkcja niedostępna", "Duplikowanie instancji nie jest obecnie zaimplementowane.")
  2230.     # This would involve copying the instance folder and adding a new entry in the config.
  2231.  
  2232. def export_selected_instance(tree):
  2233.     version = get_selected_instance_version(tree)
  2234.     if not version:
  2235.         messagebox.showwarning("Uwaga", "Wybierz instancję do eksportu.")
  2236.         return
  2237.  
  2238.     # Exporting a single instance as a zip file containing its folder and config snippet
  2239.     instance = instances.get(version)
  2240.     if not instance:
  2241.         messagebox.showerror("Błąd", "Dane instancji nie znaleziono.")
  2242.         return
  2243.  
  2244.     export_path = filedialog.asksaveasfilename(
  2245.          title=f"Eksportuj instancję {version}",
  2246.          initialfile=f"minecraft_instance_{version}.zip",
  2247.          defaultextension=".zip",
  2248.          filetypes=(("Zip files", "*.zip"), ("All files", "*.*"))
  2249.     )
  2250.     if not export_path:
  2251.          return
  2252.  
  2253.     try:
  2254.         # Create a temporary directory to prepare the export
  2255.         temp_dir = os.path.join(BASE_DIR, "temp_export", version)
  2256.         os.makedirs(temp_dir, exist_ok=True)
  2257.  
  2258.         # Copy the instance folder content (excluding potentially large logs/temp files)
  2259.         instance_folder = instance["path"]
  2260.         if os.path.exists(instance_folder):
  2261.              # Use a simpler copy tree, might need exclusions for larger projects
  2262.              shutil.copytree(instance_folder, os.path.join(temp_dir, version), dirs_exist_ok=True,
  2263.                              ignore=shutil.ignore_patterns('logs', 'temp_*', '*.log', '*.tmp'))
  2264.         else:
  2265.              messagebox.showwarning("Uwaga", f"Folder instancji {version} nie istnieje. Eksportowana będzie tylko konfiguracja.")
  2266.              log_to_console(console, f"Folder instancji {version} nie istnieje, eksportowana tylko konfiguracja.", "WARNING")
  2267.  
  2268.  
  2269.         # Save a snippet of the config for this instance in the temp dir
  2270.         instance_config_snippet = {version: instance}
  2271.         with open(os.path.join(temp_dir, "instance_config.json"), "w", encoding="utf-8") as f:
  2272.             json.dump(instance_config_snippet, f, indent=4)
  2273.  
  2274.         # Create the zip archive
  2275.         with zipfile.ZipFile(export_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
  2276.             for root_dir, _, files in os.walk(temp_dir):
  2277.                 for file in files:
  2278.                     file_path = os.path.join(root_dir, file)
  2279.                     # Create relative path inside the zip
  2280.                     relative_path = os.path.relpath(file_path, temp_dir)
  2281.                     zipf.write(file_path, arcname=relative_path)
  2282.  
  2283.         # Clean up the temporary directory
  2284.         shutil.rmtree(temp_dir)
  2285.  
  2286.         log_to_console(console, f"Instancja {version} wyeksportowana do {export_path}.", "SUCCESS")
  2287.         messagebox.showinfo("Sukces", f"Instancja {version} została wyeksportowana do:\n{export_path}")
  2288.  
  2289.     except Exception as e:
  2290.         messagebox.showerror("Błąd eksportu", f"Nie udało się wyeksportować instancji {version}: {e}")
  2291.         log_to_console(console, f"Błąd eksportu instancji {version}: {e}", "ERROR")
  2292.  
  2293.  
  2294. def create_instance():
  2295.     window = tk.Toplevel(root)
  2296.     window.title("Nowa instancja Minecraft")
  2297.     window.geometry("550x700") # Slightly larger
  2298.     window.configure(bg=PRIMARY_BG)
  2299.     window.resizable(False, False)
  2300.  
  2301.     style = ttk.Style() # Ensure style is available in Toplevel
  2302.     style.configure("TFrame", background=PRIMARY_BG)
  2303.     style.configure("TLabel", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10))
  2304.     style.configure("TButton", background=BUTTON_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10), borderwidth=0)
  2305.     style.map("TButton",
  2306.                   background=[('active', BUTTON_HOVER), ('pressed', BUTTON_HOVER)],
  2307.                   foreground=[('active', PRIMARY_FG), ('pressed', PRIMARY_FG)])
  2308.     style.configure("TEntry", fieldbackground="#333333", foreground=PRIMARY_FG, insertcolor=PRIMARY_FG, borderwidth=1)
  2309.     style.configure("TCombobox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, borderwidth=1)
  2310.     style.configure("TCheckbutton", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 9)) # Style for ttk.Checkbutton
  2311.  
  2312.     frame = ttk.Frame(window, padding="15")
  2313.     frame.pack(fill="both", expand=True)
  2314.  
  2315.     ttk.Label(frame, text="Tworzenie nowej instancji", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
  2316.  
  2317.     form_frame = ttk.Frame(frame)
  2318.     form_frame.pack(fill="x", expand=False) # Don't expand form vertically
  2319.    form_frame.columnconfigure(1, weight=1) # Allow the second column to expand
  2320.  
  2321.    # Row 0: Version Filters
  2322.    ttk.Label(form_frame, text="Filtry wersji:", style="TLabel").grid(row=0, column=0, sticky="nw", pady=(0, 5))
  2323.    filters_frame = ttk.Frame(form_frame)
  2324.    filters_frame.grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
  2325.  
  2326.    # Use tk.Checkbutton for checkboxes as ttk.Checkbutton styling is complex
  2327.    tk.Checkbutton(filters_frame, text="Snapshoty", variable=snapshots_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  2328.                   selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  2329.                   command=lambda: [save_config(), refresh_version_combo_dialog(version_combo, version_var)]).pack(anchor="w", side="left", padx=5)
  2330.    tk.Checkbutton(filters_frame, text="Release", variable=releases_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  2331.                   selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  2332.                   command=lambda: [save_config(), refresh_version_combo_dialog(version_combo, version_var)]).pack(anchor="w", side="left", padx=5)
  2333.    tk.Checkbutton(filters_frame, text="Alpha", variable=alpha_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  2334.                   selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  2335.                   command=lambda: [save_config(), refresh_version_combo_dialog(version_combo, version_var)]).pack(anchor="w", side="left", padx=5)
  2336.    tk.Checkbutton(filters_frame, text="Beta", variable=beta_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  2337.                   selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  2338.                   command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
  2339.  
  2340.  
  2341.    # Row 1: Minecraft Version
  2342.    ttk.Label(form_frame, text="Wersja Minecrafta:", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
  2343.    version_var = tk.StringVar()
  2344.    versions = get_versions()
  2345.    version_combo = ttk.Combobox(form_frame, textvariable=version_var, state="readonly", values=versions, style="TCombobox")
  2346.    version_combo.grid(row=1, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2347.    if versions:
  2348.        version_combo.current(0)
  2349.    else:
  2350.        version_var.set("Brak wersji!")
  2351.        version_combo.config(state="disabled")
  2352.        messagebox.showwarning("Uwaga", "Nie znaleziono żadnych wersji Minecrafta. Sprawdź filtry i połączenie z internetem.")
  2353.  
  2354.    # Row 2: Username
  2355.    ttk.Label(form_frame, text="Nazwa użytkownika:", style="TLabel").grid(row=2, column=0, sticky="w", pady=(10, 5))
  2356.    username_entry = ttk.Entry(form_frame, style="TEntry")
  2357.    username_entry.insert(0, username_var.get())
  2358.    username_entry.grid(row=2, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2359.  
  2360.    # Row 3: Memory
  2361.    ttk.Label(form_frame, text="Pamięć RAM (GB):", style="TLabel").grid(row=3, column=0, sticky="w", pady=(10, 5))
  2362.    memory_spin = tk.Spinbox(form_frame, from_=1, to=32, width=5, bg="#333333", fg=PRIMARY_FG, highlightthickness=0,
  2363.                             buttonbackground=TERTIARY_BG, buttonforeground=PRIMARY_FG, insertbackground=PRIMARY_FG)
  2364.    memory_spin.delete(0, tk.END)
  2365.    memory_spin.insert(0, memory_var.get())
  2366.    memory_spin.grid(row=3, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
  2367.  
  2368.    # Row 4: Java Path
  2369.    ttk.Label(form_frame, text="Ścieżka Java:", style="TLabel").grid(row=4, column=0, sticky="w", pady=(10, 5))
  2370.    java_var = tk.StringVar()
  2371.    java_paths_found = find_java()
  2372.    java_options = [f"{p} (v{v} - {src})" for p, v, src in find_java_with_source()] # Include source in display
  2373.    java_combo = ttk.Combobox(form_frame, textvariable=java_var, state="readonly", values=java_options, style="TCombobox")
  2374.    java_combo.grid(row=4, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2375.    java_combo.set("Szukam Javy...") # Initial text
  2376.  
  2377.    # Store (path, version) tuples corresponding to the options list
  2378.    java_path_tuples = find_java() # Use the original list without source for value mapping
  2379.  
  2380.    # Auto-select best Java based on default/selected MC version
  2381.    def update_java_selection(event=None):
  2382.        selected_mc_version = version_var.get()
  2383.        if not selected_mc_version or not java_path_tuples:
  2384.             java_combo.set("Brak pasującej Javy")
  2385.             java_combo.config(state="disabled")
  2386.             return
  2387.  
  2388.        java_combo.config(state="readonly") # Enable if versions found
  2389.  
  2390.        # Fetch version info to get required Java (can be slow, maybe cache?)
  2391.        # For simplicity in dialog, might use a background fetch or rely on find_java
  2392.        # Let's just find based on the list we already have.
  2393.         # A more robust way would fetch manifest and check required version
  2394.  
  2395.         # Use a simplified required version based on MC version string if manifest not fetched
  2396.         # OR pre-fetch all manifests? No, too much data.
  2397.         # Let's just use the version string heuristic for the dialog picker initially.
  2398.        # The main download logic will do a proper manifest check.
  2399.        required_java_heuristic = "1.8" # Default
  2400.        try:
  2401.            mc_v = pkg_version.parse(selected_mc_version)
  2402.            if mc_v >= pkg_version.parse("1.20.5"): required_java_heuristic = "21"
  2403.            elif mc_v >= pkg_version.parse("1.18"): required_java_heuristic = "17"
  2404.            elif mc_v >= pkg_version.parse("1.17"): required_java_heuristic = "16"
  2405.            elif mc_v >= pkg_version.parse("1.6"): required_java_heuristic = "1.8"
  2406.        except pkg_version.InvalidVersion:
  2407.            pass # Keep default heuristic
  2408.  
  2409.        best_java_index = -1
  2410.        for i, (path, ver) in enumerate(java_path_tuples):
  2411.             if check_java_version(ver, required_java_heuristic):
  2412.                 best_java_index = i
  2413.                 break # Found a suitable one, take the first (highest priority from find_java)
  2414.  
  2415.        if best_java_index != -1:
  2416.            java_combo.current(best_java_index)
  2417.        elif java_path_tuples: # If no suitable Java found, select the first available one
  2418.             java_combo.current(0)
  2419.        else: # No Java found at all
  2420.             java_combo.set("Brak Javy 64-bit")
  2421.             java_combo.config(state="disabled")
  2422.             messagebox.showwarning("Brak Javy", "Nie znaleziono żadnej pasującej 64-bitowej instalacji Javy. Upewnij się, że Java 64-bit jest zainstalowana.")
  2423.  
  2424.  
  2425.    version_combo.bind("<<ComboboxSelected>>", update_java_selection)
  2426.    # Perform initial selection after populating
  2427.    if java_options:
  2428.         update_java_selection()
  2429.  
  2430.  
  2431.    # Row 5: Mod Loader Type
  2432.    ttk.Label(form_frame, text="Mod Loader:", style="TLabel").grid(row=5, column=0, sticky="w", pady=(10, 5))
  2433.    loader_var = tk.StringVar(value="vanilla")
  2434.    # Add loaders here as supported
  2435.    loader_options = ["vanilla", "fabric", "forge"] # Add "neoforge" etc. when supported
  2436.    loader_combo = ttk.Combobox(form_frame, textvariable=loader_var, state="readonly", values=loader_options, style="TCombobox")
  2437.    loader_combo.grid(row=5, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2438.    loader_combo.current(0) # Default to vanilla
  2439.  
  2440.    # Row 6: Mod Loader Version (Optional, depends on loader)
  2441.    # This is complex as versions depend on MC version and loader type.
  2442.    # For simplicity, we won't auto-populate this dropdown in the dialog.
  2443.     # User might need to manually enter a known working version or run the installer later.
  2444.     # Alternatively, fetch available loader versions based on selected MC version.
  2445.     # Let's omit loader version selection in the dialog for now and assume user runs installer.
  2446.    # The instance config will store the *type*, and start.bat will adapt if loader files are present.
  2447.    # Added a placeholder label for loader version if we decide to add it later.
  2448.    # ttk.Label(form_frame, text="Loader Version:", style="TLabel").grid(row=6, column=0, sticky="w", pady=(10, 5))
  2449.    # loader_version_var = tk.StringVar()
  2450.    # ttk.Entry(form_frame, textvariable=loader_version_var, style="TEntry").grid(row=6, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2451.    # Note: The launcher doesn't auto-install loaders yet, user must run installer JARs.
  2452.  
  2453.     # Row 7: Shared Folders
  2454.     ttk.Label(form_frame, text="Opcje współdzielenia:", style="TLabel").grid(row=7, column=0, sticky="nw", pady=(10, 5))
  2455.     options_frame = ttk.Frame(form_frame)
  2456.     options_frame.grid(row=7, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2457.  
  2458.     shared_assets = tk.BooleanVar(value=shared_assets_var.get())
  2459.     shared_libs = tk.BooleanVar(value=shared_libraries_var.get())
  2460.     shared_natives = tk.BooleanVar(value=shared_natives_var.get())
  2461.  
  2462.     tk.Checkbutton(options_frame, text="Współdziel assets", variable=shared_assets,
  2463.                    bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
  2464.                    font=("Segoe UI", 9)).pack(anchor="w", pady=2)
  2465.     tk.Checkbutton(options_frame, text="Współdziel biblioteki", variable=shared_libs,
  2466.                    bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
  2467.                    font=("Segoe UI", 9)).pack(anchor="w", pady=2)
  2468.     tk.Checkbutton(options_frame, text="Współdziel natywne biblioteki", variable=shared_natives,
  2469.                    bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
  2470.                    font=("Segoe UI", 9)).pack(anchor="w", pady=2)
  2471.  
  2472.     # Row 8: Server Address (Optional)
  2473.     ttk.Label(form_frame, text="Adres serwera (opcjonalnie):", style="TLabel").grid(row=8, column=0, sticky="w", pady=(10, 5))
  2474.     server_ip_var = tk.StringVar()
  2475.     server_ip_entry = ttk.Entry(form_frame, textvariable=server_ip_var, style="TEntry")
  2476.     server_ip_entry.grid(row=8, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2477.  
  2478.     ttk.Label(form_frame, text="Port serwera (opcjonalnie):", style="TLabel").grid(row=9, column=0, sticky="w", pady=(10, 5))
  2479.     server_port_var = tk.StringVar()
  2480.     server_port_entry = ttk.Entry(form_frame, textvariable=server_port_var, style="TEntry", width=8) # Smaller width
  2481.     server_port_entry.grid(row=9, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
  2482.  
  2483.  
  2484.     # Action buttons at the bottom
  2485.     btn_frame = ttk.Frame(frame)
  2486.     btn_frame.pack(fill="x", pady=(15, 0))
  2487.  
  2488.     ttk.Button(btn_frame, text="Anuluj", command=window.destroy, style="TButton", width=15).pack(side="right", padx=5)
  2489.     ttk.Button(btn_frame, text="Utwórz Instancję", command=lambda: (
  2490.         # Validate version selection
  2491.         selected_version := version_var.get(),
  2492.         selected_java_option := java_combo.get(),
  2493.         selected_java_path := java_path_tuples[java_combo.current()][0] if java_combo.current() != -1 else "",
  2494.  
  2495.         process_create_instance(
  2496.             selected_version,
  2497.             {
  2498.                 "username": username_entry.get().strip() or "Player", # Ensure username is not empty
  2499.                 "memory": memory_spin.get(),
  2500.                 "java_path": selected_java_path,
  2501.                 "shared_assets": shared_assets.get(),
  2502.                 "shared_libraries": shared_libs.get(),
  2503.                 "shared_natives": shared_natives.get(),
  2504.                 "loader_type": loader_var.get(),
  2505.                 "loader_version": "", # Loader version not selected in this dialog
  2506.                 "server_ip": server_ip_var.get().strip(),
  2507.                 "server_port": server_port_var.get().strip()
  2508.             },
  2509.             window # Pass window to close it
  2510.         )
  2511.         ) if version_var.get() and version_var.get() != "Brak wersji!" else messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta!")).pack(side="right", padx=5)
  2512.  
  2513.  
  2514.  
  2515.  
  2516. def process_create_instance(version, settings, window):
  2517.     """Handles the logic after 'Create Instance' button is clicked in the dialog."""
  2518.     if not version:
  2519.         messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta.")
  2520.         return
  2521.     if version in instances:
  2522.         if not messagebox.askyesno("Instancja istnieje", f"Instancja dla wersji {version} już istnieje. Czy chcesz ją usunąć i utworzyć ponownie?"):
  2523.              return # User cancelled replacement
  2524.  
  2525.         # If user confirmed replacement, delete the existing instance first
  2526.         log_to_console(console, f"Użytkownik potwierdził zastąpienie instancji {version}. Usuwam starą...", "INFO")
  2527.         try:
  2528.             instance_path = instances[version]["path"]
  2529.             if os.path.exists(instance_path):
  2530.                  import shutil
  2531.                  shutil.rmtree(instance_path)
  2532.                  log_to_console(console, f"Usunięto folder starej instancji: {instance_path}", "INFO")
  2533.             del instances[version]
  2534.             save_config()
  2535.             log_to_console(console, f"Stara instancja {version} usunięta z konfiguracji.", "INFO")
  2536.         except Exception as e:
  2537.             messagebox.showerror("Błąd usuwania", f"Nie udało się usunąć istniejącej instancji {version}: {e}\nAnulowano tworzenie nowej.")
  2538.             log_to_console(console, f"Błąd podczas usuwania istniejącej instancji {version} przed tworzeniem nowej: {e}", "ERROR")
  2539.             return # Stop the process if old instance couldn't be removed
  2540.  
  2541.    # Now proceed with downloading the new instance
  2542.    # We need to switch to the Download tab and start the process there
  2543.    # Save the pending settings and version globally
  2544.    global pending_instance_settings, pending_version
  2545.    pending_instance_settings = settings
  2546.    pending_version = version
  2547.  
  2548.    window.destroy() # Close the dialog
  2549.    switch_tab("Pobieranie") # Switch to download tab which will pick up pending_version/settings
  2550.  
  2551. def refresh_version_combo_dialog(combo, version_var):
  2552.    """Refreshes version list in a combobox, typically in a dialog."""
  2553.    versions = get_versions()
  2554.    combo['values'] = versions
  2555.    if versions:
  2556.        # Try to keep the selected version if it's still in the filtered list
  2557.         current_selection = version_var.get()
  2558.         if current_selection in versions:
  2559.              version_var.set(current_selection)
  2560.         else:
  2561.              combo.current(0) # Select the first one
  2562.         combo.config(state="readonly")
  2563.     else:
  2564.         version_var.set("Brak wersji!")
  2565.         combo.config(state="disabled")
  2566.         messagebox.showwarning("Uwaga", "Nie znaleziono żadnych wersji Minecrafta z wybranymi filtrami.")
  2567.  
  2568.  
  2569. def edit_instance(version):
  2570.     if not version or version not in instances:
  2571.         messagebox.showwarning("Uwaga", "Wybierz instancję!")
  2572.         return
  2573.     instance = instances[version]
  2574.     settings = instance.get("settings", {})
  2575.  
  2576.     window = tk.Toplevel(root)
  2577.     window.title(f"Edytuj instancję {version}")
  2578.     window.geometry("550x700") # Same size as create
  2579.     window.configure(bg=PRIMARY_BG)
  2580.     window.resizable(False, False)
  2581.  
  2582.     style = ttk.Style() # Ensure style is available in Toplevel
  2583.     style.configure("TFrame", background=PRIMARY_BG)
  2584.     style.configure("TLabel", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10))
  2585.     style.configure("TButton", background=BUTTON_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10), borderwidth=0)
  2586.     style.map("TButton",
  2587.                   background=[('active', BUTTON_HOVER), ('pressed', BUTTON_HOVER)],
  2588.                   foreground=[('active', PRIMARY_FG), ('pressed', PRIMARY_FG)])
  2589.     style.configure("TEntry", fieldbackground="#333333", foreground=PRIMARY_FG, insertcolor=PRIMARY_FG, borderwidth=1)
  2590.     style.configure("TCombobox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, borderwidth=1)
  2591.     style.configure("TCheckbutton", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 9)) # Style for ttk.Checkbutton
  2592.  
  2593.  
  2594.     frame = ttk.Frame(window, padding="15")
  2595.     frame.pack(fill="both", expand=True)
  2596.  
  2597.     ttk.Label(frame, text=f"Edycja instancji {version}", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
  2598.  
  2599.     form_frame = ttk.Frame(frame)
  2600.     form_frame.pack(fill="x", expand=False)
  2601.     form_frame.columnconfigure(1, weight=1)
  2602.  
  2603.     # Row 0: Username
  2604.     ttk.Label(form_frame, text="Nazwa użytkownika:", style="TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5))
  2605.     username_entry = ttk.Entry(form_frame, style="TEntry")
  2606.     username_entry.insert(0, settings.get("username", username_var.get()))
  2607.     username_entry.grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
  2608.  
  2609.     # Row 1: Memory
  2610.     ttk.Label(form_frame, text="Pamięć RAM (GB):", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
  2611.     memory_spin = tk.Spinbox(form_frame, from_=1, to=32, width=5, bg="#333333", fg=PRIMARY_FG, highlightthickness=0,
  2612.                              buttonbackground=TERTIARY_BG, buttonforeground=PRIMARY_FG, insertbackground=PRIMARY_FG)
  2613.     memory_spin.delete(0, tk.END)
  2614.     memory_spin.insert(0, settings.get("memory", memory_var.get()))
  2615.     memory_spin.grid(row=1, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
  2616.  
  2617.     # Row 2: Java Path
  2618.     ttk.Label(form_frame, text="Ścieżka Java:", style="TLabel").grid(row=2, column=0, sticky="w", pady=(10, 5))
  2619.     java_var = tk.StringVar()
  2620.     # Include the instance's current Java path in options if it's not already found by find_java
  2621.     java_paths_found = find_java()
  2622.     java_path_tuples = java_paths_found[:] # Copy the list
  2623.  
  2624.     current_java_path = instance.get("java_path", settings.get("java_path", ""))
  2625.     current_java_version = instance.get("java_version", settings.get("java_version", ""))
  2626.  
  2627.     # Add the instance's current java path if it exists and wasn't found by find_java
  2628.     is_current_java_found = any(p == current_java_path for p, v in java_path_tuples)
  2629.     if current_java_path and os.path.exists(current_java_path) and not is_current_java_found:
  2630.         # Get version again if needed, but avoid checking twice if it's already in instance data
  2631.        actual_version = get_java_version(current_java_path) or current_java_version or "?"
  2632.        java_path_tuples.append((current_java_path, actual_version))
  2633.        java_options = [f"{p} (v{v})" for p, v in java_path_tuples]
  2634.        log_to_console(console, f"Dodano bieżącą Javę instancji do opcji: {current_java_path}", "INFO")
  2635.    else:
  2636.        java_options = [f"{p} (v{v})" for p, v in java_path_tuples]
  2637.  
  2638.  
  2639.    java_combo = ttk.Combobox(form_frame, textvariable=java_var, state="readonly", values=java_options, style="TCombobox")
  2640.    java_combo.grid(row=2, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2641.  
  2642.    # Set the current Java path in the combobox
  2643.    if current_java_path:
  2644.        # Find the index of the current_java_path in the java_path_tuples list
  2645.        try:
  2646.            index_to_select = [i for i, (p, v) in enumerate(java_path_tuples) if p == current_java_path][0]
  2647.            java_combo.current(index_to_select)
  2648.        except IndexError:
  2649.             # This shouldn't happen if logic above is correct, but as a fallback
  2650.              log_to_console(console, f"Nie znaleziono bieżącej ścieżki Javy {current_java_path} w liście opcji.", "WARNING")
  2651.              java_combo.set(current_java_path) # Display the path even if not in list
  2652.              java_combo.config(state="normal") # Allow editing if not in list? Or just display. Let's just display.
  2653.             java_combo.config(state="readonly") # Force readonly after setting
  2654.  
  2655.  
  2656.    # Row 3: Mod Loader Type
  2657.    ttk.Label(form_frame, text="Mod Loader:", style="TLabel").grid(row=3, column=0, sticky="w", pady=(10, 5))
  2658.    loader_var = tk.StringVar(value=settings.get("loader_type", "vanilla"))
  2659.    loader_options = ["vanilla", "fabric", "forge"] # Add "neoforge" etc. when supported
  2660.    loader_combo = ttk.Combobox(form_frame, textvariable=loader_var, state="readonly", values=loader_options, style="TCombobox")
  2661.    loader_combo.grid(row=3, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2662.    # Set current loader type
  2663.    try:
  2664.         loader_combo.current(loader_options.index(settings.get("loader_type", "vanilla")))
  2665.    except ValueError:
  2666.         loader_combo.current(0) # Default to vanilla if saved type is invalid
  2667.  
  2668.  
  2669.    # Row 4: Shared Folders
  2670.    ttk.Label(form_frame, text="Opcje współdzielenia:", style="TLabel").grid(row=4, column=0, sticky="nw", pady=(10, 5))
  2671.    options_frame = ttk.Frame(form_frame)
  2672.    options_frame.grid(row=4, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2673.  
  2674.    shared_assets = tk.BooleanVar(value=settings.get("shared_assets", shared_assets_var.get()))
  2675.    shared_libs = tk.BooleanVar(value=settings.get("shared_libraries", shared_libraries_var.get()))
  2676.    shared_natives = tk.BooleanVar(value=settings.get("shared_natives", shared_natives_var.get()))
  2677.  
  2678.    tk.Checkbutton(options_frame, text="Współdziel assets", variable=shared_assets,
  2679.                   bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
  2680.                   font=("Segoe UI", 9)).pack(anchor="w", pady=2)
  2681.    tk.Checkbutton(options_frame, text="Współdziel biblioteki", variable=shared_libs,
  2682.                   bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
  2683.                   font=("Segoe UI", 9)).pack(anchor="w", pady=2)
  2684.    tk.Checkbutton(options_frame, text="Współdziel natywne biblioteki", variable=shared_natives,
  2685.                   bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
  2686.                   font=("Segoe UI", 9)).pack(anchor="w", pady=2)
  2687.  
  2688.    # Row 5: Server Address (Optional)
  2689.    ttk.Label(form_frame, text="Adres serwera (opcjonalnie):", style="TLabel").grid(row=5, column=0, sticky="w", pady=(10, 5))
  2690.    server_ip_var = tk.StringVar(value=settings.get("server_ip", ""))
  2691.    server_ip_entry = ttk.Entry(form_frame, textvariable=server_ip_var, style="TEntry")
  2692.    server_ip_entry.grid(row=5, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2693.  
  2694.    ttk.Label(form_frame, text="Port serwera (opcjonalnie):", style="TLabel").grid(row=6, column=0, sticky="w", pady=(10, 5))
  2695.    server_port_var = tk.StringVar(value=settings.get("server_port", ""))
  2696.    server_port_entry = ttk.Entry(form_frame, textvariable=server_port_var, style="TEntry", width=8)
  2697.    server_port_entry.grid(row=6, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
  2698.  
  2699.  
  2700.    # Row 7: Stats (Read-only)
  2701.    ttk.Label(form_frame, text="Statystyki:", style="TLabel").grid(row=7, column=0, sticky="nw", pady=(10, 5))
  2702.    stats_frame = ttk.Frame(form_frame)
  2703.    stats_frame.grid(row=7, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2704.  
  2705.    # Calculate stats
  2706.    stats = get_instance_stats(instance)
  2707.  
  2708.    ttk.Label(stats_frame, text=f"Ścieżka: {stats['path']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
  2709.    ttk.Label(stats_frame, text=f"Rozmiar: {stats['size']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
  2710.    ttk.Label(stats_frame, text=f"Modów (.jar): {stats['mods']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
  2711.    ttk.Label(stats_frame, text=f"Utworzona: {stats['created']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
  2712.  
  2713.  
  2714.    # Action buttons at the bottom
  2715.    btn_frame = ttk.Frame(frame)
  2716.    btn_frame.pack(fill="x", pady=(15, 0))
  2717.  
  2718.    ttk.Button(btn_frame, text="Anuluj", command=window.destroy, style="TButton", width=15).pack(side="right", padx=5)
  2719.    ttk.Button(btn_frame, text="Zapisz", command=lambda: (
  2720.        # Get selected Java path from tuple list
  2721.        selected_java_path := java_path_tuples[java_combo.current()][0] if java_combo.current() != -1 else current_java_path,
  2722.        
  2723.        # Update instance with selected settings
  2724.        update_instance(version, {
  2725.            "username": username_entry.get().strip() or "Player",
  2726.            "memory": memory_spin.get(),
  2727.            "java_path": selected_java_path,
  2728.            "shared_assets": shared_assets.get(),
  2729.            "shared_libraries": shared_libs.get(),
  2730.            "shared_natives": shared_natives.get(),
  2731.            "loader_type": loader_var.get(),
  2732.            "loader_version": settings.get("loader_version", ""),  # Keep existing loader version
  2733.            "server_ip": server_ip_var.get().strip(),
  2734.            "server_port": server_port_var.get().strip()
  2735.        }),
  2736.  
  2737.        # Close the window
  2738.        window.destroy()
  2739.    ) if version_var.get() and version_var.get() != "Brak wersji!" else messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta!")).pack(side="right", padx=5)
  2740.  
  2741.  
  2742.  
  2743. def get_instance_stats(instance):
  2744.    """Calculates and returns statistics for a given instance."""
  2745.    stats = {
  2746.        "path": instance.get("path", "N/A"),
  2747.        "size": "N/A",
  2748.        "mods": "N/A",
  2749.        "created": datetime.fromisoformat(instance["timestamp"]).strftime("%Y-%m-%d %H:%M") if "timestamp" in instance else "Nieznana"
  2750.    }
  2751.    instance_path = instance.get("path")
  2752.    if instance_path and os.path.exists(instance_path):
  2753.        try:
  2754.            # Calculate folder size
  2755.            total_size = 0
  2756.            for dirpath, dirnames, filenames in os.walk(instance_path):
  2757.                for f in filenames:
  2758.                    fp = os.path.join(dirpath, f)
  2759.                    if not os.path.islink(fp): # Avoid counting symlinks multiple times
  2760.                         total_size += os.path.getsize(fp)
  2761.            stats["size"] = humanize.naturalsize(total_size)
  2762.  
  2763.            # Count mods (simple .jar file count in mods folder)
  2764.            mods_path = os.path.join(instance_path, "mods")
  2765.            if os.path.exists(mods_path):
  2766.                 mod_count = len([f for f in os.listdir(mods_path) if f.endswith(".jar")])
  2767.                 stats["mods"] = mod_count
  2768.            else:
  2769.                 stats["mods"] = 0 # No mods folder
  2770.  
  2771.        except Exception as e:
  2772.            log_to_console(console, f"Błąd obliczania statystyk dla instancji {instance_path}: {e}", "ERROR")
  2773.            pass # Keep N/A if error occurs
  2774.  
  2775.    return stats
  2776.  
  2777.  
  2778. def update_instance(version, settings):
  2779.    """Updates an instance's settings and regenerates its start.bat."""
  2780.    if version not in instances:
  2781.        log_to_console(console, f"Próba aktualizacji nieistniejącej instancji {version}.", "ERROR")
  2782.        return
  2783.  
  2784.    instance = instances[version]
  2785.    instance["settings"] = settings
  2786.  
  2787.    # Update Java info if the path changed
  2788.    selected_java_path = settings.get("java_path")
  2789.    if selected_java_path and selected_java_path != instance.get("java_path"):
  2790.        instance["java_path"] = selected_java_path
  2791.        instance["java_version"] = get_java_version(selected_java_path) # Update version based on new path
  2792.        instance["required_java"] = get_required_java(version, get_version_info(version)) # Recalculate required java
  2793.  
  2794.    # Regenerate start.bat with new settings
  2795.    info = get_version_info(version) # Need version info again
  2796.    if info:
  2797.        try:
  2798.            regenerate_start_bat(version, instance, info, console)
  2799.            # If bat regeneration was successful, mark ready? Not necessarily, files might be missing.
  2800.            # Let verify_instance handle the ready status.
  2801.        except Exception as e:
  2802.             log_to_console(console, f"Błąd podczas regeneracji start.bat po edycji instancji {version}: {e}", "ERROR")
  2803.             messagebox.showwarning("Błąd", f"Nie udało się zaktualizować pliku start.bat dla {version}.\nInstancja może nie działać poprawnie.")
  2804.             instance["ready"] = False # Mark as not ready if bat failed
  2805.  
  2806.  
  2807.    save_config()
  2808.    log_to_console(console, f"Instancja {version} zaktualizowana. Ustawienia i start.bat zostały zapisane.", "SUCCESS")
  2809.    refresh_instances()
  2810.  
  2811.  
  2812. def show_download():
  2813.    frame = ttk.Frame(content_frame, padding="10")
  2814.    frame.pack(fill="both", expand=True)
  2815.  
  2816.    ttk.Label(frame, text="Pobierz nową wersję Minecraft", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
  2817.  
  2818.    form_frame = ttk.Frame(frame)
  2819.    form_frame.pack(fill="x", expand=False)
  2820.    form_frame.columnconfigure(1, weight=1)
  2821.  
  2822.    # Row 0: Version Filters
  2823.    ttk.Label(form_frame, text="Filtry wersji:", style="TLabel").grid(row=0, column=0, sticky="nw", pady=(0, 5))
  2824.    filters_frame = ttk.Frame(form_frame)
  2825.    filters_frame.grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
  2826.  
  2827.    # Use tk.Checkbutton
  2828.    tk.Checkbutton(filters_frame, text="Snapshoty", variable=snapshots_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  2829.                   selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  2830.                   command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
  2831.    tk.Checkbutton(filters_frame, text="Release", variable=releases_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  2832.                   selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  2833.                   command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
  2834.    tk.Checkbutton(filters_frame, text="Alpha", variable=alpha_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  2835.                   selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  2836.                   command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
  2837.    tk.Checkbutton(filters_frame, text="Beta", variable=beta_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  2838.                   selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  2839.                   command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
  2840.  
  2841.  
  2842.    # Row 1: Minecraft Version
  2843.    ttk.Label(form_frame, text="Wersja Minecrafta:", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
  2844.    version_var = tk.StringVar()
  2845.    versions = get_versions()
  2846.    combo = ttk.Combobox(form_frame, textvariable=version_var, state="readonly", values=versions, style="TCombobox")
  2847.    combo.grid(row=1, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2848.  
  2849.    # Set initial selection based on pending_version if any
  2850.    global pending_version, pending_instance_settings  # Declare global before using
  2851.    if pending_version and pending_version in versions:
  2852.        version_var.set(pending_version)
  2853.        # Clear pending flags after setting the value
  2854.        pending_version = ""
  2855.        pending_instance_settings = {}  # Clear settings too
  2856.    elif versions:
  2857.        combo.current(0)
  2858.    else:
  2859.        version_var.set("Brak wersji!")
  2860.        combo.config(state="disabled")
  2861.        messagebox.showwarning("Uwaga", "Nie znaleziono żadnych wersji Minecrafta. Sprawdź filtry i połączenie z internetem.")
  2862.  
  2863.    # Row 2: Action Button
  2864.    btn_frame = ttk.Frame(frame)
  2865.    btn_frame.pack(fill="x", pady=(15, 0))
  2866.  
  2867.    download_btn = ttk.Button(btn_frame, text="Pobierz Wybraną Wersję", command=lambda: start_download_process(version_var.get()), style="TButton", width=25)
  2868.    download_btn.pack()
  2869.  
  2870.    # Console frame below the form and button
  2871.    console_frame = ttk.Frame(frame)
  2872.    console_frame.pack(fill="both", expand=True, pady=(15, 0))
  2873.  
  2874.  
  2875.    global console
  2876.    console = scrolledtext.ScrolledText(
  2877.        console_frame, height=15, wrap=tk.WORD, bg=CONSOLE_BG, fg=CONSOLE_FG_DEFAULT,
  2878.        state="disabled", font=("Consolas", 9)
  2879.    )
  2880.    console.pack(side="top", fill="both", expand=True)
  2881.  
  2882.    # Console action buttons
  2883.    console_btn_frame = ttk.Frame(frame)
  2884.    console_btn_frame.pack(fill="x", pady=(5, 0))
  2885.    ttk.Button(console_btn_frame, text="Kopiuj konsolę", command=lambda: copy_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
  2886.    ttk.Button(console_btn_frame, text="Wyczyść konsolę", command=lambda: clear_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
  2887.  
  2888.  
  2889. def start_download_process(version):
  2890.    global download_thread, download_active
  2891.    if download_active:
  2892.        messagebox.showwarning("Uwaga", "Pobieranie już w toku!")
  2893.        return
  2894.    if not version or version == "Brak wersji!":
  2895.        messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta do pobrania.")
  2896.        return
  2897.  
  2898.    # Ask for instance settings before starting download
  2899.    # Use default settings unless overridden
  2900.    # This creates a simpler download flow than the separate create instance dialog
  2901.    # Let's reuse the create_instance dialog logic to gather settings first.
  2902.    # The create_instance dialog will then call process_create_instance which sets pending vars and switches tab.
  2903.    # So, if we are on the Download tab and click "Download Selected Version", it means we skipped the create dialog.
  2904.    # In this case, use default settings.
  2905.  
  2906.    # Check if there are pending settings from a cancelled create dialog
  2907.    global pending_version, pending_instance_settings  # Declare global before using them
  2908.    if pending_version and pending_version == version and pending_instance_settings:
  2909.        settings_to_use = pending_instance_settings
  2910.        pending_version = ""
  2911.        pending_instance_settings = {}
  2912.        log_to_console(console, f"Używam oczekujących ustawień dla wersji {version}.", "INFO")
  2913.    else:
  2914.        # Use default settings if no pending settings
  2915.        settings_to_use = {
  2916.             "username": username_var.get(),
  2917.             "memory": memory_var.get(),
  2918.             "shared_assets": shared_assets_var.get(),
  2919.             "shared_libraries": shared_libraries_var.get(),
  2920.             "shared_natives": shared_natives_var.get(),
  2921.             "loader_type": "vanilla", # Default download is vanilla
  2922.             "loader_version": "",
  2923.             "server_ip": "",
  2924.             "server_port": ""
  2925.        }
  2926.        log_to_console(console, f"Używam domyślnych ustawień dla pobierania wersji {version}.", "INFO")
  2927.  
  2928.  
  2929.    # Handle case where instance already exists
  2930.    if version in instances:
  2931.        if not messagebox.askyesno("Instancja istnieje", f"Instancja dla wersji {version} już istnieje. Czy chcesz ją zastąpić (pobrać ponownie)?\n\nSpowoduje to usunięcie obecnego folderu:\n{instances[version]['path']}"):
  2932.             log_to_console(console, f"Pobieranie instancji {version} anulowane przez użytkownika (instancja już istnieje).", "INFO")
  2933.             return # User cancelled replacement
  2934.  
  2935.        # If user confirmed replacement, delete the existing instance first
  2936.        log_to_console(console, f"Użytkownik potwierdził zastąpienie instancji {version}. Usuwam starą...", "INFO")
  2937.        try:
  2938.            instance_path = instances[version]["path"]
  2939.            if os.path.exists(instance_path):
  2940.                 import shutil
  2941.                 shutil.rmtree(instance_path)
  2942.                 log_to_console(console, f"Usunięto folder starej instancji: {instance_path}", "INFO")
  2943.            del instances[version]
  2944.            save_config()
  2945.            log_to_console(console, f"Stara instancja {version} usunięta z konfiguracji.", "INFO")
  2946.        except Exception as e:
  2947.            messagebox.showerror("Błąd usuwania", f"Nie udało się usunąć istniejącej instancji {version}: {e}\nAnulowano pobieranie nowej.")
  2948.            log_to_console(console, f"Błąd podczas usuwania istniejącej instancji {version} przed pobieraniem nowej: {e}", "ERROR")
  2949.            return # Stop the process if old instance couldn't be removed
  2950.  
  2951.  
  2952.    # Start the download thread
  2953.    download_active = True
  2954.    # Clear console before starting
  2955.    clear_console_content(console)
  2956.    log_to_console(console, f"Rozpoczynam proces pobierania wersji {version} z ustawieniami...", "INFO")
  2957.    # Pass the settings gathered or defaulted
  2958.    download_thread = threading.Thread(
  2959.        target=download_version,
  2960.        args=(version, settings_to_use, console),
  2961.        daemon=True
  2962.    )
  2963.    download_thread.start()
  2964.  
  2965.  
  2966. def show_settings():
  2967.    frame = ttk.Frame(content_frame, padding="10")
  2968.    frame.pack(fill="both", expand=True)
  2969.  
  2970.    ttk.Label(frame, text="Ustawienia Launchera", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
  2971.  
  2972.    form_frame = ttk.Frame(frame)
  2973.    form_frame.pack(fill="x", expand=False)
  2974.    form_frame.columnconfigure(1, weight=1) # Allow second column to expand
  2975.  
  2976.  
  2977.    # Row 0: Default Username
  2978.    ttk.Label(form_frame, text="Domyślna nazwa użytkownika:", style="TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5))
  2979.    ttk.Entry(form_frame, textvariable=username_var, style="TEntry").grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
  2980.  
  2981.    # Row 1: Default Memory
  2982.    ttk.Label(form_frame, text="Domyślna pamięć RAM (GB):", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
  2983.    memory_spin = tk.Spinbox(form_frame, from_=1, to=32, textvariable=memory_var, width=5,
  2984.                                     bg="#333333", fg=PRIMARY_FG, highlightthickness=0,
  2985.                                     buttonbackground=TERTIARY_BG, buttonforeground=PRIMARY_FG, insertbackground=PRIMARY_FG)
  2986.    memory_spin.grid(row=1, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
  2987.  
  2988.    # Row 2: Default Shared Folders
  2989.    ttk.Label(form_frame, text="Domyślne opcje współdzielenia:", style="TLabel").grid(row=2, column=0, sticky="nw", pady=(10, 5))
  2990.    options_frame = ttk.Frame(form_frame)
  2991.    options_frame.grid(row=2, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  2992.  
  2993.    tk.Checkbutton(options_frame, text="Współdziel assets między instancjami", variable=shared_assets_var,
  2994.                           bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
  2995.                           font=("Segoe UI", 9), command=save_config).pack(anchor="w", pady=2)
  2996.    tk.Checkbutton(options_frame, text="Współdziel biblioteki między instancjami", variable=shared_libraries_var,
  2997.                           bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
  2998.                           font=("Segoe UI", 9), command=save_config).pack(anchor="w", pady=2)
  2999.    tk.Checkbutton(options_frame, text="Współdziel natywne biblioteki między instancjami", variable=shared_natives_var,
  3000.                           bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
  3001.                           font=("Segoe UI", 9), command=save_config).pack(anchor="w", pady=2)
  3002.  
  3003.    # Row 3: Default Version Filters
  3004.    ttk.Label(form_frame, text="Domyślne filtry wersji:", style="TLabel").grid(row=3, column=0, sticky="nw", pady=(10, 5))
  3005.    filters_frame = ttk.Frame(form_frame)
  3006.    filters_frame.grid(row=3, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  3007.  
  3008.    tk.Checkbutton(filters_frame, text="Snapshoty", variable=snapshots_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  3009.                           selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  3010.                           command=save_config).pack(anchor="w", pady=2)
  3011.    tk.Checkbutton(filters_frame, text="Release", variable=releases_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  3012.                           selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  3013.                           command=save_config).pack(anchor="w", pady=2)
  3014.    tk.Checkbutton(filters_frame, text="Alpha", variable=alpha_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  3015.                           selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  3016.                           command=save_config).pack(anchor="w", pady=2)
  3017.    tk.Checkbutton(filters_frame, text="Beta", variable=beta_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
  3018.                           selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
  3019.                           command=save_config).pack(anchor="w", pady=2)
  3020.  
  3021.    # Row 4: Java Paths Cache (Display only)
  3022.    ttk.Label(form_frame, text="Znalezione Javy (Cache):", style="TLabel").grid(row=4, column=0, sticky="nw", pady=(10, 5))
  3023.    java_cache_frame = ttk.Frame(form_frame)
  3024.    java_cache_frame.grid(row=4, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
  3025.  
  3026.    if java_versions_cache:
  3027.        cache_text = "\n".join([f"v{ver}: {path}" for ver, path in java_versions_cache.items()])
  3028.        ttk.Label(java_cache_frame, text=cache_text, style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
  3029.    else:
  3030.        ttk.Label(java_cache_frame, text="Brak znalezionych Jav w cache.", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
  3031.  
  3032.    # Action button - Save is handled by checkboxes and entry lose focus (implicitly by config save)
  3033.    # Adding an explicit save button for completeness, though not strictly needed for current vars
  3034.    btn_frame = ttk.Frame(frame)
  3035.    btn_frame.pack(fill="x", pady=(15, 0))
  3036.    ttk.Button(btn_frame, text="Zapisz Ustawienia", command=save_config, style="TButton", width=20).pack()
  3037.  
  3038.  
  3039. def show_console():
  3040.    frame = ttk.Frame(content_frame, padding="10")
  3041.    frame.pack(fill="both", expand=True)
  3042.  
  3043.    ttk.Label(frame, text="Konsola Launchera", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
  3044.  
  3045.    console_frame = ttk.Frame(frame)
  3046.    console_frame.pack(fill="both", expand=True)
  3047.  
  3048.    global console
  3049.    # Recreate the console widget if it was destroyed when switching tabs
  3050.    if console is None or not console.winfo_exists():
  3051.        console = scrolledtext.ScrolledText(
  3052.            console_frame, height=20, wrap=tk.WORD, bg=CONSOLE_BG, fg=CONSOLE_FG_DEFAULT,
  3053.            state="disabled", font=("Consolas", 9)
  3054.        )
  3055.        # Apply tags if they don't exist (will be applied on first log or in log_to_console)
  3056.  
  3057.    console.pack(side="top", fill="both", expand=True)
  3058.  
  3059.    # Console action buttons
  3060.    btn_frame = ttk.Frame(frame)
  3061.    btn_frame.pack(fill="x", pady=(10, 0))
  3062.  
  3063.    ttk.Button(btn_frame, text="Kopiuj", command=lambda: copy_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
  3064.    ttk.Button(btn_frame, text="Wyczyść", command=lambda: clear_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
  3065.  
  3066.  
  3067. def show_tools():
  3068.    frame = ttk.Frame(content_frame, padding="10")
  3069.    frame.pack(fill="both", expand=True)
  3070.  
  3071.    ttk.Label(frame, text="Narzędzia Launchera", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
  3072.  
  3073.    tools_frame = ttk.Frame(frame)
  3074.    tools_frame.pack(fill="both", expand=True)
  3075.    # tools_frame.columnconfigure(0, weight=1) # Allow buttons to expand? Or just pack.
  3076.  
  3077.    # Config Import/Export
  3078.    ttk.Label(tools_frame, text="Konfiguracja:", font=("Segoe UI", 11, "bold"), style="TLabel").pack(anchor="w", pady=(10, 5))
  3079.    config_btn_frame = ttk.Frame(tools_frame)
  3080.    config_btn_frame.pack(fill="x", pady=(5, 10))
  3081.    ttk.Button(config_btn_frame, text="Eksportuj Konfigurację", command=export_config, style="TButton", width=25).pack(side="left", padx=10)
  3082.    ttk.Button(config_btn_frame, text="Importuj Konfigurację", command=import_config, style="TButton", width=25).pack(side="left", padx=10)
  3083.    ttk.Label(tools_frame, text="Eksport/Import dotyczy wszystkich instancji i domyślnych ustawień launchera.", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w", padx=10)
  3084.  
  3085.  
  3086.    # Java Management (Optional, could list found Javas and allow removing from cache)
  3087.    # ttk.Label(tools_frame, text="Zarządzanie Javą:", font=("Segoe UI", 11, "bold"), style="TLabel").pack(anchor="w", pady=(10, 5))
  3088.    # Java management could be a complex table listing found Javas and options. Omit for now.
  3089.  
  3090.    # Other potential tools:
  3091.    # - Clean temporary files
  3092.    # - Repair shared assets/libraries
  3093.    # - Re-discover Java installations
  3094.  
  3095.  
  3096. def show_modrinth_browser():
  3097.    frame = ttk.Frame(content_frame, padding="10")
  3098.    frame.pack(fill="both", expand=True)
  3099.  
  3100.    ttk.Label(frame, text="Przeglądaj Mody (Modrinth)", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
  3101.  
  3102.    search_frame = ttk.Frame(frame)
  3103.    search_frame.pack(fill="x", expand=False, pady=(0, 10))
  3104.    search_frame.columnconfigure(1, weight=1)
  3105.  
  3106.    ttk.Label(search_frame, text="Szukaj:", style="TLabel").grid(row=0, column=0, sticky="w", padx=5)
  3107.    search_term_var = tk.StringVar()
  3108.    search_entry = ttk.Entry(search_frame, textvariable=search_term_var, style="TEntry")
  3109.    search_entry.grid(row=0, column=1, sticky="ew", padx=5)
  3110.  
  3111.    ttk.Label(search_frame, text="Wersja MC:", style="TLabel").grid(row=0, column=2, sticky="w", padx=5)
  3112.    mc_versions = sorted(list(set([inst["settings"].get("version", v) for v, inst in instances.items()] + [v for v in instances.keys()])), key=lambda v: pkg_version.parse(v) if v[0].isdigit() else v, reverse=True) # Get MC versions from existing instances + keys
  3113.    mc_version_var = tk.StringVar(value=mc_versions[0] if mc_versions else "") # Default to newest instance version or empty
  3114.    mc_version_combo = ttk.Combobox(search_frame, textvariable=mc_version_var, state="readonly", values=mc_versions, style="TCombobox", width=12)
  3115.    mc_version_combo.grid(row=0, column=3, sticky="w", padx=5)
  3116.    if mc_versions:
  3117.        mc_version_combo.current(0)
  3118.  
  3119.  
  3120.    ttk.Label(search_frame, text="Loader:", style="TLabel").grid(row=0, column=4, sticky="w", padx=5)
  3121.    loader_types = ["any", "fabric", "forge", "quilt", "neoforge"] # Modrinth supports these loaders
  3122.    loader_var = tk.StringVar(value="any")
  3123.    loader_combo = ttk.Combobox(search_frame, textvariable=loader_var, state="readonly", values=loader_types, style="TCombobox", width=8)
  3124.    loader_combo.grid(row=0, column=5, sticky="w", padx=5)
  3125.    loader_combo.current(0)
  3126.  
  3127.    search_button = ttk.Button(search_frame, text="Szukaj", command=lambda: search_modrinth(search_term_var.get(), mc_version_var.get(), loader_var.get(), modrinth_tree), style="TButton", width=10)
  3128.    search_button.grid(row=0, column=6, sticky="e", padx=5)
  3129.  
  3130.    # Mod List (Treeview)
  3131.    mod_list_frame = ttk.Frame(frame)
  3132.    mod_list_frame.pack(fill="both", expand=True)
  3133.  
  3134.    columns = ("title", "author", "downloads", "version", "loaders", "description") # Added description, loaders, version
  3135.    modrinth_tree = ttk.Treeview(
  3136.        mod_list_frame, columns=columns, show="headings", selectmode="browse",
  3137.        style="Treeview"
  3138.    )
  3139.  
  3140.    modrinth_tree.heading("title", text="Tytuł Modu", anchor="w")
  3141.    modrinth_tree.heading("author", text="Autor", anchor="w")
  3142.    modrinth_tree.heading("downloads", text="Pobrania", anchor="e")
  3143.    modrinth_tree.heading("version", text="Wersja Modu", anchor="w") # Version of the mod file, not MC version
  3144.    modrinth_tree.heading("loaders", text="Loadery", anchor="w") # Loaders supported by the mod file
  3145.    modrinth_tree.heading("description", text="Opis", anchor="w") # Hidden by default
  3146.  
  3147.    modrinth_tree.column("title", width=200, anchor="w")
  3148.    modrinth_tree.column("author", width=100, anchor="w")
  3149.    modrinth_tree.column("downloads", width=80, anchor="e", stretch=tk.NO)
  3150.    modrinth_tree.column("version", width=100, anchor="w", stretch=tk.NO)
  3151.    modrinth_tree.column("loaders", width=120, anchor="w")
  3152.    modrinth_tree.column("description", width=0, stretch=tk.NO) # Hide description initially
  3153.  
  3154.    scrollbar = ttk.Scrollbar(mod_list_frame, orient="vertical", command=modrinth_tree.yview)
  3155.    scrollbar.pack(side="right", fill="y")
  3156.    modrinth_tree.configure(yscrollcommand=scrollbar.set)
  3157.    modrinth_tree.pack(side="left", fill="both", expand=True)
  3158.  
  3159.    # Modrinth item context menu
  3160.    mod_context_menu = tk.Menu(root, tearoff=0)
  3161.    mod_context_menu.add_command(label="Pobierz do instancji", command=lambda: download_mod_to_instance(modrinth_tree))
  3162.    mod_context_menu.add_command(label="Otwórz na Modrinth", command=lambda: open_modrinth_page(modrinth_tree))
  3163.  
  3164.    def show_modrinth_context_menu(event):
  3165.         selected_item = modrinth_tree.focus()
  3166.         if selected_item:
  3167.             try:
  3168.                 mod_context_menu.tk_popup(event.x_root, event.y_root)
  3169.             finally:
  3170.                 mod_context_menu.grab_release()
  3171.  
  3172.    modrinth_tree.bind("<Button-3>", show_modrinth_context_menu)
  3173.  
  3174.    # Action frame below mod list
  3175.    mod_action_frame = ttk.Frame(frame)
  3176.    mod_action_frame.pack(fill="x", pady=(10, 0))
  3177.  
  3178.    ttk.Label(mod_action_frame, text="Pobierz do instancji:", style="TLabel").pack(side="left", padx=5)
  3179.    # Dropdown to select instance to download to
  3180.    instance_versions = sorted(instances.keys(), key=lambda v: pkg_version.parse(v) if v[0].isdigit() else v, reverse=True)
  3181.    global selected_modrinth_instance_var
  3182.    selected_modrinth_instance_var.set(instance_versions[0] if instance_versions else "")
  3183.    instance_combo = ttk.Combobox(mod_action_frame, textvariable=selected_modrinth_instance_var, state="readonly", values=instance_versions, style="TCombobox", width=20)
  3184.    instance_combo.pack(side="left", padx=5)
  3185.  
  3186.    download_mod_button = ttk.Button(mod_action_frame, text="Pobierz Wybrany Mod", command=lambda: download_mod_to_instance(modrinth_tree), style="TButton", width=20)
  3187.    download_mod_button.pack(side="left", padx=5)
  3188.  
  3189.    if not instance_versions:
  3190.        instance_combo.config(state="disabled")
  3191.        download_mod_button.config(state="disabled")
  3192.        ttk.Label(mod_action_frame, text="Brak instancji do pobrania modów.", style="TLabel", foreground=WARNING_COLOR).pack(side="left", padx=5)
  3193.    else:
  3194.         # Add tooltip to download button
  3195.         Tooltip(download_mod_button, "Pobiera wybrany mod do folderu mods/ wybranej instancji.\nUpewnij się, że instancja ma odpowiedni mod loader!")
  3196.  
  3197.  
  3198. def search_modrinth(search_term, mc_version, loader, tree):
  3199.    """Searches Modrinth API and populates the treeview."""
  3200.    # Clear previous results
  3201.    for item in tree.get_children():
  3202.        tree.delete(item)
  3203.  
  3204.    if not search_term and not mc_version:
  3205.        # No search criteria
  3206.        log_to_console(console, "Wpisz frazę do wyszukania lub wybierz wersję MC.", "WARNING")
  3207.        return
  3208.  
  3209.    log_to_console(console, f"Szukam modów na Modrinth: '{search_term}', wersja MC='{mc_version}', loader='{loader}'...", "INFO")
  3210.  
  3211.    # Modrinth API Endpoint: https://api.modrinth.com/v2/search
  3212.    url = "https://api.modrinth.com/v2/search"
  3213.    params = {
  3214.        "query": search_term,
  3215.        "limit": 50, # Limit results
  3216.        "facets": json.dumps([ # Filter by version and loader
  3217.            [f"versions:{mc_version}"] if mc_version else [],
  3218.            [f"project_type:mod"], # Search only for mods
  3219.            [f"loaders:{loader}"] if loader != "any" else []
  3220.        ])
  3221.    }
  3222.  
  3223.    try:
  3224.        response = requests.get(url, params=params, timeout=10)
  3225.        response.raise_for_status() # Raise an exception for bad status codes
  3226.        results = response.json()
  3227.  
  3228.        if results and "hits" in results:
  3229.            log_to_console(console, f"Znaleziono {results['total_hits']} wyników.", "INFO")
  3230.            for hit in results["hits"]:
  3231.                # Need to fetch file details to get exact mod version, supported loaders for that file etc.
  3232.                # The search results ("hits") provide project-level info, not file-level details directly related to the filters.
  3233.                # This makes selecting the *correct file* for download tricky from just the search result.
  3234.                # A more robust approach would be:
  3235.                # 1. Search projects.
  3236.                # 2. For a selected project, fetch its versions.
  3237.                # 3. Filter project versions by MC version and loader.
  3238.                # 4. Display file(s) for the matching versions.
  3239.                # 5. User selects a specific file to download.
  3240.  
  3241.                # Simplified Approach for now: Display project info. The download button will *attempt* to find a matching file.
  3242.                # This might download the wrong file or fail if no exact match exists.
  3243.  
  3244.                tree.insert("", "end", iid=hit["project_id"], values=(
  3245.                    hit.get("title", "Bez tytułu"),
  3246.                    hit.get("author", "Nieznany autor"),
  3247.                    hit.get("downloads", 0), # Total project downloads
  3248.                    hit.get("latest_version", "N/A"), # Latest project version string (might not be for selected MC/loader)
  3249.                    ", ".join(hit.get("loaders", [])), # Project-level loaders
  3250.                    hit.get("description", "Bez opisu")
  3251.                ))
  3252.            # Add tooltip to treeview items to show description? Or show description in a label below?
  3253.            # Tooltip on item is cleaner but requires more complex event binding.
  3254.        else:
  3255.            log_to_console(console, "Nie znaleziono modów spełniających kryteria.", "INFO")
  3256.  
  3257.    except requests.exceptions.RequestException as e:
  3258.        log_to_console(console, f"Błąd API Modrinth podczas wyszukiwania: {e}", "ERROR")
  3259.        messagebox.showerror("Błąd API Modrinth", f"Nie udało się wyszukać modów: {e}")
  3260.    except Exception as e:
  3261.         log_to_console(console, f"Nieoczekiwany błąd podczas wyszukiwania Modrinth: {e}", "ERROR")
  3262.         messagebox.showerror("Nieoczekiwany błąd", f"Wystąpił błąd podczas wyszukiwania: {e}")
  3263.  
  3264.  
  3265. def download_mod_to_instance(tree):
  3266.    """Downloads the selected mod file from Modrinth to the selected instance's mods folder."""
  3267.    selected_item = tree.focus()
  3268.    if not selected_item:
  3269.        messagebox.showwarning("Uwaga", "Wybierz mod z listy, aby go pobrać.")
  3270.        return
  3271.  
  3272.    project_id = tree.item(selected_item, "iid")
  3273.    if not project_id:
  3274.         messagebox.showerror("Błąd", "Nie udało się uzyskać ID projektu Modrinth.")
  3275.         return
  3276.  
  3277.    target_instance_version = selected_modrinth_instance_var.get()
  3278.    if not target_instance_version or target_instance_version not in instances:
  3279.         messagebox.showwarning("Uwaga", "Wybierz instancję docelową z listy rozwijanej pod wynikami wyszukiwania.")
  3280.         return
  3281.  
  3282.    instance = instances.get(target_instance_version)
  3283.    if not instance: # Should not happen if in instances list, but check
  3284.         messagebox.showerror("Błąd", "Dane wybranej instancji nie zostały znalezione.")
  3285.         return
  3286.  
  3287.    log_to_console(console, f"Przygotowanie do pobrania modu '{tree.item(selected_item)['values'][0]}' (ID: {project_id}) do instancji '{target_instance_version}'...", "INFO")
  3288.  
  3289.    # Need to find the correct file for the selected instance's MC version and loader type.
  3290.     # Modrinth API: /project/{id}/version
  3291.     versions_url = f"https://api.modrinth.com/v2/project/{project_id}/version"
  3292.     instance_mc_version = target_instance_version # MC version is the instance key
  3293.     instance_loader = instance["settings"].get("loader_type", "vanilla")
  3294.  
  3295.     # Filter by game versions and loaders
  3296.     params = {
  3297.         "game_versions": json.dumps([instance_mc_version]),
  3298.         "loaders": json.dumps([instance_loader]) if instance_loader != "vanilla" else json.dumps([]) # Filter by loader unless vanilla
  3299.     }
  3300.     if instance_loader == "vanilla":
  3301.         # Vanilla instances technically don't need a loader filter, but mods listed might require one.
  3302.        # Maybe search for files that list NO loaders or 'any'? Modrinth facets might handle this.
  3303.        # For simplicity, if instance is vanilla, we filter by MC version only.
  3304.        # If mod requires a loader, it might still be listed, but won't work. User needs to know.
  3305.         params = {"game_versions": json.dumps([instance_mc_version])}
  3306.  
  3307.  
  3308.     try:
  3309.         response = requests.get(versions_url, params=params, timeout=10)
  3310.         response.raise_for_status()
  3311.         mod_versions = response.json() # This is a list of versions of the mod project
  3312.  
  3313.         if not mod_versions:
  3314.             log_to_console(console, f"Nie znaleziono wersji modu '{tree.item(selected_item)['values'][0]}' kompatybilnych z MC {instance_mc_version} i loaderem '{instance_loader}'.", "WARNING")
  3315.             messagebox.showwarning("Brak kompatybilnej wersji", f"Nie znaleziono wersji modu '{tree.item(selected_item)['values'][0]}' kompatybilnych z Twoją instancją (MC {instance_mc_version}, Loader: {instance_loader}).")
  3316.             return
  3317.  
  3318.         # Find the most recent version that matches criteria and has a downloadable file
  3319.         # Modrinth versions are often sorted newest first by default, but explicitly sort by release date or version string
  3320.         sorted_mod_versions = sorted(mod_versions, key=lambda v: v.get("date_published", ""), reverse=True)
  3321.  
  3322.         best_file = None
  3323.         mod_version_info = None
  3324.         for mod_v in sorted_mod_versions:
  3325.             # Find a primary file in this version
  3326.             primary_file = next((f for f in mod_v.get("files", []) if f.get("primary")), None)
  3327.             if not primary_file and mod_v.get("files"):
  3328.                  primary_file = mod_v["files"][0] # Take first file if no primary marked
  3329.  
  3330.             if primary_file and primary_file.get("url"):
  3331.                  # Optional: double check loaders listed in the file itself match instance loader
  3332.                  # This is more precise than project-level loaders
  3333.                  file_loaders = primary_file.get("loaders", [])
  3334.                  if instance_loader != "vanilla" and instance_loader not in file_loaders:
  3335.                       # Skip this file if instance requires a loader but file doesn't list it
  3336.                      continue
  3337.                 if instance_loader == "vanilla" and file_loaders:
  3338.                      # Skip this file if instance is vanilla but file requires a loader
  3339.                      continue
  3340.  
  3341.                 best_file = primary_file
  3342.                 mod_version_info = mod_v
  3343.                 break # Found a suitable file, break loop
  3344.  
  3345.        if not best_file:
  3346.            log_to_console(console, f"Znaleziono wersje modu, ale brak pliku do pobrania kompatybilnego z MC {instance_mc_version} i loaderem '{instance_loader}'.", "WARNING")
  3347.            messagebox.showwarning("Brak pliku", f"Znaleziono wersje modu '{tree.item(selected_item)['values'][0]}', ale brak pliku do pobrania kompatybilnego z Twoją instancją (MC {instance_mc_version}, Loader: {instance_loader}).")
  3348.            return
  3349.  
  3350.  
  3351.        file_url = best_file["url"]
  3352.        file_name = best_file.get("filename") or file_url.split("/")[-1] # Use provided filename or extract from URL
  3353.        file_sha1 = best_file.get("hashes", {}).get("sha1") # Modrinth provides sha1 and sha512
  3354.  
  3355.        mod_target_path = os.path.join(instance["path"], "mods", file_name)
  3356.  
  3357.        if os.path.exists(mod_target_path) and file_sha1 and verify_sha1(mod_target_path, file_sha1):
  3358.            log_to_console(console, f"Mod '{file_name}' już istnieje i jest poprawny w instancji {target_instance_version}. Pomijam pobieranie.", "INFO")
  3359.            messagebox.showinfo("Już istnieje", f"Mod '{file_name}' już istnieje i jest poprawny w instancji {target_instance_version}.")
  3360.            return
  3361.  
  3362.        # Start download in a thread
  3363.        log_to_console(console, f"Pobieranie modu '{file_name}' ({mod_version_info.get('version_number', 'N/A')}) do instancji {target_instance_version}...", "INFO")
  3364.        # Pass a specific progress bar/label for mod downloads if needed, or use global ones
  3365.        # Using global ones requires careful state management if simultaneous downloads are possible (they aren't currently)
  3366.         # Let's use the global progress bar, maybe temporarily update status label format
  3367.        # Store original status label text and restore after mod download
  3368.        original_status_text = global_status_label.cget("text")
  3369.  
  3370.        def mod_download_callback(downloaded, total):
  3371.             # Use the global progress bar, but format status label for mod download
  3372.             if global_progress_bar and global_status_label:
  3373.                  progress = (downloaded / total) * 100 if total > 0 else 0
  3374.                  global_progress_bar["value"] = progress
  3375.                  remaining_bytes = max(0, total - downloaded)
  3376.                  global_status_label.config(text=f"[Modrinth] Pobieranie '{file_name}': {progress:.1f}% | Pozostało: {humanize.naturalsize(remaining_bytes)}")
  3377.                  root.update_idletasks()
  3378.  
  3379.        def mod_download_complete():
  3380.             # Restore original status label text
  3381.             if global_status_label:
  3382.                  global_status_label.config(text=original_status_text)
  3383.                  global_progress_bar["value"] = 0 # Reset bar
  3384.             log_to_console(console, f"Pobieranie modu '{file_name}' zakończone.", "INFO")
  3385.             # Optionally run verify_instance again, but it's slow.
  3386.              # Just show success message.
  3387.              messagebox.showinfo("Pobrano Mod", f"Mod '{file_name}' został pomyślnie pobrany do instancji {target_instance_version}.")
  3388.              # Check for dependencies? Very complex. Omit for now.
  3389.  
  3390.         def mod_download_failed(error_msg):
  3391.              if global_status_label:
  3392.                   global_status_label.config(text=original_status_text)
  3393.                   global_progress_bar["value"] = 0 # Reset bar
  3394.              log_to_console(console, f"Pobieranie modu '{file_name}' nie powiodło się: {error_msg}", "ERROR")
  3395.              messagebox.showerror("Błąd Pobierania Modu", f"Nie udało się pobrać modu '{file_name}': {error_msg}")
  3396.  
  3397.  
  3398.         # Run download in a new thread
  3399.         mod_download_thread = threading.Thread(
  3400.             target=lambda: [
  3401.                 log_to_console(console, f"Rozpoczynam wątek pobierania modu {file_name}", "INFO"),
  3402.                 download_success := download_file(file_url, mod_target_path, mod_download_callback, file_sha1, console, description=f"mod {file_name}"),
  3403.                 mod_download_complete() if download_success else mod_download_failed("Błąd pobierania lub weryfikacji")
  3404.             ],
  3405.             daemon=True # Allow thread to close with main app
  3406.         )
  3407.         mod_download_thread.start()
  3408.  
  3409.     except requests.exceptions.RequestException as e:
  3410.         log_to_console(console, f"Błąd API Modrinth podczas pobierania modu: {e}", "ERROR")
  3411.         messagebox.showerror("Błąd API Modrinth", f"Nie udało się pobrać szczegółów modu: {e}")
  3412.     except Exception as e:
  3413.          log_to_console(console, f"Nieoczekiwany błąd podczas pobierania modu Modrinth: {e}", "ERROR")
  3414.          messagebox.showerror("Nieoczekiwany błąd", f"Wystąpił błąd podczas pobierania modu: {e}")
  3415.  
  3416.  
  3417. def open_modrinth_page(tree):
  3418.     """Opens the Modrinth project page for the selected mod."""
  3419.     selected_item = tree.focus()
  3420.     if not selected_item:
  3421.         messagebox.showwarning("Uwaga", "Wybierz mod z listy.")
  3422.         return
  3423.  
  3424.     project_id = tree.item(selected_item, "iid")
  3425.     if not project_id:
  3426.          messagebox.showerror("Błąd", "Nie udało się uzyskać ID projektu Modrinth.")
  3427.          return
  3428.  
  3429.     project_slug = tree.item(selected_item)['values'][0].replace(" ", "-").lower() # Attempt to create a slug
  3430.     # A more reliable way would be to fetch project details to get the actual slug
  3431.     # Modrinth URL is https://modrinth.com/mod/{slug} or /project/{slug}
  3432.  
  3433.     url = f"https://modrinth.com/project/{project_id}" # Using ID is more reliable than guessing slug
  3434.  
  3435.     try:
  3436.         webbrowser.open(url)
  3437.         log_to_console(console, f"Otwieram stronę Modrinth dla projektu {project_id}: {url}", "INFO")
  3438.     except Exception as e:
  3439.         messagebox.showerror("Błąd otwierania strony", f"Nie udało się otworzyć strony w przeglądarce: {e}")
  3440.         log_to_console(console, f"Nie udało się otworzyć strony Modrinth {url}: {e}", "ERROR")
  3441.  
  3442.  
  3443. def find_java_with_source():
  3444.     """Same as find_java but returns tuples including the source (Cache, Pobrana, JAVA_HOME, etc.)."""
  3445.     # This is largely duplicated from find_java for clarity, could refactor
  3446.     possible_paths = []
  3447.     system = platform.system()
  3448.     java_exec_name = "java.exe" if system == "Windows" else "java"
  3449.  
  3450.     # Check cache first
  3451.     for ver, path in java_versions_cache.items():
  3452.         if os.path.exists(path):
  3453.              # Check if it's actually a 64-bit java executable without verbose logging
  3454.             try:
  3455.                 result = subprocess.run([path, "-version"], capture_output=True, text=True, timeout=1, encoding='utf-8', errors='ignore')
  3456.                 is_64bit = "64-Bit" in result.stderr or "64-bit" in result.stderr or "x86_64" in result.stderr.lower()
  3457.                 if is_64bit:
  3458.                      possible_paths.append((path, ver, "Cache"))
  3459.             except:
  3460.                 pass # Ignore errors for paths in cache that might be invalid
  3461.  
  3462.    # Check custom JAVA_DIR installations
  3463.    if os.path.exists(JAVA_DIR):
  3464.        for java_folder in os.listdir(JAVA_DIR):
  3465.            java_path = os.path.join(JAVA_DIR, java_folder)
  3466.            found_exec = None
  3467.            for root, _, files in os.walk(java_path):
  3468.                if java_exec_name in files:
  3469.                     found_exec = os.path.join(root, java_exec_name)
  3470.                     break
  3471.            if found_exec:
  3472.                version = get_java_version(found_exec) # This one *will* log
  3473.                if version: # get_java_version only returns 64-bit versions
  3474.                    possible_paths.append((found_exec, version, "Pobrana"))
  3475.  
  3476.  
  3477.    # Check standard system locations (JAVA_HOME, PATH, Program Files) - these will also log
  3478.    java_home = os.environ.get("JAVA_HOME")
  3479.    if java_home:
  3480.        java_path = os.path.join(java_home, "bin", java_exec_name)
  3481.        if os.path.exists(java_path):
  3482.            version = get_java_version(java_path)
  3483.            if version:
  3484.                possible_paths.append((java_path, version, "JAVA_HOME"))
  3485.  
  3486.    try:
  3487.        command = ["where", java_exec_name] if system == "Windows" else ["which", java_exec_name]
  3488.        out = subprocess.check_output(command, stderr=subprocess.DEVNULL).decode().strip()
  3489.        for line in out.splitlines():
  3490.            line = line.strip()
  3491.            if os.path.exists(line):
  3492.                version = get_java_version(line)
  3493.                if version:
  3494.                    possible_paths.append((line, version, "PATH"))
  3495.    except:
  3496.        pass
  3497.  
  3498.    if system == "Windows":
  3499.         for base in [os.environ.get('ProgramFiles'), os.environ.get('ProgramFiles(x86)'), "C:\\Program Files\\Java", "C:\\Program Files (x86)\\Java"]:
  3500.             if base and os.path.isdir(base):
  3501.                 java_root = os.path.join(base, "Java")
  3502.                 if os.path.isdir(java_root):
  3503.                     for item in os.listdir(java_root):
  3504.                         java_path = os.path.join(java_root, item, "bin", java_exec_name)
  3505.                         if os.path.exists(java_path):
  3506.                             version = get_java_version(java_path)
  3507.                             if version:
  3508.                                 possible_paths.append((java_path, version, "Program Files"))
  3509.  
  3510.    # Ensure paths are unique (based on path itself) and prefer certain sources
  3511.    unique_paths = {}
  3512.    source_order = {"Cache": 0, "Pobrana": 1, "JAVA_HOME": 2, "PATH": 3, "Program Files": 4}
  3513.    for path, ver, source in possible_paths:
  3514.        if path not in unique_paths or source_order.get(source, 99) < source_order.get(unique_paths[path][2], 99):
  3515.             unique_paths[path] = (path, ver, source)
  3516.  
  3517.    sorted_unique_paths = sorted(unique_paths.values(), key=lambda item: (source_order.get(item[2], 99), item[0]))
  3518.  
  3519.    return sorted_unique_paths
  3520.  
  3521.  
  3522. # --- Main Application Setup ---
  3523. if __name__ == "__main__":
  3524.    print("Inicjalizacja Launchera...")
  3525.  
  3526.    # Ensure base directories exist early
  3527.    os.makedirs(BASE_DIR, exist_ok=True)
  3528.    os.makedirs(ASSETS_DIR, exist_ok=True)
  3529.    os.makedirs(LIBRARIES_DIR, exist_ok=True)
  3530.    os.makedirs(NATIVES_DIR, exist_ok=True)
  3531.    os.makedirs(LOGS_DIR, exist_ok=True)
  3532.    os.makedirs(JAVA_DIR, exist_ok=True)
  3533.    os.makedirs(ICONS_DIR, exist_ok=True) # For potential custom icons
  3534.  
  3535.    # Generate a dummy logo if icons dir is empty, so Tkinter doesn't crash trying to load non-existent file
  3536.     dummy_logo_path = os.path.join(ICONS_DIR, "logo.png")
  3537.     if not os.path.exists(dummy_logo_path):
  3538.         try:
  3539.             dummy_img = Image.new('RGB', (180, 60), color = (40,40,40))
  3540.             dummy_img.save(dummy_logo_path)
  3541.             print("Utworzono domyślne logo.")
  3542.         except Exception as e:
  3543.              print(f"Nie udało się utworzyć domyślnego logo: {e}")
  3544.  
  3545.  
  3546.     root = tk.Tk()
  3547.     root.title("Minecraft Launcher by Paffcio")
  3548.     root.geometry("1200x800") # Wider window
  3549.     root.configure(bg=PRIMARY_BG)
  3550.     root.minsize(1000, 700) # Minimum size
  3551.  
  3552.     # Load configuration immediately after Tkinter root is created
  3553.     load_config()
  3554.  
  3555.     # --- Styling ---
  3556.     style = ttk.Style()
  3557.     style.theme_use('clam') # 'clam', 'alt', 'default', 'classic'
  3558.     # Configure main styles
  3559.     style.configure("TFrame", background=PRIMARY_BG)
  3560.     style.configure("TLabel", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10))
  3561.     style.configure("TButton", background=BUTTON_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10), borderwidth=0)
  3562.     style.map("TButton",
  3563.               background=[('active', BUTTON_HOVER), ('pressed', BUTTON_HOVER)],
  3564.               foreground=[('active', PRIMARY_FG), ('pressed', PRIMARY_FG)])
  3565.     style.configure("TEntry", fieldbackground="#333333", foreground=PRIMARY_FG, insertcolor=PRIMARY_FG, borderwidth=1)
  3566.     style.configure("TCombobox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, borderwidth=1,
  3567.                     arrowcolor=PRIMARY_FG, selectforeground=PRIMARY_FG, insertcolor=PRIMARY_FG) # Added colors for combobox elements
  3568.     # Style for Combobox dropdown list (requires accessing TCombobox.Listbox)
  3569.     # style.configure("TCombobox.Listbox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, selectforeground=PRIMARY_FG) # Does not work directly
  3570.  
  3571.     style.configure("Horizontal.TProgressbar", troughcolor="#333333", background=ACCENT_COLOR, thickness=10)
  3572.     # Treeview styling
  3573.     style.configure("Treeview", background=SECONDARY_BG, fieldbackground=SECONDARY_BG, foreground=PRIMARY_FG, rowheight=25, borderwidth=0)
  3574.     style.map("Treeview", background=[('selected', ACCENT_COLOR)])
  3575.     style.configure("Treeview.Heading", background=TERTIARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10, "bold"))
  3576.     style.layout("Treeview", [('Treeview.treearea', {'sticky': 'nswe'})]) # Remove borders around treearea
  3577.  
  3578.  
  3579.     # --- Layout ---
  3580.     # Progress bar and status label at the very bottom
  3581.     progress_frame = ttk.Frame(root)
  3582.     progress_frame.pack(side="bottom", fill="x", padx=10, pady=(0, 5))
  3583.     global_progress_bar = ttk.Progressbar(progress_frame, length=400, mode="determinate", style="Horizontal.TProgressbar")
  3584.     global_progress_bar.pack(side="left", fill="x", expand=True, padx=(0, 10))
  3585.     global_status_label = ttk.Label(progress_frame, text="Gotowe do działania!", foreground=PRIMARY_FG, background=PRIMARY_BG, font=("Segoe UI", 9))
  3586.     global_status_label.pack(side="left", padx=(10, 0))
  3587.  
  3588.  
  3589.     # Sidebar on the left
  3590.     sidebar = tk.Frame(root, bg=SIDEBAR_BG, width=250) # Wider sidebar
  3591.     sidebar.pack(side="left", fill="y", padx=0, pady=0)
  3592.     sidebar.pack_propagate(False) # Prevent sidebar from resizing to fit content
  3593.  
  3594.     # Logo area
  3595.     logo_frame = tk.Frame(sidebar, bg=SIDEBAR_BG)
  3596.     logo_frame.pack(fill="x", pady=(10, 20))
  3597.     try:
  3598.         # Load and display logo
  3599.         logo_img = Image.open(os.path.join(ICONS_DIR, "logo.png"))
  3600.         # Ensure image is RGB to save space and avoid transparency issues with some loaders
  3601.         if logo_img.mode == 'RGBA':
  3602.              logo_img = logo_img.convert('RGB')
  3603.         logo_img = logo_img.resize((min(logo_img.width, 200), min(logo_img.height, 80)), Image.LANCZOS) # Resize constraints
  3604.         logo_photo = ImageTk.PhotoImage(logo_img)
  3605.         logo_label = tk.Label(logo_frame, image=logo_photo, bg=SIDEBAR_BG)
  3606.         logo_label.image = logo_photo # Keep a reference!
  3607.         logo_label.pack(pady=5)
  3608.     except Exception as e:
  3609.         print(f"Błąd ładowania lub wyświetlania logo: {e}")
  3610.         # Fallback if logo fails
  3611.         ttk.Label(logo_frame, text="Minecraft Launcher", bg=SIDEBAR_BG, fg=PRIMARY_FG, font=("Segoe UI", 12, "bold")).pack(pady=20)
  3612.  
  3613.  
  3614.     # Navigation buttons (Tabs)
  3615.     tabs = [
  3616.         ("Instancje", "📋"),
  3617.         ("Pobieranie", "⬇"),
  3618.         ("Modrinth", "📦"), # New tab for Modrinth
  3619.         ("Narzędzia", "🔧"), # New tab for tools (Import/Export etc.)
  3620.         ("Ustawienia", "⚙"),
  3621.         ("Konsola", "📜")
  3622.     ]
  3623.     tab_buttons = {}
  3624.     for tab_name, icon in tabs:
  3625.         btn = tk.Button(
  3626.             sidebar, text=f"  {icon} {tab_name}", bg=SIDEBAR_BG, fg=PRIMARY_FG,
  3627.             activebackground=ACTIVE_TAB_COLOR, activeforeground=PRIMARY_FG, # Active state when clicked
  3628.             font=("Segoe UI", 11), relief="flat", anchor="w", padx=15, pady=12, # Increased padding
  3629.             command=lambda t=tab_name: switch_tab(t)
  3630.         )
  3631.         btn.pack(fill="x", pady=1) # Added small vertical padding
  3632.  
  3633.         # Initial hover binding (will be unbound for the active tab)
  3634.         btn.bind("<Enter>", lambda e, b=btn: b.config(bg=HOVER_TAB_COLOR) if current_tab.get() != tab_name else None)
  3635.         btn.bind("<Leave>", lambda e, b=btn: b.config(bg=SIDEBAR_BG) if current_tab.get() != tab_name else None)
  3636.         tab_buttons[tab_name] = btn
  3637.  
  3638.  
  3639.     # Content area on the right
  3640.     content_frame = ttk.Frame(root, style="TFrame")
  3641.     content_frame.pack(side="left", fill="both", expand=True, padx=10, pady=10)
  3642.  
  3643.     # --- Initial View ---
  3644.     switch_tab("Instancje") # Start on the instances tab
  3645.  
  3646.     # Ensure console is initialized if the starting tab is not Console
  3647.     # log_to_console function now handles console=None gracefully,
  3648.     # but for the console tab itself, it needs to be created.
  3649.     # The switch_tab function now handles creating the console widget
  3650.     # when the Console tab is selected.
  3651.  
  3652.     log_to_console(console, "Launcher uruchomiony.", "INFO")
  3653.  
  3654.     root.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement