This commit is contained in:
UnlegitDqrk
2026-02-08 22:36:42 +01:00
parent 82e2938294
commit ede26281a2
9 changed files with 821 additions and 3 deletions

View File

@@ -108,12 +108,12 @@
<dependency> <dependency>
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>Protocol</artifactId> <artifactId>Protocol</artifactId>
<version>1.0.0-BETA.7.4</version> <version>1.0.0-BETA.7.7</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.openautonomousconnection</groupId> <groupId>org.openautonomousconnection</groupId>
<artifactId>OACSwing</artifactId> <artifactId>OACSwing</artifactId>
<version>1.0.0-BETA.1.1</version> <version>1.0.0-BETA.1.2</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>

View File

@@ -95,6 +95,6 @@ public class Main {
private static void initDesigns() { private static void initDesigns() {
//TODO //TODO
DesignManager.getInstance().registerComponent(DOMContainerPanel.class, OACColor.DARK_BACKGROUND); //DesignManager.getInstance().registerComponent(DOMContainerPanel.class, OACColor.DARK_BACKGROUND);
} }
} }

View File

@@ -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<INSRecord> 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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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<String, Integer> insList = new HashMap<>();
public static void registerINS(String host, int tcpPort) {
insList.put(host, tcpPort);
}
}

View File

@@ -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<String> 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();
}
}
}

View File

@@ -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<String, BrowserTab> 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;
}
}

View File

@@ -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<String> onLocationChanged;
/**
* Creates a new tab view.
*
* @param onLocationChanged callback invoked when the WebEngine location changes
*/
public TabView(Consumer<String> 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);
}
});
}
}