Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import subprocess
- import sys
- import importlib.metadata
- import tkinter as tk
- from tkinter import ttk, messagebox, scrolledtext, filedialog, simpledialog
- import requests
- import os
- import json
- import threading
- import re
- import hashlib
- import pyperclip
- import zipfile
- import tarfile # Added for .tar.gz archives (Modrinth mods)
- from datetime import datetime
- import platform
- import webbrowser
- from packaging import version as pkg_version
- import humanize
- from colorama import init, Fore, Style
- from PIL import Image, ImageTk
- import time # For tooltips
- # Inicjalizacja colorama
- init(autoreset=True)
- # --- Utility: Tooltip ---
- class Tooltip:
- def __init__(self, widget, text):
- self.widget = widget
- self.text = text
- self.tooltip_window = None
- self.widget.bind("<Enter>", self.show_tooltip)
- self.widget.bind("<Leave>", self.hide_tooltip)
- self.id = None
- def show_tooltip(self, event=None):
- self.id = self.widget.after(500, self._show) # Delay tooltip appearance
- def hide_tooltip(self, event=None):
- if self.id:
- self.widget.after_cancel(self.id)
- self._hide()
- def _show(self):
- if self.tooltip_window:
- return
- x, y, cx, cy = self.widget.bbox("insert")
- x += self.widget.winfo_rootx() + 25
- y += self.widget.winfo_rooty() + 20
- self.tooltip_window = tk.Toplevel(self.widget)
- self.tooltip_window.wm_overrideredirect(True) # Hide window borders
- self.tooltip_window.wm_geometry(f"+{x}+{y}")
- label = tk.Label(self.tooltip_window, text=self.text, background="#ffffc0", relief="solid", borderwidth=1, font=("tahoma", "8", "normal"))
- label.pack()
- def _hide(self):
- if self.tooltip_window:
- self.tooltip_window.destroy()
- self.tooltip_window = None
- # --- End Tooltip ---
- # Instalacja zależności (przeniesione na początek skryptu, ale po importach)
- def install_requirements():
- required_libraries = ['requests', 'pyperclip', 'packaging', 'humanize', 'colorama', 'pillow']
- print(f"{Fore.CYAN}Sprawdzanie zależności...{Style.RESET_ALL}")
- for library in required_libraries:
- try:
- importlib.metadata.version(library)
- print(f"{Fore.GREEN}[OK] {library} już zainstalowane.")
- except importlib.metadata.PackageNotFoundError:
- try:
- print(f"{Fore.YELLOW}[INFO] Instaluję {library}...")
- subprocess.check_call([sys.executable, "-m", "pip", "install", library], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- print(f"{Fore.CYAN}[INFO] {library} zainstalowane.")
- except subprocess.CalledProcessError as e:
- print(f"{Fore.RED}[ERROR] Nie udało się zainstalować {library}. Błąd: {e.stderr.decode().strip()}")
- except Exception as e:
- print(f"{Fore.RED}[ERROR] Nieznany błąd podczas instalacji {library}: {e}")
- print(f"{Fore.CYAN}Zakończono sprawdzanie zależności.{Style.RESET_ALL}")
- install_requirements()
- # Ścieżki
- BASE_DIR = os.path.join(os.getcwd(), "minecraft_launcher_data") # Changed base dir to avoid conflicts
- CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
- ASSETS_DIR = os.path.join(BASE_DIR, "assets")
- LIBRARIES_DIR = os.path.join(BASE_DIR, "libraries")
- NATIVES_DIR = os.path.join(BASE_DIR, "natives")
- LOGS_DIR = os.path.join(BASE_DIR, "logs")
- JAVA_DIR = os.path.join(BASE_DIR, "java")
- ICONS_DIR = os.path.join(BASE_DIR, "icons")
- # Ensure icons directory exists if we plan to use custom icons
- os.makedirs(ICONS_DIR, exist_ok=True)
- # Kolory i styl (Ulepszone nazewnictwo)
- PRIMARY_BG = "#1a1a1a"
- SECONDARY_BG = "#2a2a2a"
- TERTIARY_BG = "#3a3a3a"
- PRIMARY_FG = "#ffffff"
- ACCENT_COLOR = "#2a9fd6" # Blue
- SUCCESS_COLOR = "#5cb85c" # Green
- ERROR_COLOR = "#d9534f" # Red
- WARNING_COLOR = "#f0ad4e" # Yellow
- INFO_COLOR = "#337ab7" # Light Blue
- CONSOLE_BG = "#0d0d0d"
- CONSOLE_FG_DEFAULT = "#cccccc"
- CONSOLE_FG_INFO = INFO_COLOR
- CONSOLE_FG_SUCCESS = SUCCESS_COLOR
- CONSOLE_FG_WARNING = WARNING_COLOR
- CONSOLE_FG_ERROR = ERROR_COLOR
- BUTTON_BG = TERTIARY_BG
- BUTTON_HOVER = SECONDARY_BG
- SIDEBAR_BG = "#222222"
- ACTIVE_TAB_COLOR = ACCENT_COLOR # Active tab is the accent color
- HOVER_TAB_COLOR = "#444444" # Slightly lighter hover
- # Globalne zmienne
- # Initialize variables before potentially loading config
- pending_instance_settings = {}
- pending_version = ""
- download_thread = None
- download_active = False
- global_progress_bar = None
- global_status_label = None
- instances = {}
- java_versions_cache = {}
- console = None
- username_var = tk.StringVar(value="Player")
- memory_var = tk.StringVar(value="2")
- shared_assets_var = tk.BooleanVar(value=True)
- shared_libraries_var = tk.BooleanVar(value=True)
- shared_natives_var = tk.BooleanVar(value=True)
- snapshots_var = tk.BooleanVar(value=True)
- releases_var = tk.BooleanVar(value=True)
- alpha_var = tk.BooleanVar(value=False)
- beta_var = tk.BooleanVar(value=False)
- current_tab = tk.StringVar(value="Instancje")
- selected_modrinth_instance_var = tk.StringVar()
- # Funkcje narzędziowe
- def log_to_console(console_widget, message, level="INFO"):
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- log_message = f"[{timestamp}] {level}: {message}\n"
- # Log to file always
- os.makedirs(LOGS_DIR, exist_ok=True)
- try:
- with open(os.path.join(LOGS_DIR, "launcher.log"), "a", encoding="utf-8") as f:
- f.write(log_message)
- except Exception as e:
- print(f"Failed to write to launcher.log: {e}") # Fallback print
- # Log to console widget if available
- if console_widget:
- try:
- console_widget.config(state="normal")
- tag = level.lower()
- if tag not in console_widget.tag_names():
- color = CONSOLE_FG_DEFAULT
- if level == "ERROR": color = CONSOLE_FG_ERROR
- elif level == "WARNING": color = CONSOLE_FG_WARNING
- elif level == "SUCCESS": color = CONSOLE_FG_SUCCESS
- elif level == "INFO": color = CONSOLE_FG_INFO
- console_widget.tag_config(tag, foreground=color)
- console_widget.insert(tk.END, log_message, tag)
- console_widget.see(tk.END)
- console_widget.config(state="disabled")
- except Exception as e:
- # This shouldn't happen if console_widget is valid, but as a fallback
- print(f"Failed to write to console widget: {e}")
- def verify_sha1(file_path, expected_sha1):
- if not os.path.exists(file_path):
- return False
- sha1 = hashlib.sha1()
- try:
- with open(file_path, "rb") as f:
- # Read in chunks to handle large files
- while chunk := f.read(4096):
- sha1.update(chunk)
- return sha1.hexdigest() == expected_sha1
- except Exception as e:
- log_to_console(console, f"Error verifying SHA1 for {file_path}: {e}", "ERROR")
- return False
- def save_config():
- config = {
- "default_settings": {
- "username": username_var.get(),
- "memory": memory_var.get(),
- "shared_assets": shared_assets_var.get(),
- "shared_libraries": shared_libraries_var.get(),
- "shared_natives": shared_natives_var.get()
- },
- "version_filters": {
- "snapshots": snapshots_var.get(),
- "releases": releases_var.get(),
- "alpha": alpha_var.get(),
- "beta": beta_var.get()
- },
- "instances": instances,
- "java_versions": java_versions_cache # Cache found/downloaded java paths
- }
- os.makedirs(BASE_DIR, exist_ok=True)
- try:
- with open(CONFIG_FILE, "w", encoding="utf-8") as f:
- json.dump(config, f, indent=4)
- log_to_console(console, "Konfiguracja zapisana.", "INFO")
- except Exception as e:
- log_to_console(console, f"Błąd podczas zapisywania konfiguracji: {e}", "ERROR")
- def load_config():
- global instances, java_versions_cache
- # Variables are initialized globally before this function is called.
- if os.path.exists(CONFIG_FILE):
- try:
- with open(CONFIG_FILE, "r", encoding="utf-8") as f:
- config = json.load(f)
- if "default_settings" in config:
- default_settings = config["default_settings"]
- username_var.set(default_settings.get("username", "Player"))
- memory_var.set(default_settings.get("memory", "2"))
- shared_assets_var.set(default_settings.get("shared_assets", True))
- shared_libraries_var.set(default_settings.get("shared_libraries", True))
- shared_natives_var.set(default_settings.get("shared_natives", True))
- if "version_filters" in config:
- version_filters = config["version_filters"]
- snapshots_var.set(version_filters.get("snapshots", True))
- releases_var.set(version_filters.get("releases", True))
- alpha_var.set(version_filters.get("alpha", False))
- beta_var.set(version_filters.get("beta", False))
- # Load instances, ensuring required keys exist with defaults
- instances = config.get("instances", {})
- for version, data in instances.items():
- data.setdefault("settings", {})
- data["settings"].setdefault("username", username_var.get())
- data["settings"].setdefault("memory", memory_var.get())
- data["settings"].setdefault("shared_assets", shared_assets_var.get())
- data["settings"].setdefault("shared_libraries", shared_libraries_var.get())
- data["settings"].setdefault("shared_natives", shared_natives_var.get())
- data["settings"].setdefault("loader_type", "vanilla") # Default to vanilla
- data["settings"].setdefault("loader_version", "")
- data["settings"].setdefault("server_ip", "")
- data["settings"].setdefault("server_port", "")
- data.setdefault("java_path", "")
- data.setdefault("java_version", "")
- data.setdefault("required_java", "1.8")
- data.setdefault("ready", False) # Assume not ready until verified or downloaded
- data.setdefault("timestamp", datetime.now().isoformat()) # Add timestamp if missing
- java_versions_cache = config.get("java_versions", {})
- log_to_console(console, "Konfiguracja wczytana.", "INFO")
- return instances, java_versions_cache
- except Exception as e:
- log_to_console(console, f"Błąd wczytywania konfiguracji. Resetowanie do domyślnych: {e}", "ERROR")
- # Reset to default values in case of error
- instances = {}
- java_versions_cache = {}
- # Variables like username_var etc. are already set to defaults
- return instances, java_versions_cache
- else:
- log_to_console(console, "Plik konfiguracji nie istnieje. Użyto domyślnych ustawień.", "INFO")
- # Default values if file doesn't exist
- instances = {}
- java_versions_cache = {}
- return instances, java_versions_cache
- def get_versions():
- try:
- url = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
- resp = requests.get(url, timeout=10)
- resp.raise_for_status()
- manifest = resp.json()
- versions = []
- allowed_types = []
- if snapshots_var.get():
- allowed_types.append("snapshot")
- if releases_var.get():
- allowed_types.append("release")
- if alpha_var.get():
- allowed_types.append("old_alpha")
- if beta_var.get():
- allowed_types.append("old_beta")
- for v in manifest["versions"]:
- if v["type"] in allowed_types:
- versions.append(v["id"])
- log_to_console(console, f"Pobrano {len(versions)} wersji (filtry: {', '.join(allowed_types)})", "INFO")
- # Sort by parsing version string, releases first, then snapshots, then old
- def version_sort_key(v_id):
- v_info = next((item for item in manifest['versions'] if item['id'] == v_id), None)
- if v_info:
- v_type = v_info['type']
- # Assign a numeric value to types for sorting: release > snapshot > beta > alpha
- type_order = {'release': 4, 'snapshot': 3, 'old_beta': 2, 'old_alpha': 1}
- type_priority = type_order.get(v_type, 0)
- try:
- # Use packaging.version for robust version comparison
- parsed_version = pkg_version.parse(v_id.split('-')[0]) # Parse core version part
- 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
- except:
- # Handle invalid versions by putting them last
- return (-type_priority, -float('inf'), v_id)
- return (0, v_id) # Default for unknown versions
- return sorted(versions, key=version_sort_key, reverse=True) # Reverse again to get newest first within categories
- except Exception as e:
- log_to_console(console, f"Nie udało się pobrać listy wersji: {e}", "ERROR")
- return []
- def get_version_info(version):
- try:
- url = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
- resp = requests.get(url, timeout=10)
- resp.raise_for_status()
- manifest = resp.json()
- for v in manifest["versions"]:
- if v["id"] == version:
- version_url = v["url"]
- resp_info = requests.get(version_url, timeout=10)
- resp_info.raise_for_status()
- return resp_info.json()
- log_to_console(console, f"Nie znaleziono wersji {version} w manifeście.", "WARNING")
- return None
- except Exception as e:
- log_to_console(console, f"Nie udało się pobrać info o wersji {version}: {e}", "ERROR")
- return None
- def download_java(java_version, console_widget):
- try:
- system = platform.system().lower()
- arch = "x64"
- base_url = "https://api.adoptium.net/v3/binary/latest/"
- # Map Minecraft required version strings to Adoptium feature versions
- java_map = {
- "1.8": "8",
- "9": "9",
- "10": "10",
- "11": "11",
- "12": "12",
- "13": "13",
- "14": "14",
- "15": "15",
- "16": "16",
- "17": "17",
- "18": "18",
- "19": "19",
- "20": "20",
- "21": "21",
- "22": "22"
- }
- feature_version = java_map.get(str(java_version), str(java_version)) # Use string keys for map lookup
- # Adoptium URL structure: /feature_version/release_type/os/arch/image_type/jvm_impl/heap_size/vendor
- # release_type: ga (General Availability)
- # os: windows, mac, linux, etc.
- # arch: x64, arm, etc.
- # image_type: jdk, jre
- # jvm_impl: hotspot (OpenJDK's default)
- # heap_size: normal (default)
- # vendor: eclipse (Temurin)
- url = f"{base_url}{feature_version}/ga/{system}/{arch}/jdk/hotspot/normal/eclipse"
- log_to_console(console_widget, f"Szukam Javy {feature_version} (Adoptium) dla {system}/{arch}...", "INFO")
- resp_metadata = requests.get(url, timeout=10)
- resp_metadata.raise_for_status()
- metadata = resp_metadata.json()
- if not metadata:
- log_to_console(console_widget, f"Nie znaleziono dostępnych pakietów Javy {feature_version} dla {system}/{arch} na Adoptium.", "ERROR")
- return None, None
- # Find a suitable package (e.g., with a .zip or .tar.gz link)
- download_link = None
- for package in metadata:
- if package['binary']['os'] == system and package['binary']['architecture'] == arch and package['binary']['image_type'] == 'jdk':
- download_link = package['binary']['package']['link']
- expected_size = package['binary']['package'].get('size', 0)
- expected_sha256 = package['binary']['package'].get('checksum', None) # Adoptium uses SHA256
- break # Take the first suitable one
- if not download_link:
- log_to_console(console_widget, f"Nie znaleziono linku do pobrania pakietu Javy {feature_version} dla {system}/{arch}.", "ERROR")
- return None, None
- file_extension = ".zip" if system == "windows" else ".tar.gz"
- java_target_dir = os.path.join(JAVA_DIR, f"jdk-{feature_version}-{system}-{arch}")
- os.makedirs(java_target_dir, exist_ok=True)
- archive_path = os.path.join(java_target_dir, f"jdk-{feature_version}{file_extension}")
- log_to_console(console_widget, f"Pobieranie Javy {feature_version} z {download_link}", "INFO")
- resp_file = requests.get(download_link, stream=True, timeout=30) # Increased timeout
- resp_file.raise_for_status()
- total_size = int(resp_file.headers.get('content-length', 0)) or expected_size
- downloaded_size = 0
- with open(archive_path, "wb") as f:
- for chunk in resp_file.iter_content(chunk_size=8192):
- if chunk:
- f.write(chunk)
- downloaded_size += len(chunk)
- if total_size > 0 and global_progress_bar:
- update_progress(global_progress_bar, global_status_label,
- (downloaded_size/total_size)*100, total_size-downloaded_size, f"Java {feature_version}", downloaded_size, total_size) # Pass bytes directly
- # Verification (SHA256 for Adoptium) - Need sha256 function if not using sha1
- # For simplicity here, we'll skip sha256 verification for now or rely on sha1 if available (less common for modern downloads)
- # if expected_sha256 and not verify_sha256(archive_path, expected_sha256):
- # log_to_console(console_widget, f"Błąd SHA256 dla archiwum Javy!", "ERROR")
- # os.remove(archive_path) # Clean up
- # return None, None
- # Note: verify_sha1 exists, but Adoptium uses sha256. Implementing sha256 needed for proper verification.
- log_to_console(console_widget, f"Rozpakowywanie Javy do {java_target_dir}", "INFO")
- extracted_folder = None
- try:
- if system == "windows":
- with zipfile.ZipFile(archive_path, 'r') as zip_ref:
- zip_ref.extractall(java_target_dir)
- # Find the actual JDK folder inside the zip (usually one level deep)
- 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)
- if extracted_folder:
- extracted_folder = os.path.dirname(extracted_folder) # Go up one level to the JDK root
- else: # macOS/Linux
- with tarfile.open(archive_path, 'r:gz') as tar_ref:
- tar_ref.extractall(java_target_dir)
- # Find the actual JDK folder inside the tar.gz
- 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)
- if extracted_folder:
- extracted_folder = os.path.dirname(extracted_folder) # Go up one level to the JDK root
- os.remove(archive_path) # Clean up archive
- log_to_console(console_widget, "Archiwum Javy rozpakowane.", "INFO")
- except Exception as e:
- log_to_console(console_widget, f"Błąd rozpakowywania archiwum Javy: {e}", "ERROR")
- if os.path.exists(archive_path):
- os.remove(archive_path)
- return None, None
- # Find java executable within extracted folder
- if extracted_folder:
- java_exec_name = "java.exe" if system == "windows" else "java"
- # Search for java executable recursively in case the inner structure is complex
- for root, _, files in os.walk(extracted_folder):
- if java_exec_name in files:
- java_path = os.path.join(root, java_exec_name)
- version = get_java_version(java_path) # Use the correct console widget
- if version:
- log_to_console(console_widget, f"Pobrano i zainstalowano Javę: {java_path} (wersja: {version})", "SUCCESS")
- # Add to cache
- java_versions_cache[version] = java_path
- save_config()
- return java_path, version
- log_to_console(console_widget, "Nie znaleziono java(.exe) w pobranym i rozpakowanym folderze!", "ERROR")
- return None, None
- else:
- log_to_console(console_widget, "Nie udało się znaleźć ścieżki JDK po rozpakowaniu.", "ERROR")
- return None, None
- except requests.exceptions.RequestException as e:
- log_to_console(console_widget, f"Błąd sieci podczas pobierania Javy {java_version}: {e}", "ERROR")
- return None, None
- except Exception as e:
- log_to_console(console_widget, f"Ogólny błąd pobierania/instalacji Javy {java_version}: {e}", "ERROR")
- return None, None
- def find_java(required_version=None):
- possible_paths = []
- system = platform.system()
- java_exec_name = "java.exe" if system == "Windows" else "java"
- log_to_console(console, f"Szukam Javy {required_version or 'dowolnej'} ({java_exec_name})...", "INFO")
- # Check cache first
- for ver, path in java_versions_cache.items():
- if os.path.exists(path) and get_java_version(path): # Verify path still exists and is valid java
- if (not required_version or check_java_version(ver, required_version)):
- possible_paths.append((path, ver, "Cache"))
- log_to_console(console, f"Znaleziono Javę w cache: {path} (wersja: {ver})", "INFO")
- # Check custom JAVA_DIR installations
- if os.path.exists(JAVA_DIR):
- for java_folder in os.listdir(JAVA_DIR):
- java_path = os.path.join(JAVA_DIR, java_folder) # Point to the root of the JDK folder
- # Search for java executable inside this folder structure
- found_exec = None
- for root, _, files in os.walk(java_path):
- if java_exec_name in files:
- found_exec = os.path.join(root, java_exec_name)
- break # Found the executable
- if found_exec:
- version = get_java_version(found_exec)
- if version and (not required_version or check_java_version(version, required_version)):
- if (found_exec, version, "Pobrana") not in possible_paths: # Avoid duplicates
- possible_paths.append((found_exec, version, "Pobrana"))
- log_to_console(console, f"Znaleziono Javę w {JAVA_DIR}: {found_exec} (wersja: {version})", "INFO")
- # Check standard system locations
- java_home = os.environ.get("JAVA_HOME")
- if java_home:
- java_path = os.path.join(java_home, "bin", java_exec_name)
- if os.path.exists(java_path):
- version = get_java_version(java_path)
- if version and (not required_version or check_java_version(version, required_version)):
- if (java_path, version, "JAVA_HOME") not in possible_paths:
- possible_paths.append((java_path, version, "JAVA_HOME"))
- log_to_console(console, f"Znaleziono Javę w JAVA_HOME: {java_path} (wersja: {version})", "INFO")
- # Check PATH
- try:
- # Use 'where' on Windows, 'which' on Unix-like
- command = ["where", java_exec_name] if system == "Windows" else ["which", java_exec_name]
- out = subprocess.check_output(command, stderr=subprocess.DEVNULL).decode().strip()
- for line in out.splitlines():
- line = line.strip()
- if os.path.exists(line):
- version = get_java_version(line)
- if version and (not required_version or check_java_version(version, required_version)):
- if (line, version, "PATH") not in possible_paths:
- possible_paths.append((line, version, "PATH"))
- log_to_console(console, f"Znaleziono Javę w PATH: {line} (wersja: {version})", "INFO")
- except:
- pass # 'where' or 'which' command not found or java not in PATH
- # Specific Windows paths (less reliable, but can catch some installs)
- if system == "Windows":
- for base in [os.environ.get('ProgramFiles'), os.environ.get('ProgramFiles(x86)'), "C:\\Program Files\\Java", "C:\\Program Files (x86)\\Java"]:
- if base and os.path.isdir(base):
- java_root = os.path.join(base, "Java")
- if os.path.isdir(java_root):
- for item in os.listdir(java_root):
- java_path = os.path.join(java_root, item, "bin", java_exec_name)
- if os.path.exists(java_path):
- version = get_java_version(java_path)
- if version and (not required_version or check_java_version(version, required_version)):
- if (java_path, version, "Program Files") not in possible_paths:
- possible_paths.append((java_path, version, "Program Files"))
- log_to_console(console, f"Znaleziono Javę w {java_root}: {java_path} (wersja: {version})", "INFO")
- # Ensure paths are unique (based on path itself) and prefer cached/downloaded ones
- unique_paths = {}
- for path, ver, source in possible_paths:
- if path not in unique_paths:
- unique_paths[path] = (path, ver, source)
- else:
- # Prefer 'Pobrana' or 'Cache' over others if duplicates exist
- existing_source = unique_paths[path][2]
- if source in ["Cache", "Pobrana"] and existing_source not in ["Cache", "Pobrana"]:
- unique_paths[path] = (path, ver, source)
- # Convert back to list, maintaining preference order if possible
- # Simple sort order preference: Cache > Pobrana > JAVA_HOME > PATH > Others
- source_order = {"Cache": 0, "Pobrana": 1, "JAVA_HOME": 2, "PATH": 3}
- sorted_unique_paths = sorted(unique_paths.values(), key=lambda item: (source_order.get(item[2], 99), item[0]))
- if not sorted_unique_paths:
- log_to_console(console, f"Nie znaleziono Javy {required_version if required_version else ''} w 64-bitowej wersji!", "WARNING")
- else:
- log_to_console(console, f"Zakończono wyszukiwanie Javy. Znaleziono {len(sorted_unique_paths)} pasujących ścieżek.", "INFO")
- # Return list of (path, version) tuples
- return [(p, v) for p, v, _ in sorted_unique_paths]
- def get_java_version(java_path):
- if not os.path.exists(java_path):
- return None
- try:
- # Use 'java -version' which outputs to stderr
- result = subprocess.run([java_path, "-version"], capture_output=True, text=True, check=True, encoding='utf-8', errors='ignore')
- version_line = result.stderr.split('\n')[0].strip()
- version_match = re.search(r'version "([^"]+)"', version_line)
- if version_match:
- version = version_match.group(1)
- # Check if it's a 64-Bit JVM
- is_64bit = "64-Bit" in result.stderr or "64-bit" in result.stderr or "x86_64" in result.stderr.lower()
- # Use only the main version part for key comparisons (e.g., 1.8, 16, 17, 21)
- # Handle different version formats (e.g., "1.8.0_301", "17.0.1", "9")
- major_version_match = re.match(r'(\d+\.\d+|\d+)', version)
- if major_version_match:
- simple_version = major_version_match.group(1)
- # Special case: 1.8 is commonly referred to as 8
- if simple_version.startswith("1.8"):
- simple_version = "1.8"
- elif "." in simple_version: # For >= 9, just the first number is often enough
- simple_version = simple_version.split('.')[0]
- # Store the full version but key it by the simple version in cache if needed
- if is_64bit:
- java_versions_cache[version] = java_path # Cache full version string to path
- return version # Return full version for display
- else:
- log_to_console(console, f"Java found at {java_path} is not 64-bit. Skipping.", "WARNING")
- return None
- else:
- log_to_console(console, f"Could not parse version from '{version_line}' for Java at {java_path}.", "WARNING")
- return None
- else:
- log_to_console(console, f"Could not find version string in output for Java at {java_path}. Output: {result.stderr.strip()}", "WARNING")
- return None
- except FileNotFoundError:
- log_to_console(console, f"Java executable not found at {java_path}", "ERROR")
- return None
- except subprocess.CalledProcessError as e:
- log_to_console(console, f"Error running java -version for {java_path}: {e.stderr.strip()}", "ERROR")
- return None
- except Exception as e:
- log_to_console(console, f"Unexpected error checking version for {java_path}: {e}", "ERROR")
- return None
- def check_java_version(installed_version_str, required_version_str):
- """
- Checks if the installed Java version meets the required minimum version.
- Handles versions like "1.8", "9", "16", "17", "21".
- """
- try:
- # Normalize required version for comparison (e.g., "1.8" -> "8")
- required_normalized = required_version_str
- if required_version_str == "1.8":
- required_normalized = "8"
- elif "." in required_version_str:
- required_normalized = required_version_str.split('.')[0] # Use major version for comparison
- # Extract major version from installed string (e.g., "1.8.0_301" -> "1.8", "17.0.1" -> "17")
- installed_major_match = re.match(r'(\d+\.\d+|\d+)', installed_version_str)
- if not installed_major_match:
- log_to_console(console, f"Nie można sparsować wersji zainstalowanej Javy: {installed_version_str}", "WARNING")
- return False
- installed_simple = installed_major_match.group(1)
- # Special case: 1.8 comparison
- if required_version_str == "1.8":
- return installed_simple.startswith("1.8.") # Java 8 needs 1.8.x
- # For >= 9, compare as integers if possible
- try:
- installed_major_int = int(installed_simple.split('.')[0])
- required_major_int = int(required_normalized)
- return installed_major_int >= required_major_int
- except ValueError:
- # Fallback to string comparison if not simple integers
- log_to_console(console, f"Porównanie wersji Javy jako stringi: '{installed_version_str}' vs '{required_version_str}'", "INFO")
- return installed_version_str.startswith(required_version_str) # Simple prefix check
- except Exception as e:
- log_to_console(console, f"Błąd podczas porównania wersji Javy: zainstalowana='{installed_version_str}', wymagana='{required_version_str}' - {e}", "ERROR")
- return False
- def is_new_launcher(version):
- """Checks if the version uses the new launcher arguments (>= 1.6)."""
- try:
- ver = pkg_version.parse(version)
- # New launcher arguments were introduced around 1.6
- return ver >= pkg_version.parse("1.6")
- except pkg_version.InvalidVersion:
- # Assume newer arguments for unparseable versions
- return True
- def get_required_java(version, version_info):
- """Determines the required major Java version for a given MC version."""
- if version_info and "javaVersion" in version_info:
- major_version_info = version_info["javaVersion"].get("majorVersion")
- if major_version_info:
- return str(major_version_info)
- # Fallback to component version if majorVersion is missing but component is present
- component_version = version_info["javaVersion"].get("component")
- if component_version:
- major_match = re.match(r'jre([\d]+)', component_version)
- if major_match:
- return major_match.group(1)
- # Fallback based on known version ranges
- try:
- ver = pkg_version.parse(version)
- if ver >= pkg_version.parse("1.20.5"): # Placeholder, check actual version manifests
- return "21" # MC 1.20.5+ requires Java 21
- if ver >= pkg_version.parse("1.18"):
- return "17" # MC 1.18+ requires Java 17 (1.17 also uses 17 technically)
- if ver >= pkg_version.parse("1.17"):
- return "16" # MC 1.17 requires Java 16 (often works with 17 too)
- # MC versions 1.6 to 1.16 require Java 8
- if ver >= pkg_version.parse("1.6"):
- return "1.8"
- # Older versions might use Java 6 or 7, but Java 8 is often compatible
- log_to_console(console, f"Using default Java 8 for older/unknown version {version}", "INFO")
- return "1.8" # Default for old/unparsed versions
- except pkg_version.InvalidVersion:
- log_to_console(console, f"Invalid version string '{version}', using default Java 8.", "WARNING")
- return "1.8" # Default for invalid versions
- def download_file(url, path, progress_callback=None, expected_sha1=None, console_widget=None, description="plik"):
- os.makedirs(os.path.dirname(path), exist_ok=True)
- try:
- log_to_console(console_widget, f"Pobieranie {description}: {url} do {path}", "INFO")
- resp = requests.get(url, stream=True, timeout=30) # Increased timeout
- resp.raise_for_status()
- total_size = int(resp.headers.get('content-length', 0)) or 1 # Avoid division by zero if size is unknown
- downloaded_size = 0
- with open(path, "wb") as f:
- for chunk in resp.iter_content(chunk_size=8192):
- if chunk:
- f.write(chunk)
- downloaded_size += len(chunk)
- if progress_callback:
- progress_callback(downloaded_size, total_size)
- log_to_console(console_widget, f"Pobrano {description}: {os.path.basename(path)}", "INFO")
- if expected_sha1 and not verify_sha1(path, expected_sha1):
- log_to_console(console_widget, f"Błąd SHA1 dla {description} {path}", "ERROR")
- # Optionally delete the corrupted file
- # os.remove(path)
- return False
- return True
- except requests.exceptions.RequestException as e:
- log_to_console(console_widget, f"Błąd sieci podczas pobierania {description} {path}: {e}", "ERROR")
- return False
- except Exception as e:
- log_to_console(console_widget, f"Ogólny błąd podczas pobierania {description} {path}: {e}", "ERROR")
- return False
- def download_libraries(libs_info, version_dir, shared_libraries, console_widget, progress_callback):
- libraries_dir = LIBRARIES_DIR if shared_libraries else os.path.join(version_dir, "libraries")
- os.makedirs(libraries_dir, exist_ok=True)
- libs_to_download = []
- total_libs_size = 0
- for lib in libs_info:
- # Check for rules allowing/denying the library based on OS (simplistic check)
- is_allowed = True
- if "rules" in lib:
- is_allowed = False # Start assuming denied unless explicitly allowed
- for rule in lib["rules"]:
- action = rule.get("action", "allow")
- os_info = rule.get("os")
- if os_info:
- current_os = platform.system().lower()
- if os_info.get("name") == current_os:
- if action == "allow":
- is_allowed = True
- break # Found an allowing rule for this OS
- elif action == "deny":
- is_allowed = False # Found a denying rule, stop checking
- break
- else: # Rule applies to all OS if no 'os' is specified
- if action == "allow":
- is_allowed = True
- break
- elif action == "deny":
- is_allowed = False
- break
- if is_allowed and "downloads" in lib and "artifact" in lib["downloads"]:
- artifact = lib["downloads"]["artifact"]
- lib_path = artifact["path"].replace("/", os.sep)
- full_lib_path = os.path.join(libraries_dir, lib_path)
- if not os.path.exists(full_lib_path) or not verify_sha1(full_lib_path, artifact["sha1"]):
- libs_to_download.append((lib, artifact))
- total_libs_size += artifact.get("size", 0)
- log_to_console(console_widget, f"Pobieranie {len(libs_to_download)} bibliotek (łączny rozmiar: {humanize.naturalsize(total_libs_size)})", "INFO")
- downloaded_libs_size = 0
- total_libs = len(libs_to_download)
- for i, (lib, artifact) in enumerate(libs_to_download):
- lib_path_rel = artifact["path"].replace("/", os.sep) # Relative path within libraries dir
- full_lib_path = os.path.join(libraries_dir, lib_path_rel)
- lib_url = artifact["url"]
- lib_sha1 = artifact["sha1"]
- lib_size = artifact.get("size", 0)
- if not download_file(
- lib_url, full_lib_path,
- lambda d, t: progress_callback(downloaded_libs_size + d, total_libs_size, "biblioteki", i + (d / t) if t > 0 else i, total_libs),
- expected_sha1=lib_sha1, console_widget=console_widget, description=f"biblioteka {os.path.basename(lib_path_rel)}"
- ):
- # If a library download fails, it might be critical. Report error but continue trying others.
- log_to_console(console_widget, f"Nie udało się pobrać lub zweryfikować biblioteki {lib_path_rel}", "ERROR")
- # Decide whether to fail completely or just mark instance as not ready
- # For now, just log error and continue
- else:
- downloaded_libs_size += lib_size # Only add size if download was successful
- # Final progress update for this stage
- progress_callback(total_libs_size, total_libs_size, "biblioteki", total_libs, total_libs)
- log_to_console(console_widget, "Pobieranie bibliotek zakończone.", "INFO")
- return True # Assuming we don't stop on individual library failures
- def download_natives(libs_info, version_dir, shared_natives, console_widget, progress_callback):
- system = platform.system().lower()
- arch = platform.architecture()[0] # '32bit' or '64bit'
- natives_dir = NATIVES_DIR if shared_natives else os.path.join(version_dir, "natives")
- os.makedirs(natives_dir, exist_ok=True)
- natives_classifier = f"natives-{system}" # e.g., natives-windows
- # Some classifiers might include architecture, check manifest details if needed
- # e.g., natives-windows-64
- natives_to_download = []
- total_size = 0
- for lib in libs_info:
- # Apply rules same as libraries
- is_allowed = True
- if "rules" in lib:
- is_allowed = False
- for rule in lib["rules"]:
- action = rule.get("action", "allow")
- os_info = rule.get("os")
- if os_info:
- current_os = platform.system().lower()
- if os_info.get("name") == current_os:
- # Basic architecture check if present in rule os info
- rule_arch = os_info.get("arch")
- if rule_arch and rule_arch != arch.replace("bit", ""):
- continue # Rule doesn't match current architecture
- if action == "allow":
- is_allowed = True
- break
- elif action == "deny":
- is_allowed = False
- break
- else:
- if action == "allow":
- is_allowed = True
- break
- elif action == "deny":
- is_allowed = False
- break
- if is_allowed and "downloads" in lib and "classifiers" in lib["downloads"]:
- classifiers = lib["downloads"]["classifiers"]
- # Find the most specific classifier that matches the system and architecture
- matching_classifier = None
- # Prioritize architecture specific if available
- if f"{natives_classifier}-{arch.replace('bit','')}" in classifiers:
- matching_classifier = classifiers[f"{natives_classifier}-{arch.replace('bit','')}"]
- elif natives_classifier in classifiers:
- matching_classifier = classifiers[natives_classifier]
- if matching_classifier:
- natives_to_download.append(matching_classifier)
- total_size += matching_classifier.get("size", 0)
- log_to_console(console_widget, f"Pobieranie {len(natives_to_download)} natywnych bibliotek (łączny rozmiar: {humanize.naturalsize(total_size)})", "INFO")
- downloaded_size = 0
- total_natives = len(natives_to_download)
- temp_native_dir = os.path.join(version_dir, "temp_natives_extract") # Temp dir for extraction
- for i, artifact in enumerate(natives_to_download):
- native_url = artifact["url"]
- native_sha1 = artifact["sha1"]
- native_size = artifact.get("size", 0)
- # Download natives as temp files
- native_temp_path = os.path.join(version_dir, f"temp_native_{i}.jar") # Or .zip, depending on content-type/url
- if not download_file(
- native_url, native_temp_path,
- lambda d, t: progress_callback(downloaded_size + d, total_size, "natives", i + (d/t) if t > 0 else i, total_natives),
- expected_sha1=native_sha1, console_widget=console_widget, description=f"natywna biblioteka {os.path.basename(native_url)}"
- ):
- log_to_console(console_widget, f"Nie udało się pobrać lub zweryfikować natywnej biblioteki {native_url}", "ERROR")
- # Decide whether to fail completely or just mark instance as not ready
- # For now, just log error and continue
- continue # Skip extraction for this failed download
- # Extract contents of the native JAR/ZIP
- os.makedirs(temp_native_dir, exist_ok=True)
- try:
- with zipfile.ZipFile(native_temp_path, 'r') as zip_ref:
- # Extract only files, ignoring directories and META-INF (as per Mojang launcher)
- for file_info in zip_ref.infolist():
- if not file_info.is_dir() and not file_info.filename.startswith('META-INF/'):
- target_path = os.path.join(natives_dir, os.path.basename(file_info.filename)) # Extract directly to natives_dir
- # Avoid extracting duplicates if multiple natives archives have the same file
- if not os.path.exists(target_path):
- zip_ref.extract(file_info, path=natives_dir) # Extract directly
- else:
- log_to_console(console_widget, f"Plik {os.path.basename(file_info.filename)} już istnieje w {natives_dir}. Pomijam.", "INFO")
- os.remove(native_temp_path) # Clean up temp file
- downloaded_size += native_size # Only add size if download was successful and extraction attempted
- except Exception as e:
- log_to_console(console_widget, f"Błąd rozpakowywania natywnej biblioteki {os.path.basename(native_temp_path)}: {e}", "ERROR")
- if os.path.exists(native_temp_path):
- os.remove(native_temp_path)
- continue # Continue with next native
- # Clean up temp native directory if it was created and is empty
- if os.path.exists(temp_native_dir) and not os.listdir(temp_native_dir):
- os.rmdir(temp_native_dir)
- elif os.path.exists(temp_native_dir):
- log_to_console(console_widget, f"Nie udało się usunąć tymczasowego katalogu natywnego: {temp_native_dir}. Może zawierać pliki.", "WARNING")
- # Final progress update for this stage
- progress_callback(total_size, total_size, "natives", total_natives, total_natives)
- log_to_console(console_widget, "Pobieranie i rozpakowywanie natywnych bibliotek zakończone.", "INFO")
- # We return True even if some failed, the verify_instance function will check integrity
- return True
- def download_assets(version_info, version_dir, shared_assets, console_widget, progress_callback):
- try:
- asset_index = version_info.get("assetIndex", {})
- asset_url = asset_index.get("url")
- asset_id = asset_index.get("id")
- if not asset_url:
- log_to_console(console_widget, "Brak assetIndex dla tej wersji.", "WARNING")
- return True # Not critical, some old versions might not have assets this way
- resp = requests.get(asset_url, timeout=10)
- resp.raise_for_status()
- assets_data = resp.json()
- assets_base_dir = ASSETS_DIR if shared_assets else os.path.join(version_dir, "assets")
- objects_dir = os.path.join(assets_base_dir, "objects")
- indexes_dir = os.path.join(assets_base_dir, "indexes")
- os.makedirs(objects_dir, exist_ok=True)
- os.makedirs(indexes_dir, exist_ok=True)
- index_file = os.path.join(indexes_dir, f"{asset_id}.json")
- # Save index file even if assets are shared, as it's version-specific
- log_to_console(console_widget, f"Zapisywanie indeksu assetów: {index_file}", "INFO")
- try:
- with open(index_file, "w", encoding="utf-8") as f:
- json.dump(assets_data, f, indent=4)
- except Exception as e:
- log_to_console(console_widget, f"Błąd zapisu indeksu assetów {index_file}: {e}", "WARNING")
- assets_to_download = []
- total_assets_size = 0
- for asset_name, asset_info in assets_data.get("objects", {}).items():
- asset_hash = asset_info["hash"]
- asset_size = asset_info["size"]
- asset_subpath = f"{asset_hash[:2]}/{asset_hash}"
- asset_path = os.path.join(objects_dir, asset_subpath)
- # Check if asset exists and is valid (optional but good practice)
- if not os.path.exists(asset_path) or not verify_sha1(asset_path, asset_hash):
- assets_to_download.append((asset_name, asset_info))
- total_assets_size += asset_size
- log_to_console(console_widget, f"Pobieranie {len(assets_to_download)} assetów (łączny rozmiar: {humanize.naturalsize(total_assets_size)})", "INFO")
- downloaded_size = 0
- total_assets = len(assets_to_download)
- for i, (asset_name, asset_info) in enumerate(assets_to_download):
- asset_hash = asset_info["hash"]
- asset_size = asset_info["size"]
- asset_subpath = f"{asset_hash[:2]}/{asset_hash}"
- asset_path = os.path.join(objects_dir, asset_subpath)
- asset_url = f"https://resources.download.minecraft.net/{asset_subpath}"
- if not download_file(
- asset_url, asset_path,
- lambda d, t: progress_callback(downloaded_size + d, total_assets_size, "assets", i + (d / t) if t > 0 else i, total_assets),
- expected_sha1=asset_hash, console_widget=console_widget, description=f"asset {asset_name}"
- ):
- log_to_console(console_widget, f"Nie udało się pobrać lub zweryfikować assetu {asset_name}", "ERROR")
- # Continue despite failure, verification will catch missing/corrupt files
- else:
- downloaded_size += asset_size # Only add size if download was successful
- # Final progress update
- progress_callback(total_assets_size, total_assets_size, "assets", total_assets, total_assets)
- log_to_console(console_widget, "Pobieranie assetów zakończone.", "INFO")
- return True # Verification function will check if all required assets are present later
- except requests.exceptions.RequestException as e:
- log_to_console(console_widget, f"Błąd sieci podczas pobierania assetów: {e}", "ERROR")
- return False
- except Exception as e:
- log_to_console(console_widget, f"Ogólny błąd pobierania assetów: {e}", "ERROR")
- return False
- def verify_instance(version, console_widget=None):
- """
- Verifies if an instance is complete based on its version manifest.
- Can optionally take a console widget to log progress.
- Returns True if complete and valid, False otherwise.
- """
- instance = instances.get(version)
- if not instance:
- log_to_console(console_widget, f"Instancja {version} nie istnieje w konfiguracji!", "ERROR")
- return False
- version_dir = instance["path"]
- settings = instance.get("settings", {}) # Use .get for safety
- info = get_version_info(version)
- if not info:
- log_to_console(console_widget, f"Nie udało się pobrać manifestu wersji {version}. Nie można zweryfikować.", "ERROR")
- instance["ready"] = False
- save_config()
- return False
- log_to_console(console_widget, f"Weryfikacja instancji {version}...", "INFO")
- all_valid = True
- missing_files = []
- corrupt_files = []
- # 1. Verify client.jar
- client_path = os.path.join(version_dir, f"{version}.jar")
- client_download_info = info.get("downloads", {}).get("client")
- if client_download_info:
- client_sha1 = client_download_info.get("sha1")
- if not os.path.exists(client_path):
- log_to_console(console_widget, f"Brak client.jar dla {version}", "WARNING")
- missing_files.append(f"client.jar ({version})")
- all_valid = False
- elif client_sha1 and not verify_sha1(client_path, client_sha1):
- log_to_console(console_widget, f"Błąd SHA1 dla client.jar ({version})", "WARNING")
- corrupt_files.append(f"client.jar ({version})")
- all_valid = False
- else:
- log_to_console(console_widget, f"client.jar OK", "INFO")
- else:
- log_to_console(console_widget, f"Brak informacji o pobieraniu client.jar w manifeście wersji {version}.", "WARNING")
- # Cannot verify, assume missing for older versions or incomplete manifests
- missing_files.append(f"client.jar ({version}) - Brak info w manifeście?")
- all_valid = False
- # 2. Verify libraries
- libraries_dir = LIBRARIES_DIR if settings.get("shared_libraries", True) else os.path.join(version_dir, "libraries")
- for lib in info.get("libraries", []):
- # Apply rules just like in download
- is_required = True
- if "rules" in lib:
- is_required = False # Start assuming denied unless explicitly allowed
- for rule in lib["rules"]:
- action = rule.get("action", "allow")
- os_info = rule.get("os")
- if os_info:
- current_os = platform.system().lower()
- if os_info.get("name") == current_os:
- rule_arch = os_info.get("arch")
- current_arch = platform.architecture()[0].replace("bit", "")
- if rule_arch and rule_arch != current_arch:
- continue
- if action == "allow":
- is_required = True
- break
- elif action == "deny":
- is_required = False
- break
- else: # Rule applies to all OS if no 'os' is specified
- if action == "allow":
- is_required = True
- break
- elif action == "deny":
- is_required = False
- break
- if is_required and "downloads" in lib and "artifact" in lib["downloads"]:
- artifact = lib["downloads"]["artifact"]
- lib_path_rel = artifact["path"].replace("/", os.sep)
- full_lib_path = os.path.join(libraries_dir, lib_path_rel)
- lib_sha1 = artifact["sha1"]
- if not os.path.exists(full_lib_path):
- log_to_console(console_widget, f"Brak wymaganej biblioteki {lib_path_rel}", "WARNING")
- missing_files.append(f"biblioteka {lib_path_rel}")
- all_valid = False
- elif not verify_sha1(full_lib_path, lib_sha1):
- log_to_console(console_widget, f"Błąd SHA1 dla biblioteki {lib_path_rel}", "WARNING")
- corrupt_files.append(f"biblioteka {lib_path_rel}")
- all_valid = False
- # else:
- # log_to_console(console_widget, f"Biblioteka {lib_path_rel} OK", "INFO") # Too verbose
- # 3. Verify assets (checks index and potentially some objects if shared)
- assets_base_dir = ASSETS_DIR if settings.get("shared_assets", True) else os.path.join(version_dir, "assets")
- asset_index_info = info.get("assetIndex", {})
- asset_id = asset_index_info.get("id", version) # Default asset index id to version if missing
- index_file_path = os.path.join(assets_base_dir, "indexes", f"{asset_id}.json")
- objects_dir = os.path.join(assets_base_dir, "objects")
- if not os.path.exists(index_file_path):
- log_to_console(console_widget, f"Brak pliku indeksu assetów: {index_file_path}", "WARNING")
- missing_files.append(f"indeks assetów ({asset_id})")
- all_valid = False
- elif asset_index_info.get("sha1") and not verify_sha1(index_file_path, asset_index_info["sha1"]):
- log_to_console(console_widget, f"Błąd SHA1 dla indeksu assetów {index_file_path}", "WARNING")
- corrupt_files.append(f"indeks assetów ({asset_id})")
- all_valid = False
- else:
- log_to_console(console_widget, f"Indeks assetów OK: {index_file_path}", "INFO")
- # If index is okay, check some random assets or a critical few
- # Full asset verification is too slow. Rely on download process to get them.
- # Just check if the objects directory exists and has content if shared assets are used.
- 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)):
- log_to_console(console_widget, f"Współdzielony folder assetów ({objects_dir}) wydaje się pusty.", "WARNING")
- # This isn't necessarily an error if no assets were needed yet, but worth a warning.
- pass # Don't set all_valid = False just for this warning
- # 4. Verify natives (basic check if folder exists and has content if not shared)
- natives_dir = NATIVES_DIR if settings.get("shared_natives", True) else os.path.join(version_dir, "natives")
- if not os.path.exists(natives_dir) or (os.path.exists(natives_dir) and not os.listdir(natives_dir)):
- log_to_console(console_widget, f"Folder natywnych bibliotek ({natives_dir}) wydaje się pusty.", "WARNING")
- # Natywne są krytyczne do uruchomienia.
- missing_files.append("natywne biblioteki")
- all_valid = False
- else:
- log_to_console(console_widget, f"Folder natywnych bibliotek ({natives_dir}) wygląda OK.", "INFO")
- # 5. Verify Java Path
- java_path = instance.get("java_path")
- required_java_version = instance.get("required_java")
- if not java_path or not os.path.exists(java_path):
- log_to_console(console_widget, f"Brak ścieżki Javy lub plik nie istnieje: {java_path}", "ERROR")
- all_valid = False
- # Try to find a suitable Java if the saved one is missing
- log_to_console(console_widget, f"Próbuję znaleźć pasującą Javę {required_java_version}", "INFO")
- found_javas = find_java(required_java_version)
- if found_javas:
- new_java_path, new_java_version = found_javas[0]
- log_to_console(console_widget, f"Znaleziono alternatywną Javę: {new_java_path} (wersja: {new_java_version}). Zapisuję w konfiguracji.", "INFO")
- instance["java_path"] = new_java_path
- instance["java_version"] = new_java_version
- # all_valid remains False because the original config had an issue,
- # but the instance data is updated for the next launch attempt.
- else:
- log_to_console(console_widget, f"Nie znaleziono żadnej pasującej Javy {required_java_version}. Nie można uruchomić.", "ERROR")
- elif required_java_version:
- actual_java_version = get_java_version(java_path)
- if not actual_java_version or not check_java_version(actual_java_version, required_java_version):
- log_to_console(console_widget, f"Wybrana Java ({java_path}, wersja: {actual_java_version}) nie spełnia wymagań wersji {required_java_version}.", "ERROR")
- all_valid = False
- else:
- log_to_console(console_widget, f"Wybrana Java ({java_path}, wersja: {actual_java_version}) spełnia wymagania wersji {required_java_version}.", "INFO")
- # 6. Check and regenerate start.bat
- # We regenerate it every time during verification to ensure it's up-to-date
- # with current settings (like username, memory, java path, loader)
- log_to_console(console_widget, f"Generowanie/regenerowanie start.bat dla {version}...", "INFO")
- try:
- regenerate_start_bat(version, instance, info, console_widget)
- log_to_console(console_widget, f"start.bat dla {version} zregenerowany.", "SUCCESS")
- except Exception as e:
- log_to_console(console_widget, f"Błąd podczas regeneracji start.bat dla {version}: {e}", "ERROR")
- # Failure to regenerate start.bat is critical
- all_valid = False
- # Final status update
- if all_valid:
- log_to_console(console_widget, f"Instancja {version} zweryfikowana poprawnie. Jest gotowa!", "SUCCESS")
- instance["ready"] = True
- message = f"Instancja {version} jest kompletna i gotowa do gry!"
- else:
- log_to_console(console_widget, f"Weryfikacja instancji {version} zakończona z błędami.", "WARNING")
- instance["ready"] = False
- message = f"Instancja {version} ma problemy:\n"
- if missing_files:
- message += "\nBrakuje plików:\n" + "\n".join(missing_files)
- if corrupt_files:
- message += "\nUszkodzone pliki:\n" + "\n".join(corrupt_files)
- if not instance.get("java_path") or not os.path.exists(instance["java_path"]):
- message += f"\nNie znaleziono działającej Javy ({instance.get('java_path')})."
- elif required_java_version and (not instance.get("java_version") or not check_java_version(instance["java_version"], required_java_version)):
- message += f"\nWybrana Java ({instance.get('java_path')}, wersja {instance.get('java_version')}) nie spełnia wymagań ({required_java_version})."
- message += "\n\nSpróbuj pobrać instancję ponownie lub edytować ustawienia Javy."
- save_config() # Save changes to instance data (java path, ready status)
- # Provide feedback to user
- if all_valid:
- messagebox.showinfo("Weryfikacja zakończona", message)
- else:
- messagebox.showwarning("Weryfikacja zakończona", message)
- refresh_instances() # Update the GUI list
- return all_valid
- def regenerate_start_bat(version, instance_data, version_info, console_widget=None):
- """Generates or regenerates the start.bat file for an instance."""
- if not version_info:
- log_to_console(console_widget, f"Nie można zregenerować start.bat dla {version}: brak info o wersji.", "ERROR")
- raise ValueError("Brak informacji o wersji.") # Raise error to indicate failure
- version_dir = instance_data["path"]
- settings = instance_data.get("settings", {})
- java_path = instance_data.get("java_path")
- loader_type = settings.get("loader_type", "vanilla")
- server_ip = settings.get("server_ip", "")
- server_port = settings.get("server_port", "")
- if not java_path or not os.path.exists(java_path):
- log_to_console(console_widget, f"Nie można zregenerować start.bat: brak ścieżki Javy lub plik nie istnieje: {java_path}", "ERROR")
- # We could try to find Java here, but verify_instance already does this.
- # Just raise the error to signal failure.
- raise FileNotFoundError(f"Java executable not found at {java_path}")
- bat_path = os.path.join(version_dir, "start.bat")
- libraries_dir = LIBRARIES_DIR if settings.get("shared_libraries", True) else os.path.join(version_dir, "libraries")
- assets_path = ASSETS_DIR if settings.get("shared_assets", True) else os.path.join(version_dir, "assets")
- # Build the classpath
- # Start with client JAR
- classpath_entries = [f'"{os.path.join(version_dir, f"{version}.jar")}"']
- # Add libraries based on manifest rules
- for lib in version_info.get("libraries", []):
- is_required = True
- # Apply rules similar to download/verify
- if "rules" in lib:
- is_required = False
- for rule in lib["rules"]:
- action = rule.get("action", "allow")
- os_info = rule.get("os")
- if os_info:
- current_os = platform.system().lower()
- if os_info.get("name") == current_os:
- rule_arch = os_info.get("arch")
- current_arch = platform.architecture()[0].replace("bit", "")
- if rule_arch and rule_arch != current_arch:
- continue
- if action == "allow":
- is_required = True
- break
- elif action == "deny":
- is_required = False
- break
- else:
- if action == "allow":
- is_required = True
- break
- elif action == "deny":
- is_required = False
- break
- if is_required and "downloads" in lib and "artifact" in lib["downloads"]:
- artifact = lib["downloads"]["artifact"]
- lib_path_rel = artifact["path"].replace("/", os.sep)
- full_lib_path = os.path.join(libraries_dir, lib_path_rel)
- if os.path.exists(full_lib_path): # Only add if the file exists
- classpath_entries.append(f'"{full_lib_path}"')
- else:
- log_to_console(console_widget, f"Biblioteka '{lib_path_rel}' brakuje, nie dodano do class ścieżki.", "WARNING")
- classpath = ';'.join(classpath_entries) # Use ';' for Windows classpath
- # Determine main class and arguments based on loader type
- mc_args = version_info.get("minecraftArguments") # Old style args
- arguments = version_info.get("arguments", {}) # New style args
- main_class = version_info.get("mainClass")
- jvm_args = []
- game_args = []
- if arguments: # New style arguments (1.13+)
- log_to_console(console_widget, f"Używam nowych argumentów uruchamiania (version_info['arguments']).", "INFO")
- # Parse JVM arguments
- jvm_arg_list = arguments.get("jvm", [])
- for arg in jvm_arg_list:
- if isinstance(arg, str):
- jvm_args.append(arg)
- elif isinstance(arg, dict):
- # Handle rules within arguments (simplified - just check for os rule)
- is_allowed = True
- if "rules" in arg:
- is_allowed = False
- for rule in arg["rules"]:
- action = rule.get("action", "allow")
- os_info = rule.get("os")
- if os_info:
- current_os = platform.system().lower()
- if os_info.get("name") == current_os:
- if action == "allow":
- is_allowed = True
- break
- elif action == "deny":
- is_allowed = False
- break
- else: # Rule applies to all OS
- if action == "allow":
- is_allowed = True
- break
- elif action == "deny":
- is_allowed = False
- break
- if is_allowed and "value" in arg:
- value = arg["value"]
- if isinstance(value, list): # Handle multi-value args like system properties
- jvm_args.extend(value)
- else:
- jvm_args.append(value)
- # Parse game arguments
- game_arg_list = arguments.get("game", [])
- for arg in game_arg_list:
- if isinstance(arg, str):
- game_args.append(arg)
- elif isinstance(arg, dict):
- is_allowed = True # Simplified rule checking
- if "rules" in arg:
- is_allowed = False
- for rule in arg["rules"]:
- action = rule.get("action", "allow")
- 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)
- is_allowed = True
- break
- elif action == "deny" and ("features" not in rule or all(settings.get(f, False) for f in rule["features"])):
- is_allowed = False
- break
- if is_allowed and "value" in arg:
- value = arg["value"]
- if isinstance(value, list):
- game_args.extend(value)
- else:
- game_args.append(value)
- # Replace placeholders (basic implementation)
- replacements = {
- "${auth_player_name}": settings.get("username", "Player"),
- "${version_name}": version,
- "${game_directory}": ".", # Relative to the instance folder
- "${assets_root}": f'"{assets_path}"',
- "${assets_index}": version_info.get("assetIndex", {}).get("id", version),
- "${auth_uuid}": "0", # Dummy UUID for offline mode
- "${auth_access_token}": "null", # Dummy token for offline mode
- "${user_type}": "legacy", # or "mojang" depending on version/auth
- "${version_type}": version_info.get("type", "release"),
- # Add other common placeholders as needed
- "${natives_directory}": f'"{natives_path}"' # Added for newer JVM args
- }
- # Apply replacements to game args first (as they are more user-facing)
- processed_game_args = []
- for arg in game_args:
- processed_arg = arg
- for placeholder, value in replacements.items():
- processed_arg = processed_arg.replace(placeholder, str(value))
- processed_game_args.append(processed_arg)
- # Apply replacements to JVM args
- processed_jvm_args = []
- for arg in jvm_args:
- processed_arg = arg
- for placeholder, value in replacements.items():
- processed_arg = processed_arg.replace(placeholder, str(value))
- processed_jvm_args.append(processed_arg)
- final_game_args = processed_game_args
- final_jvm_args = processed_jvm_args
- elif mc_args: # Old style arguments (pre-1.13) - simpler placeholder replacement
- log_to_console(console_widget, f"Używam starych argumentów uruchamiania (version_info['minecraftArguments']).", "INFO")
- # Split old args string and replace placeholders
- arg_string = mc_args
- replacements = {
- "${auth_player_name}": settings.get("username", "Player"),
- "${version_name}": version,
- "${game_directory}": ".",
- "${assets_root}": f'"{assets_path}"',
- "${assets_index}": version_info.get("assetIndex", {}).get("id", version),
- "${auth_uuid}": "0",
- "${auth_access_token}": "null",
- "${user_type}": "legacy",
- "${version_type}": version_info.get("type", "release"),
- # Old format doesn't usually include natives dir in args, uses -Djava.library.path
- }
- # Split, replace, and join
- old_game_args = arg_string.split()
- final_game_args = []
- for arg in old_game_args:
- processed_arg = arg
- for placeholder, value in replacements.items():
- processed_arg = processed_arg.replace(placeholder, str(value))
- final_game_args.append(processed_arg)
- # Old versions typically used default JVM args or few custom ones
- # Set some common default JVM args
- final_jvm_args = [
- f'-Djava.library.path="{natives_path}"', # Specify natives path
- '-Dorg.lwjgl.util.Debug=true' # Optional LWJGL debug
- ]
- # Add memory argument
- final_jvm_args.insert(0, f'-Xmx{settings.get("memory", "2")}G')
- else:
- log_to_console(console_widget, f"Brak argumentów uruchamiania w manifeście wersji {version}.", "WARNING")
- # Fallback to a basic set of arguments if manifest is weird
- main_class = "net.minecraft.client.main.Main" # Assume vanilla main class
- final_jvm_args = [
- f'-Xmx{settings.get("memory", "2")}G',
- f'-Djava.library.path="{natives_path}"'
- ]
- final_game_args = [
- f'--username', settings.get("username", "Player"),
- f'--version', version,
- f'--gameDir', '.',
- f'--assetsDir', f'"{assets_path}"',
- f'--assetIndex', version_info.get("assetIndex", {}).get("id", version),
- f'--accessToken', 'null',
- f'--uuid', '0',
- f'--userType', 'legacy'
- ]
- log_to_console(console_widget, f"Generowanie start.bat z domyślnymi argumentami.", "INFO")
- # Adjust main class and add loader args if a mod loader is selected
- loader_main_class = None
- loader_extra_jvm_args = []
- loader_extra_game_args = []
- loader_extra_classpath_entries = []
- if loader_type and loader_type != "vanilla":
- log_to_console(console_widget, f"Instancja używa mod loadera: {loader_type}", "INFO")
- if loader_type == "fabric":
- # Fabric's main class and arguments vary slightly by version,
- # but net.fabricmc.loader.launch.knot.KnotClient is common for newer Fabric.
- # The fabric-loader-*.jar needs to be on the classpath *before* the client.jar.
- # The installer places necessary files (including fabric-loader-*.jar) into the libraries folder.
- loader_main_class = "net.fabricmc.loader.launch.knot.KnotClient" # Common for modern Fabric
- # Need to find the Fabric loader JAR in libraries and add it to classpath
- fabric_loader_jar = None
- # Search for a JAR matching a pattern, e.g., 'fabric-loader-*'
- try:
- for root, _, files in os.walk(libraries_dir):
- for fname in files:
- if fname.startswith("fabric-loader-") and fname.endswith(".jar"):
- fabric_loader_jar = os.path.join(root, fname)
- break
- if fabric_loader_jar: break
- except Exception as e:
- log_to_console(console_widget, f"Błąd szukania Fabric loader JAR: {e}", "ERROR")
- if fabric_loader_jar and os.path.exists(fabric_loader_jar):
- loader_extra_classpath_entries.append(f'"{fabric_loader_jar}"')
- log_to_console(console_widget, f"Dodano Fabric loader JAR do class ścieżki: {fabric_loader_jar}", "INFO")
- else:
- log_to_console(console_widget, "Nie znaleziono Fabric loader JAR w folderze bibliotek. Upewnij się, że Fabric jest zainstalowany.", "WARNING")
- # Decide if this should be a fatal error or just a warning
- # For now, let's allow generation but log warning.
- elif loader_type == "forge":
- # Forge's main class is typically cpw.mods.modlauncher.Launcher for newer versions.
- # Similar to Fabric, its main JAR needs to be on the classpath, often after libraries but before client.jar.
- # Installer places files in libraries and a 'forge-*.jar' or similar.
- loader_main_class = "cpw.mods.modlauncher.Launcher" # Common for modern Forge
- forge_launcher_jar = None
- # Search for forge launcher JAR
- try:
- # Forge might place it directly in the instance root or libraries
- candidate_paths = [
- os.path.join(version_dir, f"forge-{version}-{settings.get('loader_version', '')}-universal.jar"), # Example name
- os.path.join(version_dir, f"forge-{version}-{settings.get('loader_version', '')}-launcher.jar"), # Example name
- ]
- # Also search libraries dir more generally
- for root, _, files in os.walk(libraries_dir):
- for fname in files:
- if fname.startswith("forge-") and "universal" in fname and fname.endswith(".jar"):
- candidate_paths.append(os.path.join(root, fname))
- break # Found one, assume it's the right one for now
- if forge_launcher_jar: break
- for p in candidate_paths:
- if os.path.exists(p):
- forge_launcher_jar = p
- break
- except Exception as e:
- log_to_console(console_widget, f"Błąd szukania Forge launcher JAR: {e}", "ERROR")
- if forge_launcher_jar and os.path.exists(forge_launcher_jar):
- # Forge's structure and required classpath entries can be complex.
- # Often the installer creates a JSON profile (like a mini version_info) in the instance folder
- # that contains the exact classpath and main class.
- # A robust launcher would parse this.
- # For this example, we'll just add the *presumed* main JAR and hope the installer handled the rest of the classpath.
- loader_extra_classpath_entries.append(f'"{forge_launcher_jar}"')
- log_to_console(console_widget, f"Dodano Forge launcher JAR do class ścieżki: {forge_launcher_jar}", "INFO")
- # Forge might also need specific game arguments like --launchwrapper.tweaker
- # These are typically found in the installer-generated JSON profile.
- # Adding basic placeholder arguments for now.
- # loader_extra_game_args.extend(["--launchwrapper.tweaker", "cpw.mods.fml.common.launcher.FMLTweaker"]) # Example for older Forge
- pass # Placeholder for potential Forge args parsing
- else:
- log_to_console(console_widget, "Nie znaleziono Forge launcher JAR. Upewnij się, że Forge jest zainstalowany.", "WARNING")
- # Decide if this is fatal... likely is for Forge.
- # Add other loaders (NeoForge, etc.) here...
- # elif loader_type == "neoforge":
- # ...
- # Prepend loader classpath entries if any
- if loader_extra_classpath_entries:
- classpath = ';'.join(loader_extra_classpath_entries) + ';' + classpath
- # Use loader's main class if found, otherwise fallback to vanilla
- if loader_main_class:
- main_class = loader_main_class
- elif loader_type != "vanilla":
- log_to_console(console_widget, f"Nie znaleziono głównej klasy dla loadera '{loader_type}'. Używam domyślnej vanilla.", "ERROR")
- main_class = version_info.get("mainClass", "net.minecraft.client.main.Main") # Fallback
- # Add server arguments if specified
- if server_ip:
- loader_extra_game_args.extend(["--server", server_ip])
- if server_port:
- loader_extra_game_args.extend(["--port", server_port])
- # Combine all game args
- final_game_args_combined = final_game_args + loader_extra_game_args
- # Build the final command string
- # Use cmd /c start "" ... for Windows to run in a new window, or just the command
- # Using just the command is simpler for Popen/subprocess
- command_parts = []
- command_parts.append(f'"{java_path}"') # Java executable in quotes
- command_parts.extend(final_jvm_args) # JVM args
- command_parts.extend([f'-cp', f'"{classpath}"']) # Classpath
- command_parts.append(main_class) # Main class
- command_parts.extend(final_game_args_combined) # Game args
- # Construct the final bat content
- # Escape special characters for .bat? No, python subprocess handles quotes well.
- # Need to quote paths with spaces. Already doing this.
- bat_content = f'@echo off\n'
- bat_content += f'title Minecraft {version} ({loader_type})\n'
- # Use PUSHD and POPD to ensure relative paths work correctly if batch file is run from elsewhere
- bat_content += f'PUSHD "{version_dir}"\n'
- bat_content += f'"{java_path}" ' # Java executable path
- bat_content += ' '.join([f'"{arg}"' if ' ' in arg and not arg.startswith('"') else arg for arg in final_jvm_args]) + ' ' # Quoted JVM args
- bat_content += f'-cp "{classpath}" ' # Classpath
- bat_content += main_class + ' ' # Main class
- bat_content += ' '.join([f'"{arg}"' if ' ' in arg and not arg.startswith('"') else arg for arg in final_game_args_combined]) # Quoted game args
- bat_content += '\n'
- bat_content += f'POPD\n' # Return to original directory
- bat_content += f'pause\n' # Keep window open after execution
- try:
- with open(bat_path, "w", encoding="utf-8") as f:
- f.write(bat_content)
- log_to_console(console_widget, f"Plik {bat_path} został utworzony/zaktualizowany.", "SUCCESS")
- except Exception as e:
- log_to_console(console_widget, f"Błąd zapisu pliku start.bat {bat_path}: {e}", "ERROR")
- raise # Re-raise to indicate failure in generation
- def update_progress(progress_bar, status_label, current_bytes, total_bytes, stage, current_item, total_items):
- """Updates the progress bar and status label."""
- if progress_bar and status_label:
- try:
- progress = (current_bytes / total_bytes) * 100 if total_bytes > 0 else 0
- progress_bar["value"] = progress
- remaining_bytes = max(0, total_bytes - current_bytes) # Ensure not negative
- remaining_str = humanize.naturalsize(remaining_bytes) if remaining_bytes > 0 else "0 B"
- total_str = humanize.naturalsize(total_bytes) if total_bytes > 0 else "Nieznany rozmiar"
- status_label.config(text=f"[{stage}] Postęp: {progress:.1f}% | Pobrano: {humanize.naturalsize(current_bytes)} / {total_str} | {current_item}/{total_items}")
- root.update_idletasks() # Update GUI immediately
- except Exception as e:
- # This can happen if widgets are destroyed during download
- print(f"Error updating progress bar: {e}")
- pass
- def download_version(version, instance_settings, console_widget):
- global download_active
- download_active = True
- instance_created = False # Flag to know if we successfully created the instance entry
- try:
- log_to_console(console_widget, f"Rozpoczynam pobieranie instancji: {version}", "INFO")
- global_status_label.config(text="Pobieranie manifestu wersji...")
- root.update_idletasks()
- info = get_version_info(version)
- if not info:
- log_to_console(console_widget, f"Nie udało się pobrać manifestu dla wersji {version}. Anulowano.", "ERROR")
- messagebox.showerror("Błąd pobierania", f"Nie udało się pobrać manifestu dla wersji {version}.")
- return # Exit thread
- # Determine required Java version early
- java_major_version = get_required_java(version, info)
- log_to_console(console_widget, f"Wymagana wersja Javy: {java_major_version}", "INFO")
- # Find or download Java
- global_status_label.config(text="Weryfikacja/pobieranie Javy...")
- root.update_idletasks()
- java_path_setting = instance_settings.get("java_path") # User specified path if any
- if java_path_setting and os.path.exists(java_path_setting) and get_java_version(java_path_setting):
- # Use user-specified Java if valid
- java_path = java_path_setting
- java_version = get_java_version(java_path)
- log_to_console(console_widget, f"Używam wybranej Javy: {java_path} (wersja {java_version})", "INFO")
- else:
- # Find or download the required version
- java_paths = find_java(java_major_version)
- if not java_paths:
- log_to_console(console_widget, f"Nie znaleziono zainstalowanej Javy {java_major_version}.", "WARNING")
- # Ask user if they want to download
- if messagebox.askyesno("Brak Javy", f"Nie znaleziono 64-bitowej Javy {java_major_version}. Czy chcesz pobrać ją automatycznie?"):
- global_status_label.config(text=f"Pobieranie Javy {java_major_version}...")
- root.update_idletasks()
- java_path, java_version = download_java(java_major_version, console_widget)
- if not java_path:
- log_to_console(console_widget, f"Nie udało się pobrać Javy {java_major_version}!", "ERROR")
- messagebox.showerror("Błąd pobierania", f"Nie udało się pobrać Javy {java_major_version}. Anulowano instalację.")
- return # Exit thread
- log_to_console(console_widget, f"Pobrano i zainstalowano Javę: {java_path} (wersja: {java_version})", "SUCCESS")
- else:
- log_to_console(console_widget, f"Pobieranie Javy {java_major_version} anulowane przez użytkownika.", "WARNING")
- messagebox.showerror("Brak Javy", f"Nie wybrano/pobrano Javy {java_major_version}. Nie można zainstalować instancji.")
- return # Exit thread
- else:
- # Use the first suitable Java found
- java_path, java_version = java_paths[0]
- log_to_console(console_widget, f"Używam znalezionej Javy: {java_path} (wersja {java_version})", "INFO")
- # Prepare instance directory structure
- version_dir = os.path.join(BASE_DIR, "instances", version)
- os.makedirs(version_dir, exist_ok=True)
- # Sub-directories like 'mods', 'config', 'saves' can be created here too
- os.makedirs(os.path.join(version_dir, "mods"), exist_ok=True)
- os.makedirs(os.path.join(version_dir, "config"), exist_ok=True)
- os.makedirs(os.path.join(version_dir, "saves"), exist_ok=True)
- os.makedirs(os.path.join(version_dir, "resourcepacks"), exist_ok=True)
- # Add instance to global list and save config early
- # Mark as not ready initially
- instances[version] = {
- "path": version_dir,
- "java_path": java_path,
- "java_version": java_version,
- "required_java": java_major_version,
- "settings": instance_settings,
- "ready": False, # Not ready until all parts are verified
- "timestamp": datetime.now().isoformat()
- }
- save_config()
- instance_created = True # Instance entry is now in config
- # --- Download Steps ---
- # Total progress tracking needs the sum of sizes for all steps.
- # Getting total sizes requires pre-fetching some info (assets, libraries).
- # A simpler approach for progress is to treat each major stage as a step (e.g., 5 steps).
- # Or, refine the update_progress to take current bytes / total bytes for the *current file*,
- # and overall stage progress (e.g., 1/5 stages complete). Let's use the latter.
- # Step 1: Client.jar
- total_stages = 5 # Client, Libraries, Natives, Assets, start.bat
- global_status_label.config(text="[1/5] Pobieranie client.jar...")
- root.update_idletasks()
- client_download_info = info.get("downloads", {}).get("client")
- if not client_download_info:
- log_to_console(console_widget, "Brak informacji o pobieraniu client.jar.", "ERROR")
- messagebox.showerror("Błąd", "Brak informacji o pobieraniu client.jar w manifeście.")
- # Mark instance as not ready and return
- instances[version]["ready"] = False
- save_config()
- return
- client_url = client_download_info.get("url")
- client_sha1 = client_download_info.get("sha1")
- client_size = client_download_info.get("size", 0)
- client_path = os.path.join(version_dir, f"{version}.jar")
- if not os.path.exists(client_path) or not verify_sha1(client_path, client_sha1):
- if not download_file(
- client_url, client_path,
- lambda d, t: update_progress(global_progress_bar, global_status_label, d, t, "client.jar", 1, 1),
- expected_sha1=client_sha1, console_widget=console_widget, description="client.jar"
- ):
- log_to_console(console_widget, "Pobieranie client.jar nie powiodło się.", "ERROR")
- # Mark instance as not ready and return
- instances[version]["ready"] = False
- save_config()
- messagebox.showerror("Błąd pobierania", "Pobieranie client.jar nie powiodło się.")
- return
- else:
- log_to_console(console_widget, "client.jar już istnieje i jest poprawny. Pomijam pobieranie.", "INFO")
- update_progress(global_progress_bar, global_status_label, client_size, client_size, "client.jar", 1, 1) # Update progress visually
- # Step 2: Libraries
- global_status_label.config(text="[2/5] Pobieranie bibliotek...")
- root.update_idletasks()
- # Pass a sub-progress callback that maps library progress to overall stage 2 progress
- lib_progress_callback = lambda current_b, total_b, stage_name, current_i, total_i: \
- update_progress(global_progress_bar, global_status_label, current_b, total_b, f"[2/5] {stage_name}", current_i, total_i)
- if not download_libraries(info.get("libraries", []), version_dir, instance_settings.get("shared_libraries", True), console_widget, lib_progress_callback):
- log_to_console(console_widget, "Pobieranie bibliotek zakończone z błędami.", "WARNING")
- # Continue, verification will catch missing libs
- # Step 3: Natives
- global_status_label.config(text="[3/5] Pobieranie natywnych bibliotek...")
- root.update_idletasks()
- native_progress_callback = lambda current_b, total_b, stage_name, current_i, total_i: \
- update_progress(global_progress_bar, global_status_label, current_b, total_b, f"[3/5] {stage_name}", current_i, total_i)
- if not download_natives(info.get("libraries", []), version_dir, instance_settings.get("shared_natives", True), console_widget, native_progress_callback):
- log_to_console(console_widget, "Pobieranie natywnych bibliotek zakończone z błędami.", "WARNING")
- # Continue, verification will catch missing natives
- # Step 4: Assets
- global_status_label.config(text="[4/5] Pobieranie assetów...")
- root.update_idletasks()
- asset_progress_callback = lambda current_b, total_b, stage_name, current_i, total_i: \
- update_progress(global_progress_bar, global_status_label, current_b, total_b, f"[4/5] {stage_name}", current_i, total_i)
- if not download_assets(info, version_dir, instance_settings.get("shared_assets", True), console_widget, asset_progress_callback):
- log_to_console(console_widget, "Pobieranie assetów zakończone z błędami.", "WARNING")
- # Continue, verification will catch missing assets
- # Step 5: Generate start.bat
- global_status_label.config(text="[5/5] Generowanie start.bat...")
- root.update_idletasks()
- try:
- regenerate_start_bat(version, instances[version], info, console_widget)
- update_progress(global_progress_bar, global_status_label, 1, 1, "start.bat", 1, 1) # Indicate completion for this small step
- except Exception as e:
- log_to_console(console_widget, f"Nie udało się zregenerować start.bat: {e}. Instancja może nie działać poprawnie.", "ERROR")
- # This is a critical error, instance is not ready
- instances[version]["ready"] = False
- save_config()
- messagebox.showerror("Błąd", "Nie udało się wygenerować pliku start.bat. Sprawdź logi konsoli.")
- return
- # --- Finalization ---
- log_to_console(console_widget, f"Pobieranie dla instancji {version} zakończone. Weryfikuję pliki...", "INFO")
- global_status_label.config(text="Weryfikacja instancji...")
- root.update_idletasks()
- # Run a final verification
- if verify_instance(version, console_widget):
- log_to_console(console_widget, f"Instancja {version} pobrana i zweryfikowana pomyślnie!", "SUCCESS")
- instances[version]["ready"] = True
- messagebox.showinfo("Sukces", f"Instancja {version} gotowa do uruchomienia!")
- else:
- log_to_console(console_widget, f"Instancja {version} pobrana, ale weryfikacja zakończyła się błędami. Sprawdź logi.", "WARNING")
- instances[version]["ready"] = False
- messagebox.showwarning("Uwaga", f"Instancja {version} została pobrana, ale wystąpiły błędy weryfikacji. Sprawdź logi konsoli.")
- save_config() # Save final instance state
- except requests.exceptions.RequestException as e:
- log_to_console(console_widget, f"Błąd sieci podczas pobierania instancji {version}: {e}", "ERROR")
- if instance_created:
- instances[version]["ready"] = False # Mark as not ready if failed after creation
- save_config()
- messagebox.showerror("Błąd sieci", f"Wystąpił błąd sieci podczas pobierania instancji {version}: {e}")
- except Exception as e:
- log_to_console(console_widget, f"Ogólny błąd podczas pobierania instancji {version}: {e}", "ERROR")
- if instance_created:
- instances[version]["ready"] = False # Mark as not ready if failed after creation
- save_config()
- messagebox.showerror("Nieoczekiwany błąd", f"Wystąpił nieoczekiwany błąd podczas pobierania instancji {version}: {e}")
- finally:
- # Ensure progress bar and status are reset regardless of success/failure
- if global_progress_bar:
- global_progress_bar["value"] = 0
- if global_status_label:
- global_status_label.config(text="Gotowe do działania!")
- refresh_instances() # Update list regardless
- download_active = False
- def run_game(version):
- instance = instances.get(version)
- if not instance:
- messagebox.showerror("Błąd", f"Instancja {version} nie istnieje!")
- return
- if not instance.get("ready"):
- if messagebox.askyesno("Instancja niegotowa", f"Instancja {version} nie jest oznaczona jako gotowa. Spróbować ją zweryfikować przed uruchomieniem?"):
- # Verify first on main thread (or show console and verify there)
- # For simplicity, let's just warn and let the user verify manually via context menu
- # Alternatively, start a verification thread and then run if successful.
- # Let's just warn for now.
- messagebox.showwarning("Uwaga", f"Instancja {version} nie jest oznaczona jako gotowa. Może nie działać poprawnie.")
- # Proceed with attempting to run anyway
- bat_path = os.path.join(instance["path"], "start.bat")
- if os.path.exists(bat_path):
- try:
- # Use subprocess.Popen to run without waiting and in a new window
- # shell=True is needed for .bat files
- log_to_console(console, f"Uruchamiam Minecraft {version} z '{bat_path}' w katalogu '{instance['path']}'", "INFO")
- subprocess.Popen(bat_path, cwd=instance["path"], shell=True)
- log_to_console(console, f"Uruchomiono Minecraft {version}", "SUCCESS")
- except Exception as e:
- log_to_console(console, f"Nie udało się uruchomić gry: {e}", "ERROR")
- messagebox.showerror("Błąd uruchamiania", f"Nie udało się uruchomić gry: {e}")
- else:
- log_to_console(console, f"Brak pliku start.bat dla instancji {version}. Spróbuj zweryfikować lub pobrać instancję ponownie.", "ERROR")
- messagebox.showerror("Błąd uruchamiania", f"Brak pliku start.bat dla {version}! Spróbuj zweryfikować lub pobrać instancję.")
- def delete_instance(version):
- if messagebox.askyesno("Potwierdź usunięcie", f"Na pewno usunąć instancję {version}?\n\nSpowoduje to usunięcie folderu:\n{instances[version]['path']}"):
- instance = instances.get(version)
- if instance:
- import shutil
- instance_path = instance["path"]
- try:
- if os.path.exists(instance_path):
- shutil.rmtree(instance_path)
- log_to_console(console, f"Folder instancji {version} usunięty: {instance_path}", "INFO")
- del instances[version]
- save_config()
- log_to_console(console, f"Instancja {version} usunięta z konfiguracji.", "SUCCESS")
- refresh_instances()
- except Exception as e:
- log_to_console(console, f"Błąd usuwania instancji {version} w {instance_path}: {e}", "ERROR")
- messagebox.showerror("Błąd usuwania", f"Nie udało się usunąć instancji {version}: {e}\n\nSpróbuj usunąć folder ręcznie.")
- def open_instance_folder(version):
- instance = instances.get(version)
- if not instance:
- messagebox.showwarning("Błąd", "Wybierz instancję.")
- return
- instance_path = instance["path"]
- if os.path.exists(instance_path):
- try:
- system = platform.system()
- if system == "Windows":
- os.startfile(instance_path)
- elif system == "Darwin": # macOS
- subprocess.Popen(["open", instance_path])
- else: # Linux and others
- subprocess.Popen(["xdg-open", instance_path])
- log_to_console(console, f"Otwieram folder instancji: {instance_path}", "INFO")
- except FileNotFoundError:
- messagebox.showerror("Błąd", f"Nie znaleziono programu do otwarcia folderu.")
- log_to_console(console, f"Nie znaleziono programu do otwarcia folderu {instance_path}", "ERROR")
- except Exception as e:
- messagebox.showerror("Błąd", f"Nie udało się otworzyć folderu: {e}")
- log_to_console(console, f"Nie udało się otworzyć folderu {instance_path}: {e}", "ERROR")
- else:
- messagebox.showwarning("Uwaga", f"Folder instancji nie istnieje:\n{instance_path}")
- log_to_console(console, f"Próba otwarcia nieistniejącego folderu instancji: {instance_path}", "WARNING")
- def copy_console_content(console_widget):
- if not console_widget: return
- try:
- console_widget.config(state="normal")
- content = console_widget.get("1.0", tk.END).strip()
- pyperclip.copy(content)
- console_widget.config(state="disabled")
- messagebox.showinfo("Skopiowano", "Zawartość konsoli skopiowana do schowka.")
- log_to_console(console_widget, "Zawartość konsoli skopiowana do schowka.", "INFO")
- except pyperclip.PyperclipException as e:
- messagebox.showerror("Błąd", f"Nie udało się skopiować do schowka: {e}")
- log_to_console(console_widget, f"Błąd kopiowania do schowka: {e}", "ERROR")
- except Exception as e:
- messagebox.showerror("Błąd", f"Nieoczekiwany błąd podczas kopiowania: {e}")
- log_to_console(console_widget, f"Nieoczekiwany błąd podczas kopiowania: {e}", "ERROR")
- def clear_console_content(console_widget):
- if not console_widget: return
- if messagebox.askyesno("Potwierdź", "Na pewno wyczyścić zawartość konsoli?"):
- try:
- console_widget.config(state="normal")
- console_widget.delete("1.0", tk.END)
- console_widget.config(state="disabled")
- log_to_console(console_widget, "Konsola została wyczyszczona.", "INFO") # This log won't appear unless added before clear
- except Exception as e:
- log_to_console(console_widget, f"Błąd czyszczenia konsoli: {e}", "ERROR")
- def import_config():
- file_path = filedialog.askopenfilename(
- title="Wybierz plik konfiguracji",
- filetypes=(("JSON files", "*.json"), ("All files", "*.*"))
- )
- if not file_path:
- return
- try:
- with open(file_path, "r", encoding="utf-8") as f:
- imported_config = json.load(f)
- # Merge/overwrite current configuration
- # Ask user how to handle existing instances? For simplicity, just overwrite if key exists.
- if messagebox.askyesno("Import konfiguracji", "Czy chcesz zastąpić obecne instancje i ustawienia importowaną konfiguracją?\n(Zaleca się wykonanie eksportu przed importem!)"):
- global instances, java_versions_cache
- # Reset to imported state
- instances = imported_config.get("instances", {})
- java_versions_cache = imported_config.get("java_versions", {})
- # Update default settings variables (optional, could just load them next time)
- default_settings = imported_config.get("default_settings", {})
- username_var.set(default_settings.get("username", "Player"))
- memory_var.set(default_settings.get("memory", "2"))
- shared_assets_var.set(default_settings.get("shared_assets", True))
- shared_libraries_var.set(default_settings.get("shared_libraries", True))
- shared_natives_var.set(default_settings.get("shared_natives", True))
- # Update version filters variables
- version_filters = imported_config.get("version_filters", {})
- snapshots_var.set(version_filters.get("snapshots", True))
- releases_var.set(version_filters.get("releases", True))
- alpha_var.set(version_filters.get("alpha", False))
- beta_var.set(version_filters.get("beta", False))
- save_config() # Save the merged config
- log_to_console(console, f"Konfiguracja wczytana z {file_path}.", "SUCCESS")
- messagebox.showinfo("Sukces", "Konfiguracja została zaimportowana.")
- refresh_instances() # Refresh GUI based on new data
- else:
- log_to_console(console, "Import konfiguracji anulowany przez użytkownika.", "INFO")
- except FileNotFoundError:
- messagebox.showerror("Błąd importu", "Wybrany plik nie istnieje.")
- log_to_console(console, f"Błąd importu: Plik nie znaleziono {file_path}", "ERROR")
- except json.JSONDecodeError:
- messagebox.showerror("Błąd importu", "Nieprawidłowy format pliku JSON.")
- log_to_console(console, f"Błąd importu: Nieprawidłowy format JSON w {file_path}", "ERROR")
- except Exception as e:
- messagebox.showerror("Błąd importu", f"Nieoczekiwany błąd podczas importu: {e}")
- log_to_console(console, f"Nieoczekiwany błąd podczas importu z {file_path}: {e}", "ERROR")
- def export_config():
- file_path = filedialog.asksaveasfilename(
- title="Zapisz plik konfiguracji",
- defaultextension=".json",
- filetypes=(("JSON files", "*.json"), ("All files", "*.*"))
- )
- if not file_path:
- return
- try:
- # Get current configuration structure
- config = {
- "default_settings": {
- "username": username_var.get(),
- "memory": memory_var.get(),
- "shared_assets": shared_assets_var.get(),
- "shared_libraries": shared_libraries_var.get(),
- "shared_natives": shared_natives_var.get()
- },
- "version_filters": {
- "snapshots": snapshots_var.get(),
- "releases": releases_var.get(),
- "alpha": alpha_var.get(),
- "beta": beta_var.get()
- },
- "instances": instances,
- "java_versions": java_versions_cache
- }
- with open(file_path, "w", encoding="utf-8") as f:
- json.dump(config, f, indent=4)
- log_to_console(console, f"Konfiguracja wyeksportowana do {file_path}.", "SUCCESS")
- messagebox.showinfo("Sukces", f"Konfiguracja została wyeksportowana do:\n{file_path}")
- except Exception as e:
- messagebox.showerror("Błąd eksportu", f"Nie udało się wyeksportować konfiguracji: {e}")
- log_to_console(console, f"Błąd eksportu do {file_path}: {e}", "ERROR")
- # --- GUI Functions ---
- def switch_tab(tab_name):
- global download_active
- # Check if a download is active before switching away from Download tab
- if download_active and current_tab.get() == "Pobieranie" and tab_name != "Pobieranie":
- if not messagebox.askyesno("Ostrzeżenie", "Pobieranie w toku! Zmiana zakładki przerwie proces. Kontynuować?"):
- return
- log_to_console(console, "Pobieranie przerwane przez użytkownika (zmiana zakładki).", "WARNING")
- download_active = False # Stop the download flag
- # The download thread will likely finish its current operation or encounter an error and exit
- current_tab.set(tab_name)
- for name, btn in tab_buttons.items():
- if name == tab_name:
- btn.config(bg=ACTIVE_TAB_COLOR, relief="sunken")
- else:
- btn.config(bg=SIDEBAR_BG, relief="flat")
- # Re-bind hover effects only for non-active tabs
- if name != tab_name:
- btn.bind("<Enter>", lambda e, b=btn: b.config(bg=HOVER_TAB_COLOR))
- btn.bind("<Leave>", lambda e, b=btn: b.config(bg=SIDEBAR_BG))
- else:
- btn.unbind("<Enter>")
- btn.unbind("<Leave>")
- # Clear current content frame
- for widget in content_frame.winfo_children():
- widget.destroy()
- # Show content for the selected tab
- if tab_name == "Instancje":
- show_instances()
- elif tab_name == "Pobieranie":
- show_download()
- elif tab_name == "Ustawienia":
- show_settings()
- elif tab_name == "Konsola":
- show_console()
- elif tab_name == "Modrinth":
- show_modrinth_browser()
- elif tab_name == "Narzędzia":
- show_tools() # Import/Export and other tools
- def show_instances():
- frame = ttk.Frame(content_frame, padding="10")
- frame.pack(fill="both", expand=True)
- header_frame = ttk.Frame(frame)
- header_frame.pack(fill="x", pady=(0, 10))
- ttk.Label(header_frame, text="Twoje instancje Minecraft", style="TLabel", font=("Segoe UI", 14, "bold")).pack(side="left")
- btn_frame = ttk.Frame(header_frame)
- btn_frame.pack(side="right")
- # Ensure global_progress_bar and global_status_label are visible if needed elsewhere
- # They are packed at the bottom of the root window, so they are always visible.
- columns = ("version", "loader", "java", "status", "date", "path") # Added loader and path
- tree = ttk.Treeview(
- frame, columns=columns, show="headings", selectmode="browse",
- style="Treeview"
- )
- tree.heading("version", text="Wersja", anchor="w")
- tree.heading("loader", text="Loader", anchor="w")
- tree.heading("java", text="Java", anchor="w")
- tree.heading("status", text="Status", anchor="w")
- tree.heading("date", text="Utworzona", anchor="w")
- tree.heading("path", text="Ścieżka", anchor="w") # Hidden by default
- tree.column("version", width=100, anchor="w", stretch=tk.NO)
- tree.column("loader", width=80, anchor="w", stretch=tk.NO)
- tree.column("java", width=100, anchor="w", stretch=tk.NO)
- tree.column("status", width=80, anchor="w", stretch=tk.NO)
- tree.column("date", width=120, anchor="w", stretch=tk.NO)
- tree.column("path", width=250, anchor="w") # Default width, can be expanded
- # Hide the 'path' column
- tree.column("path", width=0, stretch=tk.NO)
- tree.heading("path", text="") # Clear header text when hidden
- scrollbar = ttk.Scrollbar(frame, orient="vertical", command=tree.yview)
- scrollbar.pack(side="right", fill="y")
- tree.configure(yscrollcommand=scrollbar.set)
- tree.pack(side="left", fill="both", expand=True)
- # Populate the treeview
- populate_instances_treeview(tree)
- # Context Menu (Right-click)
- instance_context_menu = tk.Menu(root, tearoff=0)
- instance_context_menu.add_command(label="Uruchom", command=lambda: run_selected_instance(tree))
- instance_context_menu.add_command(label="Edytuj", command=lambda: edit_selected_instance(tree))
- instance_context_menu.add_command(label="Weryfikuj", command=lambda: verify_selected_instance(tree))
- instance_context_menu.add_command(label="Otwórz folder", command=lambda: open_selected_instance_folder(tree))
- instance_context_menu.add_separator()
- instance_context_menu.add_command(label="Zmień nazwę", command=lambda: rename_selected_instance(tree))
- instance_context_menu.add_command(label="Duplikuj", command=lambda: duplicate_selected_instance(tree))
- instance_context_menu.add_command(label="Eksportuj", command=lambda: export_selected_instance(tree))
- instance_context_menu.add_separator()
- instance_context_menu.add_command(label="Usuń", command=lambda: delete_selected_instance(tree))
- def show_context_menu(event):
- selected_item = tree.focus()
- if selected_item:
- try:
- instance_context_menu.tk_popup(event.x_root, event.y_root)
- finally:
- instance_context_menu.grab_release()
- tree.bind("<Button-3>", show_context_menu) # Bind right-click
- # Action Buttons (below the treeview)
- action_frame = ttk.Frame(frame)
- action_frame.pack(fill="x", pady=(10, 0))
- ttk.Button(action_frame, text="Nowa instancja", command=create_instance, style="TButton", width=18).pack(side="left", padx=5)
- ttk.Button(action_frame, text="Uruchom Wybraną", command=lambda: run_selected_instance(tree), style="TButton", width=18).pack(side="left", padx=5)
- ttk.Button(action_frame, text="Edytuj Wybraną", command=lambda: edit_selected_instance(tree), style="TButton", width=18).pack(side="left", padx=5)
- ttk.Button(action_frame, text="Usuń Wybraną", command=lambda: delete_selected_instance(tree), style="TButton", width=18).pack(side="right", padx=5)
- ttk.Button(action_frame, text="Weryfikuj Wybraną", command=lambda: verify_selected_instance(tree), style="TButton", width=18).pack(side="right", padx=5)
- 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))
- def populate_instances_treeview(tree):
- """Clears and repopulates the instance treeview."""
- # Clear existing items
- for item in tree.get_children():
- tree.delete(item)
- # Sort instances by version (newest first)
- # Get list of versions, sort using pkg_version.parse if possible
- sorted_versions = sorted(instances.keys(), key=lambda v: pkg_version.parse(v) if v[0].isdigit() else v, reverse=True)
- # Add instances to treeview
- for version in sorted_versions:
- data = instances[version] # Get data using the sorted version key
- instance_settings = data.get("settings", {})
- tree.insert("", "end", iid=version, values=(
- version,
- instance_settings.get("loader_type", "vanilla").capitalize(),
- data.get("java_version", "?"),
- "Gotowe" if data.get("ready", False) else "Błąd/Niegotowe",
- datetime.fromisoformat(data["timestamp"]).strftime("%Y-%m-%d %H:%M") if "timestamp" in data else "Nieznana",
- data["path"]
- ))
- def refresh_instances():
- """Refreshes the instances tab GUI."""
- # Check if the current tab is 'Instancje' before refreshing the GUI
- if current_tab.get() == "Instancje":
- for widget in content_frame.winfo_children():
- widget.destroy()
- show_instances()
- # If not on the instances tab, just update the data and rely on switching back to refresh
- def get_selected_instance_version(tree):
- """Gets the version of the currently selected item in the treeview."""
- selected_item = tree.focus()
- if selected_item:
- # Assuming the version is stored as the iid or the first value
- return tree.item(selected_item, "iid") # Using iid is safer if values order changes
- # return tree.item(selected_item)["values"][0] # Alternative if version is always the first column
- return None # Nothing selected
- def run_selected_instance(tree):
- version = get_selected_instance_version(tree)
- if version:
- run_game(version)
- else:
- messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją uruchomić.")
- def edit_selected_instance(tree):
- version = get_selected_instance_version(tree)
- if version:
- edit_instance(version)
- else:
- messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją edytować.")
- def verify_selected_instance(tree):
- version = get_selected_instance_version(tree)
- if version:
- # Verification can take time, run in a thread and show console
- switch_tab("Konsola") # Switch to console tab to see progress
- threading.Thread(target=verify_instance, args=(version, console), daemon=True).start()
- else:
- messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją zweryfikować.")
- def delete_selected_instance(tree):
- version = get_selected_instance_version(tree)
- if version:
- delete_instance(version)
- else:
- messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby ją usunąć.")
- def open_selected_instance_folder(tree):
- version = get_selected_instance_version(tree)
- if version:
- open_instance_folder(version)
- else:
- messagebox.showwarning("Uwaga", "Wybierz instancję z listy, aby otworzyć jej folder.")
- def rename_selected_instance(tree):
- version = get_selected_instance_version(tree)
- if not version:
- messagebox.showwarning("Uwaga", "Wybierz instancję do zmiany nazwy.")
- return
- # NOTE: Minecraft instances are fundamentally tied to their version ID as the folder name.
- # Renaming here would only change the display name *in the launcher*.
- # This is a complex feature as it clashes with the folder structure.
- # For now, let's add a note field instead of renaming.
- messagebox.showinfo("Funkcja niedostępna", "Zmiana nazwy instancji nie jest obecnie obsługiwana, ponieważ nazwa jest powiązana z wersją Minecrafta.")
- # Implement adding a note instead if desired
- # add_instance_note(version)
- def duplicate_selected_instance(tree):
- version = get_selected_instance_version(tree)
- if not version:
- messagebox.showwarning("Uwaga", "Wybierz instancję do zduplikowania.")
- return
- messagebox.showinfo("Funkcja niedostępna", "Duplikowanie instancji nie jest obecnie zaimplementowane.")
- # This would involve copying the instance folder and adding a new entry in the config.
- def export_selected_instance(tree):
- version = get_selected_instance_version(tree)
- if not version:
- messagebox.showwarning("Uwaga", "Wybierz instancję do eksportu.")
- return
- # Exporting a single instance as a zip file containing its folder and config snippet
- instance = instances.get(version)
- if not instance:
- messagebox.showerror("Błąd", "Dane instancji nie znaleziono.")
- return
- export_path = filedialog.asksaveasfilename(
- title=f"Eksportuj instancję {version}",
- initialfile=f"minecraft_instance_{version}.zip",
- defaultextension=".zip",
- filetypes=(("Zip files", "*.zip"), ("All files", "*.*"))
- )
- if not export_path:
- return
- try:
- # Create a temporary directory to prepare the export
- temp_dir = os.path.join(BASE_DIR, "temp_export", version)
- os.makedirs(temp_dir, exist_ok=True)
- # Copy the instance folder content (excluding potentially large logs/temp files)
- instance_folder = instance["path"]
- if os.path.exists(instance_folder):
- # Use a simpler copy tree, might need exclusions for larger projects
- shutil.copytree(instance_folder, os.path.join(temp_dir, version), dirs_exist_ok=True,
- ignore=shutil.ignore_patterns('logs', 'temp_*', '*.log', '*.tmp'))
- else:
- messagebox.showwarning("Uwaga", f"Folder instancji {version} nie istnieje. Eksportowana będzie tylko konfiguracja.")
- log_to_console(console, f"Folder instancji {version} nie istnieje, eksportowana tylko konfiguracja.", "WARNING")
- # Save a snippet of the config for this instance in the temp dir
- instance_config_snippet = {version: instance}
- with open(os.path.join(temp_dir, "instance_config.json"), "w", encoding="utf-8") as f:
- json.dump(instance_config_snippet, f, indent=4)
- # Create the zip archive
- with zipfile.ZipFile(export_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
- for root_dir, _, files in os.walk(temp_dir):
- for file in files:
- file_path = os.path.join(root_dir, file)
- # Create relative path inside the zip
- relative_path = os.path.relpath(file_path, temp_dir)
- zipf.write(file_path, arcname=relative_path)
- # Clean up the temporary directory
- shutil.rmtree(temp_dir)
- log_to_console(console, f"Instancja {version} wyeksportowana do {export_path}.", "SUCCESS")
- messagebox.showinfo("Sukces", f"Instancja {version} została wyeksportowana do:\n{export_path}")
- except Exception as e:
- messagebox.showerror("Błąd eksportu", f"Nie udało się wyeksportować instancji {version}: {e}")
- log_to_console(console, f"Błąd eksportu instancji {version}: {e}", "ERROR")
- def create_instance():
- window = tk.Toplevel(root)
- window.title("Nowa instancja Minecraft")
- window.geometry("550x700") # Slightly larger
- window.configure(bg=PRIMARY_BG)
- window.resizable(False, False)
- style = ttk.Style() # Ensure style is available in Toplevel
- style.configure("TFrame", background=PRIMARY_BG)
- style.configure("TLabel", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10))
- style.configure("TButton", background=BUTTON_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10), borderwidth=0)
- style.map("TButton",
- background=[('active', BUTTON_HOVER), ('pressed', BUTTON_HOVER)],
- foreground=[('active', PRIMARY_FG), ('pressed', PRIMARY_FG)])
- style.configure("TEntry", fieldbackground="#333333", foreground=PRIMARY_FG, insertcolor=PRIMARY_FG, borderwidth=1)
- style.configure("TCombobox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, borderwidth=1)
- style.configure("TCheckbutton", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 9)) # Style for ttk.Checkbutton
- frame = ttk.Frame(window, padding="15")
- frame.pack(fill="both", expand=True)
- ttk.Label(frame, text="Tworzenie nowej instancji", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
- form_frame = ttk.Frame(frame)
- form_frame.pack(fill="x", expand=False) # Don't expand form vertically
- form_frame.columnconfigure(1, weight=1) # Allow the second column to expand
- # Row 0: Version Filters
- ttk.Label(form_frame, text="Filtry wersji:", style="TLabel").grid(row=0, column=0, sticky="nw", pady=(0, 5))
- filters_frame = ttk.Frame(form_frame)
- filters_frame.grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
- # Use tk.Checkbutton for checkboxes as ttk.Checkbutton styling is complex
- tk.Checkbutton(filters_frame, text="Snapshoty", variable=snapshots_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=lambda: [save_config(), refresh_version_combo_dialog(version_combo, version_var)]).pack(anchor="w", side="left", padx=5)
- tk.Checkbutton(filters_frame, text="Release", variable=releases_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=lambda: [save_config(), refresh_version_combo_dialog(version_combo, version_var)]).pack(anchor="w", side="left", padx=5)
- tk.Checkbutton(filters_frame, text="Alpha", variable=alpha_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=lambda: [save_config(), refresh_version_combo_dialog(version_combo, version_var)]).pack(anchor="w", side="left", padx=5)
- tk.Checkbutton(filters_frame, text="Beta", variable=beta_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
- # Row 1: Minecraft Version
- ttk.Label(form_frame, text="Wersja Minecrafta:", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
- version_var = tk.StringVar()
- versions = get_versions()
- version_combo = ttk.Combobox(form_frame, textvariable=version_var, state="readonly", values=versions, style="TCombobox")
- version_combo.grid(row=1, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- if versions:
- version_combo.current(0)
- else:
- version_var.set("Brak wersji!")
- version_combo.config(state="disabled")
- messagebox.showwarning("Uwaga", "Nie znaleziono żadnych wersji Minecrafta. Sprawdź filtry i połączenie z internetem.")
- # Row 2: Username
- ttk.Label(form_frame, text="Nazwa użytkownika:", style="TLabel").grid(row=2, column=0, sticky="w", pady=(10, 5))
- username_entry = ttk.Entry(form_frame, style="TEntry")
- username_entry.insert(0, username_var.get())
- username_entry.grid(row=2, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- # Row 3: Memory
- ttk.Label(form_frame, text="Pamięć RAM (GB):", style="TLabel").grid(row=3, column=0, sticky="w", pady=(10, 5))
- memory_spin = tk.Spinbox(form_frame, from_=1, to=32, width=5, bg="#333333", fg=PRIMARY_FG, highlightthickness=0,
- buttonbackground=TERTIARY_BG, buttonforeground=PRIMARY_FG, insertbackground=PRIMARY_FG)
- memory_spin.delete(0, tk.END)
- memory_spin.insert(0, memory_var.get())
- memory_spin.grid(row=3, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
- # Row 4: Java Path
- ttk.Label(form_frame, text="Ścieżka Java:", style="TLabel").grid(row=4, column=0, sticky="w", pady=(10, 5))
- java_var = tk.StringVar()
- java_paths_found = find_java()
- java_options = [f"{p} (v{v} - {src})" for p, v, src in find_java_with_source()] # Include source in display
- java_combo = ttk.Combobox(form_frame, textvariable=java_var, state="readonly", values=java_options, style="TCombobox")
- java_combo.grid(row=4, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- java_combo.set("Szukam Javy...") # Initial text
- # Store (path, version) tuples corresponding to the options list
- java_path_tuples = find_java() # Use the original list without source for value mapping
- # Auto-select best Java based on default/selected MC version
- def update_java_selection(event=None):
- selected_mc_version = version_var.get()
- if not selected_mc_version or not java_path_tuples:
- java_combo.set("Brak pasującej Javy")
- java_combo.config(state="disabled")
- return
- java_combo.config(state="readonly") # Enable if versions found
- # Fetch version info to get required Java (can be slow, maybe cache?)
- # For simplicity in dialog, might use a background fetch or rely on find_java
- # Let's just find based on the list we already have.
- # A more robust way would fetch manifest and check required version
- # Use a simplified required version based on MC version string if manifest not fetched
- # OR pre-fetch all manifests? No, too much data.
- # Let's just use the version string heuristic for the dialog picker initially.
- # The main download logic will do a proper manifest check.
- required_java_heuristic = "1.8" # Default
- try:
- mc_v = pkg_version.parse(selected_mc_version)
- if mc_v >= pkg_version.parse("1.20.5"): required_java_heuristic = "21"
- elif mc_v >= pkg_version.parse("1.18"): required_java_heuristic = "17"
- elif mc_v >= pkg_version.parse("1.17"): required_java_heuristic = "16"
- elif mc_v >= pkg_version.parse("1.6"): required_java_heuristic = "1.8"
- except pkg_version.InvalidVersion:
- pass # Keep default heuristic
- best_java_index = -1
- for i, (path, ver) in enumerate(java_path_tuples):
- if check_java_version(ver, required_java_heuristic):
- best_java_index = i
- break # Found a suitable one, take the first (highest priority from find_java)
- if best_java_index != -1:
- java_combo.current(best_java_index)
- elif java_path_tuples: # If no suitable Java found, select the first available one
- java_combo.current(0)
- else: # No Java found at all
- java_combo.set("Brak Javy 64-bit")
- java_combo.config(state="disabled")
- messagebox.showwarning("Brak Javy", "Nie znaleziono żadnej pasującej 64-bitowej instalacji Javy. Upewnij się, że Java 64-bit jest zainstalowana.")
- version_combo.bind("<<ComboboxSelected>>", update_java_selection)
- # Perform initial selection after populating
- if java_options:
- update_java_selection()
- # Row 5: Mod Loader Type
- ttk.Label(form_frame, text="Mod Loader:", style="TLabel").grid(row=5, column=0, sticky="w", pady=(10, 5))
- loader_var = tk.StringVar(value="vanilla")
- # Add loaders here as supported
- loader_options = ["vanilla", "fabric", "forge"] # Add "neoforge" etc. when supported
- loader_combo = ttk.Combobox(form_frame, textvariable=loader_var, state="readonly", values=loader_options, style="TCombobox")
- loader_combo.grid(row=5, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- loader_combo.current(0) # Default to vanilla
- # Row 6: Mod Loader Version (Optional, depends on loader)
- # This is complex as versions depend on MC version and loader type.
- # For simplicity, we won't auto-populate this dropdown in the dialog.
- # User might need to manually enter a known working version or run the installer later.
- # Alternatively, fetch available loader versions based on selected MC version.
- # Let's omit loader version selection in the dialog for now and assume user runs installer.
- # The instance config will store the *type*, and start.bat will adapt if loader files are present.
- # Added a placeholder label for loader version if we decide to add it later.
- # ttk.Label(form_frame, text="Loader Version:", style="TLabel").grid(row=6, column=0, sticky="w", pady=(10, 5))
- # loader_version_var = tk.StringVar()
- # ttk.Entry(form_frame, textvariable=loader_version_var, style="TEntry").grid(row=6, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- # Note: The launcher doesn't auto-install loaders yet, user must run installer JARs.
- # Row 7: Shared Folders
- ttk.Label(form_frame, text="Opcje współdzielenia:", style="TLabel").grid(row=7, column=0, sticky="nw", pady=(10, 5))
- options_frame = ttk.Frame(form_frame)
- options_frame.grid(row=7, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- shared_assets = tk.BooleanVar(value=shared_assets_var.get())
- shared_libs = tk.BooleanVar(value=shared_libraries_var.get())
- shared_natives = tk.BooleanVar(value=shared_natives_var.get())
- tk.Checkbutton(options_frame, text="Współdziel assets", variable=shared_assets,
- bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
- font=("Segoe UI", 9)).pack(anchor="w", pady=2)
- tk.Checkbutton(options_frame, text="Współdziel biblioteki", variable=shared_libs,
- bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
- font=("Segoe UI", 9)).pack(anchor="w", pady=2)
- tk.Checkbutton(options_frame, text="Współdziel natywne biblioteki", variable=shared_natives,
- bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
- font=("Segoe UI", 9)).pack(anchor="w", pady=2)
- # Row 8: Server Address (Optional)
- ttk.Label(form_frame, text="Adres serwera (opcjonalnie):", style="TLabel").grid(row=8, column=0, sticky="w", pady=(10, 5))
- server_ip_var = tk.StringVar()
- server_ip_entry = ttk.Entry(form_frame, textvariable=server_ip_var, style="TEntry")
- server_ip_entry.grid(row=8, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- ttk.Label(form_frame, text="Port serwera (opcjonalnie):", style="TLabel").grid(row=9, column=0, sticky="w", pady=(10, 5))
- server_port_var = tk.StringVar()
- server_port_entry = ttk.Entry(form_frame, textvariable=server_port_var, style="TEntry", width=8) # Smaller width
- server_port_entry.grid(row=9, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
- # Action buttons at the bottom
- btn_frame = ttk.Frame(frame)
- btn_frame.pack(fill="x", pady=(15, 0))
- ttk.Button(btn_frame, text="Anuluj", command=window.destroy, style="TButton", width=15).pack(side="right", padx=5)
- ttk.Button(btn_frame, text="Utwórz Instancję", command=lambda: (
- # Validate version selection
- selected_version := version_var.get(),
- selected_java_option := java_combo.get(),
- selected_java_path := java_path_tuples[java_combo.current()][0] if java_combo.current() != -1 else "",
- process_create_instance(
- selected_version,
- {
- "username": username_entry.get().strip() or "Player", # Ensure username is not empty
- "memory": memory_spin.get(),
- "java_path": selected_java_path,
- "shared_assets": shared_assets.get(),
- "shared_libraries": shared_libs.get(),
- "shared_natives": shared_natives.get(),
- "loader_type": loader_var.get(),
- "loader_version": "", # Loader version not selected in this dialog
- "server_ip": server_ip_var.get().strip(),
- "server_port": server_port_var.get().strip()
- },
- window # Pass window to close it
- )
- ) if version_var.get() and version_var.get() != "Brak wersji!" else messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta!")).pack(side="right", padx=5)
- def process_create_instance(version, settings, window):
- """Handles the logic after 'Create Instance' button is clicked in the dialog."""
- if not version:
- messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta.")
- return
- if version in instances:
- if not messagebox.askyesno("Instancja istnieje", f"Instancja dla wersji {version} już istnieje. Czy chcesz ją usunąć i utworzyć ponownie?"):
- return # User cancelled replacement
- # If user confirmed replacement, delete the existing instance first
- log_to_console(console, f"Użytkownik potwierdził zastąpienie instancji {version}. Usuwam starą...", "INFO")
- try:
- instance_path = instances[version]["path"]
- if os.path.exists(instance_path):
- import shutil
- shutil.rmtree(instance_path)
- log_to_console(console, f"Usunięto folder starej instancji: {instance_path}", "INFO")
- del instances[version]
- save_config()
- log_to_console(console, f"Stara instancja {version} usunięta z konfiguracji.", "INFO")
- except Exception as e:
- messagebox.showerror("Błąd usuwania", f"Nie udało się usunąć istniejącej instancji {version}: {e}\nAnulowano tworzenie nowej.")
- log_to_console(console, f"Błąd podczas usuwania istniejącej instancji {version} przed tworzeniem nowej: {e}", "ERROR")
- return # Stop the process if old instance couldn't be removed
- # Now proceed with downloading the new instance
- # We need to switch to the Download tab and start the process there
- # Save the pending settings and version globally
- global pending_instance_settings, pending_version
- pending_instance_settings = settings
- pending_version = version
- window.destroy() # Close the dialog
- switch_tab("Pobieranie") # Switch to download tab which will pick up pending_version/settings
- def refresh_version_combo_dialog(combo, version_var):
- """Refreshes version list in a combobox, typically in a dialog."""
- versions = get_versions()
- combo['values'] = versions
- if versions:
- # Try to keep the selected version if it's still in the filtered list
- current_selection = version_var.get()
- if current_selection in versions:
- version_var.set(current_selection)
- else:
- combo.current(0) # Select the first one
- combo.config(state="readonly")
- else:
- version_var.set("Brak wersji!")
- combo.config(state="disabled")
- messagebox.showwarning("Uwaga", "Nie znaleziono żadnych wersji Minecrafta z wybranymi filtrami.")
- def edit_instance(version):
- if not version or version not in instances:
- messagebox.showwarning("Uwaga", "Wybierz instancję!")
- return
- instance = instances[version]
- settings = instance.get("settings", {})
- window = tk.Toplevel(root)
- window.title(f"Edytuj instancję {version}")
- window.geometry("550x700") # Same size as create
- window.configure(bg=PRIMARY_BG)
- window.resizable(False, False)
- style = ttk.Style() # Ensure style is available in Toplevel
- style.configure("TFrame", background=PRIMARY_BG)
- style.configure("TLabel", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10))
- style.configure("TButton", background=BUTTON_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10), borderwidth=0)
- style.map("TButton",
- background=[('active', BUTTON_HOVER), ('pressed', BUTTON_HOVER)],
- foreground=[('active', PRIMARY_FG), ('pressed', PRIMARY_FG)])
- style.configure("TEntry", fieldbackground="#333333", foreground=PRIMARY_FG, insertcolor=PRIMARY_FG, borderwidth=1)
- style.configure("TCombobox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, borderwidth=1)
- style.configure("TCheckbutton", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 9)) # Style for ttk.Checkbutton
- frame = ttk.Frame(window, padding="15")
- frame.pack(fill="both", expand=True)
- ttk.Label(frame, text=f"Edycja instancji {version}", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
- form_frame = ttk.Frame(frame)
- form_frame.pack(fill="x", expand=False)
- form_frame.columnconfigure(1, weight=1)
- # Row 0: Username
- ttk.Label(form_frame, text="Nazwa użytkownika:", style="TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5))
- username_entry = ttk.Entry(form_frame, style="TEntry")
- username_entry.insert(0, settings.get("username", username_var.get()))
- username_entry.grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
- # Row 1: Memory
- ttk.Label(form_frame, text="Pamięć RAM (GB):", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
- memory_spin = tk.Spinbox(form_frame, from_=1, to=32, width=5, bg="#333333", fg=PRIMARY_FG, highlightthickness=0,
- buttonbackground=TERTIARY_BG, buttonforeground=PRIMARY_FG, insertbackground=PRIMARY_FG)
- memory_spin.delete(0, tk.END)
- memory_spin.insert(0, settings.get("memory", memory_var.get()))
- memory_spin.grid(row=1, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
- # Row 2: Java Path
- ttk.Label(form_frame, text="Ścieżka Java:", style="TLabel").grid(row=2, column=0, sticky="w", pady=(10, 5))
- java_var = tk.StringVar()
- # Include the instance's current Java path in options if it's not already found by find_java
- java_paths_found = find_java()
- java_path_tuples = java_paths_found[:] # Copy the list
- current_java_path = instance.get("java_path", settings.get("java_path", ""))
- current_java_version = instance.get("java_version", settings.get("java_version", ""))
- # Add the instance's current java path if it exists and wasn't found by find_java
- is_current_java_found = any(p == current_java_path for p, v in java_path_tuples)
- if current_java_path and os.path.exists(current_java_path) and not is_current_java_found:
- # Get version again if needed, but avoid checking twice if it's already in instance data
- actual_version = get_java_version(current_java_path) or current_java_version or "?"
- java_path_tuples.append((current_java_path, actual_version))
- java_options = [f"{p} (v{v})" for p, v in java_path_tuples]
- log_to_console(console, f"Dodano bieżącą Javę instancji do opcji: {current_java_path}", "INFO")
- else:
- java_options = [f"{p} (v{v})" for p, v in java_path_tuples]
- java_combo = ttk.Combobox(form_frame, textvariable=java_var, state="readonly", values=java_options, style="TCombobox")
- java_combo.grid(row=2, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- # Set the current Java path in the combobox
- if current_java_path:
- # Find the index of the current_java_path in the java_path_tuples list
- try:
- index_to_select = [i for i, (p, v) in enumerate(java_path_tuples) if p == current_java_path][0]
- java_combo.current(index_to_select)
- except IndexError:
- # This shouldn't happen if logic above is correct, but as a fallback
- log_to_console(console, f"Nie znaleziono bieżącej ścieżki Javy {current_java_path} w liście opcji.", "WARNING")
- java_combo.set(current_java_path) # Display the path even if not in list
- java_combo.config(state="normal") # Allow editing if not in list? Or just display. Let's just display.
- java_combo.config(state="readonly") # Force readonly after setting
- # Row 3: Mod Loader Type
- ttk.Label(form_frame, text="Mod Loader:", style="TLabel").grid(row=3, column=0, sticky="w", pady=(10, 5))
- loader_var = tk.StringVar(value=settings.get("loader_type", "vanilla"))
- loader_options = ["vanilla", "fabric", "forge"] # Add "neoforge" etc. when supported
- loader_combo = ttk.Combobox(form_frame, textvariable=loader_var, state="readonly", values=loader_options, style="TCombobox")
- loader_combo.grid(row=3, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- # Set current loader type
- try:
- loader_combo.current(loader_options.index(settings.get("loader_type", "vanilla")))
- except ValueError:
- loader_combo.current(0) # Default to vanilla if saved type is invalid
- # Row 4: Shared Folders
- ttk.Label(form_frame, text="Opcje współdzielenia:", style="TLabel").grid(row=4, column=0, sticky="nw", pady=(10, 5))
- options_frame = ttk.Frame(form_frame)
- options_frame.grid(row=4, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- shared_assets = tk.BooleanVar(value=settings.get("shared_assets", shared_assets_var.get()))
- shared_libs = tk.BooleanVar(value=settings.get("shared_libraries", shared_libraries_var.get()))
- shared_natives = tk.BooleanVar(value=settings.get("shared_natives", shared_natives_var.get()))
- tk.Checkbutton(options_frame, text="Współdziel assets", variable=shared_assets,
- bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
- font=("Segoe UI", 9)).pack(anchor="w", pady=2)
- tk.Checkbutton(options_frame, text="Współdziel biblioteki", variable=shared_libs,
- bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
- font=("Segoe UI", 9)).pack(anchor="w", pady=2)
- tk.Checkbutton(options_frame, text="Współdziel natywne biblioteki", variable=shared_natives,
- bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
- font=("Segoe UI", 9)).pack(anchor="w", pady=2)
- # Row 5: Server Address (Optional)
- ttk.Label(form_frame, text="Adres serwera (opcjonalnie):", style="TLabel").grid(row=5, column=0, sticky="w", pady=(10, 5))
- server_ip_var = tk.StringVar(value=settings.get("server_ip", ""))
- server_ip_entry = ttk.Entry(form_frame, textvariable=server_ip_var, style="TEntry")
- server_ip_entry.grid(row=5, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- ttk.Label(form_frame, text="Port serwera (opcjonalnie):", style="TLabel").grid(row=6, column=0, sticky="w", pady=(10, 5))
- server_port_var = tk.StringVar(value=settings.get("server_port", ""))
- server_port_entry = ttk.Entry(form_frame, textvariable=server_port_var, style="TEntry", width=8)
- server_port_entry.grid(row=6, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
- # Row 7: Stats (Read-only)
- ttk.Label(form_frame, text="Statystyki:", style="TLabel").grid(row=7, column=0, sticky="nw", pady=(10, 5))
- stats_frame = ttk.Frame(form_frame)
- stats_frame.grid(row=7, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- # Calculate stats
- stats = get_instance_stats(instance)
- ttk.Label(stats_frame, text=f"Ścieżka: {stats['path']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
- ttk.Label(stats_frame, text=f"Rozmiar: {stats['size']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
- ttk.Label(stats_frame, text=f"Modów (.jar): {stats['mods']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
- ttk.Label(stats_frame, text=f"Utworzona: {stats['created']}", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
- # Action buttons at the bottom
- btn_frame = ttk.Frame(frame)
- btn_frame.pack(fill="x", pady=(15, 0))
- ttk.Button(btn_frame, text="Anuluj", command=window.destroy, style="TButton", width=15).pack(side="right", padx=5)
- ttk.Button(btn_frame, text="Zapisz", command=lambda: (
- # Get selected Java path from tuple list
- selected_java_path := java_path_tuples[java_combo.current()][0] if java_combo.current() != -1 else current_java_path,
- # Update instance with selected settings
- update_instance(version, {
- "username": username_entry.get().strip() or "Player",
- "memory": memory_spin.get(),
- "java_path": selected_java_path,
- "shared_assets": shared_assets.get(),
- "shared_libraries": shared_libs.get(),
- "shared_natives": shared_natives.get(),
- "loader_type": loader_var.get(),
- "loader_version": settings.get("loader_version", ""), # Keep existing loader version
- "server_ip": server_ip_var.get().strip(),
- "server_port": server_port_var.get().strip()
- }),
- # Close the window
- window.destroy()
- ) if version_var.get() and version_var.get() != "Brak wersji!" else messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta!")).pack(side="right", padx=5)
- def get_instance_stats(instance):
- """Calculates and returns statistics for a given instance."""
- stats = {
- "path": instance.get("path", "N/A"),
- "size": "N/A",
- "mods": "N/A",
- "created": datetime.fromisoformat(instance["timestamp"]).strftime("%Y-%m-%d %H:%M") if "timestamp" in instance else "Nieznana"
- }
- instance_path = instance.get("path")
- if instance_path and os.path.exists(instance_path):
- try:
- # Calculate folder size
- total_size = 0
- for dirpath, dirnames, filenames in os.walk(instance_path):
- for f in filenames:
- fp = os.path.join(dirpath, f)
- if not os.path.islink(fp): # Avoid counting symlinks multiple times
- total_size += os.path.getsize(fp)
- stats["size"] = humanize.naturalsize(total_size)
- # Count mods (simple .jar file count in mods folder)
- mods_path = os.path.join(instance_path, "mods")
- if os.path.exists(mods_path):
- mod_count = len([f for f in os.listdir(mods_path) if f.endswith(".jar")])
- stats["mods"] = mod_count
- else:
- stats["mods"] = 0 # No mods folder
- except Exception as e:
- log_to_console(console, f"Błąd obliczania statystyk dla instancji {instance_path}: {e}", "ERROR")
- pass # Keep N/A if error occurs
- return stats
- def update_instance(version, settings):
- """Updates an instance's settings and regenerates its start.bat."""
- if version not in instances:
- log_to_console(console, f"Próba aktualizacji nieistniejącej instancji {version}.", "ERROR")
- return
- instance = instances[version]
- instance["settings"] = settings
- # Update Java info if the path changed
- selected_java_path = settings.get("java_path")
- if selected_java_path and selected_java_path != instance.get("java_path"):
- instance["java_path"] = selected_java_path
- instance["java_version"] = get_java_version(selected_java_path) # Update version based on new path
- instance["required_java"] = get_required_java(version, get_version_info(version)) # Recalculate required java
- # Regenerate start.bat with new settings
- info = get_version_info(version) # Need version info again
- if info:
- try:
- regenerate_start_bat(version, instance, info, console)
- # If bat regeneration was successful, mark ready? Not necessarily, files might be missing.
- # Let verify_instance handle the ready status.
- except Exception as e:
- log_to_console(console, f"Błąd podczas regeneracji start.bat po edycji instancji {version}: {e}", "ERROR")
- messagebox.showwarning("Błąd", f"Nie udało się zaktualizować pliku start.bat dla {version}.\nInstancja może nie działać poprawnie.")
- instance["ready"] = False # Mark as not ready if bat failed
- save_config()
- log_to_console(console, f"Instancja {version} zaktualizowana. Ustawienia i start.bat zostały zapisane.", "SUCCESS")
- refresh_instances()
- def show_download():
- frame = ttk.Frame(content_frame, padding="10")
- frame.pack(fill="both", expand=True)
- ttk.Label(frame, text="Pobierz nową wersję Minecraft", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
- form_frame = ttk.Frame(frame)
- form_frame.pack(fill="x", expand=False)
- form_frame.columnconfigure(1, weight=1)
- # Row 0: Version Filters
- ttk.Label(form_frame, text="Filtry wersji:", style="TLabel").grid(row=0, column=0, sticky="nw", pady=(0, 5))
- filters_frame = ttk.Frame(form_frame)
- filters_frame.grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
- # Use tk.Checkbutton
- tk.Checkbutton(filters_frame, text="Snapshoty", variable=snapshots_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
- tk.Checkbutton(filters_frame, text="Release", variable=releases_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
- tk.Checkbutton(filters_frame, text="Alpha", variable=alpha_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
- tk.Checkbutton(filters_frame, text="Beta", variable=beta_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=lambda: [save_config(), refresh_version_combo_dialog(combo, version_var)]).pack(anchor="w", side="left", padx=5)
- # Row 1: Minecraft Version
- ttk.Label(form_frame, text="Wersja Minecrafta:", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
- version_var = tk.StringVar()
- versions = get_versions()
- combo = ttk.Combobox(form_frame, textvariable=version_var, state="readonly", values=versions, style="TCombobox")
- combo.grid(row=1, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- # Set initial selection based on pending_version if any
- global pending_version, pending_instance_settings # Declare global before using
- if pending_version and pending_version in versions:
- version_var.set(pending_version)
- # Clear pending flags after setting the value
- pending_version = ""
- pending_instance_settings = {} # Clear settings too
- elif versions:
- combo.current(0)
- else:
- version_var.set("Brak wersji!")
- combo.config(state="disabled")
- messagebox.showwarning("Uwaga", "Nie znaleziono żadnych wersji Minecrafta. Sprawdź filtry i połączenie z internetem.")
- # Row 2: Action Button
- btn_frame = ttk.Frame(frame)
- btn_frame.pack(fill="x", pady=(15, 0))
- download_btn = ttk.Button(btn_frame, text="Pobierz Wybraną Wersję", command=lambda: start_download_process(version_var.get()), style="TButton", width=25)
- download_btn.pack()
- # Console frame below the form and button
- console_frame = ttk.Frame(frame)
- console_frame.pack(fill="both", expand=True, pady=(15, 0))
- global console
- console = scrolledtext.ScrolledText(
- console_frame, height=15, wrap=tk.WORD, bg=CONSOLE_BG, fg=CONSOLE_FG_DEFAULT,
- state="disabled", font=("Consolas", 9)
- )
- console.pack(side="top", fill="both", expand=True)
- # Console action buttons
- console_btn_frame = ttk.Frame(frame)
- console_btn_frame.pack(fill="x", pady=(5, 0))
- ttk.Button(console_btn_frame, text="Kopiuj konsolę", command=lambda: copy_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
- ttk.Button(console_btn_frame, text="Wyczyść konsolę", command=lambda: clear_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
- def start_download_process(version):
- global download_thread, download_active
- if download_active:
- messagebox.showwarning("Uwaga", "Pobieranie już w toku!")
- return
- if not version or version == "Brak wersji!":
- messagebox.showwarning("Uwaga", "Wybierz wersję Minecrafta do pobrania.")
- return
- # Ask for instance settings before starting download
- # Use default settings unless overridden
- # This creates a simpler download flow than the separate create instance dialog
- # Let's reuse the create_instance dialog logic to gather settings first.
- # The create_instance dialog will then call process_create_instance which sets pending vars and switches tab.
- # So, if we are on the Download tab and click "Download Selected Version", it means we skipped the create dialog.
- # In this case, use default settings.
- # Check if there are pending settings from a cancelled create dialog
- global pending_version, pending_instance_settings # Declare global before using them
- if pending_version and pending_version == version and pending_instance_settings:
- settings_to_use = pending_instance_settings
- pending_version = ""
- pending_instance_settings = {}
- log_to_console(console, f"Używam oczekujących ustawień dla wersji {version}.", "INFO")
- else:
- # Use default settings if no pending settings
- settings_to_use = {
- "username": username_var.get(),
- "memory": memory_var.get(),
- "shared_assets": shared_assets_var.get(),
- "shared_libraries": shared_libraries_var.get(),
- "shared_natives": shared_natives_var.get(),
- "loader_type": "vanilla", # Default download is vanilla
- "loader_version": "",
- "server_ip": "",
- "server_port": ""
- }
- log_to_console(console, f"Używam domyślnych ustawień dla pobierania wersji {version}.", "INFO")
- # Handle case where instance already exists
- if version in instances:
- 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']}"):
- log_to_console(console, f"Pobieranie instancji {version} anulowane przez użytkownika (instancja już istnieje).", "INFO")
- return # User cancelled replacement
- # If user confirmed replacement, delete the existing instance first
- log_to_console(console, f"Użytkownik potwierdził zastąpienie instancji {version}. Usuwam starą...", "INFO")
- try:
- instance_path = instances[version]["path"]
- if os.path.exists(instance_path):
- import shutil
- shutil.rmtree(instance_path)
- log_to_console(console, f"Usunięto folder starej instancji: {instance_path}", "INFO")
- del instances[version]
- save_config()
- log_to_console(console, f"Stara instancja {version} usunięta z konfiguracji.", "INFO")
- except Exception as e:
- messagebox.showerror("Błąd usuwania", f"Nie udało się usunąć istniejącej instancji {version}: {e}\nAnulowano pobieranie nowej.")
- log_to_console(console, f"Błąd podczas usuwania istniejącej instancji {version} przed pobieraniem nowej: {e}", "ERROR")
- return # Stop the process if old instance couldn't be removed
- # Start the download thread
- download_active = True
- # Clear console before starting
- clear_console_content(console)
- log_to_console(console, f"Rozpoczynam proces pobierania wersji {version} z ustawieniami...", "INFO")
- # Pass the settings gathered or defaulted
- download_thread = threading.Thread(
- target=download_version,
- args=(version, settings_to_use, console),
- daemon=True
- )
- download_thread.start()
- def show_settings():
- frame = ttk.Frame(content_frame, padding="10")
- frame.pack(fill="both", expand=True)
- ttk.Label(frame, text="Ustawienia Launchera", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
- form_frame = ttk.Frame(frame)
- form_frame.pack(fill="x", expand=False)
- form_frame.columnconfigure(1, weight=1) # Allow second column to expand
- # Row 0: Default Username
- ttk.Label(form_frame, text="Domyślna nazwa użytkownika:", style="TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5))
- ttk.Entry(form_frame, textvariable=username_var, style="TEntry").grid(row=0, column=1, sticky="ew", pady=(0, 5), padx=(5, 0))
- # Row 1: Default Memory
- ttk.Label(form_frame, text="Domyślna pamięć RAM (GB):", style="TLabel").grid(row=1, column=0, sticky="w", pady=(10, 5))
- memory_spin = tk.Spinbox(form_frame, from_=1, to=32, textvariable=memory_var, width=5,
- bg="#333333", fg=PRIMARY_FG, highlightthickness=0,
- buttonbackground=TERTIARY_BG, buttonforeground=PRIMARY_FG, insertbackground=PRIMARY_FG)
- memory_spin.grid(row=1, column=1, sticky="w", pady=(10, 5), padx=(5, 0))
- # Row 2: Default Shared Folders
- ttk.Label(form_frame, text="Domyślne opcje współdzielenia:", style="TLabel").grid(row=2, column=0, sticky="nw", pady=(10, 5))
- options_frame = ttk.Frame(form_frame)
- options_frame.grid(row=2, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- tk.Checkbutton(options_frame, text="Współdziel assets między instancjami", variable=shared_assets_var,
- bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
- font=("Segoe UI", 9), command=save_config).pack(anchor="w", pady=2)
- tk.Checkbutton(options_frame, text="Współdziel biblioteki między instancjami", variable=shared_libraries_var,
- bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
- font=("Segoe UI", 9), command=save_config).pack(anchor="w", pady=2)
- tk.Checkbutton(options_frame, text="Współdziel natywne biblioteki między instancjami", variable=shared_natives_var,
- bg=PRIMARY_BG, fg=PRIMARY_FG, selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG,
- font=("Segoe UI", 9), command=save_config).pack(anchor="w", pady=2)
- # Row 3: Default Version Filters
- ttk.Label(form_frame, text="Domyślne filtry wersji:", style="TLabel").grid(row=3, column=0, sticky="nw", pady=(10, 5))
- filters_frame = ttk.Frame(form_frame)
- filters_frame.grid(row=3, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- tk.Checkbutton(filters_frame, text="Snapshoty", variable=snapshots_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=save_config).pack(anchor="w", pady=2)
- tk.Checkbutton(filters_frame, text="Release", variable=releases_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=save_config).pack(anchor="w", pady=2)
- tk.Checkbutton(filters_frame, text="Alpha", variable=alpha_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=save_config).pack(anchor="w", pady=2)
- tk.Checkbutton(filters_frame, text="Beta", variable=beta_var, bg=PRIMARY_BG, fg=PRIMARY_FG,
- selectcolor=PRIMARY_BG, activebackground=PRIMARY_BG, activeforeground=PRIMARY_FG, font=("Segoe UI", 9),
- command=save_config).pack(anchor="w", pady=2)
- # Row 4: Java Paths Cache (Display only)
- ttk.Label(form_frame, text="Znalezione Javy (Cache):", style="TLabel").grid(row=4, column=0, sticky="nw", pady=(10, 5))
- java_cache_frame = ttk.Frame(form_frame)
- java_cache_frame.grid(row=4, column=1, sticky="ew", pady=(10, 5), padx=(5, 0))
- if java_versions_cache:
- cache_text = "\n".join([f"v{ver}: {path}" for ver, path in java_versions_cache.items()])
- ttk.Label(java_cache_frame, text=cache_text, style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
- else:
- ttk.Label(java_cache_frame, text="Brak znalezionych Jav w cache.", style="TLabel", font=("Segoe UI", 9)).pack(anchor="w")
- # Action button - Save is handled by checkboxes and entry lose focus (implicitly by config save)
- # Adding an explicit save button for completeness, though not strictly needed for current vars
- btn_frame = ttk.Frame(frame)
- btn_frame.pack(fill="x", pady=(15, 0))
- ttk.Button(btn_frame, text="Zapisz Ustawienia", command=save_config, style="TButton", width=20).pack()
- def show_console():
- frame = ttk.Frame(content_frame, padding="10")
- frame.pack(fill="both", expand=True)
- ttk.Label(frame, text="Konsola Launchera", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
- console_frame = ttk.Frame(frame)
- console_frame.pack(fill="both", expand=True)
- global console
- # Recreate the console widget if it was destroyed when switching tabs
- if console is None or not console.winfo_exists():
- console = scrolledtext.ScrolledText(
- console_frame, height=20, wrap=tk.WORD, bg=CONSOLE_BG, fg=CONSOLE_FG_DEFAULT,
- state="disabled", font=("Consolas", 9)
- )
- # Apply tags if they don't exist (will be applied on first log or in log_to_console)
- console.pack(side="top", fill="both", expand=True)
- # Console action buttons
- btn_frame = ttk.Frame(frame)
- btn_frame.pack(fill="x", pady=(10, 0))
- ttk.Button(btn_frame, text="Kopiuj", command=lambda: copy_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
- ttk.Button(btn_frame, text="Wyczyść", command=lambda: clear_console_content(console), style="TButton", width=15).pack(side="left", padx=5)
- def show_tools():
- frame = ttk.Frame(content_frame, padding="10")
- frame.pack(fill="both", expand=True)
- ttk.Label(frame, text="Narzędzia Launchera", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
- tools_frame = ttk.Frame(frame)
- tools_frame.pack(fill="both", expand=True)
- # tools_frame.columnconfigure(0, weight=1) # Allow buttons to expand? Or just pack.
- # Config Import/Export
- ttk.Label(tools_frame, text="Konfiguracja:", font=("Segoe UI", 11, "bold"), style="TLabel").pack(anchor="w", pady=(10, 5))
- config_btn_frame = ttk.Frame(tools_frame)
- config_btn_frame.pack(fill="x", pady=(5, 10))
- ttk.Button(config_btn_frame, text="Eksportuj Konfigurację", command=export_config, style="TButton", width=25).pack(side="left", padx=10)
- ttk.Button(config_btn_frame, text="Importuj Konfigurację", command=import_config, style="TButton", width=25).pack(side="left", padx=10)
- 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)
- # Java Management (Optional, could list found Javas and allow removing from cache)
- # ttk.Label(tools_frame, text="Zarządzanie Javą:", font=("Segoe UI", 11, "bold"), style="TLabel").pack(anchor="w", pady=(10, 5))
- # Java management could be a complex table listing found Javas and options. Omit for now.
- # Other potential tools:
- # - Clean temporary files
- # - Repair shared assets/libraries
- # - Re-discover Java installations
- def show_modrinth_browser():
- frame = ttk.Frame(content_frame, padding="10")
- frame.pack(fill="both", expand=True)
- ttk.Label(frame, text="Przeglądaj Mody (Modrinth)", font=("Segoe UI", 14, "bold")).pack(pady=(0, 15))
- search_frame = ttk.Frame(frame)
- search_frame.pack(fill="x", expand=False, pady=(0, 10))
- search_frame.columnconfigure(1, weight=1)
- ttk.Label(search_frame, text="Szukaj:", style="TLabel").grid(row=0, column=0, sticky="w", padx=5)
- search_term_var = tk.StringVar()
- search_entry = ttk.Entry(search_frame, textvariable=search_term_var, style="TEntry")
- search_entry.grid(row=0, column=1, sticky="ew", padx=5)
- ttk.Label(search_frame, text="Wersja MC:", style="TLabel").grid(row=0, column=2, sticky="w", padx=5)
- 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
- mc_version_var = tk.StringVar(value=mc_versions[0] if mc_versions else "") # Default to newest instance version or empty
- mc_version_combo = ttk.Combobox(search_frame, textvariable=mc_version_var, state="readonly", values=mc_versions, style="TCombobox", width=12)
- mc_version_combo.grid(row=0, column=3, sticky="w", padx=5)
- if mc_versions:
- mc_version_combo.current(0)
- ttk.Label(search_frame, text="Loader:", style="TLabel").grid(row=0, column=4, sticky="w", padx=5)
- loader_types = ["any", "fabric", "forge", "quilt", "neoforge"] # Modrinth supports these loaders
- loader_var = tk.StringVar(value="any")
- loader_combo = ttk.Combobox(search_frame, textvariable=loader_var, state="readonly", values=loader_types, style="TCombobox", width=8)
- loader_combo.grid(row=0, column=5, sticky="w", padx=5)
- loader_combo.current(0)
- 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)
- search_button.grid(row=0, column=6, sticky="e", padx=5)
- # Mod List (Treeview)
- mod_list_frame = ttk.Frame(frame)
- mod_list_frame.pack(fill="both", expand=True)
- columns = ("title", "author", "downloads", "version", "loaders", "description") # Added description, loaders, version
- modrinth_tree = ttk.Treeview(
- mod_list_frame, columns=columns, show="headings", selectmode="browse",
- style="Treeview"
- )
- modrinth_tree.heading("title", text="Tytuł Modu", anchor="w")
- modrinth_tree.heading("author", text="Autor", anchor="w")
- modrinth_tree.heading("downloads", text="Pobrania", anchor="e")
- modrinth_tree.heading("version", text="Wersja Modu", anchor="w") # Version of the mod file, not MC version
- modrinth_tree.heading("loaders", text="Loadery", anchor="w") # Loaders supported by the mod file
- modrinth_tree.heading("description", text="Opis", anchor="w") # Hidden by default
- modrinth_tree.column("title", width=200, anchor="w")
- modrinth_tree.column("author", width=100, anchor="w")
- modrinth_tree.column("downloads", width=80, anchor="e", stretch=tk.NO)
- modrinth_tree.column("version", width=100, anchor="w", stretch=tk.NO)
- modrinth_tree.column("loaders", width=120, anchor="w")
- modrinth_tree.column("description", width=0, stretch=tk.NO) # Hide description initially
- scrollbar = ttk.Scrollbar(mod_list_frame, orient="vertical", command=modrinth_tree.yview)
- scrollbar.pack(side="right", fill="y")
- modrinth_tree.configure(yscrollcommand=scrollbar.set)
- modrinth_tree.pack(side="left", fill="both", expand=True)
- # Modrinth item context menu
- mod_context_menu = tk.Menu(root, tearoff=0)
- mod_context_menu.add_command(label="Pobierz do instancji", command=lambda: download_mod_to_instance(modrinth_tree))
- mod_context_menu.add_command(label="Otwórz na Modrinth", command=lambda: open_modrinth_page(modrinth_tree))
- def show_modrinth_context_menu(event):
- selected_item = modrinth_tree.focus()
- if selected_item:
- try:
- mod_context_menu.tk_popup(event.x_root, event.y_root)
- finally:
- mod_context_menu.grab_release()
- modrinth_tree.bind("<Button-3>", show_modrinth_context_menu)
- # Action frame below mod list
- mod_action_frame = ttk.Frame(frame)
- mod_action_frame.pack(fill="x", pady=(10, 0))
- ttk.Label(mod_action_frame, text="Pobierz do instancji:", style="TLabel").pack(side="left", padx=5)
- # Dropdown to select instance to download to
- instance_versions = sorted(instances.keys(), key=lambda v: pkg_version.parse(v) if v[0].isdigit() else v, reverse=True)
- global selected_modrinth_instance_var
- selected_modrinth_instance_var.set(instance_versions[0] if instance_versions else "")
- instance_combo = ttk.Combobox(mod_action_frame, textvariable=selected_modrinth_instance_var, state="readonly", values=instance_versions, style="TCombobox", width=20)
- instance_combo.pack(side="left", padx=5)
- download_mod_button = ttk.Button(mod_action_frame, text="Pobierz Wybrany Mod", command=lambda: download_mod_to_instance(modrinth_tree), style="TButton", width=20)
- download_mod_button.pack(side="left", padx=5)
- if not instance_versions:
- instance_combo.config(state="disabled")
- download_mod_button.config(state="disabled")
- ttk.Label(mod_action_frame, text="Brak instancji do pobrania modów.", style="TLabel", foreground=WARNING_COLOR).pack(side="left", padx=5)
- else:
- # Add tooltip to download button
- Tooltip(download_mod_button, "Pobiera wybrany mod do folderu mods/ wybranej instancji.\nUpewnij się, że instancja ma odpowiedni mod loader!")
- def search_modrinth(search_term, mc_version, loader, tree):
- """Searches Modrinth API and populates the treeview."""
- # Clear previous results
- for item in tree.get_children():
- tree.delete(item)
- if not search_term and not mc_version:
- # No search criteria
- log_to_console(console, "Wpisz frazę do wyszukania lub wybierz wersję MC.", "WARNING")
- return
- log_to_console(console, f"Szukam modów na Modrinth: '{search_term}', wersja MC='{mc_version}', loader='{loader}'...", "INFO")
- # Modrinth API Endpoint: https://api.modrinth.com/v2/search
- url = "https://api.modrinth.com/v2/search"
- params = {
- "query": search_term,
- "limit": 50, # Limit results
- "facets": json.dumps([ # Filter by version and loader
- [f"versions:{mc_version}"] if mc_version else [],
- [f"project_type:mod"], # Search only for mods
- [f"loaders:{loader}"] if loader != "any" else []
- ])
- }
- try:
- response = requests.get(url, params=params, timeout=10)
- response.raise_for_status() # Raise an exception for bad status codes
- results = response.json()
- if results and "hits" in results:
- log_to_console(console, f"Znaleziono {results['total_hits']} wyników.", "INFO")
- for hit in results["hits"]:
- # Need to fetch file details to get exact mod version, supported loaders for that file etc.
- # The search results ("hits") provide project-level info, not file-level details directly related to the filters.
- # This makes selecting the *correct file* for download tricky from just the search result.
- # A more robust approach would be:
- # 1. Search projects.
- # 2. For a selected project, fetch its versions.
- # 3. Filter project versions by MC version and loader.
- # 4. Display file(s) for the matching versions.
- # 5. User selects a specific file to download.
- # Simplified Approach for now: Display project info. The download button will *attempt* to find a matching file.
- # This might download the wrong file or fail if no exact match exists.
- tree.insert("", "end", iid=hit["project_id"], values=(
- hit.get("title", "Bez tytułu"),
- hit.get("author", "Nieznany autor"),
- hit.get("downloads", 0), # Total project downloads
- hit.get("latest_version", "N/A"), # Latest project version string (might not be for selected MC/loader)
- ", ".join(hit.get("loaders", [])), # Project-level loaders
- hit.get("description", "Bez opisu")
- ))
- # Add tooltip to treeview items to show description? Or show description in a label below?
- # Tooltip on item is cleaner but requires more complex event binding.
- else:
- log_to_console(console, "Nie znaleziono modów spełniających kryteria.", "INFO")
- except requests.exceptions.RequestException as e:
- log_to_console(console, f"Błąd API Modrinth podczas wyszukiwania: {e}", "ERROR")
- messagebox.showerror("Błąd API Modrinth", f"Nie udało się wyszukać modów: {e}")
- except Exception as e:
- log_to_console(console, f"Nieoczekiwany błąd podczas wyszukiwania Modrinth: {e}", "ERROR")
- messagebox.showerror("Nieoczekiwany błąd", f"Wystąpił błąd podczas wyszukiwania: {e}")
- def download_mod_to_instance(tree):
- """Downloads the selected mod file from Modrinth to the selected instance's mods folder."""
- selected_item = tree.focus()
- if not selected_item:
- messagebox.showwarning("Uwaga", "Wybierz mod z listy, aby go pobrać.")
- return
- project_id = tree.item(selected_item, "iid")
- if not project_id:
- messagebox.showerror("Błąd", "Nie udało się uzyskać ID projektu Modrinth.")
- return
- target_instance_version = selected_modrinth_instance_var.get()
- if not target_instance_version or target_instance_version not in instances:
- messagebox.showwarning("Uwaga", "Wybierz instancję docelową z listy rozwijanej pod wynikami wyszukiwania.")
- return
- instance = instances.get(target_instance_version)
- if not instance: # Should not happen if in instances list, but check
- messagebox.showerror("Błąd", "Dane wybranej instancji nie zostały znalezione.")
- return
- log_to_console(console, f"Przygotowanie do pobrania modu '{tree.item(selected_item)['values'][0]}' (ID: {project_id}) do instancji '{target_instance_version}'...", "INFO")
- # Need to find the correct file for the selected instance's MC version and loader type.
- # Modrinth API: /project/{id}/version
- versions_url = f"https://api.modrinth.com/v2/project/{project_id}/version"
- instance_mc_version = target_instance_version # MC version is the instance key
- instance_loader = instance["settings"].get("loader_type", "vanilla")
- # Filter by game versions and loaders
- params = {
- "game_versions": json.dumps([instance_mc_version]),
- "loaders": json.dumps([instance_loader]) if instance_loader != "vanilla" else json.dumps([]) # Filter by loader unless vanilla
- }
- if instance_loader == "vanilla":
- # Vanilla instances technically don't need a loader filter, but mods listed might require one.
- # Maybe search for files that list NO loaders or 'any'? Modrinth facets might handle this.
- # For simplicity, if instance is vanilla, we filter by MC version only.
- # If mod requires a loader, it might still be listed, but won't work. User needs to know.
- params = {"game_versions": json.dumps([instance_mc_version])}
- try:
- response = requests.get(versions_url, params=params, timeout=10)
- response.raise_for_status()
- mod_versions = response.json() # This is a list of versions of the mod project
- if not mod_versions:
- 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")
- 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}).")
- return
- # Find the most recent version that matches criteria and has a downloadable file
- # Modrinth versions are often sorted newest first by default, but explicitly sort by release date or version string
- sorted_mod_versions = sorted(mod_versions, key=lambda v: v.get("date_published", ""), reverse=True)
- best_file = None
- mod_version_info = None
- for mod_v in sorted_mod_versions:
- # Find a primary file in this version
- primary_file = next((f for f in mod_v.get("files", []) if f.get("primary")), None)
- if not primary_file and mod_v.get("files"):
- primary_file = mod_v["files"][0] # Take first file if no primary marked
- if primary_file and primary_file.get("url"):
- # Optional: double check loaders listed in the file itself match instance loader
- # This is more precise than project-level loaders
- file_loaders = primary_file.get("loaders", [])
- if instance_loader != "vanilla" and instance_loader not in file_loaders:
- # Skip this file if instance requires a loader but file doesn't list it
- continue
- if instance_loader == "vanilla" and file_loaders:
- # Skip this file if instance is vanilla but file requires a loader
- continue
- best_file = primary_file
- mod_version_info = mod_v
- break # Found a suitable file, break loop
- if not best_file:
- log_to_console(console, f"Znaleziono wersje modu, ale brak pliku do pobrania kompatybilnego z MC {instance_mc_version} i loaderem '{instance_loader}'.", "WARNING")
- 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}).")
- return
- file_url = best_file["url"]
- file_name = best_file.get("filename") or file_url.split("/")[-1] # Use provided filename or extract from URL
- file_sha1 = best_file.get("hashes", {}).get("sha1") # Modrinth provides sha1 and sha512
- mod_target_path = os.path.join(instance["path"], "mods", file_name)
- if os.path.exists(mod_target_path) and file_sha1 and verify_sha1(mod_target_path, file_sha1):
- log_to_console(console, f"Mod '{file_name}' już istnieje i jest poprawny w instancji {target_instance_version}. Pomijam pobieranie.", "INFO")
- messagebox.showinfo("Już istnieje", f"Mod '{file_name}' już istnieje i jest poprawny w instancji {target_instance_version}.")
- return
- # Start download in a thread
- log_to_console(console, f"Pobieranie modu '{file_name}' ({mod_version_info.get('version_number', 'N/A')}) do instancji {target_instance_version}...", "INFO")
- # Pass a specific progress bar/label for mod downloads if needed, or use global ones
- # Using global ones requires careful state management if simultaneous downloads are possible (they aren't currently)
- # Let's use the global progress bar, maybe temporarily update status label format
- # Store original status label text and restore after mod download
- original_status_text = global_status_label.cget("text")
- def mod_download_callback(downloaded, total):
- # Use the global progress bar, but format status label for mod download
- if global_progress_bar and global_status_label:
- progress = (downloaded / total) * 100 if total > 0 else 0
- global_progress_bar["value"] = progress
- remaining_bytes = max(0, total - downloaded)
- global_status_label.config(text=f"[Modrinth] Pobieranie '{file_name}': {progress:.1f}% | Pozostało: {humanize.naturalsize(remaining_bytes)}")
- root.update_idletasks()
- def mod_download_complete():
- # Restore original status label text
- if global_status_label:
- global_status_label.config(text=original_status_text)
- global_progress_bar["value"] = 0 # Reset bar
- log_to_console(console, f"Pobieranie modu '{file_name}' zakończone.", "INFO")
- # Optionally run verify_instance again, but it's slow.
- # Just show success message.
- messagebox.showinfo("Pobrano Mod", f"Mod '{file_name}' został pomyślnie pobrany do instancji {target_instance_version}.")
- # Check for dependencies? Very complex. Omit for now.
- def mod_download_failed(error_msg):
- if global_status_label:
- global_status_label.config(text=original_status_text)
- global_progress_bar["value"] = 0 # Reset bar
- log_to_console(console, f"Pobieranie modu '{file_name}' nie powiodło się: {error_msg}", "ERROR")
- messagebox.showerror("Błąd Pobierania Modu", f"Nie udało się pobrać modu '{file_name}': {error_msg}")
- # Run download in a new thread
- mod_download_thread = threading.Thread(
- target=lambda: [
- log_to_console(console, f"Rozpoczynam wątek pobierania modu {file_name}", "INFO"),
- download_success := download_file(file_url, mod_target_path, mod_download_callback, file_sha1, console, description=f"mod {file_name}"),
- mod_download_complete() if download_success else mod_download_failed("Błąd pobierania lub weryfikacji")
- ],
- daemon=True # Allow thread to close with main app
- )
- mod_download_thread.start()
- except requests.exceptions.RequestException as e:
- log_to_console(console, f"Błąd API Modrinth podczas pobierania modu: {e}", "ERROR")
- messagebox.showerror("Błąd API Modrinth", f"Nie udało się pobrać szczegółów modu: {e}")
- except Exception as e:
- log_to_console(console, f"Nieoczekiwany błąd podczas pobierania modu Modrinth: {e}", "ERROR")
- messagebox.showerror("Nieoczekiwany błąd", f"Wystąpił błąd podczas pobierania modu: {e}")
- def open_modrinth_page(tree):
- """Opens the Modrinth project page for the selected mod."""
- selected_item = tree.focus()
- if not selected_item:
- messagebox.showwarning("Uwaga", "Wybierz mod z listy.")
- return
- project_id = tree.item(selected_item, "iid")
- if not project_id:
- messagebox.showerror("Błąd", "Nie udało się uzyskać ID projektu Modrinth.")
- return
- project_slug = tree.item(selected_item)['values'][0].replace(" ", "-").lower() # Attempt to create a slug
- # A more reliable way would be to fetch project details to get the actual slug
- # Modrinth URL is https://modrinth.com/mod/{slug} or /project/{slug}
- url = f"https://modrinth.com/project/{project_id}" # Using ID is more reliable than guessing slug
- try:
- webbrowser.open(url)
- log_to_console(console, f"Otwieram stronę Modrinth dla projektu {project_id}: {url}", "INFO")
- except Exception as e:
- messagebox.showerror("Błąd otwierania strony", f"Nie udało się otworzyć strony w przeglądarce: {e}")
- log_to_console(console, f"Nie udało się otworzyć strony Modrinth {url}: {e}", "ERROR")
- def find_java_with_source():
- """Same as find_java but returns tuples including the source (Cache, Pobrana, JAVA_HOME, etc.)."""
- # This is largely duplicated from find_java for clarity, could refactor
- possible_paths = []
- system = platform.system()
- java_exec_name = "java.exe" if system == "Windows" else "java"
- # Check cache first
- for ver, path in java_versions_cache.items():
- if os.path.exists(path):
- # Check if it's actually a 64-bit java executable without verbose logging
- try:
- result = subprocess.run([path, "-version"], capture_output=True, text=True, timeout=1, encoding='utf-8', errors='ignore')
- is_64bit = "64-Bit" in result.stderr or "64-bit" in result.stderr or "x86_64" in result.stderr.lower()
- if is_64bit:
- possible_paths.append((path, ver, "Cache"))
- except:
- pass # Ignore errors for paths in cache that might be invalid
- # Check custom JAVA_DIR installations
- if os.path.exists(JAVA_DIR):
- for java_folder in os.listdir(JAVA_DIR):
- java_path = os.path.join(JAVA_DIR, java_folder)
- found_exec = None
- for root, _, files in os.walk(java_path):
- if java_exec_name in files:
- found_exec = os.path.join(root, java_exec_name)
- break
- if found_exec:
- version = get_java_version(found_exec) # This one *will* log
- if version: # get_java_version only returns 64-bit versions
- possible_paths.append((found_exec, version, "Pobrana"))
- # Check standard system locations (JAVA_HOME, PATH, Program Files) - these will also log
- java_home = os.environ.get("JAVA_HOME")
- if java_home:
- java_path = os.path.join(java_home, "bin", java_exec_name)
- if os.path.exists(java_path):
- version = get_java_version(java_path)
- if version:
- possible_paths.append((java_path, version, "JAVA_HOME"))
- try:
- command = ["where", java_exec_name] if system == "Windows" else ["which", java_exec_name]
- out = subprocess.check_output(command, stderr=subprocess.DEVNULL).decode().strip()
- for line in out.splitlines():
- line = line.strip()
- if os.path.exists(line):
- version = get_java_version(line)
- if version:
- possible_paths.append((line, version, "PATH"))
- except:
- pass
- if system == "Windows":
- for base in [os.environ.get('ProgramFiles'), os.environ.get('ProgramFiles(x86)'), "C:\\Program Files\\Java", "C:\\Program Files (x86)\\Java"]:
- if base and os.path.isdir(base):
- java_root = os.path.join(base, "Java")
- if os.path.isdir(java_root):
- for item in os.listdir(java_root):
- java_path = os.path.join(java_root, item, "bin", java_exec_name)
- if os.path.exists(java_path):
- version = get_java_version(java_path)
- if version:
- possible_paths.append((java_path, version, "Program Files"))
- # Ensure paths are unique (based on path itself) and prefer certain sources
- unique_paths = {}
- source_order = {"Cache": 0, "Pobrana": 1, "JAVA_HOME": 2, "PATH": 3, "Program Files": 4}
- for path, ver, source in possible_paths:
- if path not in unique_paths or source_order.get(source, 99) < source_order.get(unique_paths[path][2], 99):
- unique_paths[path] = (path, ver, source)
- sorted_unique_paths = sorted(unique_paths.values(), key=lambda item: (source_order.get(item[2], 99), item[0]))
- return sorted_unique_paths
- # --- Main Application Setup ---
- if __name__ == "__main__":
- print("Inicjalizacja Launchera...")
- # Ensure base directories exist early
- os.makedirs(BASE_DIR, exist_ok=True)
- os.makedirs(ASSETS_DIR, exist_ok=True)
- os.makedirs(LIBRARIES_DIR, exist_ok=True)
- os.makedirs(NATIVES_DIR, exist_ok=True)
- os.makedirs(LOGS_DIR, exist_ok=True)
- os.makedirs(JAVA_DIR, exist_ok=True)
- os.makedirs(ICONS_DIR, exist_ok=True) # For potential custom icons
- # Generate a dummy logo if icons dir is empty, so Tkinter doesn't crash trying to load non-existent file
- dummy_logo_path = os.path.join(ICONS_DIR, "logo.png")
- if not os.path.exists(dummy_logo_path):
- try:
- dummy_img = Image.new('RGB', (180, 60), color = (40,40,40))
- dummy_img.save(dummy_logo_path)
- print("Utworzono domyślne logo.")
- except Exception as e:
- print(f"Nie udało się utworzyć domyślnego logo: {e}")
- root = tk.Tk()
- root.title("Minecraft Launcher by Paffcio")
- root.geometry("1200x800") # Wider window
- root.configure(bg=PRIMARY_BG)
- root.minsize(1000, 700) # Minimum size
- # Load configuration immediately after Tkinter root is created
- load_config()
- # --- Styling ---
- style = ttk.Style()
- style.theme_use('clam') # 'clam', 'alt', 'default', 'classic'
- # Configure main styles
- style.configure("TFrame", background=PRIMARY_BG)
- style.configure("TLabel", background=PRIMARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10))
- style.configure("TButton", background=BUTTON_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10), borderwidth=0)
- style.map("TButton",
- background=[('active', BUTTON_HOVER), ('pressed', BUTTON_HOVER)],
- foreground=[('active', PRIMARY_FG), ('pressed', PRIMARY_FG)])
- style.configure("TEntry", fieldbackground="#333333", foreground=PRIMARY_FG, insertcolor=PRIMARY_FG, borderwidth=1)
- style.configure("TCombobox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, borderwidth=1,
- arrowcolor=PRIMARY_FG, selectforeground=PRIMARY_FG, insertcolor=PRIMARY_FG) # Added colors for combobox elements
- # Style for Combobox dropdown list (requires accessing TCombobox.Listbox)
- # style.configure("TCombobox.Listbox", fieldbackground="#333333", foreground=PRIMARY_FG, selectbackground=ACCENT_COLOR, selectforeground=PRIMARY_FG) # Does not work directly
- style.configure("Horizontal.TProgressbar", troughcolor="#333333", background=ACCENT_COLOR, thickness=10)
- # Treeview styling
- style.configure("Treeview", background=SECONDARY_BG, fieldbackground=SECONDARY_BG, foreground=PRIMARY_FG, rowheight=25, borderwidth=0)
- style.map("Treeview", background=[('selected', ACCENT_COLOR)])
- style.configure("Treeview.Heading", background=TERTIARY_BG, foreground=PRIMARY_FG, font=("Segoe UI", 10, "bold"))
- style.layout("Treeview", [('Treeview.treearea', {'sticky': 'nswe'})]) # Remove borders around treearea
- # --- Layout ---
- # Progress bar and status label at the very bottom
- progress_frame = ttk.Frame(root)
- progress_frame.pack(side="bottom", fill="x", padx=10, pady=(0, 5))
- global_progress_bar = ttk.Progressbar(progress_frame, length=400, mode="determinate", style="Horizontal.TProgressbar")
- global_progress_bar.pack(side="left", fill="x", expand=True, padx=(0, 10))
- global_status_label = ttk.Label(progress_frame, text="Gotowe do działania!", foreground=PRIMARY_FG, background=PRIMARY_BG, font=("Segoe UI", 9))
- global_status_label.pack(side="left", padx=(10, 0))
- # Sidebar on the left
- sidebar = tk.Frame(root, bg=SIDEBAR_BG, width=250) # Wider sidebar
- sidebar.pack(side="left", fill="y", padx=0, pady=0)
- sidebar.pack_propagate(False) # Prevent sidebar from resizing to fit content
- # Logo area
- logo_frame = tk.Frame(sidebar, bg=SIDEBAR_BG)
- logo_frame.pack(fill="x", pady=(10, 20))
- try:
- # Load and display logo
- logo_img = Image.open(os.path.join(ICONS_DIR, "logo.png"))
- # Ensure image is RGB to save space and avoid transparency issues with some loaders
- if logo_img.mode == 'RGBA':
- logo_img = logo_img.convert('RGB')
- logo_img = logo_img.resize((min(logo_img.width, 200), min(logo_img.height, 80)), Image.LANCZOS) # Resize constraints
- logo_photo = ImageTk.PhotoImage(logo_img)
- logo_label = tk.Label(logo_frame, image=logo_photo, bg=SIDEBAR_BG)
- logo_label.image = logo_photo # Keep a reference!
- logo_label.pack(pady=5)
- except Exception as e:
- print(f"Błąd ładowania lub wyświetlania logo: {e}")
- # Fallback if logo fails
- ttk.Label(logo_frame, text="Minecraft Launcher", bg=SIDEBAR_BG, fg=PRIMARY_FG, font=("Segoe UI", 12, "bold")).pack(pady=20)
- # Navigation buttons (Tabs)
- tabs = [
- ("Instancje", "📋"),
- ("Pobieranie", "⬇"),
- ("Modrinth", "📦"), # New tab for Modrinth
- ("Narzędzia", "🔧"), # New tab for tools (Import/Export etc.)
- ("Ustawienia", "⚙"),
- ("Konsola", "📜")
- ]
- tab_buttons = {}
- for tab_name, icon in tabs:
- btn = tk.Button(
- sidebar, text=f" {icon} {tab_name}", bg=SIDEBAR_BG, fg=PRIMARY_FG,
- activebackground=ACTIVE_TAB_COLOR, activeforeground=PRIMARY_FG, # Active state when clicked
- font=("Segoe UI", 11), relief="flat", anchor="w", padx=15, pady=12, # Increased padding
- command=lambda t=tab_name: switch_tab(t)
- )
- btn.pack(fill="x", pady=1) # Added small vertical padding
- # Initial hover binding (will be unbound for the active tab)
- btn.bind("<Enter>", lambda e, b=btn: b.config(bg=HOVER_TAB_COLOR) if current_tab.get() != tab_name else None)
- btn.bind("<Leave>", lambda e, b=btn: b.config(bg=SIDEBAR_BG) if current_tab.get() != tab_name else None)
- tab_buttons[tab_name] = btn
- # Content area on the right
- content_frame = ttk.Frame(root, style="TFrame")
- content_frame.pack(side="left", fill="both", expand=True, padx=10, pady=10)
- # --- Initial View ---
- switch_tab("Instancje") # Start on the instances tab
- # Ensure console is initialized if the starting tab is not Console
- # log_to_console function now handles console=None gracefully,
- # but for the console tab itself, it needs to be created.
- # The switch_tab function now handles creating the console widget
- # when the Console tab is selected.
- log_to_console(console, "Launcher uruchomiony.", "INFO")
- root.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement