diff --git a/src/main/java/com/pokemongo/GlobalHotkey.java b/src/main/java/com/pokemongo/GlobalHotkey.java index caa49eb..30c62a8 100644 --- a/src/main/java/com/pokemongo/GlobalHotkey.java +++ b/src/main/java/com/pokemongo/GlobalHotkey.java @@ -7,40 +7,43 @@ import java.util.Set; import javax.swing.JLabel; /** - * Globální hotkey listener pro detekci CTRL+ALT+Z kombinace - * Funguje i když není okno fokusované + * Globální hotkey listener pro detekci CTRL+ALT+Q. + * Na Linuxu funguje i bez fokusu přes nativní polling X11. */ public class GlobalHotkey extends JLabel { private static GlobalHotkey instance; - private GlobalKeyListener globalKeyListener; + private static final long HOTKEY_COOLDOWN_MS = 700; + + private final GlobalKeyListener globalKeyListener; + private final Runnable onHotkey; + private volatile boolean running = true; + private volatile long lastHotkeyTrigger = 0L; + private Thread pollingThread; public GlobalHotkey(Runnable onHotkey) { - this.globalKeyListener = new GlobalKeyListener(onHotkey); + this.onHotkey = onHotkey; + this.globalKeyListener = new GlobalKeyListener(); // Registrace globálního key listeneru pro případy, kdy má aplikace fokus KeyboardFocusManager.getCurrentKeyboardFocusManager() .addKeyEventDispatcher(globalKeyListener); - // Spustit nativní polling pro globální STOP hotkey na Linuxu (funguje i bez fokusu) - if (System.getProperty("os.name").toLowerCase().contains("linux")) { - Thread pollingThread = new Thread(() -> { - while (true) { + // Nativní polling globální hotkey na Linuxu (funguje i bez fokusu) + if (isLinux()) { + pollingThread = new Thread(() -> { + while (running && !Thread.currentThread().isInterrupted()) { try { Thread.sleep(150); - // Kontrola CTRL+ALT+X globálně - // XK_Control_L/R: 0xFFE3/4, XK_Alt_L/R: 0xFFE9/A, XK_x: 0x78, XK_X: 0x58 + // XK_Control_L/R: 0xFFE3/4, XK_Alt_L/R: 0xFFE9/A, XK_q: 0x71, XK_Q: 0x51 boolean ctrl = WindowFinder.isKeyPressedGlobally(0xFFE3) || WindowFinder.isKeyPressedGlobally(0xFFE4); boolean alt = WindowFinder.isKeyPressedGlobally(0xFFE9) || WindowFinder.isKeyPressedGlobally(0xFFEA); - boolean xPressed = WindowFinder.isKeyPressedGlobally(0x78) || WindowFinder.isKeyPressedGlobally(0x58); + boolean qPressed = WindowFinder.isKeyPressedGlobally(0x71) || WindowFinder.isKeyPressedGlobally(0x51); - if (ctrl && alt && xPressed) { - if (onHotkey != null) { - System.out.println("🔌 Globální NATIVNÍ hotkey detekován: CTRL+ALT+X"); - onHotkey.run(); - Thread.sleep(1000); // Prevence vícenásobného spuštění - } + if (ctrl && alt && qPressed) { + triggerHotkey("Globální NATIVNÍ hotkey detekován: CTRL+ALT+Q"); } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); break; } catch (Exception e) { // Ignorovat chyby v pollingu @@ -62,7 +65,7 @@ public class GlobalHotkey extends JLabel { public boolean isKeyPressed(int keyCode) { // Na Linuxu pro šipky používáme výhradně nativní globální detekci. // Standardní KeyEventDispatcher se může "zaseknout", pokud okno ztratí fokus během stisku. - if (System.getProperty("os.name").toLowerCase().contains("linux")) { + if (isLinux()) { if (keyCode == KeyEvent.VK_DOWN) { try { return WindowFinder.isKeyPressedGlobally(0xFF54); // XK_Down @@ -75,17 +78,40 @@ public class GlobalHotkey extends JLabel { } return globalKeyListener.isKeyPressed(keyCode); } + + public void cleanup() { + running = false; + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .removeKeyEventDispatcher(globalKeyListener); + + if (pollingThread != null) { + pollingThread.interrupt(); + pollingThread = null; + } + } + + private static boolean isLinux() { + return System.getProperty("os.name").toLowerCase().contains("linux"); + } + + private void triggerHotkey(String logMessage) { + long now = System.currentTimeMillis(); + if (now - lastHotkeyTrigger < HOTKEY_COOLDOWN_MS) { + return; + } + lastHotkeyTrigger = now; + + if (onHotkey != null) { + System.out.println("🔌 " + logMessage); + onHotkey.run(); + } + } /** * Interní třída pro globální naslouchání na klávesnici */ - private static class GlobalKeyListener implements java.awt.KeyEventDispatcher { - private Runnable callback; + private class GlobalKeyListener implements java.awt.KeyEventDispatcher { private Set pressedKeys = new HashSet<>(); - - public GlobalKeyListener(Runnable callback) { - this.callback = callback; - } public boolean isKeyPressed(int keyCode) { return pressedKeys.contains(keyCode); @@ -96,13 +122,10 @@ public class GlobalHotkey extends JLabel { if (e.getID() == KeyEvent.KEY_PRESSED) { pressedKeys.add(e.getKeyCode()); - // Detekce CTRL+ALT+X - if (e.getKeyCode() == KeyEvent.VK_X && + // Detekce CTRL+ALT+Q + if (e.getKeyCode() == KeyEvent.VK_Q && e.isControlDown() && e.isAltDown()) { - System.out.println("🔌 Globální hotkey detekován: CTRL+ALT+X"); - if (callback != null) { - callback.run(); - } + triggerHotkey("Globální hotkey detekován: CTRL+ALT+Q"); return true; // Konzumovat event } } else if (e.getID() == KeyEvent.KEY_RELEASED) { diff --git a/src/main/java/com/pokemongo/PokemonGoAutomation.java b/src/main/java/com/pokemongo/PokemonGoAutomation.java index d16e3a7..2dd2d21 100644 --- a/src/main/java/com/pokemongo/PokemonGoAutomation.java +++ b/src/main/java/com/pokemongo/PokemonGoAutomation.java @@ -1,15 +1,11 @@ package com.pokemongo; import java.awt.AWTException; -import java.awt.GraphicsDevice; -import java.awt.GraphicsEnvironment; -import java.awt.Insets; import java.awt.MouseInfo; import java.awt.Point; import java.awt.PointerInfo; import java.awt.Rectangle; import java.awt.Robot; -import java.awt.Toolkit; import java.awt.event.InputEvent; import java.awt.image.BufferedImage; import java.io.IOException; @@ -35,6 +31,7 @@ public class PokemonGoAutomation { private static final int DELAY_AFTER_ACTION = 100; // ms private int transferredPokemonCount = 0; // Počet transfernutých pokémonů private Point initialMousePosition = null; // Pozice kurzoru před začátkem automatizace + private volatile boolean stopRequested = false; public PokemonGoAutomation() throws AWTException { this.robot = new Robot(); @@ -57,6 +54,14 @@ public class PokemonGoAutomation { transferredPokemonCount = 0; } + public void requestStop() { + stopRequested = true; + } + + private boolean shouldStop() { + return stopRequested || Thread.currentThread().isInterrupted(); + } + /** * Pořídí screenshot oblasti okna */ @@ -120,6 +125,10 @@ public class PokemonGoAutomation { if (template == null) return 0.0; + if (shouldStop()) { + return 0.0; + } + int tWidth = template.getWidth(); int tHeight = template.getHeight(); @@ -136,6 +145,9 @@ public class PokemonGoAutomation { int requiredMatch = (int) (totalPixels * 0.75); // 75% shody for (int y = 0; y < tHeight; y++) { + if (shouldStop()) { + return 0.0; + } for (int x = 0; x < tWidth; x++) { int templateRGB = template.getRGB(x, y); int screenRGB = screenshot.getRGB(startX + x, startY + y); @@ -175,6 +187,10 @@ public class PokemonGoAutomation { private Point findConfirmTransferButtonPosition(BufferedImage template, int tolerance) { System.out.println("Hledám potvrzovací TRANSFER tlačítko pomocí template matchingu..."); + if (shouldStop()) { + return null; + } + BufferedImage screenshot = captureScreen(windowBounds); int tWidth = template.getWidth(); @@ -190,6 +206,9 @@ public class PokemonGoAutomation { // Skenovat Y-ové pozice s větším krokem - 15px místo 5px pro zrychlení for (int y = searchStartY; y <= searchEndY - tHeight; y += 15) { + if (shouldStop()) { + return null; + } // Skenovat také X pozice pro nalezení středu shody for (int x = 0; x <= windowBounds.width - tWidth; x += 30) { double score = templateMatch(screenshot, x, y, template, tolerance); @@ -218,6 +237,10 @@ public class PokemonGoAutomation { private Point findIncludeButtonPosition(int tolerance) { System.out.println("Hledám tlačítko INCLUDE pomocí template matchingu (include.png)..."); + if (shouldStop()) { + return null; + } + BufferedImage screenshot = captureScreen(windowBounds); int tWidth = includeTemplate.getWidth(); @@ -233,6 +256,9 @@ public class PokemonGoAutomation { // Skenovat Y-ové pozice s větším krokem - 15px místo 5px pro zrychlení for (int y = searchStartY; y <= searchEndY - tHeight; y += 15) { + if (shouldStop()) { + return null; + } // Skenovat také X pozice pro nalezení středu shody for (int x = 0; x <= windowBounds.width - tWidth; x += 30) { double score = templateMatch(screenshot, x, y, includeTemplate, tolerance); @@ -309,6 +335,10 @@ public class PokemonGoAutomation { * Stiskne tlačítko Transfer na spodní části obrazovky */ public void clickTransferButton() { + if (shouldStop()) { + return; + } + // Použít detekci tlačítka // Point buttonPos = findTransferButtonPosition(); Point buttonPos = getAbsolutePoint(300, 1156); @@ -334,6 +364,10 @@ public class PokemonGoAutomation { public void clickConfirmTransferButton(BufferedImage template, int tolerance) { System.out.println("Potvrzuji transfer - hledám confirmation button..."); + if (shouldStop()) { + return; + } + robot.delay(100); // Počkat na zobrazení dialogu // Najít zelené TRANSFER tlačítko v potvrzovacím dialogu @@ -356,6 +390,10 @@ public class PokemonGoAutomation { public boolean clickIncludeButton(int tolerance) { System.out.println("Potvrzuji transfer - hledám include.png (confirmation button)..."); + if (shouldStop()) { + return false; + } + robot.delay(100); // Počkat na zobrazení dialogu // Najít zelené INCLUDE tlačítko v potvrzovacím dialogu @@ -408,11 +446,11 @@ public class PokemonGoAutomation { // Transferovat dokud nedosáhneme požadovaného počtu try { while (transferredCount < totalPokemonCount) { - // Kontrola přerušení - if (Thread.currentThread().isInterrupted()) { - System.out.println("\n⚠️ Automatizace byla přerušena!"); - return; - } + // Kontrola přerušení + if (shouldStop()) { + System.out.println("\n⚠️ Automatizace byla přerušena!"); + return; + } int pokemonThisRound = Math.min(12, totalPokemonCount - transferredCount); @@ -421,43 +459,53 @@ public class PokemonGoAutomation { + " pokémonů ==="); // Vybrat pokémony a získat skutečný počet vybraných - int actualTransferredCount = selectAllPokemonCount(pokemonThisRound); + int actualTransferredCount = selectAllPokemonCount(pokemonThisRound); + + if (shouldStop()) { + System.out.println("\n⚠️ Automatizace byla přerušena během označování!"); + return; + } System.out.println("Čekám na TRANSFER..."); - clickTransferButton(); + clickTransferButton(); + + if (shouldStop()) { + System.out.println("\n⚠️ Automatizace byla přerušena po kliknutí na TRANSFER!"); + return; + } System.out.println("Čekám na INCLUDE dialog..."); - robot.delay(100); - if (clickIncludeButton(110)) { - System.out.println("Čekám na potvrzovací dialog t3..."); robot.delay(100); - clickConfirmTransferButton(t3Template, 70); - } else { - System.out.println("Čekám na potvrzovací dialog t2..."); - robot.delay(100); - clickConfirmTransferButton(t2Template, 70); - } + if (clickIncludeButton(110)) { + System.out.println("Čekám na potvrzovací dialog t3..."); + robot.delay(100); + clickConfirmTransferButton(t3Template, 70); + } else { + System.out.println("Čekám na potvrzovací dialog t2..."); + robot.delay(100); + clickConfirmTransferButton(t2Template, 70); + } - transferredCount += actualTransferredCount; - transferredPokemonCount += actualTransferredCount; + transferredCount += actualTransferredCount; + transferredPokemonCount += actualTransferredCount; - System.out.println("Transferováno v této iteraci: " + actualTransferredCount + " pokémonů (celkem: " + System.out.println("Transferováno v této iteraci: " + actualTransferredCount + " pokémonů (celkem: " + transferredCount + ")"); // Pokud ještě zbývá co transferovat, počkat - if (transferredCount < totalPokemonCount) { - System.out.println("Přestávka: " + delaySeconds + " sekund..."); - // Čekat s kontrolou přerušení - long endTime = System.currentTimeMillis() + (delaySeconds * 1000L); - while (System.currentTimeMillis() < endTime) { - if (Thread.currentThread().isInterrupted()) { - System.out.println("\n⚠️ Automatizace byla přerušena během přestávky!"); - break; + if (transferredCount < totalPokemonCount) { + System.out.println("Přestávka: " + delaySeconds + " sekund..."); + // Čekat s kontrolou přerušení + long endTime = System.currentTimeMillis() + (delaySeconds * 1000L); + while (System.currentTimeMillis() < endTime) { + if (shouldStop()) { + System.out.println("\n⚠️ Automatizace byla přerušena během přestávky!"); + break; + } + robot.delay(100); } - robot.delay(100); } } - } } finally { // Obnovit pozici kurzoru if (initialMousePosition != null) { @@ -487,6 +535,11 @@ public class PokemonGoAutomation { System.out.println("Budu označovat " + maxPokemon + " pokémonů..."); for (int i = 0; i < maxPokemon; i++) { + if (shouldStop()) { + System.out.println("Označování přerušeno."); + return i; + } + Point pos = positions.get(i); System.out.println("Klikám na Pokémona " + (i + 1) + "/" + maxPokemon + " na pozici: " + pos); diff --git a/src/main/java/com/pokemongo/PokemonGoGUI.java b/src/main/java/com/pokemongo/PokemonGoGUI.java index 0f57fde..8be51de 100644 --- a/src/main/java/com/pokemongo/PokemonGoGUI.java +++ b/src/main/java/com/pokemongo/PokemonGoGUI.java @@ -96,7 +96,7 @@ public class PokemonGoGUI extends JFrame { versionLabel.setFont(SMALL_FONT); versionLabel.setForeground(new Color(120, 120, 120)); - stopButton = new JButton("⏹ ZASTAVIT (CTRL+ALT+X)"); + stopButton = new JButton("⏹ ZASTAVIT (CTRL+ALT+Q)"); stopButton.setPreferredSize(new Dimension(150, 30)); styleButton(stopButton, PRIMARY_RED, Color.WHITE, SMALL_FONT); stopButton.setEnabled(false); @@ -261,8 +261,8 @@ public class PokemonGoGUI extends JFrame { add(mainPanel); - // Klávesová zkratka: Ctrl+Alt+Z pro zastavení - KeyStroke hotkey = KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK); + // Klávesová zkratka: Ctrl+Alt+Q pro zastavení + KeyStroke hotkey = KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK); getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(hotkey, "stopAutomationAction"); getRootPane().getActionMap().put("stopAutomationAction", new AbstractAction() { @Override @@ -273,7 +273,7 @@ public class PokemonGoGUI extends JFrame { } }); - // Globální hotkey pro ESC (bonus, když okno není fokusované) + // Globální hotkey (funguje i když okno není fokusované) globalHotkey = new GlobalHotkey(new Runnable() { @Override public void run() { @@ -290,7 +290,7 @@ public class PokemonGoGUI extends JFrame { logArea.append("=== Pokémon GO Automatizace ===\n\n"); logArea.append("Vyberte jednu z automatizací a klikněte na tlačítko START.\n"); logArea.append("Ujistěte se, že je aplikace Pokémon GO spuštěná.\n"); - logArea.append("Pro zastavení stiskněte Ctrl+Alt+X nebo klikněte ZASTAVIT.\n\n"); + logArea.append("Pro zastavení stiskněte Ctrl+Alt+Q nebo klikněte ZASTAVIT.\n\n"); } /** @@ -482,6 +482,7 @@ public class PokemonGoGUI extends JFrame { } // Získat počet pokémonů a čekání z GUI + @SuppressWarnings("unchecked") JComboBox countComboBox = (JComboBox) transferCard.getClientProperty("countComboBox"); JSpinner delaySpinner = (JSpinner) transferCard.getClientProperty("delaySpinner"); @@ -742,10 +743,17 @@ public class PokemonGoGUI extends JFrame { * Zastaví automatizaci */ private void stopAutomation() { + if (!isRunning && !autoClickRunning) { + return; + } + shouldStop = true; autoClickRunning = false; isRunning = false; - stopButton.setEnabled(false); + + if (automation != null) { + automation.requestStop(); + } if (automationThread != null && automationThread.isAlive()) { automationThread.interrupt(); @@ -760,6 +768,7 @@ public class PokemonGoGUI extends JFrame { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { + stopButton.setEnabled(false); logArea.append("\n⚠️ Automatizace byla přerušena uživatelem\n"); statusLabel.setText("Přerušeno"); statusLabel.setForeground(new Color(244, 67, 54)); @@ -852,6 +861,9 @@ public class PokemonGoGUI extends JFrame { @Override public void windowClosing(WindowEvent e) { System.out.println("Zavírám okno..."); + if (frame.globalHotkey != null) { + frame.globalHotkey.cleanup(); + } frame.saveSettings(); System.exit(0); }