commit 8bafe167acb55bf9907432ebaddadbcfbfbbba02 Author: VIDOKS Date: Fri Sep 5 02:00:17 2025 +0200 Initial commit - PedaloLauncher with Microsoft Auth - Modern Microsoft OAuth2 authentication - Always-available connect button (never gets disabled) - Dynamic port allocation (25565-25575) for Edge compatibility - Auto-connect to PEDALO server (pedalo.vidoks.fr:25565) - Dark theme UI with modern design - Version management with auto-download - Fixed browser closure handling Features: ✅ Button always clickable - no more gray/disabled state ✅ Compatible with Microsoft Edge, Chrome, Firefox ✅ Automatic port conflict resolution ✅ Clear error messages and user guidance ✅ Professional launcher interface Ready for PEDALO server deployment! diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67200a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +### Java ### +*.class +*.jar +*.war +*.ear +*.aar +hs_err_pid* +replay_pid* + +### Build directories ### +build/ +out/ +target/ +dist/ +temp/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +### Windows ### +Thumbs.db +ehthumbs.db +Desktop.ini + +### Logs ### +*.log + +### Temporary files ### +*.tmp +*.temp +*~ + +### Claude Code ### +.claude/ \ No newline at end of file diff --git a/MANIFEST.MF b/MANIFEST.MF new file mode 100644 index 0000000..e8ba9b0 --- /dev/null +++ b/MANIFEST.MF @@ -0,0 +1 @@ +Main-Class: com.minecraftlauncher.Main diff --git a/README.md b/README.md new file mode 100644 index 0000000..78db07c --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Minecraft Launcher + +Launcher Minecraft personnalisé avec authentification Microsoft moderne et connexion automatique au serveur PEDALO. + +## Fonctionnalités + +- 🔐 **Authentification Microsoft** avec OAuth2 moderne +- 🎮 **Connexion automatique** au serveur PEDALO (pedalo.vidoks.fr:25565) +- 📦 **Gestion des versions** Minecraft avec téléchargement automatique +- 🎨 **Interface moderne** avec thème sombre +- 🔄 **Bouton de connexion toujours disponible** - cliquez autant de fois que nécessaire +- 🌐 **Compatible avec tous les navigateurs** (Edge, Chrome, Firefox) + +## Installation + +### Prérequis +- Java 8 ou supérieur +- Connexion Internet +- Compte Microsoft avec Minecraft Java Edition + +### Utilisation +```bash +java -jar MinecraftLauncher.jar +``` + +## Structure du Projet + +``` +MinecraftLauncher/ +├── src/com/minecraftlauncher/ +│ ├── Main.java # Point d'entrée +│ ├── auth/ +│ │ └── ModernAuthManager.java # Authentification Microsoft OAuth2 +│ ├── game/ +│ │ └── GameLauncher.java # Lancement du jeu +│ ├── model/ +│ │ └── Account.java # Modèle de compte utilisateur +│ └── ui/ +│ ├── LauncherFrame.java # Interface principale +│ ├── VersionsTabDialog.java # Dialogue de versions +│ ├── InstalledVersionsDialog.java # Dialogue versions installées +│ └── DarkThemeUtils.java # Utilitaires thème sombre +├── lib/ # Bibliothèques (Gson) +├── MANIFEST.MF # Manifeste JAR +└── MinecraftLauncher.jar # Exécutable final +``` + +## Développement + +### Compilation +```bash +javac -cp "lib/*" -d build src/com/minecraftlauncher/*.java src/com/minecraftlauncher/*/*.java +jar cfm MinecraftLauncher.jar MANIFEST.MF -C build . +``` + +### Authentification Microsoft +Le launcher utilise l'API OAuth2 v2.0 de Microsoft avec : +- Client ID : `54fd49e4-2103-4044-9603-2b028c814ec3` +- Port local dynamique (25565-25575) pour éviter les conflits +- Gestion automatique des timeouts et fermetures de navigateur + +### Fonctionnalités Spéciales +- **Bouton toujours disponible** : Plus de blocage si le navigateur se ferme +- **Port dynamique** : Trouve automatiquement un port libre +- **Messages informatifs** : Indique clairement l'état de l'authentification +- **Compatibilité Edge** : Gestion spéciale pour Microsoft Edge + +## Auteur + +Développé avec l'aide de Claude Code (Anthropic) \ No newline at end of file diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF new file mode 100644 index 0000000..acf886d --- /dev/null +++ b/src/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Main-Class: com.minecraftlauncher.Main + diff --git a/src/com/minecraftlauncher/Main.java b/src/com/minecraftlauncher/Main.java new file mode 100644 index 0000000..e91e02f --- /dev/null +++ b/src/com/minecraftlauncher/Main.java @@ -0,0 +1,52 @@ +// src/com/minecraftlauncher/Main.java +package com.minecraftlauncher; + +import com.minecraftlauncher.ui.LauncherFrame; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +public class Main { + public static void main(String[] args) { + try { + // Configuration avancée pour une meilleure apparence + System.setProperty("awt.useSystemAAFontSettings","on"); + System.setProperty("swing.aatext", "true"); + System.setProperty("swing.plaf.nimbus.useJavaFont", "true"); + + // Utiliser l'apparence système pour une meilleure intégration + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + + // Configuration supplémentaire pour le thème sombre + UIManager.put("control", new java.awt.Color(17, 24, 39)); + UIManager.put("info", new java.awt.Color(17, 24, 39)); + UIManager.put("nimbusBase", new java.awt.Color(17, 24, 39)); + UIManager.put("nimbusAlertYellow", new java.awt.Color(248, 187, 0)); + UIManager.put("nimbusDisabledText", new java.awt.Color(128, 128, 128)); + UIManager.put("nimbusFocus", new java.awt.Color(115, 164, 209)); + UIManager.put("nimbusGreen", new java.awt.Color(176, 179, 50)); + UIManager.put("nimbusInfoBlue", new java.awt.Color(66, 139, 221)); + UIManager.put("nimbusLightBackground", new java.awt.Color(18, 30, 49)); + UIManager.put("nimbusOrange", new java.awt.Color(191, 98, 4)); + UIManager.put("nimbusRed", new java.awt.Color(169, 46, 34)); + UIManager.put("nimbusSelectedText", new java.awt.Color(255, 255, 255)); + UIManager.put("nimbusSelectionBackground", new java.awt.Color(104, 93, 156)); + UIManager.put("text", new java.awt.Color(230, 230, 230)); + + } catch (Exception e) { + System.err.println("Impossible de configurer l'apparence système: " + e.getMessage()); + // Garder l'apparence par défaut si erreur + } + + SwingUtilities.invokeLater(() -> { + try { + System.out.println("[START] Demarrage du Minecraft Launcher Moderne..."); + LauncherFrame frame = new LauncherFrame(); + frame.setVisible(true); + System.out.println("[OK] Interface lancee avec succes !"); + } catch (Exception e) { + System.err.println("[ERROR] Erreur lors du lancement: " + e.getMessage()); + e.printStackTrace(); + } + }); + } +} \ No newline at end of file diff --git a/src/com/minecraftlauncher/auth/ModernAuthManager.java b/src/com/minecraftlauncher/auth/ModernAuthManager.java new file mode 100644 index 0000000..f199bd6 --- /dev/null +++ b/src/com/minecraftlauncher/auth/ModernAuthManager.java @@ -0,0 +1,636 @@ +// src/com/minecraftlauncher/auth/ModernAuthManager.java +package com.minecraftlauncher.auth; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.minecraftlauncher.model.Account; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; +import java.awt.Desktop; +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.HashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.UUID; + +public class ModernAuthManager { + private static final String CLIENT_ID = "54fd49e4-2103-4044-9603-2b028c814ec3"; + private static int LOCAL_PORT = 25565; + private static String REDIRECT_URI = "http://localhost:25565"; + private static final String SCOPE = "XboxLive.signin offline_access"; + + // Endpoints OAuth2 v2.0 modernes + private static final String MICROSOFT_AUTH_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"; + private static final String MICROSOFT_TOKEN_URL = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + private static final String XBOX_AUTH_URL = "https://user.auth.xboxlive.com/user/authenticate"; + private static final String XSTS_AUTH_URL = "https://xsts.auth.xboxlive.com/xsts/authorize"; + private static final String MINECRAFT_AUTH_URL = "https://api.minecraftservices.com/authentication/login_with_xbox"; + private static final String MINECRAFT_PROFILE_URL = "https://api.minecraftservices.com/minecraft/profile"; + private static final String MINECRAFT_STORE_URL = "https://api.minecraftservices.com/entitlements/mcstore"; + + private Gson gson = new Gson(); + private HttpServer localServer; + private CountDownLatch authLatch; + private String receivedAuthCode; + private String stateToken; + private volatile boolean authCancelled = false; + + public Account authenticate() { + try { + System.out.println("=== AUTHENTIFICATION MICROSOFT MODERNE ==="); + System.out.println("Capture automatique du code d'autorisation"); + + // Réinitialiser l'état d'annulation + authCancelled = false; + receivedAuthCode = null; + + // Générer un token state pour la sécurité CSRF + stateToken = UUID.randomUUID().toString(); + + // Démarrer le serveur HTTP local + startLocalServer(); + + // Étape 1: Ouvrir le navigateur pour l'authentification + String authUrl = buildAuthUrl(); + System.out.println("URL de connexion: " + authUrl); + System.out.println("Démarrage du serveur local sur le port " + LOCAL_PORT); + + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().browse(URI.create(authUrl)); + } + + System.out.println("En attente de la réponse d'authentification..."); + + // Attendre la réponse avec timeout optimisé pour Edge (15 secondes par cycle) + boolean received = false; + int attempts = 0; + while (!received && attempts < 20 && !authCancelled) { + received = authLatch.await(15, TimeUnit.SECONDS); + attempts++; + + if (!received && !authCancelled) { + System.out.println("Attente... (" + (attempts * 15) + "s écoulées - Le bouton reste disponible pour réessayer)"); + + // Après 2 tentatives (30s), encourager l'utilisateur à réessayer + if (attempts >= 2) { + System.out.println("[INFO] Si Microsoft Edge s'est fermé, cliquez à nouveau sur 'Se connecter à Microsoft' - Le système trouvera automatiquement un port libre"); + } + + // Après 4 tentatives (1 minute), conseil spécifique Edge + if (attempts >= 4) { + System.out.println("[CONSEIL EDGE] Edge peut parfois rester en arrière-plan. Le système utilise maintenant le port " + LOCAL_PORT + " pour éviter les conflits"); + } + } + } + + // Arrêter le serveur + stopLocalServer(); + + if (authCancelled) { + throw new RuntimeException("Authentification annulée - Le bouton reste disponible pour réessayer."); + } + + if (!received) { + throw new RuntimeException("Timeout d'authentification - Le navigateur s'est peut-être fermé. Cliquez à nouveau sur 'Se connecter à Microsoft' pour réessayer."); + } + + if (receivedAuthCode == null) { + throw new RuntimeException("Code d'authentification non reçu - Le bouton reste disponible pour réessayer immédiatement."); + } + + System.out.println("Code d'autorisation reçu automatiquement"); + + // Flux d'authentification complet + return performFullAuthentication(receivedAuthCode); + + } catch (Exception e) { + stopLocalServer(); + System.err.println("Erreur d'authentification: " + e.getMessage()); + + // Messages d'erreur plus informatifs + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "Erreur d'authentification inconnue - Le bouton reste disponible pour réessayer"; + } + + // Conseils supplémentaires selon le type d'erreur + if (errorMessage.contains("Timeout") || errorMessage.contains("fermé")) { + System.out.println("[CONSEIL] Le navigateur s'est fermé avant la connexion. Cliquez simplement à nouveau sur 'Se connecter à Microsoft' pour relancer immédiatement l'authentification."); + } else if (errorMessage.contains("java.net.ConnectException")) { + errorMessage = "Impossible de se connecter à Microsoft. Vérifiez votre connexion Internet et réessayez."; + } else if (errorMessage.contains("Address already in use")) { + System.out.println("[INFO] Le serveur local se libère... Veuillez réessayer dans quelques secondes."); + errorMessage = "Serveur local occupé - Réessayez dans 2-3 secondes"; + } + + e.printStackTrace(); + throw new RuntimeException(errorMessage, e); + } + } + + public Account authenticateWithCode(String authCode) { + try { + System.out.println("=== TRAITEMENT DU CODE D'AUTORISATION ==="); + + // Flux d'authentification complet + return performFullAuthentication(authCode); + + } catch (Exception e) { + System.err.println("Erreur d'authentification: " + e.getMessage()); + e.printStackTrace(); + throw new RuntimeException("Échec de l'authentification: " + e.getMessage(), e); + } + } + + private void startLocalServer() throws IOException { + // Arrêter tout serveur existant d'abord + stopLocalServer(); + + // Essayer de trouver un port libre + for (int port = 25565; port <= 25575; port++) { + try { + authLatch = new CountDownLatch(1); + localServer = HttpServer.create(new InetSocketAddress(port), 0); + + // Mettre à jour le port et l'URI de redirection + LOCAL_PORT = port; + REDIRECT_URI = "http://localhost:" + port; + + localServer.createContext("/", new AuthHandler()); + localServer.setExecutor(null); + localServer.start(); + + System.out.println("Serveur HTTP local démarré sur http://localhost:" + LOCAL_PORT); + return; // Succès, sortir de la boucle + + } catch (IOException e) { + if (e.getMessage().contains("Address already in use")) { + System.out.println("[INFO] Port " + port + " occupé, essai du port " + (port + 1)); + if (port == 25575) { + // Dernière tentative, essayer de forcer la libération du port principal + try { + Thread.sleep(3000); + LOCAL_PORT = 25565; + REDIRECT_URI = "http://localhost:25565"; + localServer = HttpServer.create(new InetSocketAddress(25565), 0); + localServer.createContext("/", new AuthHandler()); + localServer.setExecutor(null); + localServer.start(); + System.out.println("Serveur HTTP local redémarré sur le port 25565 après délai"); + return; + } catch (Exception finalException) { + throw new IOException("Impossible de démarrer le serveur local - Tous les ports occupés. Fermez complètement l'application et relancez."); + } + } + continue; // Essayer le port suivant + } else { + throw e; + } + } + } + } + + private void stopLocalServer() { + if (localServer != null) { + try { + localServer.stop(0); // Arrêt immédiat + System.out.println("Serveur HTTP local arrêté (port " + LOCAL_PORT + ")"); + } catch (Exception e) { + System.out.println("Avertissement: Erreur lors de l'arrêt du serveur local: " + e.getMessage()); + } finally { + localServer = null; // Toujours libérer la référence + + // Délai plus long pour Edge qui garde les connexions ouvertes + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void cancelAuthentication() { + authCancelled = true; + stopLocalServer(); + if (authLatch != null) { + authLatch.countDown(); + } + System.out.println("[INFO] Authentification annulée - Le bouton est maintenant disponible pour une nouvelle tentative"); + } + + private class AuthHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + String query = exchange.getRequestURI().getQuery(); + System.out.println("Requête reçue: " + query); + + if (query != null) { + Map params = parseQuery(query); + + if (params.containsKey("error")) { + String error = params.get("error"); + String errorDesc = params.get("error_description"); + System.err.println("Erreur OAuth: " + error + " - " + errorDesc); + sendErrorResponse(exchange, error, errorDesc); + authLatch.countDown(); + return; + } + + if (params.containsKey("code") && params.containsKey("state")) { + String receivedState = params.get("state"); + if (!stateToken.equals(receivedState)) { + System.err.println("Token state invalide - possible attaque CSRF"); + sendErrorResponse(exchange, "invalid_state", "Token state invalide"); + authLatch.countDown(); + return; + } + + receivedAuthCode = params.get("code"); + System.out.println("Code d'autorisation capturé: " + receivedAuthCode.substring(0, Math.min(10, receivedAuthCode.length())) + "..."); + sendSuccessResponse(exchange); + authLatch.countDown(); + return; + } + } + + // Si pas de paramètres, afficher la page d'accueil + sendWelcomePage(exchange); + } + } + + private void sendWelcomePage(HttpExchange exchange) throws IOException { + String response = String.format( + "" + + "Minecraft Launcher - Authentification" + + "" + + "

Authentification Minecraft Launcher

" + + "

Redirection vers Microsoft OAuth2...

" + + "" + + "" + + "", buildAuthUrl()); + + exchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8"); + exchange.sendResponseHeaders(200, response.getBytes(StandardCharsets.UTF_8).length); + exchange.getResponseBody().write(response.getBytes(StandardCharsets.UTF_8)); + exchange.close(); + } + + private void sendSuccessResponse(HttpExchange exchange) throws IOException { + String response = + "" + + "Authentification Réussie" + + "" + + "
" + + "
" + + "

Authentification Réussie !

" + + "

" + + "Vous êtes maintenant connecté à votre compte Microsoft.
" + + "Vous pouvez fermer cette fenêtre et retourner au launcher." + + "

" + + "
" + + "Prochaines étapes :
" + + "1. Retournez au launcher Minecraft
" + + "2. Sélectionnez une version à télécharger
" + + "3. Lancez votre jeu !" + + "
" + + "
" + + "" + + "" + + ""; + + exchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8"); + exchange.sendResponseHeaders(200, response.getBytes(StandardCharsets.UTF_8).length); + exchange.getResponseBody().write(response.getBytes(StandardCharsets.UTF_8)); + exchange.close(); + } + + private void sendErrorResponse(HttpExchange exchange, String error, String description) throws IOException { + String response = String.format( + "" + + "Erreur d'Authentification" + + "" + + "
" + + "
" + + "

Erreur d'Authentification

" + + "

Erreur : %s

" + + "

Description : %s

" + + "
" + + "Solutions possibles :
" + + "• Réessayez l'authentification
" + + "• Vérifiez votre connexion Internet
" + + "• Assurez-vous d'avoir un compte Microsoft valide avec Minecraft" + + "
" + + "
" + + "" + + "", error, description); + + exchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8"); + exchange.sendResponseHeaders(500, response.getBytes(StandardCharsets.UTF_8).length); + exchange.getResponseBody().write(response.getBytes(StandardCharsets.UTF_8)); + exchange.close(); + } + + private Map parseQuery(String query) { + Map params = new HashMap<>(); + if (query == null) return params; + + for (String param : query.split("&")) { + String[] pair = param.split("=", 2); + if (pair.length == 2) { + try { + params.put( + URLDecoder.decode(pair[0], StandardCharsets.UTF_8), + URLDecoder.decode(pair[1], StandardCharsets.UTF_8) + ); + } catch (Exception e) { + System.err.println("Erreur décodage paramètre: " + param); + } + } + } + return params; + } + + private String buildAuthUrl() { + return MICROSOFT_AUTH_URL + + "?client_id=" + CLIENT_ID + + "&response_type=code" + + "&redirect_uri=" + URLEncoder.encode(REDIRECT_URI, StandardCharsets.UTF_8) + + "&scope=" + URLEncoder.encode(SCOPE, StandardCharsets.UTF_8) + + "&response_mode=query" + + "&state=" + stateToken + + "&prompt=select_account"; + } + + private Account performFullAuthentication(String authCode) throws Exception { + // Étape 1: Échanger le code contre un token d'accès Microsoft + System.out.println("Étape 1: Obtention du token d'accès Microsoft..."); + String accessToken = getAccessToken(authCode); + System.out.println("Token Microsoft obtenu"); + + // Étape 2: Authentification Xbox Live + System.out.println("Étape 2: Authentification Xbox Live..."); + Map xboxData = authenticateXboxLive(accessToken); + String xblToken = xboxData.get("token"); + String userHash = xboxData.get("userHash"); + System.out.println("Token Xbox Live obtenu"); + + // Étape 3: Obtenir le token XSTS + System.out.println("Étape 3: Obtention du token XSTS..."); + String xstsToken = getXSTSToken(xblToken); + System.out.println("Token XSTS obtenu"); + + // Étape 4: Authentification finale Minecraft + System.out.println("Étape 4: Authentification finale Minecraft..."); + String minecraftToken = authenticateMinecraft(xstsToken, userHash); + System.out.println("Token Minecraft obtenu"); + + // Étape 5: Vérifier la propriété de Minecraft + System.out.println("Étape 5: Vérification de la propriété de Minecraft..."); + boolean ownsMinecraft = checkMinecraftOwnership(minecraftToken); + if (!ownsMinecraft) { + throw new RuntimeException("Ce compte ne possède pas Minecraft Java Edition"); + } + System.out.println("Propriété de Minecraft confirmée"); + + // Étape 6: Récupérer le profil utilisateur + System.out.println("Étape 6: Récupération du profil utilisateur..."); + Account account = getMinecraftProfile(minecraftToken); + + System.out.println("=== AUTHENTIFICATION RÉUSSIE ==="); + System.out.println("Utilisateur: " + account.getUsername()); + System.out.println("UUID: " + account.getUuid()); + + return account; + } + + private String getAccessToken(String authCode) throws Exception { + System.out.println("POST " + MICROSOFT_TOKEN_URL); + + String postData = "client_id=" + CLIENT_ID + + "&code=" + URLEncoder.encode(authCode, StandardCharsets.UTF_8) + + "&grant_type=authorization_code" + + "&redirect_uri=" + URLEncoder.encode(REDIRECT_URI, StandardCharsets.UTF_8) + + "&scope=" + URLEncoder.encode(SCOPE, StandardCharsets.UTF_8); + + String response = makePostRequest(MICROSOFT_TOKEN_URL, postData, "application/x-www-form-urlencoded"); + JsonObject jsonResponse = gson.fromJson(response, JsonObject.class); + + if (jsonResponse.has("error")) { + String error = jsonResponse.get("error").getAsString(); + String errorDesc = jsonResponse.has("error_description") ? + jsonResponse.get("error_description").getAsString() : "Erreur inconnue"; + throw new RuntimeException("Erreur Microsoft OAuth2: " + error + " - " + errorDesc); + } + + return jsonResponse.get("access_token").getAsString(); + } + + private Map authenticateXboxLive(String accessToken) throws Exception { + System.out.println("POST " + XBOX_AUTH_URL); + + JsonObject requestBody = new JsonObject(); + JsonObject properties = new JsonObject(); + properties.addProperty("AuthMethod", "RPS"); + properties.addProperty("SiteName", "user.auth.xboxlive.com"); + properties.addProperty("RpsTicket", "d=" + accessToken); + + requestBody.add("Properties", properties); + requestBody.addProperty("RelyingParty", "http://auth.xboxlive.com"); + requestBody.addProperty("TokenType", "JWT"); + + String response = makePostRequest(XBOX_AUTH_URL, requestBody.toString(), "application/json"); + JsonObject jsonResponse = gson.fromJson(response, JsonObject.class); + + String token = jsonResponse.get("Token").getAsString(); + String userHash = jsonResponse.getAsJsonObject("DisplayClaims") + .getAsJsonArray("xui") + .get(0).getAsJsonObject() + .get("uhs").getAsString(); + + Map result = new HashMap<>(); + result.put("token", token); + result.put("userHash", userHash); + return result; + } + + private String getXSTSToken(String xblToken) throws Exception { + System.out.println("POST " + XSTS_AUTH_URL); + + JsonObject requestBody = new JsonObject(); + JsonObject properties = new JsonObject(); + properties.addProperty("SandboxId", "RETAIL"); + properties.add("UserTokens", gson.toJsonTree(new String[]{xblToken})); + + requestBody.add("Properties", properties); + requestBody.addProperty("RelyingParty", "rp://api.minecraftservices.com/"); + requestBody.addProperty("TokenType", "JWT"); + + String response = makePostRequest(XSTS_AUTH_URL, requestBody.toString(), "application/json"); + JsonObject jsonResponse = gson.fromJson(response, JsonObject.class); + + if (jsonResponse.has("XErr")) { + long errorCode = jsonResponse.get("XErr").getAsLong(); + String errorMessage = getXSTSErrorMessage(errorCode); + throw new RuntimeException("Erreur XSTS " + errorCode + ": " + errorMessage); + } + + return jsonResponse.get("Token").getAsString(); + } + + private String authenticateMinecraft(String xstsToken, String userHash) throws Exception { + System.out.println("POST " + MINECRAFT_AUTH_URL); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("identityToken", "XBL3.0 x=" + userHash + ";" + xstsToken); + + String response = makePostRequest(MINECRAFT_AUTH_URL, requestBody.toString(), "application/json"); + JsonObject jsonResponse = gson.fromJson(response, JsonObject.class); + + if (!jsonResponse.has("access_token")) { + throw new RuntimeException("Token d'accès Minecraft non reçu"); + } + + return jsonResponse.get("access_token").getAsString(); + } + + private boolean checkMinecraftOwnership(String minecraftToken) throws Exception { + System.out.println("GET " + MINECRAFT_STORE_URL); + + URL url = new URL(MINECRAFT_STORE_URL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Authorization", "Bearer " + minecraftToken); + connection.setRequestProperty("Accept", "application/json"); + + int responseCode = connection.getResponseCode(); + System.out.println("Response Code: " + responseCode); + + if (responseCode == 404) { + // Compte existe mais ne possède pas Minecraft + return false; + } + + if (responseCode != 200) { + System.err.println("Erreur vérification propriété: " + responseCode); + // On continue quand même, certains comptes anciens peuvent avoir ce problème + return true; + } + + String response = readResponse(connection.getInputStream()); + JsonObject jsonResponse = gson.fromJson(response, JsonObject.class); + + // Vérifier qu'il y a au moins un produit (Minecraft Java) + return jsonResponse.has("items") && jsonResponse.getAsJsonArray("items").size() > 0; + } + + private Account getMinecraftProfile(String minecraftToken) throws Exception { + System.out.println("GET " + MINECRAFT_PROFILE_URL); + + URL url = new URL(MINECRAFT_PROFILE_URL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Authorization", "Bearer " + minecraftToken); + connection.setRequestProperty("Accept", "application/json"); + + int responseCode = connection.getResponseCode(); + System.out.println("Response Code: " + responseCode); + + if (responseCode == 404) { + throw new RuntimeException("Aucun profil Minecraft trouvé. Assurez-vous d'avoir créé un nom d'utilisateur Minecraft."); + } + + if (responseCode != 200) { + throw new RuntimeException("Erreur lors de la récupération du profil: HTTP " + responseCode); + } + + String response = readResponse(connection.getInputStream()); + JsonObject jsonResponse = gson.fromJson(response, JsonObject.class); + + String username = jsonResponse.get("name").getAsString(); + String uuid = jsonResponse.get("id").getAsString(); + + // Formatter l'UUID avec les tirets + String formattedUuid = formatUUID(uuid); + + return new Account(username, minecraftToken, formattedUuid); + } + + private String formatUUID(String uuid) { + if (uuid.length() == 32) { + return String.format("%s-%s-%s-%s-%s", + uuid.substring(0, 8), + uuid.substring(8, 12), + uuid.substring(12, 16), + uuid.substring(16, 20), + uuid.substring(20, 32) + ); + } + return uuid; + } + + private String makePostRequest(String urlString, String postData, String contentType) throws Exception { + URL url = new URL(urlString); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", contentType); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("User-Agent", "MinecraftLauncher/1.0"); + connection.setDoOutput(true); + + try (OutputStream os = connection.getOutputStream()) { + byte[] input = postData.getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + + int responseCode = connection.getResponseCode(); + System.out.println("Response Code: " + responseCode); + + if (responseCode >= 400) { + String errorResponse = readResponse(connection.getErrorStream()); + System.err.println("Erreur HTTP " + responseCode + ": " + errorResponse); + throw new RuntimeException("Erreur HTTP " + responseCode + ": " + errorResponse); + } + + return readResponse(connection.getInputStream()); + } + + private String readResponse(InputStream inputStream) throws IOException { + if (inputStream == null) return ""; + + StringBuilder response = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + } + return response.toString(); + } + + private String getXSTSErrorMessage(long errorCode) { + if (errorCode == 2148916238L) { + return "Compte enfant (moins de 18 ans). Doit être ajouté à une famille Xbox avec consentement parental."; + } else if (errorCode == 2148916233L) { + return "Pas de compte Xbox Live associé. Créez un compte Xbox Live depuis xbox.com."; + } else if (errorCode == 2148916235L) { + return "Xbox Live n'est pas disponible dans votre pays/région."; + } else if (errorCode == 2148916236L || errorCode == 2148916237L) { + return "Compte adulte mais consentement parental requis dans votre région."; + } else { + return "Erreur XSTS inconnue: " + errorCode + ". Contactez le support Microsoft."; + } + } +} \ No newline at end of file diff --git a/src/com/minecraftlauncher/game/GameLauncher.java b/src/com/minecraftlauncher/game/GameLauncher.java new file mode 100644 index 0000000..53a28da --- /dev/null +++ b/src/com/minecraftlauncher/game/GameLauncher.java @@ -0,0 +1,500 @@ +// src/com/minecraftlauncher/game/GameLauncher.java +package com.minecraftlauncher.game; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.minecraftlauncher.model.Account; +import java.io.*; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +public class GameLauncher { + private static final String MINECRAFT_DIR = System.getProperty("user.home") + "/.minecraft"; + private static final String VERSIONS_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; + private Gson gson = new Gson(); + + // Variables pour la connexion serveur + private String serverAddress = null; + private String serverPort = "25565"; + + // Méthodes pour configurer le serveur + public void setServerInfo(String address, String port) { + this.serverAddress = address; + this.serverPort = port != null ? port : "25565"; + } + + public Map> getAvailableVersionsByType() { + Map> versionsByType = new HashMap<>(); + versionsByType.put("release", new ArrayList<>()); + versionsByType.put("snapshot", new ArrayList<>()); + versionsByType.put("old_beta", new ArrayList<>()); + versionsByType.put("old_alpha", new ArrayList<>()); + + try { + System.out.println("[VERSIONS] Recuperation de toutes les versions disponibles..."); + String response = downloadString(VERSIONS_URL); + JsonObject manifest = gson.fromJson(response, JsonObject.class); + JsonArray versions = manifest.getAsJsonArray("versions"); + + for (JsonElement element : versions) { + JsonObject version = element.getAsJsonObject(); + String type = version.get("type").getAsString(); + String id = version.get("id").getAsString(); + + switch (type) { + case "release": + versionsByType.get("release").add(id); + break; + case "snapshot": + versionsByType.get("snapshot").add(id); + break; + case "old_beta": + versionsByType.get("old_beta").add(id); + break; + case "old_alpha": + versionsByType.get("old_alpha").add(id); + break; + } + } + + System.out.println("Versions trouvées - Release: " + versionsByType.get("release").size() + + ", Snapshots: " + versionsByType.get("snapshot").size() + + ", Beta: " + versionsByType.get("old_beta").size() + + ", Alpha: " + versionsByType.get("old_alpha").size()); + + return versionsByType; + + } catch (Exception e) { + System.err.println("Erreur lors de la récupération des versions: " + e.getMessage()); + versionsByType.get("release").addAll(Arrays.asList("1.21.6", "1.21.5", "1.21.4", "1.21.3", "1.21.1", "1.20.1")); + return versionsByType; + } + } + + public List getAvailableVersions() { + Map> versionsByType = getAvailableVersionsByType(); + List allVersions = new ArrayList<>(); + allVersions.addAll(versionsByType.get("release")); + allVersions.addAll(versionsByType.get("snapshot")); + allVersions.addAll(versionsByType.get("old_beta")); + allVersions.addAll(versionsByType.get("old_alpha")); + return allVersions; + } + + public String getVersionStatus(String version) { + Path versionDir = Paths.get(MINECRAFT_DIR, "versions", version); + Path jsonFile = versionDir.resolve(version + ".json"); + Path jarFile = versionDir.resolve(version + ".jar"); + + if (!Files.exists(jsonFile) || !Files.exists(jarFile)) { + return "Non téléchargée"; + } + + try { + long jarSize = Files.size(jarFile) / (1024 * 1024); + return String.format("Téléchargée (%d MB)", jarSize); + } catch (IOException e) { + return "Téléchargement incomplet"; + } + } + + public boolean downloadVersion(String version) { + try { + System.out.println("=== TÉLÉCHARGEMENT DE " + version + " ==="); + + // Créer les répertoires + Path versionDir = Paths.get(MINECRAFT_DIR, "versions", version); + Files.createDirectories(versionDir); + + // Récupérer les métadonnées de la version + String manifestResponse = downloadString(VERSIONS_URL); + JsonObject manifest = gson.fromJson(manifestResponse, JsonObject.class); + JsonArray versions = manifest.getAsJsonArray("versions"); + + String versionUrl = null; + for (JsonElement element : versions) { + JsonObject v = element.getAsJsonObject(); + if (version.equals(v.get("id").getAsString())) { + versionUrl = v.get("url").getAsString(); + break; + } + } + + if (versionUrl == null) { + throw new RuntimeException("Version " + version + " non trouvée"); + } + + System.out.println("Téléchargement des métadonnées de " + version + "..."); + String versionMetadata = downloadString(versionUrl); + + // Sauvegarder le JSON de version + Path jsonFile = versionDir.resolve(version + ".json"); + Files.write(jsonFile, versionMetadata.getBytes()); + System.out.println("Métadonnées sauvegardées: " + jsonFile); + + JsonObject versionData = gson.fromJson(versionMetadata, JsonObject.class); + + // Télécharger le JAR client + if (versionData.has("downloads")) { + JsonObject downloads = versionData.getAsJsonObject("downloads"); + if (downloads.has("client")) { + JsonObject client = downloads.getAsJsonObject("client"); + String jarUrl = client.get("url").getAsString(); + long jarSize = client.get("size").getAsLong(); + + System.out.println("Téléchargement du JAR client (" + (jarSize / 1024 / 1024) + " MB)..."); + Path jarFile = versionDir.resolve(version + ".jar"); + downloadFile(jarUrl, jarFile.toFile()); + System.out.println("JAR client téléchargé: " + jarFile); + } + } + + // Télécharger les librairies + downloadLibraries(versionData); + + System.out.println("=== TÉLÉCHARGEMENT TERMINÉ ==="); + return true; + + } catch (Exception e) { + System.err.println("Erreur lors du téléchargement: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + private void downloadLibraries(JsonObject versionData) throws IOException { + if (!versionData.has("libraries")) { + return; + } + + JsonArray libraries = versionData.getAsJsonArray("libraries"); + System.out.println("Téléchargement de " + libraries.size() + " librairies..."); + + Path librariesDir = Paths.get(MINECRAFT_DIR, "libraries"); + Files.createDirectories(librariesDir); + + int downloaded = 0; + for (JsonElement element : libraries) { + JsonObject library = element.getAsJsonObject(); + + // Vérifier les règles (OS, etc.) + if (!shouldIncludeLibrary(library)) { + continue; + } + + if (library.has("downloads") && library.getAsJsonObject("downloads").has("artifact")) { + JsonObject artifact = library.getAsJsonObject("downloads").getAsJsonObject("artifact"); + String url = artifact.get("url").getAsString(); + String path = artifact.get("path").getAsString(); + + Path libFile = librariesDir.resolve(path); + Files.createDirectories(libFile.getParent()); + + if (!Files.exists(libFile)) { + downloadFile(url, libFile.toFile()); + downloaded++; + } + } + } + + System.out.println(downloaded + " nouvelles librairies téléchargées"); + } + + private boolean shouldIncludeLibrary(JsonObject library) { + if (!library.has("rules")) { + return true; + } + + JsonArray rules = library.getAsJsonArray("rules"); + boolean allowed = false; + + for (JsonElement element : rules) { + JsonObject rule = element.getAsJsonObject(); + String action = rule.get("action").getAsString(); + + if (rule.has("os")) { + JsonObject os = rule.getAsJsonObject("os"); + if (os.has("name")) { + String osName = os.get("name").getAsString(); + String currentOS = System.getProperty("os.name").toLowerCase(); + + boolean matches = false; + switch (osName) { + case "windows": + matches = currentOS.contains("win"); + break; + case "linux": + matches = currentOS.contains("linux"); + break; + case "osx": + matches = currentOS.contains("mac"); + break; + } + + if (matches) { + allowed = "allow".equals(action); + } + } + } else { + allowed = "allow".equals(action); + } + } + + return allowed; + } + + public boolean launchGame(String version, Account account) { + try { + System.out.println("=== DÉBUT DU LANCEMENT DE MINECRAFT ==="); + System.out.println("Version: " + version); + System.out.println("Utilisateur: " + account.getUsername()); + + Path versionDir = Paths.get(MINECRAFT_DIR, "versions", version); + Path jsonFile = versionDir.resolve(version + ".json"); + Path jarFile = versionDir.resolve(version + ".jar"); + + // Vérifications + System.out.println("Vérification des fichiers..."); + if (!Files.exists(versionDir)) { + throw new RuntimeException("Répertoire version manquant: " + versionDir); + } + if (!Files.exists(jsonFile)) { + throw new RuntimeException("Fichier JSON manquant: " + jsonFile); + } + if (!Files.exists(jarFile)) { + throw new RuntimeException("JAR client manquant: " + jarFile); + } + + System.out.println("Répertoire version: " + versionDir + " (existe: " + Files.exists(versionDir) + ")"); + System.out.println("Fichier JSON: " + jsonFile + " (existe: " + Files.exists(jsonFile) + ")"); + System.out.println("JAR client: " + jarFile + " (existe: " + Files.exists(jarFile) + ")"); + + // Lire les métadonnées + String jsonContent = new String(Files.readAllBytes(jsonFile)); + JsonObject versionData = gson.fromJson(jsonContent, JsonObject.class); + + // Construire le classpath + String classpath = buildClasspath(versionData, jarFile); + System.out.println("Classpath construit avec " + classpath.split(File.pathSeparator).length + " éléments"); + + // Obtenir la classe principale + String mainClass = versionData.get("mainClass").getAsString(); + System.out.println("Classe principale: " + mainClass); + + // Construire les arguments + List command = new ArrayList<>(); + command.add("java"); + command.add("-Xmx2G"); + command.add("-Xms1G"); + command.add("-Djava.library.path=" + Paths.get(MINECRAFT_DIR, "versions", version, "natives")); + command.add("-cp"); + command.add(classpath); + command.add(mainClass); + + // Arguments de jeu + Map variables = createVariableMap(version, account); + List gameArgs = getGameArguments(versionData, variables); + command.addAll(gameArgs); + + // Arguments de connexion serveur - OBLIGATOIRE pour PEDALO + if (serverAddress != null && !serverAddress.isEmpty()) { + System.out.println("🌐 [SERVER] Connexion automatique au serveur PEDALO: " + serverAddress + ":" + serverPort); + command.add("--server"); + command.add(serverAddress); + command.add("--port"); + command.add(serverPort); + } else { + System.out.println("⚠️ [WARNING] Pas de serveur configuré - Utilisation du serveur par défaut"); + // Configuration par défaut pour PEDALO + command.add("--server"); + command.add("pedalo.vidoks.fr"); + command.add("--port"); + command.add("25565"); + } + + // Afficher la commande complète + System.out.println("=== COMMANDE DE LANCEMENT ==="); + for (int i = 0; i < command.size(); i++) { + System.out.println(i + ": " + command.get(i)); + } + + // Créer le répertoire natives si nécessaire + Path nativesDir = Paths.get(MINECRAFT_DIR, "versions", version, "natives"); + Files.createDirectories(nativesDir); + + // Lancer le processus + System.out.println("Lancement du processus..."); + ProcessBuilder pb = new ProcessBuilder(command); + pb.directory(new File(MINECRAFT_DIR)); + Process process = pb.start(); + + System.out.println("Minecraft lancé avec succès! PID: " + process.pid()); + + // Capturer les logs (optionnel) + captureProcessOutput(process); + + return true; + + } catch (Exception e) { + System.err.println("Erreur lors du lancement: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + private String buildClasspath(JsonObject versionData, Path jarFile) { + List classpathElements = new ArrayList<>(); + + // Ajouter les librairies + if (versionData.has("libraries")) { + JsonArray libraries = versionData.getAsJsonArray("libraries"); + Path librariesDir = Paths.get(MINECRAFT_DIR, "libraries"); + + for (JsonElement element : libraries) { + JsonObject library = element.getAsJsonObject(); + + if (!shouldIncludeLibrary(library)) { + continue; + } + + if (library.has("downloads") && library.getAsJsonObject("downloads").has("artifact")) { + JsonObject artifact = library.getAsJsonObject("downloads").getAsJsonObject("artifact"); + String path = artifact.get("path").getAsString(); + Path libFile = librariesDir.resolve(path); + + if (Files.exists(libFile)) { + classpathElements.add(libFile.toString()); + } + } + } + } + + // Ajouter le JAR principal + classpathElements.add(jarFile.toString()); + + System.out.println("Librairies ajoutées au classpath: " + (classpathElements.size() - 1)); + + return String.join(File.pathSeparator, classpathElements); + } + + private Map createVariableMap(String version, Account account) { + Map variables = new HashMap<>(); + variables.put("auth_player_name", account.getUsername()); + variables.put("version_name", version); + variables.put("game_directory", MINECRAFT_DIR); + variables.put("assets_root", Paths.get(MINECRAFT_DIR, "assets").toString()); + variables.put("assets_index_name", version); + variables.put("auth_uuid", account.getUuid()); + variables.put("auth_access_token", account.getAccessToken()); + variables.put("user_type", "msa"); + variables.put("version_type", "release"); + variables.put("resolution_width", "1920"); + variables.put("resolution_height", "1080"); + variables.put("natives_directory", Paths.get(MINECRAFT_DIR, "versions", version, "natives").toString()); + variables.put("launcher_name", "MinecraftLauncher"); + variables.put("launcher_version", "1.0"); + return variables; + } + + private List getGameArguments(JsonObject versionData, Map variables) { + List args = new ArrayList<>(); + + if (versionData.has("arguments")) { + // Arguments modernes (1.13+) + JsonObject arguments = versionData.getAsJsonObject("arguments"); + if (arguments.has("game")) { + JsonArray gameArgs = arguments.getAsJsonArray("game"); + for (JsonElement element : gameArgs) { + if (element.isJsonPrimitive()) { + args.add(replaceVariables(element.getAsString(), variables)); + } + } + } + } else if (versionData.has("minecraftArguments")) { + // Arguments legacy (< 1.13) + String legacyArgs = versionData.get("minecraftArguments").getAsString(); + String[] parts = legacyArgs.split(" "); + for (String part : parts) { + args.add(replaceVariables(part, variables)); + } + } else { + // Arguments par défaut si aucun n'est spécifié + args.addAll(Arrays.asList( + "--username", variables.get("auth_player_name"), + "--version", variables.get("version_name"), + "--gameDir", variables.get("game_directory"), + "--assetsDir", variables.get("assets_root"), + "--assetIndex", variables.get("assets_index_name"), + "--uuid", variables.get("auth_uuid"), + "--accessToken", variables.get("auth_access_token"), + "--userType", variables.get("user_type"), + "--versionType", variables.get("version_type") + )); + } + + return args; + } + + private String replaceVariables(String input, Map variables) { + String result = input; + for (Map.Entry entry : variables.entrySet()) { + result = result.replace("${" + entry.getKey() + "}", entry.getValue()); + } + return result; + } + + private void captureProcessOutput(Process process) { + // Capturer stdout de Minecraft + new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + System.out.println("[MINECRAFT] " + line); + } + } catch (IOException e) { + System.err.println("Erreur lecture stdout: " + e.getMessage()); + } + }).start(); + + // Capturer stderr de Minecraft + new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = reader.readLine()) != null) { + System.err.println("[MINECRAFT ERROR] " + line); + } + } catch (IOException e) { + System.err.println("Erreur lecture stderr: " + e.getMessage()); + } + }).start(); + } + + private String downloadString(String urlString) throws IOException { + URL url = new URL(urlString); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()))) { + StringBuilder content = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + content.append(line); + } + return content.toString(); + } + } + + private void downloadFile(String urlString, File destination) throws IOException { + URL url = new URL(urlString); + try (InputStream in = url.openStream(); + FileOutputStream out = new FileOutputStream(destination)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + } + } +} \ No newline at end of file diff --git a/src/com/minecraftlauncher/model/Account.java b/src/com/minecraftlauncher/model/Account.java new file mode 100644 index 0000000..63d535f --- /dev/null +++ b/src/com/minecraftlauncher/model/Account.java @@ -0,0 +1,28 @@ +// src/com/minecraftlauncher/model/Account.java +package com.minecraftlauncher.model; + +public class Account { + private String username; + private String accessToken; + private String uuid; + + public Account(String username, String accessToken) { + this.username = username; + this.accessToken = accessToken; + this.uuid = "00000000-0000-0000-0000-000000000000"; // UUID par défaut + } + + public Account(String username, String accessToken, String uuid) { + this.username = username; + this.accessToken = accessToken; + this.uuid = uuid != null ? uuid : "00000000-0000-0000-0000-000000000000"; + } + + public String getUsername() { return username; } + public String getAccessToken() { return accessToken; } + public String getUuid() { return uuid; } + + public void setUsername(String username) { this.username = username; } + public void setAccessToken(String accessToken) { this.accessToken = accessToken; } + public void setUuid(String uuid) { this.uuid = uuid; } +} \ No newline at end of file diff --git a/src/com/minecraftlauncher/model/Profile.java b/src/com/minecraftlauncher/model/Profile.java new file mode 100644 index 0000000..9f991eb --- /dev/null +++ b/src/com/minecraftlauncher/model/Profile.java @@ -0,0 +1,15 @@ +// src/com/minecraftlauncher/model/Profile.java +package com.minecraftlauncher.model; + +public class Profile { + private String id; + private String name; + + public Profile(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { return id; } + public String getName() { return name; } +} \ No newline at end of file diff --git a/src/com/minecraftlauncher/ui/DarkThemeUtils.java b/src/com/minecraftlauncher/ui/DarkThemeUtils.java new file mode 100644 index 0000000..3405f9c --- /dev/null +++ b/src/com/minecraftlauncher/ui/DarkThemeUtils.java @@ -0,0 +1,108 @@ +// src/com/minecraftlauncher/ui/DarkThemeUtils.java +package com.minecraftlauncher.ui; + +import javax.swing.*; +import java.awt.*; + +public class DarkThemeUtils { + // Couleurs du thème sombre + public static final Color BACKGROUND_PRIMARY = new Color(17, 24, 39); + public static final Color BACKGROUND_SECONDARY = new Color(31, 41, 55); + public static final Color BACKGROUND_TERTIARY = new Color(55, 65, 81); + public static final Color TEXT_PRIMARY = new Color(243, 244, 246); + public static final Color TEXT_SECONDARY = new Color(209, 213, 219); + public static final Color BORDER_COLOR = new Color(75, 85, 99); + + public static void applyDarkThemeToOptionPane() { + // Configuration pour les JOptionPane + UIManager.put("OptionPane.background", BACKGROUND_PRIMARY); + UIManager.put("OptionPane.messageForeground", TEXT_PRIMARY); + UIManager.put("OptionPane.messageBackground", BACKGROUND_PRIMARY); + + // Panneaux dans les dialog + UIManager.put("Panel.background", BACKGROUND_PRIMARY); + UIManager.put("Panel.foreground", TEXT_PRIMARY); + + // Boutons dans les dialogs + UIManager.put("Button.background", BACKGROUND_TERTIARY); + UIManager.put("Button.foreground", TEXT_PRIMARY); + UIManager.put("Button.select", BACKGROUND_SECONDARY); + UIManager.put("Button.focus", BORDER_COLOR); + } + + public static JDialog createDarkDialog(Frame parent, String title, boolean modal) { + JDialog dialog = new JDialog(parent, title, modal); + dialog.getContentPane().setBackground(BACKGROUND_PRIMARY); + return dialog; + } + + public static void showDarkOptionPane(Component parent, String message, String title, int messageType) { + // Sauvegarder les anciennes valeurs + Color oldBg = UIManager.getColor("OptionPane.background"); + Color oldFg = UIManager.getColor("OptionPane.messageForeground"); + Color oldPanelBg = UIManager.getColor("Panel.background"); + Color oldButtonBg = UIManager.getColor("Button.background"); + Color oldButtonFg = UIManager.getColor("Button.foreground"); + + try { + // Appliquer le thème sombre temporairement + applyDarkThemeToOptionPane(); + + // Créer un panel personnalisé avec le bon style + JPanel panel = new JPanel(new BorderLayout(10, 10)); + panel.setBackground(BACKGROUND_PRIMARY); + panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + JLabel messageLabel = new JLabel("
" + + message.replace("\n", "
") + "
"); + messageLabel.setFont(new Font("SansSerif", Font.PLAIN, 13)); + messageLabel.setForeground(TEXT_PRIMARY); + panel.add(messageLabel, BorderLayout.CENTER); + + JOptionPane.showMessageDialog(parent, panel, title, messageType); + + } finally { + // Restaurer les anciennes valeurs + UIManager.put("OptionPane.background", oldBg); + UIManager.put("OptionPane.messageForeground", oldFg); + UIManager.put("Panel.background", oldPanelBg); + UIManager.put("Button.background", oldButtonBg); + UIManager.put("Button.foreground", oldButtonFg); + } + } + + public static int showDarkConfirmDialog(Component parent, String message, String title, int optionType) { + // Sauvegarder les anciennes valeurs + Color oldBg = UIManager.getColor("OptionPane.background"); + Color oldFg = UIManager.getColor("OptionPane.messageForeground"); + Color oldPanelBg = UIManager.getColor("Panel.background"); + Color oldButtonBg = UIManager.getColor("Button.background"); + Color oldButtonFg = UIManager.getColor("Button.foreground"); + + try { + // Appliquer le thème sombre temporairement + applyDarkThemeToOptionPane(); + + // Créer un panel personnalisé avec le bon style + JPanel panel = new JPanel(new BorderLayout(10, 10)); + panel.setBackground(BACKGROUND_PRIMARY); + panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + JLabel messageLabel = new JLabel("
" + + message.replace("\n", "
") + "
"); + messageLabel.setFont(new Font("SansSerif", Font.PLAIN, 13)); + messageLabel.setForeground(TEXT_PRIMARY); + panel.add(messageLabel, BorderLayout.CENTER); + + return JOptionPane.showConfirmDialog(parent, panel, title, optionType, JOptionPane.QUESTION_MESSAGE); + + } finally { + // Restaurer les anciennes valeurs + UIManager.put("OptionPane.background", oldBg); + UIManager.put("OptionPane.messageForeground", oldFg); + UIManager.put("Panel.background", oldPanelBg); + UIManager.put("Button.background", oldButtonBg); + UIManager.put("Button.foreground", oldButtonFg); + } + } +} \ No newline at end of file diff --git a/src/com/minecraftlauncher/ui/InstalledVersionsDialog.java b/src/com/minecraftlauncher/ui/InstalledVersionsDialog.java new file mode 100644 index 0000000..625a7a0 --- /dev/null +++ b/src/com/minecraftlauncher/ui/InstalledVersionsDialog.java @@ -0,0 +1,277 @@ +// src/com/minecraftlauncher/ui/InstalledVersionsDialog.java +package com.minecraftlauncher.ui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public class InstalledVersionsDialog extends JDialog { + private static final String MINECRAFT_DIR = System.getProperty("user.home") + "/.minecraft"; + private JList versionsList; + private DefaultListModel listModel; + private JButton deleteButton; + private JButton refreshButton; + private JLabel sizeLabel; + + public InstalledVersionsDialog(JFrame parent) { + super(parent, "Versions Installées", true); + initializeComponents(); + setupLayout(); + setupEventListeners(); + loadInstalledVersions(); + + setSize(500, 400); + setLocationRelativeTo(parent); + setResizable(true); + } + + private void initializeComponents() { + // Liste des versions + listModel = new DefaultListModel<>(); + versionsList = new JList<>(listModel); + versionsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + versionsList.setFont(new Font("SansSerif", Font.PLAIN, 12)); + versionsList.setBackground(Color.WHITE); + versionsList.setForeground(Color.BLACK); + versionsList.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10)); + + // Boutons + deleteButton = new JButton("Supprimer"); + deleteButton.setEnabled(false); + styleButton(deleteButton, new Color(220, 38, 38), new Color(185, 28, 28)); // Rouge + + refreshButton = new JButton("Actualiser"); + styleButton(refreshButton, new Color(37, 99, 235), new Color(29, 78, 216)); // Bleu + + JButton closeButton = new JButton("Fermer"); + styleButton(closeButton, new Color(107, 114, 128), new Color(75, 85, 99)); // Gris + + // Label d'information + sizeLabel = new JLabel("Aucune version installée"); + sizeLabel.setFont(new Font("SansSerif", Font.PLAIN, 11)); + sizeLabel.setForeground(new Color(107, 114, 128)); + + // Événements des boutons + closeButton.addActionListener(e -> dispose()); + } + + private void styleButton(JButton button, Color bgColor, Color hoverColor) { + button.setBackground(bgColor); + button.setForeground(Color.WHITE); + button.setFocusPainted(false); + button.setBorderPainted(false); + button.setFont(new Font("SansSerif", Font.BOLD, 11)); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); + button.setBorder(BorderFactory.createEmptyBorder(8, 16, 8, 16)); + + // Effet hover + button.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseEntered(java.awt.event.MouseEvent evt) { + button.setBackground(hoverColor); + } + public void mouseExited(java.awt.event.MouseEvent evt) { + button.setBackground(bgColor); + } + }); + } + + private void setupLayout() { + setLayout(new BorderLayout(10, 10)); + + // Panel principal + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // Titre + JLabel titleLabel = new JLabel("Versions de Minecraft Installées"); + titleLabel.setFont(new Font("SansSerif", Font.BOLD, 16)); + titleLabel.setHorizontalAlignment(SwingConstants.CENTER); + titleLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 15, 0)); + + // Liste avec scroll + JScrollPane scrollPane = new JScrollPane(versionsList); + scrollPane.setBorder(BorderFactory.createLineBorder(new Color(75, 85, 99), 1)); + scrollPane.setPreferredSize(new Dimension(400, 250)); + + // Panel des boutons + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); + buttonPanel.add(deleteButton); + buttonPanel.add(refreshButton); + buttonPanel.add(new JButton("Fermer") {{ + styleButton(this, new Color(107, 114, 128), new Color(75, 85, 99)); + addActionListener(e -> dispose()); + }}); + + // Panel du bas avec info et boutons + JPanel bottomPanel = new JPanel(new BorderLayout()); + bottomPanel.add(sizeLabel, BorderLayout.WEST); + bottomPanel.add(buttonPanel, BorderLayout.EAST); + + mainPanel.add(titleLabel, BorderLayout.NORTH); + mainPanel.add(scrollPane, BorderLayout.CENTER); + mainPanel.add(bottomPanel, BorderLayout.SOUTH); + + add(mainPanel); + } + + private void setupEventListeners() { + // Sélection dans la liste + versionsList.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + deleteButton.setEnabled(versionsList.getSelectedValue() != null); + } + }); + + // Bouton supprimer + deleteButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + deleteSelectedVersion(); + } + }); + + // Bouton actualiser + refreshButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + loadInstalledVersions(); + } + }); + } + + private void loadInstalledVersions() { + listModel.clear(); + List installedVersions = getInstalledVersions(); + + if (installedVersions.isEmpty()) { + sizeLabel.setText("Aucune version installée"); + } else { + for (String version : installedVersions) { + listModel.addElement(version); + } + + // Calculer la taille totale + long totalSize = calculateTotalSize(installedVersions); + String sizeText = formatSize(totalSize); + sizeLabel.setText(installedVersions.size() + " version(s) - " + sizeText); + } + } + + private List getInstalledVersions() { + List versions = new ArrayList<>(); + Path versionsDir = Paths.get(MINECRAFT_DIR, "versions"); + + try { + if (Files.exists(versionsDir) && Files.isDirectory(versionsDir)) { + Files.list(versionsDir) + .filter(Files::isDirectory) + .forEach(versionDir -> { + String versionName = versionDir.getFileName().toString(); + Path jarFile = versionDir.resolve(versionName + ".jar"); + Path jsonFile = versionDir.resolve(versionName + ".json"); + + // Vérifier que les fichiers essentiels existent + if (Files.exists(jarFile) && Files.exists(jsonFile)) { + versions.add(versionName); + } + }); + } + } catch (Exception e) { + System.err.println("Erreur lors de la lecture des versions: " + e.getMessage()); + } + + versions.sort(String::compareTo); + return versions; + } + + private long calculateTotalSize(List versions) { + long totalSize = 0; + for (String version : versions) { + Path versionDir = Paths.get(MINECRAFT_DIR, "versions", version); + try { + totalSize += Files.walk(versionDir) + .filter(Files::isRegularFile) + .mapToLong(path -> { + try { + return Files.size(path); + } catch (Exception e) { + return 0; + } + }) + .sum(); + } catch (Exception e) { + // Ignorer les erreurs pour cette version + } + } + return totalSize; + } + + private String formatSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); + if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); + return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0)); + } + + private void deleteSelectedVersion() { + String selectedVersion = versionsList.getSelectedValue(); + if (selectedVersion == null) return; + + int result = JOptionPane.showConfirmDialog( + this, + "Voulez-vous vraiment supprimer la version " + selectedVersion + " ?\n\n" + + "Cette action est irréversible.", + "Confirmer la suppression", + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE + ); + + if (result == JOptionPane.YES_OPTION) { + if (deleteVersion(selectedVersion)) { + JOptionPane.showMessageDialog( + this, + "Version " + selectedVersion + " Supprimée avec succès.", + "Suppression réussie", + JOptionPane.INFORMATION_MESSAGE + ); + loadInstalledVersions(); // Actualiser la liste + } else { + JOptionPane.showMessageDialog( + this, + "Erreur lors de la suppression de la version " + selectedVersion + ".\n" + + "Vérifiez que les fichiers ne sont pas en cours d'utilisation.", + "Erreur de suppression", + JOptionPane.ERROR_MESSAGE + ); + } + } + } + + private boolean deleteVersion(String version) { + Path versionDir = Paths.get(MINECRAFT_DIR, "versions", version); + + try { + // Supprimer récursivement le dossier de la version + Files.walk(versionDir) + .sorted((a, b) -> b.compareTo(a)) // Ordre inverse pour supprimer les fichiers avant les dossiers + .forEach(path -> { + try { + Files.delete(path); + } catch (Exception e) { + System.err.println("Impossible de supprimer: " + path); + } + }); + return !Files.exists(versionDir); + } catch (Exception e) { + System.err.println("[ERROR] Erreur lors de la suppression: " + e.getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/src/com/minecraftlauncher/ui/LauncherFrame.java b/src/com/minecraftlauncher/ui/LauncherFrame.java new file mode 100644 index 0000000..b611975 --- /dev/null +++ b/src/com/minecraftlauncher/ui/LauncherFrame.java @@ -0,0 +1,704 @@ +// src/com/minecraftlauncher/ui/LauncherFrame.java +package com.minecraftlauncher.ui; + +import com.minecraftlauncher.auth.ModernAuthManager; +import com.minecraftlauncher.game.GameLauncher; +import com.minecraftlauncher.model.Account; +import javax.swing.*; +import javax.swing.Timer; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; +import java.util.Arrays; +import java.util.ArrayList; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class LauncherFrame extends JFrame { + // Gestionnaires réels + private ModernAuthManager authManager; + private GameLauncher gameLauncher; + private Account currentAccount; + + // Composants UI + private JLabel userLabel; + private JButton loginButton; + private JComboBox versionComboBox; + private JButton downloadButton; + private JButton launchButton; + private JButton versionsButton; + private JLabel statusLabel; + private JLabel serverInfoLabel; + + public LauncherFrame() { + initializeComponents(); + setupLayout(); + setupEventListeners(); + + // Initialisation des gestionnaires réels + authManager = new ModernAuthManager(); + gameLauncher = new GameLauncher(); + + // Charger les versions disponibles + loadAvailableVersions(); + } + + private void initializeComponents() { + // Configuration de base + setTitle("Le Launcher du PEDALO"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(700, 650); + setLocationRelativeTo(null); + setResizable(true); + setMinimumSize(new Dimension(650, 600)); + + // Thème sombre moderne + setupModernTheme(); + + // Composants d'authentification avec style moderne + userLabel = new JLabel("Non connecte"); + userLabel.setFont(new Font("SansSerif", Font.BOLD, 15)); + userLabel.setForeground(new Color(239, 68, 68)); // Rouge moderne + userLabel.setBorder(BorderFactory.createEmptyBorder(8, 12, 8, 12)); + userLabel.setOpaque(true); + userLabel.setBackground(new Color(55, 65, 81)); + userLabel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(75, 85, 99), 1), + BorderFactory.createEmptyBorder(8, 12, 8, 12))); + + loginButton = new JButton("Se connecter à Microsoft"); + styleModernButton(loginButton, new Color(37, 99, 235), new Color(29, 78, 216)); // Bleu moderne + loginButton.setFont(new Font("SansSerif", Font.BOLD, 13)); + loginButton.setPreferredSize(new Dimension(250, 40)); + + // Composants de version avec style moderne + versionComboBox = new JComboBox<>(); + versionComboBox.setEnabled(false); + styleModernComboBox(versionComboBox); + versionComboBox.setPreferredSize(new Dimension(200, 35)); + + downloadButton = new JButton("Chargement..."); + downloadButton.setEnabled(false); + styleModernButton(downloadButton, new Color(249, 115, 22), new Color(234, 88, 12)); // Orange moderne + downloadButton.setPreferredSize(new Dimension(150, 35)); + + launchButton = new JButton("🚀 REJOINDRE LE SERVEUR PEDALO"); + launchButton.setEnabled(false); + styleModernButton(launchButton, new Color(34, 197, 94), new Color(22, 163, 74)); // Vert moderne + launchButton.setFont(new Font("SansSerif", Font.BOLD, 16)); + launchButton.setPreferredSize(new Dimension(300, 50)); + + // Effet de brillance + launchButton.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(74, 222, 128), 2), + BorderFactory.createEmptyBorder(5, 20, 5, 20))); + + versionsButton = new JButton("📦 Toutes les Versions"); + styleModernButton(versionsButton, new Color(99, 102, 241), new Color(79, 70, 229)); // Indigo moderne + versionsButton.setPreferredSize(new Dimension(150, 35)); + + // Label d'information serveur fixe + serverInfoLabel = new JLabel("🌐 Connexion automatique au serveur : pedalo.vidoks.fr:25565"); + serverInfoLabel.setFont(new Font("SansSerif", Font.BOLD, 14)); + serverInfoLabel.setForeground(new Color(34, 197, 94)); // Vert + serverInfoLabel.setHorizontalAlignment(SwingConstants.CENTER); + serverInfoLabel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(34, 197, 94), 2), + BorderFactory.createEmptyBorder(10, 15, 10, 15))); + serverInfoLabel.setOpaque(true); + serverInfoLabel.setBackground(new Color(21, 128, 61, 20)); + + statusLabel = new JLabel("🚀 Prêt - Cliquez sur 'Se connecter' pour rejoindre le serveur PEDALO"); + statusLabel.setBorder(BorderFactory.createEmptyBorder(10, 15, 10, 15)); + statusLabel.setFont(new Font("SansSerif", Font.PLAIN, 12)); + statusLabel.setForeground(new Color(156, 163, 175)); + statusLabel.setOpaque(true); + statusLabel.setBackground(new Color(17, 24, 39)); + statusLabel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(75, 85, 99)), + BorderFactory.createEmptyBorder(10, 15, 10, 15))); + } + + // Méthodes de stylisation moderne + private void setupModernTheme() { + // Couleur de fond principale - thème sombre moderne + getContentPane().setBackground(new Color(17, 24, 39)); // Gray-900 + + // Configuration globale des composants pour thème sombre uniforme + UIManager.put("Panel.background", new Color(17, 24, 39)); + UIManager.put("Label.foreground", new Color(243, 244, 246)); + + // Boîtes de dialogue et menus déroulants + UIManager.put("OptionPane.background", new Color(17, 24, 39)); + UIManager.put("OptionPane.messageForeground", new Color(243, 244, 246)); + UIManager.put("OptionPane.messageBackground", new Color(17, 24, 39)); + UIManager.put("Button.background", new Color(55, 65, 81)); + UIManager.put("Button.foreground", new Color(243, 244, 246)); + + // ComboBox thème sombre - Fix pour éviter blanc sur blanc + UIManager.put("ComboBox.background", new Color(31, 41, 55)); + UIManager.put("ComboBox.foreground", new Color(243, 244, 246)); + UIManager.put("ComboBox.selectionBackground", new Color(37, 99, 235)); + UIManager.put("ComboBox.selectionForeground", Color.WHITE); + UIManager.put("ComboBox.buttonBackground", new Color(55, 65, 81)); + UIManager.put("ComboBox.disabledBackground", new Color(55, 65, 81)); + UIManager.put("ComboBox.disabledForeground", new Color(156, 163, 175)); + + // Menu popup pour ComboBox + UIManager.put("PopupMenu.background", new Color(31, 41, 55)); + UIManager.put("MenuItem.background", new Color(31, 41, 55)); + UIManager.put("MenuItem.foreground", new Color(243, 244, 246)); + UIManager.put("MenuItem.selectionBackground", new Color(55, 65, 81)); + UIManager.put("MenuItem.selectionForeground", Color.WHITE); + + // Scrollbars + UIManager.put("ScrollBar.background", new Color(31, 41, 55)); + UIManager.put("ScrollBar.thumb", new Color(75, 85, 99)); + UIManager.put("ScrollBar.thumbShadow", new Color(55, 65, 81)); + UIManager.put("ScrollBar.thumbHighlight", new Color(107, 114, 128)); + + // Curseur personnalisé + setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); + + // AJOUT : Fix supplémentaire pour ComboBox + fixComboBoxUIManager(); + } + + // NOUVELLE METHODE : Fix pour ComboBox + private void fixComboBoxUIManager() { + // Désactiver les couleurs par défaut qui peuvent interférer + UIManager.put("ComboBox.border", BorderFactory.createEmptyBorder()); + UIManager.put("ComboBox.borderSelectionColor", null); + UIManager.put("ComboBox.buttonDarkShadow", new Color(75, 85, 99)); + UIManager.put("ComboBox.buttonHighlight", new Color(107, 114, 128)); + UIManager.put("ComboBox.buttonShadow", new Color(55, 65, 81)); + UIManager.put("ComboBox.focus", new Color(37, 99, 235)); + + // Force les couleurs pour la liste popup + UIManager.put("List.background", new Color(31, 41, 55)); + UIManager.put("List.foreground", new Color(243, 244, 246)); + UIManager.put("List.selectionBackground", new Color(37, 99, 235)); + UIManager.put("List.selectionForeground", Color.WHITE); + UIManager.put("List.focusCellHighlightBorder", BorderFactory.createEmptyBorder()); + } + + private void styleModernButton(JButton button, Color bgColor, Color hoverColor) { + button.setBackground(bgColor); + button.setForeground(Color.WHITE); + button.setFocusPainted(false); + button.setBorderPainted(false); + button.setFont(new Font("SansSerif", Font.BOLD, 12)); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + // Bordures arrondies avec padding + button.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(bgColor.brighter(), 1), + BorderFactory.createEmptyBorder(8, 16, 8, 16))); + + // Effet hover + button.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseEntered(java.awt.event.MouseEvent evt) { + button.setBackground(hoverColor); + } + public void mouseExited(java.awt.event.MouseEvent evt) { + button.setBackground(bgColor); + } + }); + } + + // METHODE MODIFIEE : Fix pour ComboBox + private void styleModernComboBox(JComboBox comboBox) { + // Configuration de base + comboBox.setBackground(new Color(31, 41, 55)); + comboBox.setForeground(new Color(243, 244, 246)); + comboBox.setFont(new Font("SansSerif", Font.PLAIN, 12)); + comboBox.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(75, 85, 99), 1), + BorderFactory.createEmptyBorder(5, 10, 5, 10))); + + // SOLUTION : Renderer personnalisé avec couleurs forcées + comboBox.setRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + // CORRECTION : Force les couleurs explicitement + if (isSelected) { + setBackground(new Color(37, 99, 235)); // Bleu pour sélection + setForeground(Color.WHITE); + } else { + setBackground(new Color(31, 41, 55)); // Fond sombre + setForeground(new Color(243, 244, 246)); // Texte clair + } + + // IMPORTANT : Force l'opacité pour que les couleurs s'appliquent + setOpaque(true); + setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10)); + + return this; + } + }); + + // AJOUT : Configuration supplémentaire pour éviter les conflits UIManager + comboBox.putClientProperty("JComboBox.isTableCellEditor", Boolean.FALSE); + + // AJOUT : Listener pour forcer le style au focus + comboBox.addFocusListener(new java.awt.event.FocusAdapter() { + @Override + public void focusGained(java.awt.event.FocusEvent evt) { + comboBox.setBackground(new Color(31, 41, 55)); + comboBox.setForeground(new Color(243, 244, 246)); + } + }); + } + + + private JLabel createSectionTitle(String title, String icon) { + JLabel label = new JLabel(icon + " " + title); + label.setFont(new Font("SansSerif", Font.BOLD, 14)); + label.setForeground(new Color(147, 197, 253)); // Blue-300 + label.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0)); + return label; + } + + private void setupLayout() { + setLayout(new BorderLayout()); + + // Panel principal avec thème sombre + JPanel mainPanel = new JPanel(new GridBagLayout()); + mainPanel.setBackground(new Color(17, 24, 39)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 25, 20, 25)); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(8, 8, 8, 8); + + // Section authentification avec design moderne + gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; + JLabel authTitle = createSectionTitle("Authentification", "🔐"); + mainPanel.add(authTitle, gbc); + + gbc.gridy = 1; gbc.gridwidth = 1; + mainPanel.add(userLabel, gbc); + + gbc.gridx = 1; + mainPanel.add(loginButton, gbc); + + // Séparateur moderne + gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.HORIZONTAL; + JSeparator sep1 = new JSeparator(); + sep1.setForeground(new Color(75, 85, 99)); + sep1.setBackground(new Color(75, 85, 99)); + gbc.insets = new Insets(15, 0, 15, 0); + mainPanel.add(sep1, gbc); + gbc.insets = new Insets(8, 8, 8, 8); + + // Section versions avec design moderne + gbc.gridy = 3; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.NONE; + JLabel versionTitle = createSectionTitle("Gestion des Versions Minecraft", "📦"); + mainPanel.add(versionTitle, gbc); + + gbc.gridy = 4; gbc.gridwidth = 1; + JLabel versionLabel = new JLabel("Version:"); + versionLabel.setForeground(new Color(209, 213, 219)); + versionLabel.setFont(new Font("SansSerif", Font.PLAIN, 12)); + mainPanel.add(versionLabel, gbc); + + gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; + mainPanel.add(versionComboBox, gbc); + + gbc.gridx = 0; gbc.gridy = 5; gbc.fill = GridBagConstraints.NONE; + mainPanel.add(downloadButton, gbc); + + gbc.gridx = 1; + mainPanel.add(versionsButton, gbc); + + // Séparateur moderne + gbc.gridx = 0; gbc.gridy = 6; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.HORIZONTAL; + JSeparator sep2 = new JSeparator(); + sep2.setForeground(new Color(75, 85, 99)); + sep2.setBackground(new Color(75, 85, 99)); + gbc.insets = new Insets(15, 0, 15, 0); + mainPanel.add(sep2, gbc); + gbc.insets = new Insets(8, 8, 8, 8); + + // Information serveur fixe + gbc.gridy = 7; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(15, 0, 15, 0); + mainPanel.add(serverInfoLabel, gbc); + gbc.insets = new Insets(8, 8, 8, 8); + + // Bouton de lancement avec style spécial + gbc.gridy = 8; gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(20, 0, 10, 0); + launchButton.setPreferredSize(new Dimension(300, 50)); + mainPanel.add(launchButton, gbc); + gbc.insets = new Insets(8, 8, 8, 8); + + add(mainPanel, BorderLayout.CENTER); + + // Barre de statut + add(statusLabel, BorderLayout.SOUTH); + } + + private void setupEventListeners() { + loginButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + performAuthentication(); + } + }); + + versionComboBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateVersionStatus(); + } + }); + + downloadButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + downloadSelectedVersion(); + } + }); + + launchButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + launchMinecraft(); + } + }); + + versionsButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + showAllVersionsDialog(); + } + }); + } + + private void loadAvailableVersions() { + SwingUtilities.invokeLater(() -> statusLabel.setText("Chargement des versions installées...")); + + new Thread(() -> { + try { + // Charger uniquement les versions installées + List installedVersions = getInstalledVersions(); + SwingUtilities.invokeLater(() -> { + versionComboBox.removeAllItems(); + if (installedVersions.isEmpty()) { + versionComboBox.addItem("Aucune version installée"); + versionComboBox.setEnabled(false); + downloadButton.setEnabled(false); + statusLabel.setText("Aucune version installée - Utilisez \"📦 Toutes les Versions\" pour en télécharger"); + } else { + for (String version : installedVersions) { + versionComboBox.addItem(version); + } + versionComboBox.setEnabled(true); + downloadButton.setEnabled(true); + updateVersionStatus(); + statusLabel.setText("✅ " + installedVersions.size() + " version(s) installée(s) - Prêt pour l'authentification !"); + } + }); + } catch (Exception e) { + SwingUtilities.invokeLater(() -> { + statusLabel.setText("Erreur de chargement des versions installées"); + DarkThemeUtils.showDarkOptionPane(this, + "Impossible de charger les versions installées :\n\n" + e.getMessage(), + "Erreur de Chargement", JOptionPane.ERROR_MESSAGE); + }); + } + }).start(); + } + + private List getInstalledVersions() { + List versions = new ArrayList<>(); + Path versionsDir = Paths.get(System.getProperty("user.home") + "/.minecraft", "versions"); + + try { + if (Files.exists(versionsDir) && Files.isDirectory(versionsDir)) { + Files.list(versionsDir) + .filter(Files::isDirectory) + .forEach(versionDir -> { + String versionName = versionDir.getFileName().toString(); + Path jarFile = versionDir.resolve(versionName + ".jar"); + Path jsonFile = versionDir.resolve(versionName + ".json"); + + // Vérifier que les fichiers essentiels existent + if (Files.exists(jarFile) && Files.exists(jsonFile)) { + versions.add(versionName); + } + }); + } + } catch (Exception e) { + System.err.println("[ERROR] Erreur lors de la lecture des versions installées: " + e.getMessage()); + } + + versions.sort(String::compareTo); + return versions; + } + + private void performAuthentication() { + // LE BOUTON NE SE DÉSACTIVE JAMAIS - Solution simple ! + statusLabel.setText("🔐 Ouverture du navigateur pour l'authentification Microsoft... (Vous pouvez recliquer si besoin)"); + + new Thread(() -> { + try { + Account account = authManager.authenticate(); + currentAccount = account; + + SwingUtilities.invokeLater(() -> { + userLabel.setText("👤 " + account.getUsername()); + userLabel.setForeground(new Color(34, 197, 94)); + userLabel.setBackground(new Color(21, 128, 61, 20)); + loginButton.setText("🔁 Reconnexion"); + launchButton.setEnabled(true); + statusLabel.setText("✅ Authentification réussie - Prêt à rejoindre le serveur PEDALO !"); + }); + + } catch (Exception e) { + SwingUtilities.invokeLater(() -> { + // Messages d'erreur informatifs + if (e.getMessage() != null && e.getMessage().contains("Timeout")) { + statusLabel.setText("⏱️ Navigateur fermé ou délai expiré - Cliquez à nouveau quand vous voulez"); + } else if (e.getMessage() != null && e.getMessage().contains("fermé")) { + statusLabel.setText("🌐 Navigateur fermé - Recliquez pour relancer l'authentification"); + } else if (e.getMessage() != null && e.getMessage().contains("occupé")) { + statusLabel.setText("⚠️ Port occupé - Attendez 2 secondes et recliquez"); + } else if (e.getMessage() != null && e.getMessage().contains("annulée")) { + statusLabel.setText("🔄 Authentification annulée - Recliquez quand vous voulez"); + } else { + statusLabel.setText("❌ Erreur d'authentification - Recliquez pour réessayer"); + } + + System.out.println("[INFO] Authentification échouée: " + e.getMessage()); + System.out.println("[INFO] Vous pouvez recliquer sur le bouton à tout moment pour réessayer"); + }); + } + }).start(); + } + + private void updateVersionStatus() { + String selectedVersion = (String) versionComboBox.getSelectedItem(); + if (selectedVersion != null) { + String status = gameLauncher.getVersionStatus(selectedVersion); + + if (status.startsWith("Non telechargee")) { + downloadButton.setText("📥 Telecharger"); + styleModernButton(downloadButton, new Color(249, 115, 22), new Color(234, 88, 12)); // Orange + } else if (status.startsWith("Telechargee")) { + downloadButton.setText("🔄 Retelecharger (" + status + ")"); + styleModernButton(downloadButton, new Color(34, 197, 94), new Color(22, 163, 74)); // Vert + } else { + downloadButton.setText("🔧 Reparer (" + status + ")"); + styleModernButton(downloadButton, new Color(239, 68, 68), new Color(220, 38, 38)); // Rouge + } + } + } + + private void downloadSelectedVersion() { + String selectedVersion = (String) versionComboBox.getSelectedItem(); + if (selectedVersion == null) return; + + downloadButton.setEnabled(false); + statusLabel.setText("[DL] Telechargement en cours... Veuillez patienter"); + + new Thread(() -> { + try { + boolean success = gameLauncher.downloadVersion(selectedVersion); + + SwingUtilities.invokeLater(() -> { + downloadButton.setEnabled(true); + if (success) { + statusLabel.setText("[OK] Telechargement termine avec succes !"); + updateVersionStatus(); + // Plus de popup de téléchargement réussi + } else { + statusLabel.setText("[ERROR] Echec du telechargement - Veuillez reessayer"); + DarkThemeUtils.showDarkOptionPane(this, + "Erreur lors du téléchargement de " + selectedVersion + "\n\n" + + "Consultez la console pour plus de détails.\n" + + "Essayez de retélécharger la version.", + "Erreur de Téléchargement", JOptionPane.ERROR_MESSAGE); + } + }); + + } catch (Exception e) { + SwingUtilities.invokeLater(() -> { + downloadButton.setEnabled(true); + statusLabel.setText("[ERROR] Erreur de telechargement"); + DarkThemeUtils.showDarkOptionPane(LauncherFrame.this, + "Erreur de téléchargement:\n\n" + e.getMessage(), + "Erreur de Téléchargement", JOptionPane.ERROR_MESSAGE); + }); + } + }).start(); + } + + private void launchMinecraft() { + if (currentAccount == null) { + DarkThemeUtils.showDarkOptionPane(this, + "Vous devez vous connecter avant de lancer Minecraft.\n\n" + + "Cliquez sur 'Se connecter avec Microsoft' pour commencer.\n" + + "L'authentification est automatique et sécurisée !", + "Connexion Requise", JOptionPane.WARNING_MESSAGE); + return; + } + + String selectedVersion = (String) versionComboBox.getSelectedItem(); + if (selectedVersion == null) { + DarkThemeUtils.showDarkOptionPane(this, + "Veuillez sélectionner une version de Minecraft à lancer.\n\n" + + "Choisissez une version dans le menu déroulant ci-dessus.", + "Version Requise", JOptionPane.WARNING_MESSAGE); + return; + } + + String status = gameLauncher.getVersionStatus(selectedVersion); + if (status.startsWith("Non téléchargée")) { + DarkThemeUtils.showDarkOptionPane(this, + "La version " + selectedVersion + " n'est pas encore téléchargée.\n\n" + + "Veuillez la télécharger avant de lancer le jeu.\n" + + "Cliquez sur le bouton 'Télécharger' ci-dessus.", + "Téléchargement Requis", JOptionPane.WARNING_MESSAGE); + return; + } + + launchButton.setEnabled(false); + statusLabel.setText("🚀 Connexion au serveur PEDALO en cours..."); + + new Thread(() -> { + try { + // Configuration serveur fixe + gameLauncher.setServerInfo("pedalo.vidoks.fr", "25565"); + + boolean success = gameLauncher.launchGame(selectedVersion, currentAccount); + + SwingUtilities.invokeLater(() -> { + if (success) { + statusLabel.setText("✅ Connexion au serveur PEDALO réussie ! Bon jeu !"); + launchButton.setEnabled(true); + + // Plus de popup de lancement réussi + + } else { + statusLabel.setText("[ERROR] Echec du lancement - Verifiez les prerequis"); + launchButton.setEnabled(true); + DarkThemeUtils.showDarkOptionPane(this, + "Erreur lors du lancement de Minecraft.\n\n" + + "Vérifiez les prérequis :\n" + + " - Java est installé (java -version)\n" + + " - RAM suffisante (2GB minimum)\n" + + " - Espace disque disponible\n" + + " - Antivirus n'interfère pas\n\n" + + "Consultez les logs dans la console", + "Erreur de Lancement", JOptionPane.ERROR_MESSAGE); + } + }); + + } catch (Exception e) { + SwingUtilities.invokeLater(() -> { + launchButton.setEnabled(true); + statusLabel.setText("[ERROR] Erreur critique de lancement"); + DarkThemeUtils.showDarkOptionPane(this, + "Erreur critique de lancement :\n\n" + e.getMessage() + "\n\n" + + "Consultez la console pour plus de détails.\n" + + "Essayez de retélécharger la version.", + "Erreur Critique", JOptionPane.ERROR_MESSAGE); + }); + } + }).start(); + } + + private void showAllVersionsDialog() { + new VersionsTabDialog(this, gameLauncher).setVisible(true); + } + + // Méthodes pour les popups modernes + private void showSuccessDialog(String title, String mainMessage, String details, String uuid) { + JPanel panel = new JPanel(new BorderLayout(10, 15)); + panel.setBackground(Color.WHITE); + panel.setBorder(BorderFactory.createEmptyBorder(20, 25, 20, 25)); + + // Titre avec icône de succès + JLabel titleLabel = new JLabel("✓ " + mainMessage); + titleLabel.setFont(new Font("SansSerif", Font.BOLD, 18)); + titleLabel.setForeground(new Color(22, 163, 74)); // Vert succès + titleLabel.setHorizontalAlignment(SwingConstants.CENTER); + + // Message de détails + JTextArea detailsArea = new JTextArea(details); + detailsArea.setFont(new Font("SansSerif", Font.PLAIN, 13)); + detailsArea.setForeground(new Color(55, 65, 81)); + detailsArea.setBackground(Color.WHITE); + detailsArea.setEditable(false); + detailsArea.setOpaque(false); + detailsArea.setLineWrap(true); + detailsArea.setWrapStyleWord(true); + + // UUID (optionnel, en petit) + JLabel uuidLabel = new JLabel("UUID: " + uuid); + uuidLabel.setFont(new Font("Monospaced", Font.PLAIN, 10)); + uuidLabel.setForeground(new Color(107, 114, 128)); // Gris + uuidLabel.setHorizontalAlignment(SwingConstants.CENTER); + + // Panel pour l'icône de succès + JPanel iconPanel = new JPanel(); + iconPanel.setBackground(Color.WHITE); + JLabel iconLabel = new JLabel("✓"); + iconLabel.setFont(new Font("SansSerif", Font.BOLD, 48)); + iconLabel.setForeground(new Color(22, 163, 74)); + iconPanel.add(iconLabel); + + panel.add(iconPanel, BorderLayout.NORTH); + panel.add(titleLabel, BorderLayout.CENTER); + + JPanel bottomPanel = new JPanel(new BorderLayout(5, 5)); + bottomPanel.setBackground(Color.WHITE); + bottomPanel.add(detailsArea, BorderLayout.CENTER); + bottomPanel.add(uuidLabel, BorderLayout.SOUTH); + panel.add(bottomPanel, BorderLayout.SOUTH); + + JOptionPane.showMessageDialog(this, panel, title, JOptionPane.PLAIN_MESSAGE); + } + + private void showErrorDialog(String title, String mainMessage, String details) { + JPanel panel = new JPanel(new BorderLayout(10, 15)); + panel.setBackground(Color.WHITE); + panel.setBorder(BorderFactory.createEmptyBorder(20, 25, 20, 25)); + + // Titre avec icône d'erreur + JLabel titleLabel = new JLabel("✗ " + mainMessage); + titleLabel.setFont(new Font("SansSerif", Font.BOLD, 18)); + titleLabel.setForeground(new Color(220, 38, 38)); // Rouge erreur + titleLabel.setHorizontalAlignment(SwingConstants.CENTER); + + // Message de détails + JTextArea detailsArea = new JTextArea(details); + detailsArea.setFont(new Font("SansSerif", Font.PLAIN, 13)); + detailsArea.setForeground(new Color(55, 65, 81)); + detailsArea.setBackground(Color.WHITE); + detailsArea.setEditable(false); + detailsArea.setOpaque(false); + detailsArea.setLineWrap(true); + detailsArea.setWrapStyleWord(true); + + // Panel pour l'icône d'erreur + JPanel iconPanel = new JPanel(); + iconPanel.setBackground(Color.WHITE); + JLabel iconLabel = new JLabel("✗"); + iconLabel.setFont(new Font("SansSerif", Font.BOLD, 48)); + iconLabel.setForeground(new Color(220, 38, 38)); + iconPanel.add(iconLabel); + + panel.add(iconPanel, BorderLayout.NORTH); + panel.add(titleLabel, BorderLayout.CENTER); + panel.add(detailsArea, BorderLayout.SOUTH); + + JOptionPane.showMessageDialog(this, panel, title, JOptionPane.PLAIN_MESSAGE); + } +} \ No newline at end of file diff --git a/src/com/minecraftlauncher/ui/VersionsTabDialog.java b/src/com/minecraftlauncher/ui/VersionsTabDialog.java new file mode 100644 index 0000000..f07cff8 --- /dev/null +++ b/src/com/minecraftlauncher/ui/VersionsTabDialog.java @@ -0,0 +1,376 @@ +// src/com/minecraftlauncher/ui/VersionsTabDialog.java +package com.minecraftlauncher.ui; + +import com.minecraftlauncher.game.GameLauncher; +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeSelectionModel; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; +import java.util.Map; + +public class VersionsTabDialog extends JDialog { + private GameLauncher gameLauncher; + private JTree versionsTree; + private DefaultTreeModel treeModel; + private JButton downloadButton; + private JButton installButton; + private JLabel statusLabel; + private JProgressBar progressBar; + + public VersionsTabDialog(JFrame parent, GameLauncher gameLauncher) { + super(parent, "Toutes les Versions Minecraft", true); + this.gameLauncher = gameLauncher; + + initializeComponents(); + setupLayout(); + setupEventListeners(); + loadAllVersions(); + + setSize(850, 650); + setLocationRelativeTo(parent); + setResizable(true); + } + + private void initializeComponents() { + // Configuration du thème sombre + getContentPane().setBackground(new Color(17, 24, 39)); + + // Arbre des versions avec icônes + DefaultMutableTreeNode root = new DefaultMutableTreeNode("Versions Minecraft"); + treeModel = new DefaultTreeModel(root); + versionsTree = new JTree(treeModel); + versionsTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + versionsTree.setShowsRootHandles(true); + versionsTree.setRootVisible(false); + + // Style amélioré de l'arbre + versionsTree.setBackground(new Color(31, 41, 55)); + versionsTree.setForeground(new Color(243, 244, 246)); + versionsTree.setFont(new Font("SansSerif", Font.PLAIN, 13)); + versionsTree.setRowHeight(25); // Plus d'espace entre les lignes + versionsTree.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Boutons améliorés + downloadButton = new JButton("📥 Télécharger"); + downloadButton.setEnabled(false); + styleModernButton(downloadButton, new Color(34, 197, 94), new Color(22, 163, 74)); // Vert + downloadButton.setPreferredSize(new Dimension(160, 45)); + downloadButton.setToolTipText("Télécharger la version sélectionnée"); + + installButton = new JButton("⚡ Installer"); + installButton.setEnabled(false); + styleModernButton(installButton, new Color(37, 99, 235), new Color(29, 78, 216)); // Bleu + installButton.setPreferredSize(new Dimension(140, 45)); + installButton.setToolTipText("Installer et définir comme version par défaut"); + + JButton refreshButton = new JButton("🔄 Actualiser"); + styleModernButton(refreshButton, new Color(75, 85, 99), new Color(55, 65, 81)); // Gris + refreshButton.setPreferredSize(new Dimension(130, 45)); + refreshButton.addActionListener(e -> loadAllVersions()); + refreshButton.setToolTipText("Actualiser la liste des versions"); + + JButton closeButton = new JButton("✖ Fermer"); + styleModernButton(closeButton, new Color(220, 38, 38), new Color(185, 28, 28)); // Rouge + closeButton.setPreferredSize(new Dimension(110, 45)); + closeButton.addActionListener(e -> dispose()); + + // Barre de progression + progressBar = new JProgressBar(); + progressBar.setStringPainted(true); + progressBar.setVisible(false); + progressBar.setBackground(new Color(31, 41, 55)); + progressBar.setForeground(new Color(34, 197, 94)); + + // Label de statut + statusLabel = new JLabel("Chargement des versions..."); + statusLabel.setForeground(new Color(156, 163, 175)); + statusLabel.setFont(new Font("SansSerif", Font.PLAIN, 11)); + } + + private void styleModernButton(JButton button, Color bgColor, Color hoverColor) { + button.setBackground(bgColor); + button.setForeground(Color.WHITE); + button.setFocusPainted(false); + button.setBorderPainted(false); + button.setFont(new Font("SansSerif", Font.BOLD, 12)); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); + button.setBorder(BorderFactory.createEmptyBorder(8, 16, 8, 16)); + + // Effet hover + button.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseEntered(java.awt.event.MouseEvent evt) { + button.setBackground(hoverColor); + } + public void mouseExited(java.awt.event.MouseEvent evt) { + button.setBackground(bgColor); + } + }); + } + + private void setupLayout() { + setLayout(new BorderLayout(10, 10)); + + // Panel principal avec thème sombre + JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); + mainPanel.setBackground(new Color(17, 24, 39)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // Titre avec icône amélioré + JLabel titleLabel = new JLabel("📦 Télécharger toutes les versions Minecraft", SwingConstants.CENTER); + titleLabel.setFont(new Font("SansSerif", Font.BOLD, 20)); + titleLabel.setForeground(new Color(147, 197, 253)); + titleLabel.setBorder(BorderFactory.createEmptyBorder(5, 0, 10, 0)); + + // Description améliorée + JLabel descLabel = new JLabel("
Explorez toutes les versions disponibles
" + + "🟢 Versions Stables • " + + "🟡 Snapshots • " + + "🔵 Betas • " + + "🟣 Alphas
"); + descLabel.setFont(new Font("SansSerif", Font.PLAIN, 13)); + descLabel.setForeground(new Color(209, 213, 219)); + descLabel.setHorizontalAlignment(SwingConstants.CENTER); + + // Panel du haut + JPanel topPanel = new JPanel(new BorderLayout(5, 5)); + topPanel.setBackground(new Color(17, 24, 39)); + topPanel.add(titleLabel, BorderLayout.CENTER); + topPanel.add(descLabel, BorderLayout.SOUTH); + + // Panel central avec l'arbre amélioré + JScrollPane scrollPane = new JScrollPane(versionsTree); + scrollPane.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(75, 85, 99), 2), + BorderFactory.createEmptyBorder(5, 5, 5, 5) + )); + scrollPane.setPreferredSize(new Dimension(750, 420)); + scrollPane.getViewport().setBackground(new Color(31, 41, 55)); + + // Amélioration du scrollbar + scrollPane.getVerticalScrollBar().setBackground(new Color(31, 41, 55)); + scrollPane.getHorizontalScrollBar().setBackground(new Color(31, 41, 55)); + + // Panel des boutons + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10)); + buttonPanel.setBackground(new Color(17, 24, 39)); + buttonPanel.add(downloadButton); + buttonPanel.add(installButton); + buttonPanel.add(new JButton("Actualiser") {{ + styleModernButton(this, new Color(75, 85, 99), new Color(55, 65, 81)); + setPreferredSize(new Dimension(100, 40)); + addActionListener(e -> loadAllVersions()); + }}); + buttonPanel.add(new JButton("Fermer") {{ + styleModernButton(this, new Color(220, 38, 38), new Color(185, 28, 28)); + setPreferredSize(new Dimension(80, 40)); + addActionListener(e -> dispose()); + }}); + + // Panel du bas avec status et progress + JPanel bottomPanel = new JPanel(new BorderLayout(5, 5)); + bottomPanel.setBackground(new Color(17, 24, 39)); + bottomPanel.add(statusLabel, BorderLayout.WEST); + bottomPanel.add(progressBar, BorderLayout.CENTER); + + mainPanel.add(topPanel, BorderLayout.NORTH); + mainPanel.add(scrollPane, BorderLayout.CENTER); + mainPanel.add(buttonPanel, BorderLayout.SOUTH); + + // Panel final + JPanel finalPanel = new JPanel(new BorderLayout()); + finalPanel.add(mainPanel, BorderLayout.CENTER); + finalPanel.add(bottomPanel, BorderLayout.SOUTH); + + add(finalPanel); + } + + private void setupEventListeners() { + // Sélection dans l'arbre + versionsTree.addTreeSelectionListener(e -> { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) versionsTree.getLastSelectedPathComponent(); + if (node != null && node.isLeaf() && !node.isRoot()) { + String version = node.getUserObject().toString(); + if (!version.contains(" (") && !version.equals("Versions Minecraft")) { // Pas un dossier de catégorie + downloadButton.setEnabled(true); + installButton.setEnabled(true); + updateVersionInfo(version); + } else { + downloadButton.setEnabled(false); + installButton.setEnabled(false); + } + } else { + downloadButton.setEnabled(false); + installButton.setEnabled(false); + } + }); + + // Bouton de téléchargement + downloadButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + downloadSelectedVersion(); + } + }); + + // Bouton d'installation + installButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + installSelectedVersion(); + } + }); + } + + private void loadAllVersions() { + statusLabel.setText("Chargement des versions depuis Mojang..."); + downloadButton.setEnabled(false); + installButton.setEnabled(false); + + new Thread(() -> { + try { + Map> versionsByType = gameLauncher.getAvailableVersionsByType(); + + SwingUtilities.invokeLater(() -> { + populateVersionTree(versionsByType); + statusLabel.setText("Versions chargées avec succès - Sélectionnez une version pour la télécharger"); + }); + + } catch (Exception e) { + SwingUtilities.invokeLater(() -> { + statusLabel.setText("Erreur lors du chargement des versions"); + DarkThemeUtils.showDarkOptionPane(this, + "Erreur lors du chargement des versions :\n" + e.getMessage(), + "Erreur", JOptionPane.ERROR_MESSAGE); + }); + } + }).start(); + } + + private void populateVersionTree(Map> versionsByType) { + DefaultMutableTreeNode root = (DefaultMutableTreeNode) treeModel.getRoot(); + root.removeAllChildren(); + + // Ajouter les versions stables + if (!versionsByType.get("release").isEmpty()) { + DefaultMutableTreeNode releaseNode = new DefaultMutableTreeNode("🟢 Versions Stables (" + versionsByType.get("release").size() + ")"); + for (String version : versionsByType.get("release")) { + releaseNode.add(new DefaultMutableTreeNode(version)); + } + root.add(releaseNode); + } + + // Ajouter les snapshots + if (!versionsByType.get("snapshot").isEmpty()) { + DefaultMutableTreeNode snapshotNode = new DefaultMutableTreeNode("🟡 Snapshots (" + versionsByType.get("snapshot").size() + ")"); + for (String version : versionsByType.get("snapshot")) { + snapshotNode.add(new DefaultMutableTreeNode(version)); + } + root.add(snapshotNode); + } + + // Ajouter les versions beta + if (!versionsByType.get("old_beta").isEmpty()) { + DefaultMutableTreeNode betaNode = new DefaultMutableTreeNode("🔵 Versions Beta (" + versionsByType.get("old_beta").size() + ")"); + for (String version : versionsByType.get("old_beta")) { + betaNode.add(new DefaultMutableTreeNode(version)); + } + root.add(betaNode); + } + + // Ajouter les versions alpha + if (!versionsByType.get("old_alpha").isEmpty()) { + DefaultMutableTreeNode alphaNode = new DefaultMutableTreeNode("🟣 Versions Alpha (" + versionsByType.get("old_alpha").size() + ")"); + for (String version : versionsByType.get("old_alpha")) { + alphaNode.add(new DefaultMutableTreeNode(version)); + } + root.add(alphaNode); + } + + treeModel.reload(); + + // Expand les nœuds par défaut + for (int i = 0; i < versionsTree.getRowCount(); i++) { + versionsTree.expandRow(i); + } + } + + private void updateVersionInfo(String version) { + String status = gameLauncher.getVersionStatus(version); + if (status.startsWith("Non téléchargée")) { + statusLabel.setText("Version " + version + " - Non installée, prête à télécharger"); + } else { + statusLabel.setText("Version " + version + " - " + status); + } + } + + private void downloadSelectedVersion() { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) versionsTree.getLastSelectedPathComponent(); + if (node == null || !node.isLeaf()) return; + + String version = node.getUserObject().toString(); + + downloadButton.setEnabled(false); + installButton.setEnabled(false); + progressBar.setVisible(true); + progressBar.setIndeterminate(true); + statusLabel.setText("Téléchargement de " + version + " en cours..."); + + new Thread(() -> { + try { + boolean success = gameLauncher.downloadVersion(version); + + SwingUtilities.invokeLater(() -> { + progressBar.setVisible(false); + downloadButton.setEnabled(true); + installButton.setEnabled(true); + + if (success) { + statusLabel.setText("Téléchargement de " + version + " terminé avec succès!"); + DarkThemeUtils.showDarkOptionPane(this, + "Version " + version + " téléchargée avec succès!\n" + + "Vous pouvez maintenant la lancer depuis l'interface principale.", + "Téléchargement Réussi", JOptionPane.INFORMATION_MESSAGE); + } else { + statusLabel.setText("Erreur lors du téléchargement de " + version); + DarkThemeUtils.showDarkOptionPane(this, + "Erreur lors du téléchargement de " + version + "\n" + + "Vérifiez votre connexion Internet et réessayez.", + "Erreur de Téléchargement", JOptionPane.ERROR_MESSAGE); + } + }); + + } catch (Exception e) { + SwingUtilities.invokeLater(() -> { + progressBar.setVisible(false); + downloadButton.setEnabled(true); + installButton.setEnabled(true); + statusLabel.setText("Erreur lors du téléchargement"); + DarkThemeUtils.showDarkOptionPane(this, + "Erreur lors du téléchargement :\n" + e.getMessage(), + "Erreur", JOptionPane.ERROR_MESSAGE); + }); + } + }).start(); + } + + private void installSelectedVersion() { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) versionsTree.getLastSelectedPathComponent(); + if (node == null || !node.isLeaf()) return; + + String version = node.getUserObject().toString(); + + int choice = DarkThemeUtils.showDarkConfirmDialog(this, + "Voulez-vous télécharger et définir " + version + " comme version par défaut ?", + "Installation et Configuration", + JOptionPane.YES_NO_OPTION); + + if (choice == JOptionPane.YES_OPTION) { + downloadSelectedVersion(); + // TODO: Définir comme version par défaut dans LauncherFrame + } + } +} \ No newline at end of file