diff --git a/pom.xml b/pom.xml index 2acb3dd..9c315a3 100644 --- a/pom.xml +++ b/pom.xml @@ -108,12 +108,12 @@ org.openautonomousconnection Protocol - 1.0.0-BETA.7.4 + 1.0.0-BETA.7.7 org.openautonomousconnection OACSwing - 1.0.0-BETA.1.1 + 1.0.0-BETA.1.2 org.projectlombok diff --git a/src/main/java/org/openautonomousconnection/webclient/Main.java b/src/main/java/org/openautonomousconnection/webclient/Main.java index bc00485..1f13b71 100644 --- a/src/main/java/org/openautonomousconnection/webclient/Main.java +++ b/src/main/java/org/openautonomousconnection/webclient/Main.java @@ -95,6 +95,6 @@ public class Main { private static void initDesigns() { //TODO - DesignManager.getInstance().registerComponent(DOMContainerPanel.class, OACColor.DARK_BACKGROUND); + //DesignManager.getInstance().registerComponent(DOMContainerPanel.class, OACColor.DARK_BACKGROUND); } } diff --git a/src/main/java/org/openautonomousconnection/webclient/recode/ClientImpl.java b/src/main/java/org/openautonomousconnection/webclient/recode/ClientImpl.java new file mode 100644 index 0000000..39474ce --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webclient/recode/ClientImpl.java @@ -0,0 +1,110 @@ +package org.openautonomousconnection.webclient.recode; + +import dev.unlegitdqrk.unlegitlibrary.event.EventListener; +import dev.unlegitdqrk.unlegitlibrary.event.Listener; +import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.C_PacketSendEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.ClientConnectedEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.ClientDisconnectedEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol; +import org.openautonomousconnection.oacswing.component.OACOptionPane; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; +import org.openautonomousconnection.protocol.side.client.ProtocolClient; +import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolINSServerEvent; +import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolServerEvent; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSResponseStatus; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod; + +import javax.swing.*; +import java.util.List; +import java.util.Map; + +public class ClientImpl extends ProtocolClient { + @Override + public boolean trustINS(String caFingerprint) { + Object[] options = {"Continue", "Cancel"}; + int result = OACOptionPane.showOptionDialog( + Main.getUi(), + "You never connected to this INS before!\n" + + "Fingerprint: " + caFingerprint + "\nDo you want to connect?", + "INS Connection", + OACOptionPane.YES_NO_OPTION, + OACOptionPane.INFORMATION_MESSAGE, + null, + options, + options[0] // default button: Continue + ); + + return result == 0; + } + + @Override + public boolean trustNewINSFingerprint(String oldCAFingerprint, String newCAFingerprint) { + Object[] options = {"Continue", "Cancel"}; + + String table = String.format(""" + Saved Fingerprint\tNew Fingerprint + %s\t%s + """, oldCAFingerprint, newCAFingerprint); + + int result = OACOptionPane.showOptionDialog( + Main.getUi(), + "The fingerprint does not match with the saved fingerprint!\n" + + "Saved Fingerprint: " + oldCAFingerprint + "\n" + + "New Fingerprint: " + newCAFingerprint + "\n" + + "Do you want to connect?", + "INS Connection", + OACOptionPane.YES_NO_OPTION, + OACOptionPane.INFORMATION_MESSAGE, + null, + options, + options[0] // default button: Continue + ); + + return result == 0; + } + + @Override + public void onResponse(INSResponseStatus status, List records) { + try { + String host = records.getFirst().value; + if (!host.contains(":")) host = host + ":1028"; + String[] split = host.split(":"); + Main.getClient().getClientServerConnection().connect(split[0], Integer.parseInt(split[1])); + } catch (Exception e) { + Main.getClient().getProtocolBridge().getLogger().exception("Failed to connect to Server", e); + OACOptionPane.showMessageDialog(Main.getUi(), "Failed to connect to Server:\n" + e.getMessage(), + "Server Connection", OACOptionPane.ERROR_MESSAGE); + } + + super.onResponse(status, records); + } + + @Listener + public void onConnected(ConnectedToProtocolINSServerEvent event) { + try { + Main.getClient().buildServerConnection(null, true); + } catch (Exception e) { + Main.getClient().getProtocolBridge().getLogger().exception("Failed to build Server connection", e); + OACOptionPane.showMessageDialog(Main.getUi(), "Failed to connect to build Server connection:\n" + e.getMessage(), + "Server Connection", OACOptionPane.ERROR_MESSAGE); + } + + Main.getUi().openNewTab("register.oac"); + } + + @Listener + public void onConnected(ConnectedToProtocolServerEvent event) { + try { + this.getClientServerConnection().sendPacket(new WebRequestPacket( + "index.html", + WebRequestMethod.GET, + Map.of(), + null + ), TransportProtocol.TCP); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/org/openautonomousconnection/webclient/recode/FxBootstrap.java b/src/main/java/org/openautonomousconnection/webclient/recode/FxBootstrap.java new file mode 100644 index 0000000..7a602f6 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webclient/recode/FxBootstrap.java @@ -0,0 +1,34 @@ +package org.openautonomousconnection.webclient.recode; + +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Initializes the JavaFX Toolkit exactly once for Swing embedding. + */ +public final class FxBootstrap { + + private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); + + private FxBootstrap() { + // Utility class + } + + /** + * Ensures JavaFX Toolkit is initialized. + * Must be called before any Platform.runLater() usage. + */ + public static void ensureInitialized() { + if (!INITIALIZED.compareAndSet(false, true)) { + return; + } + + // Creating a JFXPanel initializes the JavaFX toolkit in Swing apps. + new JFXPanel(); + + // Optional: keep JavaFX runtime alive even if last window closes. + Platform.setImplicitExit(false); + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webclient/recode/Main.java b/src/main/java/org/openautonomousconnection/webclient/recode/Main.java new file mode 100644 index 0000000..b717d0a --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webclient/recode/Main.java @@ -0,0 +1,72 @@ +package org.openautonomousconnection.webclient.recode; + +import dev.unlegitdqrk.unlegitlibrary.event.EventManager; +import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler; +import lombok.Getter; +import org.openautonomousconnection.infonamelib.InfoNames; +import org.openautonomousconnection.oacswing.component.OACOptionPane; +import org.openautonomousconnection.oacswing.component.design.Design; +import org.openautonomousconnection.oacswing.component.design.DesignManager; +import org.openautonomousconnection.protocol.ProtocolBridge; +import org.openautonomousconnection.protocol.ProtocolValues; +import org.openautonomousconnection.protocol.versions.ProtocolVersion; +import org.openautonomousconnection.webclient.recode.settings.INSList; +import org.openautonomousconnection.webclient.recode.ui.BrowserUI; + +import javax.swing.*; +import java.io.File; + +public class Main { + @Getter + private static ClientImpl client; + private static ProtocolBridge bridge; + + @Getter + private static BrowserUI ui; + + private static void initProtocol() { + InfoNames.registerOACInfoNameProtocols(); + + ProtocolValues values = new ProtocolValues(); + + values.packetHandler = new PacketHandler(); + values.eventManager = new EventManager(); + + client = new ClientImpl(); + + try { + bridge = new ProtocolBridge( + client, + values, + ProtocolVersion.PV_1_0_0_BETA, + new File("logs") + ); + + client.buildINSConnection(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void main(String[] args) { + initProtocol(); + FxBootstrap.ensureInitialized(); + DesignManager.setGlobalDesign(Design.DARK); + + SwingUtilities.invokeLater(() -> { + ui = new BrowserUI(); + ui.setSize(1200, 800); + ui.setLocationRelativeTo(null); + ui.setVisible(true); + + try { + bridge.getProtocolValues().eventManager.registerListener(client); + client.getClientINSConnection().connect(INSList.DEFAULT_INS, INSList.DEFAULT_PORT); + } catch (Exception exception) { + client.getProtocolBridge().getLogger().exception("Failed to connect to INS", exception); + OACOptionPane.showMessageDialog(Main.getUi(), "Failed to connect to INS Server:\n" + exception.getMessage(), + "INS Connection", OACOptionPane.ERROR_MESSAGE); + } + }); + } +} diff --git a/src/main/java/org/openautonomousconnection/webclient/recode/settings/INSList.java b/src/main/java/org/openautonomousconnection/webclient/recode/settings/INSList.java new file mode 100644 index 0000000..c110ea1 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webclient/recode/settings/INSList.java @@ -0,0 +1,18 @@ +package org.openautonomousconnection.webclient.recode.settings; + +import javafx.embed.swing.JFXPanel; + +import java.util.HashMap; + +public class INSList { + + public static String DEFAULT_INS = "open-autonomous-connection.org"; + public static int DEFAULT_PORT = 1026; + + private static HashMap insList = new HashMap<>(); + + public static void registerINS(String host, int tcpPort) { + insList.put(host, tcpPort); + } + +} diff --git a/src/main/java/org/openautonomousconnection/webclient/recode/ui/BrowserTab.java b/src/main/java/org/openautonomousconnection/webclient/recode/ui/BrowserTab.java new file mode 100644 index 0000000..942daae --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webclient/recode/ui/BrowserTab.java @@ -0,0 +1,157 @@ +package org.openautonomousconnection.webclient.recode.ui; + +import org.openautonomousconnection.webclient.recode.Main; + +import java.util.Objects; +import java.util.function.Consumer; + +/** + * A logical browser tab consisting of a key (tab id), a TabView (WebView), + * and navigation helpers. + */ +public class BrowserTab { + + private final String key; + private final TabView view; + + /** + * Creates a browser tab. + * + * @param initialUrl initial URL (used for initial location value) + * @param onLocationChange callback invoked on URL changes + */ + public BrowserTab(String initialUrl, Consumer onLocationChange) { + this.key = Objects.requireNonNull(initialUrl, "initialUrl"); // placeholder key overwritten by BrowserUI + this.view = new TabView(onLocationChange); + } + + /** + * Sets the key (tab id) used by the UI host. + * + * @param key tab key + * @return this + */ + public BrowserTab withKey(String key) { + // The BrowserUI uses titles as keys; keep logic simple. + return new BrowserTabKeyed(key, view); + } + + /** + * Loads a URL. + * + * @param url URL + */ + public void load(String url) { + view.load(url); + } + + /** + * Goes back in history if possible. + */ + public void goBack() { + view.back(); + } + + /** + * Goes forward in history if possible. + */ + public void goForward() { + view.forward(); + } + + /** + * Reloads the page. + */ + public void reload() { + view.reload(); + } + + /** + * Returns current location if known. + * + * @return URL or null + */ + public String getLocation() { + return view.getEngineLocation(); + } + + /** + * Releases resources. + */ + public void dispose() { + view.dispose(); + } + + /** + * Returns the tab key. + * + * @return key + */ + public String getKey() { + return key; + } + + /** + * Returns the Swing component that renders the web content. + * + * @return tab view + */ + public TabView getView() { + return view; + } + + /** + * Internal keyed wrapper so BrowserUI can store a stable key without re-creating the WebView. + */ + private static final class BrowserTabKeyed extends BrowserTab { + + private final String fixedKey; + private final TabView fixedView; + + private BrowserTabKeyed(String fixedKey, TabView fixedView) { + super("about:blank", s -> { }); + this.fixedKey = Objects.requireNonNull(fixedKey, "fixedKey"); + this.fixedView = Objects.requireNonNull(fixedView, "fixedView"); + } + + @Override + public String getKey() { + return fixedKey; + } + + @Override + public TabView getView() { + return fixedView; + } + + @Override + public void load(String url) { + fixedView.load(url); + } + + @Override + public void goBack() { + fixedView.back(); + } + + @Override + public void goForward() { + fixedView.forward(); + } + + @Override + public void reload() { + fixedView.reload(); + } + + @Override + public String getLocation() { + return fixedView.getEngineLocation(); + } + + @Override + public void dispose() { + fixedView.dispose(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webclient/recode/ui/BrowserUI.java b/src/main/java/org/openautonomousconnection/webclient/recode/ui/BrowserUI.java new file mode 100644 index 0000000..0e99536 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webclient/recode/ui/BrowserUI.java @@ -0,0 +1,232 @@ +package org.openautonomousconnection.webclient.recode.ui; + +import org.openautonomousconnection.oacswing.component.OACButton; +import org.openautonomousconnection.oacswing.component.OACFrame; +import org.openautonomousconnection.oacswing.component.OACPanel; +import org.openautonomousconnection.oacswing.component.OACTextField; +import org.openautonomousconnection.oacswing.component.design.DesignManager; + +import javax.swing.*; +import java.awt.*; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * A simple multi-tab browser UI: + * - Tab headers live in the custom title bar (OACTitleBar / OACTabbedPane). + * - Tab content (JavaFX WebView) lives in the frame content area. + * - Address bar controls the currently selected tab. + */ +public class BrowserUI extends OACFrame { + + private static final int TITLE_BAR_HEIGHT = 42; + + private final OACTextField addressField; + private final OACButton goButton; + private final OACButton newTabButton; + private final OACButton closeTabButton; + private final OACButton backButton; + private final OACButton forwardButton; + private final OACButton reloadButton; + + private final CardLayout cardLayout; + private final OACPanel pageHost; + + private final Map tabsByKey = new LinkedHashMap<>(); + private int tabCounter = 0; + + /** + * Creates the browser UI and wires tab selection and address bar to tab content. + */ + public BrowserUI() { + super("OAC Browser"); + + // Content pane must be offset because your title bar is an overlay in the layered pane. + JComponent content = (JComponent) getContentPane(); + content.setLayout(new BorderLayout()); + content.setBorder(BorderFactory.createEmptyBorder(TITLE_BAR_HEIGHT, 0, 0, 0)); + + // Address bar (top of content area) + OACPanel navBar = new OACPanel(new BorderLayout(8, 0)); + navBar.setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10)); + + OACPanel leftControls = new OACPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); + backButton = new OACButton("←"); + forwardButton = new OACButton("→"); + reloadButton = new OACButton("⟳"); + leftControls.add(backButton); + leftControls.add(forwardButton); + leftControls.add(reloadButton); + + addressField = new OACTextField(); + goButton = new OACButton("Go"); + + OACPanel rightControls = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); + newTabButton = new OACButton("+"); + closeTabButton = new OACButton("✕"); + rightControls.add(newTabButton); + rightControls.add(closeTabButton); + + navBar.add(leftControls, BorderLayout.WEST); + navBar.add(addressField, BorderLayout.CENTER); + navBar.add(goButton, BorderLayout.EAST); + navBar.add(rightControls, BorderLayout.EAST); + + // Fix: BorderLayout only allows one EAST; wrap Go+RightControls in a single panel + OACPanel east = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); + + east.add(goButton); + east.add(newTabButton); + east.add(closeTabButton); + navBar.add(east, BorderLayout.EAST); + + content.add(navBar, BorderLayout.NORTH); + + // Page host + cardLayout = new CardLayout(); + pageHost = new OACPanel(cardLayout); + content.add(pageHost, BorderLayout.CENTER); + + // Wire title bar tab selection to content host + getTitleBar().getTabs().addChangeListener(e -> onHeaderTabChanged()); + + // Wire address bar actions + addressField.addActionListener(e -> navigateCurrent(addressField.getText())); + goButton.addActionListener(e -> navigateCurrent(addressField.getText())); + + // Wire navigation buttons + backButton.addActionListener(e -> { + BrowserTab tab = getCurrentTab(); + if (tab != null) tab.goBack(); + }); + forwardButton.addActionListener(e -> { + BrowserTab tab = getCurrentTab(); + if (tab != null) tab.goForward(); + }); + reloadButton.addActionListener(e -> { + BrowserTab tab = getCurrentTab(); + if (tab != null) tab.reload(); + }); + + newTabButton.addActionListener(e -> openNewTab("info.oac")); + closeTabButton.addActionListener(e -> closeCurrentTab()); + + // Create first tab + DesignManager.apply(this); + } + + /** + * Opens a new tab and navigates to the given URL. + * + * @param url initial URL + */ + public void openNewTab(String url) { + String key = nextTabKey(); + BrowserTab tab = new BrowserTab(url, newLocation -> SwingUtilities.invokeLater(() -> { + BrowserTab current = getCurrentTab(); + if (current != null && Objects.equals(current.getKey(), key)) { + addressField.setText(newLocation); + } + })); + + tabsByKey.put(key, tab); + + // Real page content in center host + pageHost.add(tab.getView(), key); + + // Header tab in title bar: DO NOT place the real page here (title bar is only ~42px high). + getTitleBar().addTab(key, new OACPanel()); + + // Select it + int idx = getTitleBar().getTabs().getTabCount() - 1; + getTitleBar().getTabs().setSelectedIndex(idx); + + // Navigate + tab.load(url); + + // Show content + cardLayout.show(pageHost, key); + pageHost.revalidate(); + pageHost.repaint(); + + // Update address field immediately + addressField.setText(url); + } + + /** + * Navigates the current tab to the given input (URL or host). + * + * @param input user input + */ + public void navigateCurrent(String input) { + BrowserTab tab = getCurrentTab(); + if (tab == null) return; + + String url = normalizeUrl(input); + addressField.setText(url); + tab.load(url); + } + + /** + * Closes the currently selected tab. + */ + public void closeCurrentTab() { + int idx = getTitleBar().getTabs().getSelectedIndex(); + if (idx < 0) return; + + String key = getTitleBar().getTabs().getTitleAt(idx); + + BrowserTab removed = tabsByKey.remove(key); + if (removed != null) { + removed.dispose(); + pageHost.remove(removed.getView()); + } + + getTitleBar().getTabs().removeTabAt(idx); + + // If no tabs left, open a new one + if (getTitleBar().getTabs().getTabCount() == 0) { + openNewTab("info.oac"); + return; + } + + // Show selected tab content + onHeaderTabChanged(); + } + + private void onHeaderTabChanged() { + BrowserTab tab = getCurrentTab(); + if (tab == null) return; + + cardLayout.show(pageHost, tab.getKey()); + pageHost.revalidate(); + pageHost.repaint(); + + // Sync address bar + String loc = tab.getLocation(); + if (loc != null && !loc.isBlank()) { + addressField.setText(loc); + } + } + + public BrowserTab getCurrentTab() { + int idx = getTitleBar().getTabs().getSelectedIndex(); + if (idx < 0) return null; + + String key = getTitleBar().getTabs().getTitleAt(idx); + return tabsByKey.get(key); + } + + private String nextTabKey() { + tabCounter++; + return "Tab " + tabCounter; + } + + private static String normalizeUrl(String input) { + String s = input == null ? "" : input.trim(); + if (s.isEmpty()) return "info.oac"; + if (s.startsWith("web://")) return s; + return "web://" + s; + } +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/webclient/recode/ui/TabView.java b/src/main/java/org/openautonomousconnection/webclient/recode/ui/TabView.java new file mode 100644 index 0000000..b5be366 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/webclient/recode/ui/TabView.java @@ -0,0 +1,195 @@ +package org.openautonomousconnection.webclient.recode.ui; + +import dev.unlegitdqrk.unlegitlibrary.event.EventListener; +import dev.unlegitdqrk.unlegitlibrary.event.Listener; +import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.C_PacketReadEvent; +import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportProtocol; +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.scene.Scene; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebHistory; +import javafx.scene.web.WebView; +import org.openautonomousconnection.infonamelib.InfoNames; +import org.openautonomousconnection.oacswing.component.OACOptionPane; +import org.openautonomousconnection.oacswing.component.OACPanel; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebRequestPacket; +import org.openautonomousconnection.protocol.packets.v1_0_0.beta.web.WebResponsePacket; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType; +import org.openautonomousconnection.protocol.versions.v1_0_0.beta.WebRequestMethod; +import org.openautonomousconnection.webclient.recode.Main; + +import javax.swing.*; +import java.awt.*; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +/** + * A Swing panel that embeds a JavaFX WebView. + * The JavaFX scene is initialized on first addNotify(). + */ +public final class TabView extends OACPanel { + + private final AtomicBoolean initialized = new AtomicBoolean(false); + + private JFXPanel fxPanel; + private WebView webView; + private WebEngine engine; + + private final Consumer onLocationChanged; + + /** + * Creates a new tab view. + * + * @param onLocationChanged callback invoked when the WebEngine location changes + */ + public TabView(Consumer onLocationChanged) { + super(); + this.onLocationChanged = Objects.requireNonNull(onLocationChanged, "onLocationChanged"); + setLayout(new BorderLayout()); + Main.getClient().getProtocolBridge().getProtocolValues().eventManager.registerListener(new TabListener(this)); + } + + @Override + public void addNotify() { + super.addNotify(); + + if (!initialized.compareAndSet(false, true)) { + return; + } + + fxPanel = new JFXPanel(); + add(fxPanel, BorderLayout.CENTER); + + Platform.runLater(() -> { + webView = new WebView(); + engine = webView.getEngine(); + + engine.locationProperty().addListener((obs, oldV, newV) -> { + if (newV != null) { + onLocationChanged.accept(newV); + } + }); + + fxPanel.setScene(new Scene(webView)); + }); + } + + /** + * Loads a URL in this tab. + * + * @param url URL to load + */ + public void load(String url) { + String[] parts = url.split("\\."); + if (parts.length < 2 || parts.length > 3) { + throw new IllegalArgumentException( + "Invalid INS address format: " + url + + " (expected name.tln or sub.name.tln)" + ); + } + + String tln = parts[parts.length - 1]; + String name = parts[parts.length - 2]; + String sub = (parts.length == 3) ? parts[0] : null; + + try { + Main.getClient().sendINSQuery(tln, name, sub, INSRecordType.A); + } catch (Exception e) { + Main.getClient().getProtocolBridge().getLogger().exception("Failed to send INS Query", e); + OACOptionPane.showMessageDialog(Main.getUi(), "Failed to send INS Query:\n" + e.getMessage(), + "INS Connection", OACOptionPane.ERROR_MESSAGE); + return; + } + } + + public static class TabListener extends EventListener { + private TabView view; + + public TabListener(TabView view) { + this.view = view; + } + + @Listener + public void onListen(C_PacketReadEvent event) { + if (event.getPacket() instanceof WebResponsePacket response) { + view.parseHtml(new String(response.getBody())); + } + } + } + + public void parseHtml(String html) { + Platform.runLater(() -> { + if (engine != null) engine.loadContent(html); + }); + } + + /** + * Reloads the current page. + */ + public void reload() { + Platform.runLater(() -> { + if (engine != null) { + engine.reload(); + } + }); + } + + /** + * Navigates one step back in history if possible. + */ + public void back() { + Platform.runLater(() -> { + if (engine == null) return; + WebHistory h = engine.getHistory(); + if (h.getCurrentIndex() > 0) { + h.go(-1); + } + }); + } + + /** + * Navigates one step forward in history if possible. + */ + public void forward() { + Platform.runLater(() -> { + if (engine == null) return; + WebHistory h = engine.getHistory(); + if (h.getCurrentIndex() < h.getEntries().size() - 1) { + h.go(1); + } + }); + } + + /** + * Returns the current location (best-effort, may be null until initialized). + * + * @return current location or null + */ + public String getEngineLocation() { + // No blocking: best-effort for UI sync. + return engine != null ? engine.getLocation() : null; + } + + /** + * Disposes JavaFX scene references (best-effort). + */ + public void dispose() { + Platform.runLater(() -> { + if (engine != null) { + try { + engine.load(null); + } catch (Exception ignored) { + // Best-effort cleanup. + } + } + engine = null; + webView = null; + if (fxPanel != null) { + fxPanel.setScene(null); + } + }); + } +} \ No newline at end of file