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