Usable Browser

This commit is contained in:
UnlegitDqrk
2026-02-14 22:16:15 +01:00
parent c525fd3ae6
commit 016051e2ed
24 changed files with 2706 additions and 561 deletions

62
pom.xml
View File

@@ -53,46 +53,6 @@
<name>Open Autonomous Public License (OAPL)</name>
<url>https://open-autonomous-connection.org/license.html</url>
</license>
<license>
<name>GNU General Public License v3.0</name>
<url>https://www.gnu.org/licenses/gpl-3.0.html</url>
<comments>
Default license: Applies to all users and projects unless an explicit alternative license has been
granted.
</comments>
</license>
<license>
<name>LPGL 3</name>
<url>https://www.gnu.org/licenses/lgpl-3.0.html#license-text</url>
</license>
<license>
<name>LPGL 2.1</name>
<url>https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.en#SEC1</url>
</license>
<license>
<name>WTPL License</name>
<url>https://github.com/ronmamo/reflections/tree/master?webTab=WTFPL-1-ov-file</url>
</license>
<license>
<name>Apache License 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
<license>
<name>MIT License</name>
<url>https://opensource.org/license/mit</url>
</license>
<license>
<name>javassist</name>
<url>https://github.com/jboss-javassist/javassist/blob/master/License.html</url>
</license>
<license>
<name>projectlombok</name>
<url>https://github.com/projectlombok/lombok?webTab=License-1-ov-file</url>
</license>
<license>
<name>Eclipse Public License v2.0</name>
<url>https://www.eclipse.org/legal/epl-2.0/</url>
</license>
</licenses>
<repositories>
@@ -109,7 +69,17 @@
<dependency>
<groupId>org.openautonomousconnection</groupId>
<artifactId>OACSwing</artifactId>
<version>1.0.0-BETA.1.0</version>
<version>1.0.0-BETA.1.1</version>
</dependency>
<dependency>
<groupId>org.openautonomousconnection</groupId>
<artifactId>LuaScript</artifactId>
<version>1.0.0-BETA.1.1</version>
</dependency>
<dependency>
<groupId>org.openautonomousconnection</groupId>
<artifactId>InfoNameLib</artifactId>
<version>1.0.0-BETA.1.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
@@ -117,16 +87,6 @@
<version>1.18.38</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openautonomousconnection</groupId>
<artifactId>LuaScript</artifactId>
<version>1.0.0-BETA.1.0</version>
</dependency>
<dependency>
<groupId>org.openautonomousconnection</groupId>
<artifactId>InfoNameLib</artifactId>
<version>1.0.0-BETA.1.0</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>

View File

@@ -1,19 +1,36 @@
package org.openautonomousconnection.webclient;
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
import org.openautonomousconnection.infonamelib.LibClientImpl;
import org.openautonomousconnection.infonamelib.OacWebUrlInstaller;
import org.openautonomousconnection.oacswing.component.OACOptionPane;
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolINSServerEvent;
import javax.swing.*;
import java.awt.*;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Protocol client implementation for the WebClient.
*/
public class ClientImpl extends ProtocolClient {
private final LibImpl libImpl = new LibImpl();
private final AtomicBoolean connectedInitialized = new AtomicBoolean(false);
private final Component dialogParent;
private final Runnable onServerReady;
public ClientImpl(Component dialogParent, Runnable onServerReady) {
this.dialogParent = dialogParent;
this.onServerReady = Objects.requireNonNull(onServerReady, "onServerReady");
}
@Override
public boolean trustINS(String caFingerprint) {
Object[] options = {"Continue", "Cancel"};
int result = OACOptionPane.showOptionDialog(
Main.getUi(),
dialogParent,
"You never connected to this INS before!\n" +
"Fingerprint: " + caFingerprint + "\nDo you want to connect?",
"INS Connection",
@@ -21,7 +38,7 @@ public class ClientImpl extends ProtocolClient {
OACOptionPane.INFORMATION_MESSAGE,
null,
options,
options[0] // default button: Continue
options[0]
);
return result == 0;
@@ -31,13 +48,8 @@ public class ClientImpl extends ProtocolClient {
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(),
dialogParent,
"The fingerprint does not match with the saved fingerprint!\n" +
"Saved Fingerprint: " + oldCAFingerprint + "\n" +
"New Fingerprint: " + newCAFingerprint + "\n" +
@@ -47,7 +59,7 @@ public class ClientImpl extends ProtocolClient {
OACOptionPane.INFORMATION_MESSAGE,
null,
options,
options[0] // default button: Continue
options[0]
);
return result == 0;
@@ -57,15 +69,31 @@ public class ClientImpl extends ProtocolClient {
public void onConnected(ConnectedToProtocolINSServerEvent event) {
try {
buildServerConnection(null, getProtocolBridge().getProtocolValues().ssl);
//ProtocolHandlerPackages.installPackage("org.openautonomousconnection.infonamelib");
OacWebUrlInstaller.installOnce(getProtocolBridge().getProtocolValues().eventManager, this);
SwingUtilities.invokeLater(() -> Main.getUi().openNewTab("web://info.oac/"));
OacWebUrlInstaller.installOnce(getProtocolBridge().getProtocolValues().eventManager, this, libImpl);
if (connectedInitialized.compareAndSet(false, true)) {
onServerReady.run();
}
} 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);
getProtocolBridge().getLogger().exception("Failed to build Server connection", e);
OACOptionPane.showMessageDialog(
dialogParent,
"Failed to to build Server connection:\n" + e.getMessage(),
"Server Connection",
OACOptionPane.ERROR_MESSAGE
);
}
}
private class LibImpl extends LibClientImpl {
@Override
public void serverConnectionFailed(Exception exception) {
getProtocolBridge().getLogger().exception("Failed to connect to server", exception);
OACOptionPane.showMessageDialog(
dialogParent,
"Failed to connect to Server:\n" + exception.getMessage(),
"Server Connection",
OACOptionPane.ERROR_MESSAGE
);
}
}
}

View File

@@ -1,4 +1,4 @@
package org.openautonomousconnection.webclient.ui;
package org.openautonomousconnection.webclient;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
@@ -28,7 +28,7 @@ public final class FxBootstrap {
// Creating a JFXPanel initializes the JavaFX toolkit in Swing apps.
new JFXPanel();
// Optional: keep JavaFX runtime alive even if last window closes.
// Keep JavaFX runtime alive even if last window closes.
Platform.setImplicitExit(false);
}
}

View File

@@ -1,53 +1,35 @@
package org.openautonomousconnection.webclient;
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
import lombok.Getter;
import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader;
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.settings.INSList;
import org.openautonomousconnection.webclient.settings.AppSettings;
import org.openautonomousconnection.webclient.settings.SettingsManager;
import org.openautonomousconnection.webclient.ui.BrowserUI;
import org.openautonomousconnection.webclient.ui.FxBootstrap;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
/**
* Application entry point.
*/
public class Main {
@Getter
private static ClientImpl client;
private static ProtocolBridge bridge;
@Getter
private static BrowserUI ui;
private static void initProtocol() {
ProtocolValues values = new ProtocolValues();
values.packetHandler = new PacketHandler();
values.eventManager = new EventManager();
values.ssl = true;
private static AppSettings settings;
client = new ClientImpl();
private static AddonLoader addonLoader;
try {
bridge = new ProtocolBridge(
client,
values,
ProtocolVersion.PV_1_0_0_BETA,
new File("logs")
);
public static BrowserUI getUi() {
return ui;
}
client.buildINSConnection();
} catch (Exception e) {
throw new RuntimeException(e);
}
public static AppSettings getSettings() {
return settings;
}
private static void installDefaultCookieManager() {
@@ -58,24 +40,19 @@ public class Main {
CookieHandler.setDefault(cm);
}
public static void main(String[] args) {
initProtocol();
public static void main(String[] args) throws IOException {
settings = SettingsManager.load();
FxBootstrap.ensureInitialized();
installDefaultCookieManager();
DesignManager.setGlobalDesign(Design.DARK);
SwingUtilities.invokeLater(() -> {
ui = new BrowserUI();
ui = new BrowserUI(settings);
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) {
exception.printStackTrace(System.out);
}
ui.openNewTab(settings.getStartPageUrl());
});
}
}
}

View File

@@ -1,36 +1,40 @@
package org.openautonomousconnection.webclient.lua;
import org.openautonomousconnection.webclient.Main;
import org.openautonomousconnection.webclient.ClientImpl;
import java.util.Objects;
public class WebLogger {
private final String host;
private final ClientImpl client;
public WebLogger(String host) {
public WebLogger(String host, ClientImpl client) {
this.host = host;
this.client = Objects.requireNonNull(client, "client");
}
public void log(String string) {
Main.getClient().getProtocolBridge().getLogger().log(host + ": " + string);
client.getProtocolBridge().getLogger().log(host + ": " + string);
}
public void info(String info) {
Main.getClient().getProtocolBridge().getLogger().info(host + ": " + info);
client.getProtocolBridge().getLogger().info(host + ": " + info);
}
public void warn(String warn) {
Main.getClient().getProtocolBridge().getLogger().warn(host + ": " + warn);
client.getProtocolBridge().getLogger().warn(host + ": " + warn);
}
public void error(String error) {
Main.getClient().getProtocolBridge().getLogger().error(host + ": " + error);
client.getProtocolBridge().getLogger().error(host + ": " + error);
}
public void exception(String infoLine, Exception exception) {
Main.getClient().getProtocolBridge().getLogger().exception(host + ": " + infoLine, exception);
client.getProtocolBridge().getLogger().exception(host + ": " + infoLine, exception);
}
public void debug(String debug) {
Main.getClient().getProtocolBridge().getLogger().debug(host + ": " + debug);
client.getProtocolBridge().getLogger().debug(host + ": " + debug);
}
}

View File

@@ -1,4 +0,0 @@
package org.openautonomousconnection.webclient.lua.hosts;
public class FxFormNavigationBridge {
}

View File

@@ -0,0 +1,192 @@
package org.openautonomousconnection.webclient.settings;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* In-memory settings model.
*
* <p>Persisted by {@link SettingsManager}.</p>
*/
public final class AppSettings {
private final List<InsEndpoint> insEndpoints = new ArrayList<>();
private final List<String> favorites = new ArrayList<>();
private String startPageUrl = "web://info.oac/";
private boolean sslEnabled = true;
private boolean luaEnabled = true;
private boolean historyEnabled = true;
private InsEndpoint selectedIns;
private LuaExecutionPolicy luaPolicy = LuaExecutionPolicy.uiDefault();
/**
* Creates settings with defaults.
*/
public AppSettings() {
// Defaults: include the current INSList defaults as initial endpoint.
insEndpoints.add(new InsEndpoint("open-autonomous-connection.org", 1026));
selectedIns = insEndpoints.get(0);
}
/**
* Returns the configured start page URL.
*
* @return start page URL
*/
public String getStartPageUrl() {
return startPageUrl;
}
/**
* Sets the start page URL.
*
* @param startPageUrl URL (non-null, non-blank)
*/
public void setStartPageUrl(String startPageUrl) {
String s = Objects.requireNonNull(startPageUrl, "startPageUrl").trim();
if (s.isEmpty()) throw new IllegalArgumentException("startPageUrl must not be blank");
this.startPageUrl = s;
}
/**
* Returns whether SSL is enabled for protocol connections.
*
* @return true if enabled
*/
public boolean isSslEnabled() {
return sslEnabled;
}
/**
* Enables/disables SSL.
*
* @param sslEnabled enabled
*/
public void setSslEnabled(boolean sslEnabled) {
this.sslEnabled = sslEnabled;
}
/**
* Returns whether Lua runtime is enabled in WebView.
*
* @return true if enabled
*/
public boolean isLuaEnabled() {
return luaEnabled;
}
/**
* Enables/disables Lua.
*
* @param luaEnabled enabled
*/
public void setLuaEnabled(boolean luaEnabled) {
this.luaEnabled = luaEnabled;
}
/**
* Returns whether history is enabled.
*
* @return true if enabled
*/
public boolean isHistoryEnabled() {
return historyEnabled;
}
/**
* Enables/disables history tracking.
*
* @param historyEnabled enabled
*/
public void setHistoryEnabled(boolean historyEnabled) {
this.historyEnabled = historyEnabled;
}
/**
* Returns a mutable INS endpoint list.
*
* @return list (mutable)
*/
public List<InsEndpoint> getInsEndpointsMutable() {
return insEndpoints;
}
/**
* Returns an immutable view of INS endpoints.
*
* @return endpoints
*/
public List<InsEndpoint> getInsEndpoints() {
return Collections.unmodifiableList(insEndpoints);
}
/**
* Returns currently selected INS.
*
* @return selected endpoint
*/
public InsEndpoint getSelectedIns() {
return selectedIns;
}
/**
* Sets selected INS endpoint.
*
* @param selectedIns endpoint (must exist in list or will be added)
*/
public void setSelectedIns(InsEndpoint selectedIns) {
Objects.requireNonNull(selectedIns, "selectedIns");
if (!insEndpoints.contains(selectedIns)) {
insEndpoints.add(selectedIns);
}
this.selectedIns = selectedIns;
}
/**
* Returns a mutable favorites list.
*
* @return favorites (mutable)
*/
public List<String> getFavoritesMutable() {
return favorites;
}
/**
* Returns immutable favorites.
*
* @return favorites
*/
public List<String> getFavorites() {
return Collections.unmodifiableList(favorites);
}
/**
* Returns Lua execution policy.
*
* @return policy
*/
public LuaExecutionPolicy getLuaPolicy() {
return luaPolicy;
}
/**
* Sets Lua execution policy.
*
* @param luaPolicy policy (non-null)
*/
public void setLuaPolicy(LuaExecutionPolicy luaPolicy) {
this.luaPolicy = Objects.requireNonNull(luaPolicy, "luaPolicy");
}
/**
* Resets the Lua policy back to ui default.
*/
public void resetLuaPolicyToUiDefault() {
this.luaPolicy = new LuaExecutionPolicy(Duration.ofMillis(50L), 200_000L, 5_000);
}
}

View File

@@ -39,6 +39,7 @@ public final class FxEngine implements AutoCloseable {
*
* @param engine web engine
* @param webView web view
* @param logger web logger
*/
public FxEngine(WebEngine engine, WebView webView, WebLogger logger) {
this(engine, webView, LuaExecutionPolicy.uiDefault(), logger);
@@ -50,12 +51,13 @@ public final class FxEngine implements AutoCloseable {
* @param engine web engine
* @param webView web view
* @param policy execution policy
* @param logger web logger
*/
public FxEngine(WebEngine engine, WebView webView, LuaExecutionPolicy policy, WebLogger logger) {
this.engine = Objects.requireNonNull(engine, "engine");
this.webView = webView;
this.webView = Objects.requireNonNull(webView, "webView");
this.policy = Objects.requireNonNull(policy, "policy");
this.logger = logger;
this.logger = Objects.requireNonNull(logger, "logger");
}
/**
@@ -81,32 +83,24 @@ public final class FxEngine implements AutoCloseable {
closeRuntimeQuietly();
// DOM host must exist before event/UI tables, and must ensure stable ids.
FxDomHost dom = new FxDomHost(engine);
dom.ensureAllElementsHaveId();
// Create per-page globals; harden sandbox in production.
Globals globals = LuaGlobalsFactory.create(
new LuaGlobalsFactory.Options()
.enableDebug(false)
.sandbox(true)
);
// Create runtime first (router lives inside it).
ConsoleHostImpl console = new ConsoleHostImpl(logger);
UiHostImpl uiHost = new UiHostImpl(engine, webView, dom);
FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine);
// runtime depends on services; events depends on runtime router.
// We'll create eventHost after runtime, then build HostServices with it.
LuaRuntime rt = new LuaRuntime(globals, new HostServices.Default(uiHost, dom, null, resourceHost, console), policy);
FxEventHost eventHost = new FxEventHost(dom, rt.eventRouter());
// Rebuild services including eventHost and reinstall tables.
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console);
// Replace runtime with correct services (clean and deterministic).
rt.close();
rt = new LuaRuntime(globals, services, policy);

View File

@@ -0,0 +1,82 @@
package org.openautonomousconnection.webclient.settings;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* In-memory history tracker (URL + timestamp).
*
* <p>Persistence can be added later; for now it follows settings toggle and supports clearing.</p>
*/
public final class HistoryManager {
private final List<Entry> entries = new ArrayList<>();
private volatile boolean enabled = true;
/**
* Returns whether history is enabled.
*
* @return enabled
*/
public boolean isEnabled() {
return enabled;
}
/**
* Sets whether history is enabled.
*
* @param enabled enabled
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
if (!enabled) {
clear();
}
}
/**
* Adds a URL to history if enabled.
*
* @param url url (non-null, non-blank)
*/
public void add(String url) {
if (!enabled) return;
String s = Objects.requireNonNull(url, "url").trim();
if (s.isEmpty()) return;
// De-dup consecutive duplicates
if (!entries.isEmpty()) {
Entry last = entries.get(entries.size() - 1);
if (last.url().equals(s)) return;
}
entries.add(new Entry(s, Instant.now()));
}
/**
* Clears history.
*/
public void clear() {
entries.clear();
}
/**
* Returns immutable entries.
*
* @return entries
*/
public List<Entry> entries() {
return Collections.unmodifiableList(entries);
}
/**
* Single history entry.
*
* @param url visited URL
* @param visitedAt timestamp
*/
public record Entry(String url, Instant visitedAt) {
}
}

View File

@@ -0,0 +1,125 @@
package org.openautonomousconnection.webclient.settings;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Persists browsing history to disk.
*
* <p>Format per line: {@code <epochMillis>\t<url>}</p>
*/
public final class HistoryStore {
private static final String FILE_NAME = "history.log";
private final File file;
/**
* Creates a history store in the default user settings directory.
*/
public HistoryStore() {
this(historyFile());
}
/**
* Creates a history store for a specific file.
*
* @param file history file
*/
public HistoryStore(File file) {
this.file = Objects.requireNonNull(file, "file");
File dir = file.getParentFile();
if (dir != null && !dir.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
dir.mkdirs();
}
}
/**
* Returns the default history file location.
*
* @return file
*/
public static File historyFile() {
return new File(FILE_NAME);
}
/**
* Loads history entries from disk.
*
* @return entries (immutable)
*/
public List<Entry> load() {
if (!file.isFile()) return List.of();
List<Entry> out = new ArrayList<>();
try (BufferedReader br = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
int tab = line.indexOf('\t');
if (tab <= 0) continue;
String tsS = line.substring(0, tab).trim();
String url = line.substring(tab + 1).trim();
if (url.isEmpty()) continue;
long ms;
try {
ms = Long.parseLong(tsS);
} catch (Exception ignored) {
continue;
}
out.add(new Entry(url, Instant.ofEpochMilli(ms)));
}
} catch (Exception ignored) {
return List.of();
}
return Collections.unmodifiableList(out);
}
/**
* Appends a single entry to disk (best-effort).
*
* @param url visited URL
* @param at timestamp
*/
public void append(String url, Instant at) {
Objects.requireNonNull(url, "url");
Objects.requireNonNull(at, "at");
String u = url.trim();
if (u.isEmpty()) return;
try (Writer w = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(file, true), StandardCharsets.UTF_8))) {
w.write(Long.toString(at.toEpochMilli()));
w.write('\t');
w.write(u.replace('\n', ' ').replace('\r', ' '));
w.write('\n');
} catch (Exception ignored) {
// Best-effort
}
}
/**
* Clears history file (best-effort).
*/
public void clear() {
try {
Files.deleteIfExists(file.toPath());
} catch (Exception ignored) {
// Best-effort
}
}
/**
* History entry.
*
* @param url visited URL
* @param visitedAt timestamp
*/
public record Entry(String url, Instant visitedAt) {
}
}

View File

@@ -1,16 +0,0 @@
package org.openautonomousconnection.webclient.settings;
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,59 @@
package org.openautonomousconnection.webclient.settings;
import java.util.Objects;
/**
* Represents an INS endpoint (host + TCP port).
*/
public record InsEndpoint(String host, int port) {
/**
* Creates an INS endpoint.
*
* @param host endpoint host (non-null, non-blank)
* @param port tcp port (1..65535)
*/
public InsEndpoint(String host, int port) {
String h = Objects.requireNonNull(host, "host").trim();
if (h.isEmpty()) throw new IllegalArgumentException("host must not be blank");
if (port < 1 || port > 65535) throw new IllegalArgumentException("port out of range: " + port);
this.host = h;
this.port = port;
}
/**
* Returns the host.
*
* @return host
*/
@Override
public String host() {
return host;
}
/**
* Returns the tcp port.
*
* @return port
*/
@Override
public int port() {
return port;
}
@Override
public String toString() {
return host + ":" + port;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof InsEndpoint other)) return false;
return host.equalsIgnoreCase(other.host) && port == other.port;
}
@Override
public int hashCode() {
return host.toLowerCase().hashCode() * 31 + port;
}
}

View File

@@ -0,0 +1,190 @@
package org.openautonomousconnection.webclient.settings;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
/**
* Loads/saves {@link AppSettings} to a simple properties file.
*
* <p>Location: {@code settings.properties}</p>
*/
public final class SettingsManager {
private static final String FILE_NAME = "settings.properties";
private SettingsManager() {
// Utility class
}
/**
* Returns the settings file location.
*
* @return settings file
*/
public static File settingsFile() {
return new File(FILE_NAME);
}
/**
* Loads settings from disk. If missing, returns defaults.
*
* @return settings
*/
public static AppSettings load() {
AppSettings s = new AppSettings();
File f = settingsFile();
if (!f.isFile()) return s;
Properties p = new Properties();
try (InputStream in = Files.newInputStream(f.toPath())) {
p.load(new InputStreamReader(in, StandardCharsets.UTF_8));
} catch (Exception ignored) {
return s;
}
trySetString(p, "startPageUrl", s::setStartPageUrl);
s.setSslEnabled(parseBool(p.getProperty("sslEnabled"), s.isSslEnabled()));
s.setLuaEnabled(parseBool(p.getProperty("luaEnabled"), s.isLuaEnabled()));
s.setHistoryEnabled(parseBool(p.getProperty("historyEnabled"), s.isHistoryEnabled()));
// INS endpoints
List<InsEndpoint> endpoints = new ArrayList<>();
int count = parseInt(p.getProperty("ins.count"), 0);
for (int i = 0; i < count; i++) {
String host = p.getProperty("ins." + i + ".host");
int port = parseInt(p.getProperty("ins." + i + ".port"), -1);
if (host == null || host.isBlank() || port < 1 || port > 65535) continue;
endpoints.add(new InsEndpoint(host.trim(), port));
}
if (!endpoints.isEmpty()) {
s.getInsEndpointsMutable().clear();
s.getInsEndpointsMutable().addAll(endpoints);
}
String selHost = p.getProperty("ins.selected.host");
int selPort = parseInt(p.getProperty("ins.selected.port"), -1);
if (selHost != null && !selHost.isBlank() && selPort >= 1 && selPort <= 65535) {
s.setSelectedIns(new InsEndpoint(selHost.trim(), selPort));
} else {
// Keep default selection, but ensure it exists in list.
s.setSelectedIns(s.getSelectedIns());
}
// Favorites
int favCount = parseInt(p.getProperty("favorites.count"), 0);
s.getFavoritesMutable().clear();
for (int i = 0; i < favCount; i++) {
String url = p.getProperty("favorites." + i);
if (url != null && !url.isBlank()) s.getFavoritesMutable().add(url.trim());
}
// Lua policy
long timeoutMs = parseLong(p.getProperty("lua.timeoutMs"), 50L);
long instr = parseLong(p.getProperty("lua.instructionLimit"), 200_000L);
int hook = parseInt(p.getProperty("lua.hookStep"), 5_000);
try {
s.setLuaPolicy(new LuaExecutionPolicy(Duration.ofMillis(timeoutMs), instr, hook));
} catch (Exception ignored) {
s.resetLuaPolicyToUiDefault();
}
return s;
}
/**
* Saves settings to disk (best-effort).
*
* @param s settings
*/
public static void save(AppSettings s) {
Objects.requireNonNull(s, "s");
File f = settingsFile();
File dir = f.getParentFile();
if (dir != null && !dir.isDirectory()) {
//noinspection ResultOfMethodCallIgnored
dir.mkdirs();
}
Properties p = new Properties();
p.setProperty("startPageUrl", s.getStartPageUrl());
p.setProperty("sslEnabled", Boolean.toString(s.isSslEnabled()));
p.setProperty("luaEnabled", Boolean.toString(s.isLuaEnabled()));
p.setProperty("historyEnabled", Boolean.toString(s.isHistoryEnabled()));
List<InsEndpoint> endpoints = s.getInsEndpoints();
p.setProperty("ins.count", Integer.toString(endpoints.size()));
for (int i = 0; i < endpoints.size(); i++) {
InsEndpoint ep = endpoints.get(i);
p.setProperty("ins." + i + ".host", ep.host());
p.setProperty("ins." + i + ".port", Integer.toString(ep.port()));
}
InsEndpoint sel = s.getSelectedIns();
if (sel != null) {
p.setProperty("ins.selected.host", sel.host());
p.setProperty("ins.selected.port", Integer.toString(sel.port()));
}
List<String> fav = s.getFavorites();
p.setProperty("favorites.count", Integer.toString(fav.size()));
for (int i = 0; i < fav.size(); i++) {
p.setProperty("favorites." + i, fav.get(i));
}
p.setProperty("lua.timeoutMs", Long.toString(s.getLuaPolicy().timeout().toMillis()));
p.setProperty("lua.instructionLimit", Long.toString(s.getLuaPolicy().instructionLimit()));
p.setProperty("lua.hookStep", Integer.toString(s.getLuaPolicy().hookStep()));
try (OutputStream out = Files.newOutputStream(f.toPath())) {
p.store(new OutputStreamWriter(out, StandardCharsets.UTF_8), "OAC WebClient Settings");
} catch (Exception ignored) {
// Best-effort persistence
}
}
private static void trySetString(Properties p, String key, java.util.function.Consumer<String> setter) {
String v = p.getProperty(key);
if (v != null && !v.isBlank()) {
try {
setter.accept(v.trim());
} catch (Exception ignored) {
// Ignore malformed value
}
}
}
private static boolean parseBool(String v, boolean def) {
if (v == null) return def;
String s = v.trim().toLowerCase();
if (s.equals("true")) return true;
if (s.equals("false")) return false;
return def;
}
private static int parseInt(String v, int def) {
if (v == null) return def;
try {
return Integer.parseInt(v.trim());
} catch (Exception e) {
return def;
}
}
private static long parseLong(String v, long def) {
if (v == null) return def;
try {
return Long.parseLong(v.trim());
} catch (Exception e) {
return def;
}
}
}

View File

@@ -0,0 +1,136 @@
package org.openautonomousconnection.webclient.ui;
import org.openautonomousconnection.oacswing.component.OACButton;
import org.openautonomousconnection.oacswing.component.OACPanel;
import org.openautonomousconnection.oacswing.component.OACTextField;
import javax.swing.border.EmptyBorder;
import java.awt.*;
public final class BrowserDesign extends OACPanel {
private static final int HEIGHT_NAV = 44;
private static final int RADIUS = 16;
private final OACButton backButton = iconButton("«");
private final OACButton forwardButton = iconButton("»");
private final OACButton reloadButton = iconButton("");
private final OACTextField addressField = new OACTextField();
private final OACButton goButton = pillButton("\uD83D\uDD0D");
private final OACButton starButton = iconButton("");
private final OACButton menuButton = iconButton("");
private final FavChipBar favoritesBar = new FavChipBar();
public BrowserDesign() {
super(new BorderLayout(0, 0));
setOpaque(false);
GlassPanel card = new GlassPanel(new BorderLayout(0, 0), RADIUS,
Color.decode("#131e34"),
Color.decode("#0f172a"),
Color.decode("#2a3756"));
card.setBorder(new EmptyBorder(8, 10, 8, 10));
card.add(buildNavRow(), BorderLayout.NORTH);
card.add(favoritesBar, BorderLayout.CENTER);
add(card, BorderLayout.CENTER);
setBorder(new EmptyBorder(8, 10, 8, 10));
addressField.setBorder(new EmptyBorder(6, 10, 6, 10));
addressField.setPreferredSize(new Dimension(1, 32));
backButton.setToolTipText("Back");
forwardButton.setToolTipText("Forward");
reloadButton.setToolTipText("Reload");
goButton.setToolTipText("Open URL");
starButton.setToolTipText("Add to favorites");
menuButton.setToolTipText("Menu");
}
private static OACButton iconButton(String text) {
OACButton b = new OACButton(text);
b.setMargin(new Insets(3, 10, 3, 10));
b.setFocusable(false);
b.setPreferredSize(new Dimension(44, 32));
return b;
}
private static OACButton pillButton(String text) {
OACButton b = new OACButton(text);
b.setMargin(new Insets(3, 14, 3, 14));
b.setFocusable(false);
b.setPreferredSize(new Dimension(64, 32));
return b;
}
private Component buildNavRow() {
OACPanel row = new OACPanel(new BorderLayout(10, 0));
row.setOpaque(false);
row.setBorder(new EmptyBorder(2, 2, 8, 2));
row.setPreferredSize(new Dimension(1, HEIGHT_NAV));
OACPanel left = new OACPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
left.setOpaque(false);
left.add(backButton);
left.add(forwardButton);
left.add(reloadButton);
GlassPanel addressPill = new GlassPanel(new BorderLayout(8, 0), 14,
Color.decode("#0f172a"), Color.decode("#0b1220"), Color.decode("#2a3756"));
addressPill.setBorder(new EmptyBorder(2, 8, 2, 8));
addressPill.add(addressField, BorderLayout.CENTER);
OACPanel right = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
right.setOpaque(false);
right.add(goButton);
right.add(starButton);
right.add(menuButton);
row.add(left, BorderLayout.WEST);
row.add(addressPill, BorderLayout.CENTER);
row.add(right, BorderLayout.EAST);
return row;
}
/**
* Returns the favorites bar.
*
* @return favorites bar
*/
public FavChipBar favoritesBar() {
return favoritesBar;
}
public OACButton backButton() {
return backButton;
}
public OACButton forwardButton() {
return forwardButton;
}
public OACButton reloadButton() {
return reloadButton;
}
public OACTextField addressField() {
return addressField;
}
public OACButton goButton() {
return goButton;
}
public OACButton starButton() {
return starButton;
}
public OACButton menuButton() {
return menuButton;
}
}

View File

@@ -1,37 +1,152 @@
package org.openautonomousconnection.webclient.ui;
import javafx.application.Platform;
import javafx.concurrent.Worker;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebHistory;
import javafx.scene.web.WebView;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import org.openautonomousconnection.oacswing.component.OACPanel;
import org.openautonomousconnection.webclient.ClientImpl;
import org.openautonomousconnection.webclient.lua.WebLogger;
import org.openautonomousconnection.webclient.settings.FxEngine;
import org.w3c.dom.Document;
import javax.swing.*;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.awt.*;
import java.io.StringWriter;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
/**
* A logical browser tab consisting of a key (tab id), a TabView (WebView),
* and navigation helpers.
* Logical browser tab: stable UI key + embedded JavaFX WebView.
*
* <p>This class merges the previous {@code BrowserTab} + {@code TabView} into one component.</p>
*/
public class BrowserTab {
public final class BrowserTab extends OACPanel {
private final String key;
private final TabView view;
private final AtomicBoolean initialized = new AtomicBoolean(false);
private final Consumer<String> onLocationChanged;
private final WebLogger webLogger;
private final ClientImpl protocolClient;
private final boolean luaEnabled;
private final LuaExecutionPolicy luaPolicy;
private volatile Runnable openInNewTab;
private JFXPanel fxPanel;
private WebView webView;
private WebEngine engine;
private volatile FxEngine luaEngine;
/**
* Creates a browser tab.
*
* @param initialUrl initial URL (used for initial location value)
* @param key stable UI key (must match CardLayout key and titlebar tab title)
* @param initialUrl initial URL (used for logger context)
* @param onLocationChange callback invoked on URL changes
* @param luaEnabled whether Lua is enabled for this tab
* @param luaPolicy execution policy for Lua
*/
public BrowserTab(String initialUrl, Consumer<String> onLocationChange) {
this.key = Objects.requireNonNull(initialUrl, "initialUrl"); // placeholder key overwritten by BrowserUI
this.view = new TabView(onLocationChange, initialUrl);
public BrowserTab(String key, String initialUrl, Consumer<String> onLocationChange, boolean luaEnabled, LuaExecutionPolicy luaPolicy, ClientImpl protocolClient) {
super();
this.key = Objects.requireNonNull(key, "key");
this.onLocationChanged = Objects.requireNonNull(onLocationChange, "onLocationChange");
this.protocolClient = Objects.requireNonNull(protocolClient, "protocolClient");
this.webLogger = new WebLogger(Objects.requireNonNull(initialUrl, "initialUrl"), protocolClient);
this.luaEnabled = luaEnabled;
this.luaPolicy = Objects.requireNonNull(luaPolicy, "luaPolicy");
setLayout(new BorderLayout());
}
private static String serializeDom(Document doc) throws Exception {
TransformerFactory tf = TransformerFactory.newInstance();
Transformer t = tf.newTransformer();
t.setOutputProperty(OutputKeys.METHOD, "html");
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
t.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
t.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter sw = new StringWriter(64 * 1024);
t.transform(new DOMSource(doc), new StreamResult(sw));
return sw.toString();
}
/**
* Sets the key (tab id) used by the UI host.
* Returns the stable tab key.
*
* @param key tab key
* @return this
* @return key
*/
public BrowserTab withKey(String key) {
// The BrowserUI uses titles as keys; keep logic simple.
return new BrowserTabKeyed(key, view);
public String getKey() {
return key;
}
public ClientImpl getProtocolClient() {
return protocolClient;
}
/**
* Sets callback for opening current page in a new tab.
*
* @param callback callback
*/
public void setOpenInNewTabCallback(Runnable callback) {
this.openInNewTab = callback;
}
@Override
public void addNotify() {
super.addNotify();
if (!initialized.compareAndSet(false, true)) {
return;
}
fxPanel = new JFXPanel();
add(fxPanel, BorderLayout.CENTER);
Platform.runLater(() -> {
webView = new WebView();
webView.setContextMenuEnabled(false);
engine = webView.getEngine();
engine.setJavaScriptEnabled(false);
engine.locationProperty().addListener((obs, oldV, newV) -> fireLocationChanged(newV));
engine.getLoadWorker().stateProperty().addListener((obs, oldS, newS) -> {
if (newS == Worker.State.RUNNING || newS == Worker.State.SUCCEEDED) {
fireLocationChanged(engine.getLocation());
}
});
installCustomContextMenu();
if (luaEnabled) {
luaEngine = new FxEngine(engine, webView, luaPolicy, webLogger);
luaEngine.install();
} else {
luaEngine = null;
}
fxPanel.setScene(new Scene(webView));
});
}
/**
@@ -40,28 +155,54 @@ public class BrowserTab {
* @param url URL
*/
public void loadUrl(String url) {
view.loadUrl(url);
String target = Objects.requireNonNull(url, "url").trim();
if (target.isEmpty()) return;
Platform.runLater(() -> {
WebEngine e = engine;
if (e != null) e.load(target);
});
}
/**
* Reloads the current page.
*/
public void reload() {
String current = getEngineLocation();
if (current == null || current.isBlank()) return;
Platform.runLater(() -> {
WebEngine e = engine;
if (e != null) e.reload();
});
}
/**
* Goes back in history if possible.
*/
public void goBack() {
view.back();
if (peekHistoryTarget(-1) == null) return;
Platform.runLater(() -> {
WebEngine e = engine;
if (e == null) return;
WebHistory h = e.getHistory();
if (h.getCurrentIndex() > 0) h.go(-1);
});
}
/**
* Goes forward in history if possible.
*/
public void goForward() {
view.forward();
}
if (peekHistoryTarget(+1) == null) return;
/**
* Reloads the page.
*/
public void reload() {
view.reload();
Platform.runLater(() -> {
WebEngine e = engine;
if (e == null) return;
WebHistory h = e.getHistory();
if (h.getCurrentIndex() < h.getEntries().size() - 1) h.go(1);
});
}
/**
@@ -69,88 +210,171 @@ public class BrowserTab {
*
* @return URL or null
*/
public String getLocation() {
return view.getEngineLocation();
public String getLocationUrl() {
return getEngineLocation();
}
/**
* Returns current engine location.
*
* @return url or null
*/
public String getEngineLocation() {
WebEngine e = engine;
return e != null ? e.getLocation() : null;
}
/**
* Applies HTML live to the current tab (no saving).
*
* @param html html text
*/
public void applyHtml(String html) {
String content = html == null ? "" : html;
Platform.runLater(() -> {
WebEngine e = engine;
if (e != null) e.loadContent(content, "text/html");
});
}
/**
* Returns the current rendered HTML (DOM serialization).
*
* @return current html (never null)
*/
public String getCurrentHtml() {
CompletableFuture<String> fut = new CompletableFuture<>();
Platform.runLater(() -> {
try {
WebEngine e = engine;
if (e == null) {
fut.complete("");
return;
}
Document doc = e.getDocument();
if (doc == null) {
fut.complete("");
return;
}
fut.complete(serializeDom(doc));
} catch (Throwable t) {
fut.completeExceptionally(t);
}
});
try {
return fut.join();
} catch (Throwable t) {
return "";
}
}
/**
* Releases resources.
*/
public void dispose() {
view.dispose();
FxEngine le = luaEngine;
luaEngine = null;
if (le != null) {
try {
le.close();
} catch (Exception ignored) {
// Intentionally ignored
}
}
Platform.runLater(() -> {
WebEngine e = engine;
if (e != null) {
try {
e.load(null);
} catch (Exception ignored) {
// Intentionally ignored
}
}
engine = null;
webView = null;
if (fxPanel != null) {
fxPanel.setScene(null);
}
});
disconnectProtocolQuietly();
}
/**
* Returns the tab key.
*
* @return key
*/
public String getKey() {
return key;
}
private void fireLocationChanged(String location) {
if (location == null) return;
String s = location.trim();
if (s.isEmpty()) return;
/**
* 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 loadUrl(String url) {
fixedView.loadUrl(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();
try {
onLocationChanged.accept(s);
} catch (Exception ignored) {
// Must not break FX thread
}
}
}
private void installCustomContextMenu() {
final ContextMenu menu = new ContextMenu();
MenuItem back = new MenuItem("Back");
back.setOnAction(e -> SwingUtilities.invokeLater(this::goBack));
MenuItem forward = new MenuItem("Forward");
forward.setOnAction(e -> SwingUtilities.invokeLater(this::goForward));
MenuItem reload = new MenuItem("Reload");
reload.setOnAction(e -> SwingUtilities.invokeLater(this::reload));
MenuItem copyLink = new MenuItem("Copy Link");
copyLink.setOnAction(e -> {
WebEngine e2 = engine;
String loc = e2 != null ? e2.getLocation() : null;
if (loc == null) return;
ClipboardContent cc = new ClipboardContent();
cc.putString(loc);
Clipboard.getSystemClipboard().setContent(cc);
});
MenuItem openNewTab = new MenuItem("Open in New Tab");
openNewTab.setOnAction(e -> {
Runnable r = this.openInNewTab;
if (r != null) SwingUtilities.invokeLater(r);
});
menu.getItems().addAll(back, forward, reload, copyLink, openNewTab);
webView.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, ev -> {
menu.hide();
menu.show(webView, ev.getScreenX(), ev.getScreenY());
ev.consume();
});
}
private String peekHistoryTarget(int delta) {
WebEngine e = engine;
if (e == null) return null;
WebHistory h = e.getHistory();
int idx = h.getCurrentIndex() + delta;
if (idx < 0 || idx >= h.getEntries().size()) return null;
return h.getEntries().get(idx).getUrl();
}
private void disconnectProtocolQuietly() {
try {
if (protocolClient.getClientServerConnection() != null) {
protocolClient.getClientServerConnection().disconnect();
}
} catch (Exception ignored) {
// Best-effort shutdown.
}
try {
if (protocolClient.getClientINSConnection() != null) {
protocolClient.getClientINSConnection().disconnect();
}
} catch (Exception ignored) {
// Best-effort shutdown.
}
}
}

View File

@@ -1,126 +1,113 @@
package org.openautonomousconnection.webclient.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 dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader;
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
import org.openautonomousconnection.oacswing.component.*;
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.ClientImpl;
import org.openautonomousconnection.webclient.settings.AppSettings;
import org.openautonomousconnection.webclient.settings.HistoryStore;
import org.openautonomousconnection.webclient.settings.InsEndpoint;
import org.openautonomousconnection.webclient.settings.SettingsManager;
import org.openautonomousconnection.webclient.ui.menus.AboutDialog;
import org.openautonomousconnection.webclient.ui.menus.AddonsDialog;
import org.openautonomousconnection.webclient.ui.menus.SettingsDialog;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
/**
* 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.
* Multi-tab browser UI.
*/
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 AppSettings settings;
private final HistoryStore historyStore = new HistoryStore();
private final CardLayout cardLayout;
private final OACPanel pageHost;
private final Map<String, BrowserTab> tabsByKey = new LinkedHashMap<>();
private final Map<String, ProtocolBridge> protocolByKey = new LinkedHashMap<>();
private final Map<String, AddonLoader> addonLoaderByKey = new LinkedHashMap<>();
private final BrowserDesign browser;
private final PlusTabSupport plusTabSupport;
private int tabCounter = 0;
private int lastSelectedRealTab = -1;
private boolean handlingTabSwitch = false;
private boolean suppressPlusAutoOpen = false;
/**
* Creates the browser UI and wires tab selection and address bar to tab content.
*/
public BrowserUI() {
public BrowserUI(AppSettings settings) {
super("OAC Browser");
setBackground(DesignManager.getGlobalDesign().getElements().get(OACFrame.class).background().getColor());
this.settings = Objects.requireNonNull(settings, "settings");
// Content pane must be offset because your title bar is an overlay in the layered pane.
JComponent content = (JComponent) getContentPane();
OACPanel content = (OACPanel) 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));
browser = new BrowserDesign();
content.add(browser, BorderLayout.NORTH);
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
plusTabSupport = new PlusTabSupport(getTitleBar().getTabs(), () -> openNewTab(settings.getStartPageUrl()));
plusTabSupport.ensurePlusTab();
getTitleBar().getTabs().addChangeListener(e -> onHeaderTabChanged());
// Wire address bar actions
addressField.addActionListener(e -> navigateCurrent(addressField.getText()));
goButton.addActionListener(e -> navigateCurrent(addressField.getText()));
browser.addressField().addActionListener(e -> navigateCurrent(browser.addressField().getText()));
browser.goButton().addActionListener(e -> navigateCurrent(browser.addressField().getText()));
// Wire navigation buttons
backButton.addActionListener(e -> {
browser.backButton().addActionListener(e -> {
BrowserTab tab = getCurrentTab();
if (tab != null) tab.goBack();
});
forwardButton.addActionListener(e -> {
browser.forwardButton().addActionListener(e -> {
BrowserTab tab = getCurrentTab();
if (tab != null) tab.goForward();
});
reloadButton.addActionListener(e -> {
browser.reloadButton().addActionListener(e -> {
BrowserTab tab = getCurrentTab();
if (tab != null) tab.reload();
});
newTabButton.addActionListener(e -> openNewTab("web://info.oac/"));
closeTabButton.addActionListener(e -> closeCurrentTab());
browser.starButton().addActionListener(e -> addCurrentToFavorites());
browser.menuButton().addActionListener(e -> showAppMenu(browser.menuButton()));
// Create first tab
DesignManager.apply(this);
browser.favoritesBar().setOnNavigate(this::navigateCurrent);
browser.favoritesBar().setOnEdit(this::openSettings);
browser.favoritesBar().setFavorites(settings.getFavorites());
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
SettingsManager.save(settings);
}
});
}
private static String normalizeUrl(String input) {
String s = input == null ? "" : input.trim();
if (s.isEmpty()) return "web://info.oac/";
if (s.startsWith("web://")) {
// Ensure trailing slash for "host only" URLs
String rest = s.substring("web://".length());
if (!rest.contains("/")) return s + "/";
return s;
@@ -128,110 +115,359 @@ public class BrowserUI extends OACFrame {
return "web://" + s + (s.contains("/") ? "" : "/");
}
/**
* Opens a new tab and navigates to the given URL.
*
* @param url initial URL
*/
private static String displayTitleFromUrl(String location) {
if (location == null) return "New Tab";
String s = location.trim();
if (s.isEmpty()) return "New Tab";
if (s.startsWith("web://")) s = s.substring("web://".length());
if (s.startsWith("https://")) s = s.substring("https://".length());
if (s.startsWith("http://")) s = s.substring("http://".length());
int slash = s.indexOf('/');
if (slash > 0) s = s.substring(0, slash);
if (s.isEmpty()) return "New Tab";
if (s.length() <= 24) return s;
return s.substring(0, 24) + "...";
}
private void showAppMenu(Component anchor) {
OACPopupMenu menu = new OACPopupMenu();
OACMenuItem settingsItem = new OACMenuItem("Settings");
settingsItem.addActionListener(e -> openSettings());
OACMenuItem addonsItem = new OACMenuItem("Addons");
addonsItem.addActionListener(e -> new AddonsDialog(this, settings).setVisible(true));
OACMenuItem aboutItem = new OACMenuItem("About");
aboutItem.addActionListener(e -> new AboutDialog(this, settings).setVisible(true));
OACMenu history = new OACMenu("History");
OACCheckBoxMenuItem historyEnabled = new OACCheckBoxMenuItem("Enable History", settings.isHistoryEnabled());
historyEnabled.addActionListener(e -> {
settings.setHistoryEnabled(historyEnabled.isSelected());
SettingsManager.save(settings);
if (!settings.isHistoryEnabled()) historyStore.clear();
});
OACMenuItem show = new OACMenuItem("Show History");
show.addActionListener(e -> showHistoryDialog());
OACMenuItem clear = new OACMenuItem("Clear History");
clear.addActionListener(e -> {
historyStore.clear();
OACOptionPane.showMessageDialog(this, "History cleared.", "History", OACOptionPane.INFORMATION_MESSAGE);
});
history.add(historyEnabled);
history.addSeparator();
history.add(show);
history.add(clear);
menu.add(settingsItem);
menu.addSeparator();
menu.add(history);
menu.addSeparator();
menu.add(addonsItem);
menu.add(aboutItem);
menu.show(anchor, 0, anchor.getHeight());
}
private void showHistoryDialog() {
java.util.List<HistoryStore.Entry> entries = historyStore.load();
OACTextArea area = new OACTextArea();
area.setEditable(false);
area.setLineWrap(true);
area.setWrapStyleWord(true);
StringBuilder sb = new StringBuilder(32_768);
for (int i = entries.size() - 1; i >= 0; i--) {
HistoryStore.Entry e = entries.get(i);
sb.append(e.visitedAt()).append(" ").append(e.url()).append('\n');
}
area.setText(sb.toString());
OACScrollPane sp = new OACScrollPane(area);
sp.setPreferredSize(new Dimension(860, 520));
OACOptionPane.showOptionDialog(this, sp, "History", OACOptionPane.DEFAULT_OPTION, OACOptionPane.INFORMATION_MESSAGE,
null, null, null);
}
private void openSettings() {
SettingsDialog dlg = new SettingsDialog(this, settings, () -> {
SettingsManager.save(settings);
browser.favoritesBar().setFavorites(settings.getFavorites());
if (!settings.isHistoryEnabled()) historyStore.clear();
});
dlg.setVisible(true);
}
private void navigateCurrent(String input) {
BrowserTab tab = getCurrentTab();
if (tab == null) return;
String normalized = normalizeUrl(input);
tab.loadUrl(normalized);
browser.addressField().setText(normalized);
updateTabPresentation(tab.getKey(), normalized);
onVisited(normalized);
}
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);
}
String normalized = normalizeUrl(url);
AtomicReference<BrowserTab> tabRef = new AtomicReference<>();
ClientImpl client = new ClientImpl(this, () -> SwingUtilities.invokeLater(() -> {
BrowserTab readyTab = tabRef.get();
if (readyTab == null) return;
readyTab.loadUrl(normalized);
}));
BrowserTab tab = new BrowserTab(
key,
normalized,
newLocation -> SwingUtilities.invokeLater(() -> {
String selectedKey = getSelectedTabKey();
if (Objects.equals(selectedKey, key)) {
browser.addressField().setText(newLocation);
}
updateTabPresentation(key, newLocation);
onVisited(newLocation);
}),
settings.isLuaEnabled(),
settings.getLuaPolicy(),
client
);
tabRef.set(tab);
tab.setOpenInNewTabCallback(() -> openNewTab(
tab.getEngineLocation() == null ? settings.getStartPageUrl() : tab.getEngineLocation()
));
tabsByKey.put(key, tab);
pageHost.add(tab, key);
// Real page content in center host
pageHost.add(tab.getView(), key);
OACTabbedPane tabs = getTitleBar().getTabs();
suppressPlusAutoOpen = true;
try {
plusTabSupport.ensurePlusTab();
// Header tab in title bar: DO NOT place the real page here (title bar is only ~42px high).
getTitleBar().addTab(key, new OACPanel());
int insertIndex = findPlusIndex(tabs);
if (insertIndex < 0) insertIndex = tabs.getTabCount();
// Select it
int idx = getTitleBar().getTabs().getTabCount() - 1;
getTitleBar().getTabs().setSelectedIndex(idx);
tabs.insertTab(key, null, new OACPanel(), null, insertIndex);
tabs.setTabComponentAt(insertIndex, new TabButton(displayTitleFromUrl(normalized), () -> closeTabByKey(key)));
tabs.setSelectedIndex(insertIndex);
} finally {
suppressPlusAutoOpen = false;
}
// Navigate
tab.loadUrl(url);
// Show content
cardLayout.show(pageHost, key);
pageHost.revalidate();
pageHost.repaint();
browser.addressField().setText(normalized);
// Update address field immediately
addressField.setText(url);
connectTabClient(key, client);
}
/**
* 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;
private void connectTabClient(String key, ClientImpl client) {
try {
File logsFolder = new File("logs");
if (!logsFolder.exists()) logsFolder.mkdir();
String url = normalizeUrl(input);
addressField.setText(url);
tab.loadUrl(url);
}
ProtocolValues values = new ProtocolValues();
values.packetHandler = new PacketHandler();
values.eventManager = new EventManager();
values.ssl = settings.isSslEnabled();
AddonLoader addonLoader = new AddonLoader(values.eventManager,
new Logger(new File(logsFolder, "addons"), false, true));
/**
* Closes the currently selected tab.
*/
public void closeCurrentTab() {
int idx = getTitleBar().getTabs().getSelectedIndex();
if (idx < 0) return;
ProtocolBridge bridge = new ProtocolBridge(
client,
values,
ProtocolVersion.PV_1_0_0_BETA,
new File(logsFolder, "client")
);
String key = getTitleBar().getTabs().getTitleAt(idx);
protocolByKey.put(key, bridge);
addonLoaderByKey.put(key, addonLoader);
BrowserTab removed = tabsByKey.remove(key);
if (removed != null) {
removed.dispose();
pageHost.remove(removed.getView());
}
client.buildINSConnection();
bridge.getProtocolValues().eventManager.registerListener(client);
getTitleBar().getTabs().removeTabAt(idx);
InsEndpoint ep = Objects.requireNonNull(settings.getSelectedIns(), "selectedIns");
client.getClientINSConnection().connect(ep.host(), ep.port());
// If no tabs left, open a new one
if (getTitleBar().getTabs().getTabCount() == 0) {
openNewTab("web://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);
File addonsFolder = new File("addons");
if (!addonsFolder.exists()) addonsFolder.mkdir();
addonLoader.loadAddonsFromDirectory(addonsFolder);
} catch (Exception e) {
OACOptionPane.showMessageDialog(
this,
"Tab connection failed:\n" + e.getMessage(),
"Connection",
OACOptionPane.ERROR_MESSAGE
);
}
}
public BrowserTab getCurrentTab() {
int idx = getTitleBar().getTabs().getSelectedIndex();
if (idx < 0) return null;
String key = getTitleBar().getTabs().getTitleAt(idx);
return tabsByKey.get(key);
private int findPlusIndex(OACTabbedPane tabs) {
for (int i = 0; i < tabs.getTabCount(); i++) {
if ("+".equals(tabs.getTitleAt(i))) return i;
}
return -1;
}
private String nextTabKey() {
tabCounter++;
return "Tab " + tabCounter;
return "Tab-" + tabCounter;
}
}
private void onVisited(String url) {
if (!settings.isHistoryEnabled()) return;
if (url == null || url.isBlank()) return;
historyStore.append(url.trim(), Instant.now());
}
private void addCurrentToFavorites() {
BrowserTab tab = getCurrentTab();
if (tab == null) return;
String loc = tab.getEngineLocation();
if (loc == null || loc.isBlank()) return;
String u = loc.trim();
if (!settings.getFavoritesMutable().contains(u)) {
settings.getFavoritesMutable().add(u);
SettingsManager.save(settings);
browser.favoritesBar().setFavorites(settings.getFavorites());
}
}
private void closeCurrentTab() {
String key = getSelectedTabKey();
if (key == null) return;
closeTabByKey(key);
}
private void closeTabByKey(String key) {
int idx = findTabIndexByKey(key);
if (idx < 0) return;
BrowserTab removed = tabsByKey.remove(key);
if (removed != null) {
removed.dispose();
pageHost.remove(removed);
}
protocolByKey.remove(key);
addonLoaderByKey.remove(key);
OACTabbedPane tabs = getTitleBar().getTabs();
suppressPlusAutoOpen = true;
try {
tabs.removeTabAt(idx);
} finally {
suppressPlusAutoOpen = false;
}
if (tabsByKey.isEmpty()) {
openNewTab(settings.getStartPageUrl());
return;
}
plusTabSupport.ensurePlusTab();
int plusIdx = findPlusIndex(tabs);
if (plusIdx > 0) {
int target = Math.min(Math.max(0, idx), plusIdx - 1);
tabs.setSelectedIndex(target);
}
onHeaderTabChanged();
}
private int findTabIndexByKey(String key) {
OACTabbedPane tabs = getTitleBar().getTabs();
for (int i = 0; i < tabs.getTabCount(); i++) {
if (Objects.equals(tabs.getTitleAt(i), key)) return i;
}
return -1;
}
private void updateTabPresentation(String key, String location) {
int idx = findTabIndexByKey(key);
if (idx < 0) return;
Component component = getTitleBar().getTabs().getTabComponentAt(idx);
if (component instanceof TabButton tabButton) {
tabButton.setTitle(displayTitleFromUrl(location));
}
}
private void onHeaderTabChanged() {
if (handlingTabSwitch) return;
OACTabbedPane tabs = getTitleBar().getTabs();
int idx = tabs.getSelectedIndex();
if (idx < 0) return;
if (plusTabSupport.isPlusTab(idx)) {
if (suppressPlusAutoOpen) {
return;
}
if (tabsByKey.isEmpty()) {
return;
}
handlingTabSwitch = true;
try {
int fallback = lastSelectedRealTab >= 0
? Math.min(lastSelectedRealTab, Math.max(0, tabs.getTabCount() - 1))
: Math.max(0, tabs.getTabCount() - 2);
int next = plusTabSupport.handleIfPlusSelected(idx, fallback);
if (next >= 0 && next < tabs.getTabCount()) {
tabs.setSelectedIndex(next);
}
} finally {
handlingTabSwitch = false;
}
return;
}
lastSelectedRealTab = idx;
String key = tabs.getTitleAt(idx);
BrowserTab tab = tabsByKey.get(key);
if (tab == null) return;
cardLayout.show(pageHost, key);
String loc = tab.getLocationUrl();
if (loc != null && !loc.isBlank()) {
browser.addressField().setText(loc);
}
}
public BrowserTab getCurrentTab() {
String key = getSelectedTabKey();
if (key == null) return null;
return tabsByKey.get(key);
}
public AddonLoader getCurrentAddonLoader() {
String key = getSelectedTabKey();
if (key == null) return null;
return addonLoaderByKey.get(key);
}
private String getSelectedTabKey() {
int idx = getTitleBar().getTabs().getSelectedIndex();
if (idx < 0) return null;
if (plusTabSupport.isPlusTab(idx)) return null;
return getTitleBar().getTabs().getTitleAt(idx);
}
}

View File

@@ -0,0 +1,102 @@
package org.openautonomousconnection.webclient.ui;
import org.openautonomousconnection.oacswing.component.OACButton;
import org.openautonomousconnection.oacswing.component.OACPanel;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Favorites bar rendered as compact chips.
*/
public final class FavChipBar extends OACPanel {
private final List<String> favorites = new ArrayList<>();
private Consumer<String> onNavigate = u -> {
};
private Runnable onEdit = () -> {
};
/**
* Creates a chip-based favorites bar.
*/
public FavChipBar() {
super(new BorderLayout());
setOpaque(false);
setBorder(new EmptyBorder(4, 8, 6, 8));
rebuild();
}
private static String compactLabel(String url) {
String s = url;
if (s.startsWith("web://")) s = s.substring("web://".length());
if (s.endsWith("/")) s = s.substring(0, s.length() - 1);
if (s.isEmpty()) s = url;
if (s.length() <= 24) return s;
return s.substring(0, 24) + "...";
}
/**
* Sets navigation callback.
*
* @param onNavigate callback
*/
public void setOnNavigate(Consumer<String> onNavigate) {
this.onNavigate = Objects.requireNonNull(onNavigate, "onNavigate");
rebuild();
}
/**
* Sets edit callback.
*
* @param onEdit callback
*/
public void setOnEdit(Runnable onEdit) {
this.onEdit = Objects.requireNonNull(onEdit, "onEdit");
rebuild();
}
/**
* Replaces favorites list.
*
* @param items favorites
*/
public void setFavorites(List<String> items) {
favorites.clear();
if (items != null) favorites.addAll(items);
rebuild();
}
private void rebuild() {
removeAll();
OACPanel chips = new OACPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
chips.setOpaque(false);
for (String url : favorites) {
if (url == null || url.isBlank()) continue;
String u = url.trim();
OACButton chip = new OACButton(compactLabel("" + u));
chip.setToolTipText(u);
chip.setMargin(new Insets(3, 10, 3, 10));
chip.setFocusable(false);
chip.addActionListener(e -> onNavigate.accept(u));
chips.add(chip);
}
OACPanel right = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
right.setOpaque(false);
add(chips, BorderLayout.CENTER);
add(right, BorderLayout.EAST);
revalidate();
repaint();
}
}

View File

@@ -0,0 +1,56 @@
package org.openautonomousconnection.webclient.ui;
import org.openautonomousconnection.oacswing.component.OACPanel;
import java.awt.*;
/**
* A modern painted surface with subtle gradient and a hairline border.
*/
public class GlassPanel extends OACPanel {
private final int radius;
private final Color border;
private final Color top;
private final Color bottom;
/**
* Creates a glass-like panel.
*
* @param layout layout
* @param radius corner radius
* @param top gradient top color
* @param bottom gradient bottom color
* @param border border color
*/
public GlassPanel(LayoutManager layout, int radius, Color top, Color bottom, Color border) {
super(layout);
this.radius = radius;
this.top = top;
this.bottom = bottom;
this.border = border;
setOpaque(false);
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
try {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
GradientPaint gp = new GradientPaint(0, 0, top, 0, h, bottom);
g2.setPaint(gp);
g2.fillRoundRect(0, 0, w - 1, h - 1, radius, radius);
g2.setColor(border);
g2.drawRoundRect(0, 0, w - 1, h - 1, radius, radius);
} finally {
g2.dispose();
}
super.paintComponent(g);
}
}

View File

@@ -0,0 +1,77 @@
package org.openautonomousconnection.webclient.ui;
import org.openautonomousconnection.oacswing.component.OACPanel;
import org.openautonomousconnection.oacswing.component.OACTabbedPane;
import java.util.Objects;
/**
* Manages a trailing "+" tab in a tabbed pane component.
*/
public final class PlusTabSupport {
private final OACTabbedPane tabs;
private final Runnable onNewTab;
private final OACPanel dummy = new OACPanel(); // never shown as content
/**
* Creates plus-tab support.
*
* @param tabs tabbed pane
* @param onNewTab callback when "+" is pressed/selected
*/
public PlusTabSupport(OACTabbedPane tabs, Runnable onNewTab) {
this.tabs = Objects.requireNonNull(tabs, "tabs");
this.onNewTab = Objects.requireNonNull(onNewTab, "onNewTab");
}
/**
* Ensures the "+" tab exists as the last tab.
*/
public void ensurePlusTab() {
int plusIdx = findPlusIndex();
if (plusIdx >= 0) return;
tabs.addTab("+", dummy);
}
/**
* Returns whether the index points to the "+" tab.
*
* @param index index
* @return true if plus tab
*/
public boolean isPlusTab(int index) {
int plusIdx = findPlusIndex();
return plusIdx >= 0 && index == plusIdx;
}
/**
* Handles selection change; if "+" selected, triggers new tab and returns previous index to reselect.
*
* @param selectedIndex currently selected index
* @param previousIndex previous index (fallback)
* @return index that should be selected after handling
*/
public int handleIfPlusSelected(int selectedIndex, int previousIndex) {
if (!isPlusTab(selectedIndex)) return selectedIndex;
onNewTab.run();
int plusIdx = findPlusIndex();
if (plusIdx < 0) return 0;
// Select last "real" tab if exists, else 0
int lastReal = Math.max(0, plusIdx - 1);
return lastReal;
}
private int findPlusIndex() {
for (int i = 0; i < tabs.getTabCount(); i++) {
String t = tabs.getTitleAt(i);
if ("+".equals(t)) return i;
}
return -1;
}
}

View File

@@ -0,0 +1,77 @@
package org.openautonomousconnection.webclient.ui;
import org.openautonomousconnection.oacswing.component.OACButton;
import org.openautonomousconnection.oacswing.component.OACLabel;
import org.openautonomousconnection.oacswing.component.OACPanel;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.util.Objects;
/**
* Custom tab header component (favicon + title + close button).
*/
public final class TabButton extends OACPanel {
private final OACLabel iconLabel = new OACLabel("");
private final OACLabel titleLabel = new OACLabel("");
private final OACButton closeButton = new OACButton("x");
/**
* Creates a tab button component.
*
* @param initialTitle initial title
* @param onClose close action
*/
public TabButton(String initialTitle, Runnable onClose) {
super(new BorderLayout(6, 0));
Objects.requireNonNull(onClose, "onClose");
setOpaque(false);
setBorder(new EmptyBorder(3, 10, 3, 6));
iconLabel.setPreferredSize(new Dimension(16, 16));
iconLabel.setMinimumSize(new Dimension(16, 16));
titleLabel.setText(safeTitle(initialTitle));
titleLabel.setBorder(new EmptyBorder(0, 2, 0, 2));
closeButton.setFocusable(false);
closeButton.setMargin(new Insets(1, 8, 1, 8));
closeButton.addActionListener(e -> onClose.run());
add(iconLabel, BorderLayout.WEST);
add(titleLabel, BorderLayout.CENTER);
add(closeButton, BorderLayout.EAST);
}
private static String safeTitle(String title) {
String s = title == null ? "" : title.trim();
if (s.isEmpty()) return "New Tab";
if (s.length() <= 22) return s;
return s.substring(0, 22) + "...";
}
/**
* Sets the tab title.
*
* @param title title
*/
public void setTitle(String title) {
titleLabel.setText(safeTitle(title));
revalidate();
repaint();
}
/**
* Sets the favicon icon (scaled by caller if needed).
*
* @param icon icon or null
*/
public void setFavicon(Icon icon) {
iconLabel.setIcon(icon);
revalidate();
repaint();
}
}

View File

@@ -1,162 +0,0 @@
package org.openautonomousconnection.webclient.ui;
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.oacswing.component.OACPanel;
import org.openautonomousconnection.webclient.lua.WebLogger;
import org.openautonomousconnection.webclient.settings.FxEngine;
import java.awt.*;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
/**
* A Swing panel that embeds a JavaFX WebView and runs LuaScript (no JavaScript).
*
* <p>Loads "web://" URLs so JavaFX can request subresources (CSS, images, href navigations)
* through URLConnection (mapped to OAC WebRequestPacket).</p>
*/
public final class TabView extends OACPanel {
private final AtomicBoolean initialized = new AtomicBoolean(false);
private final Consumer<String> onLocationChanged;
private final WebLogger webLogger;
private JFXPanel fxPanel;
private WebView webView;
private WebEngine engine;
private volatile FxEngine luaEngine;
/**
* Creates a new tab view.
*
* @param onLocationChanged callback invoked when the WebEngine location changes
* @param url callback invoked on URL changes
*/
public TabView(Consumer<String> onLocationChanged, String url) {
super();
this.onLocationChanged = Objects.requireNonNull(onLocationChanged, "onLocationChanged");
this.webLogger = new WebLogger(url);
setLayout(new BorderLayout());
}
@Override
public void addNotify() {
super.addNotify();
if (!initialized.compareAndSet(false, true)) {
return;
}
fxPanel = new JFXPanel();
add(fxPanel, BorderLayout.CENTER);
Platform.runLater(() -> {
webView = new WebView();
webView.setContextMenuEnabled(false);
engine = webView.getEngine();
engine.setJavaScriptEnabled(false);
engine.locationProperty().addListener((obs, oldV, newV) -> {
if (newV != null) {
onLocationChanged.accept(newV);
}
});
// Proper Lua integration from your library
luaEngine = new FxEngine(engine, webView, webLogger);
luaEngine.install();
fxPanel.setScene(new Scene(webView));
});
}
/**
* Loads a normalized URL (expected: web://...).
*
* @param url URL to load
*/
public void loadUrl(String url) {
String u = Objects.requireNonNull(url, "url").trim();
if (u.isEmpty()) return;
Platform.runLater(() -> {
if (engine != null) {
engine.load(u);
}
});
}
/**
* 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 current engine location.
*
* @return location or null
*/
public String getEngineLocation() {
return engine != null ? engine.getLocation() : null;
}
/**
* Disposes resources.
*/
public void dispose() {
FxEngine le = luaEngine;
luaEngine = null;
if (le != null) {
try {
le.close();
} catch (Exception ignored) {
}
}
Platform.runLater(() -> {
if (engine != null) {
try {
engine.load(null);
} catch (Exception ignored) {
}
}
engine = null;
webView = null;
if (fxPanel != null) {
fxPanel.setScene(null);
}
});
}
}

View File

@@ -0,0 +1,134 @@
package org.openautonomousconnection.webclient.ui.menus;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import org.openautonomousconnection.oacswing.component.*;
import org.openautonomousconnection.webclient.ClientImpl;
import org.openautonomousconnection.webclient.Main;
import org.openautonomousconnection.webclient.settings.AppSettings;
import org.openautonomousconnection.webclient.settings.InsEndpoint;
import org.openautonomousconnection.webclient.ui.BrowserTab;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.util.Objects;
/**
* About dialog showing basic client information and quick actions.
*/
public final class AboutDialog extends OACDialog {
/**
* Creates an about dialog.
*
* @param owner owner frame
* @param settings app settings
*/
public AboutDialog(Frame owner, AppSettings settings) {
super(owner, "About", true);
Objects.requireNonNull(settings, "settings");
buildUi(settings);
pack();
setMinimumSize(new Dimension(640, 420));
setLocationRelativeTo(owner);
}
private static String safeClientInsId() {
ClientImpl c = currentClient();
if (c == null) return "N/A";
if (c.getClientINSConnection() == null) return "N/A";
return String.valueOf(c.getClientINSConnection().getUniqueID());
}
/**
* Tries to resolve an additional "web server" id if available in your client.
*
* <p>Adjust this method once you confirm where your web-server id is exposed.</p>
*
* @return id or N/A
*/
private static String safeClientWebId() {
ClientImpl c = currentClient();
if (c == null) return "N/A";
if (c.getClientServerConnection() == null) return "N/A";
return String.valueOf(c.getClientServerConnection().getUniqueID());
}
private static ClientImpl currentClient() {
if (Main.getUi() == null) return null;
BrowserTab tab = Main.getUi().getCurrentTab();
if (tab == null) return null;
return tab.getProtocolClient();
}
private void buildUi(AppSettings settings) {
setLayout(new BorderLayout());
OACPanel root = new OACPanel(new BorderLayout(10, 10));
root.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
OACTextArea info = new OACTextArea();
info.setEditable(false);
info.setLineWrap(true);
info.setWrapStyleWord(true);
InsEndpoint selected = settings.getSelectedIns();
LuaExecutionPolicy pol = settings.getLuaPolicy();
String insId = safeClientInsId();
String webId = safeClientWebId();
info.setText(
"OAC WebClient\n\n" +
"This client embeds JavaFX WebView into Swing and executes Lua scripts (optional) instead of JavaScript.\n\n" +
"Selected INS: " + (selected == null ? "N/A" : selected) + "\n\n" +
"Client IDs:\n" +
" - INS Connection ID: " + insId + "\n" +
" - Web Connection ID: " + webId + "\n\n" +
"Lua Execution Policy:\n" +
" - timeoutMs: " + pol.timeout().toMillis() + "\n" +
" - instructionLimit: " + pol.instructionLimit() + "\n" +
" - hookStep: " + pol.hookStep() + "\n"
);
root.add(new OACScrollPane(info), BorderLayout.CENTER);
OACPanel actions = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
OACButton openCerts = new OACButton("Open Certificates Folder");
openCerts.addActionListener(e -> {
ClientImpl c = currentClient();
if (c == null) {
OACOptionPane.showMessageDialog(this, "No active tab client.", "Certificates", OACOptionPane.WARNING_MESSAGE);
return;
}
openDir(c.getFolderStructure().certificatesFolder.getAbsolutePath(), "Certificates");
});
OACButton openLicenses = new OACButton("Open Licenses Folder");
openLicenses.addActionListener(e -> openDir(new File("licenses").getAbsolutePath(), "Licenses"));
OACButton close = new OACButton("Close");
close.addActionListener(e -> dispose());
actions.add(openCerts);
actions.add(openLicenses);
actions.add(close);
root.add(actions, BorderLayout.SOUTH);
add(root, BorderLayout.CENTER);
}
private void openDir(String dir, String title) {
try {
File f = new File(dir);
Desktop.getDesktop().open(f.getAbsoluteFile());
} catch (Exception ex) {
OACOptionPane.showMessageDialog(this,
"Failed to open " + title + " folder:\n" + ex.getMessage(),
title,
OACOptionPane.ERROR_MESSAGE);
}
}
}

View File

@@ -0,0 +1,257 @@
package org.openautonomousconnection.webclient.ui.menus;
import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader;
import dev.unlegitdqrk.unlegitlibrary.addon.events.AddonLoadedEvent;
import dev.unlegitdqrk.unlegitlibrary.addon.impl.Addon;
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
import org.openautonomousconnection.oacswing.component.*;
import org.openautonomousconnection.webclient.settings.AppSettings;
import org.openautonomousconnection.webclient.ui.BrowserUI;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.util.Objects;
/**
* Addons management dialog for the currently selected tab/client.
*/
public final class AddonsDialog extends OACDialog {
private final BrowserUI browserUi;
private final DefaultListModel<Addon> addonModel = new DefaultListModel<>();
private final OACList<Addon> addonList = new OACList<>(addonModel);
private final OACTextArea details = new OACTextArea();
private final AddonListener addonListener = new AddonListener();
private AddonLoader currentLoader;
public AddonsDialog(Frame owner, AppSettings settings) {
super(owner, "Addons", true);
Objects.requireNonNull(settings, "settings");
this.browserUi = owner instanceof BrowserUI bui ? bui : null;
buildUi();
bindLoader();
refreshAddons();
pack();
setMinimumSize(new Dimension(820, 520));
setLocationRelativeTo(owner);
}
private static String safe(ValueSupplier s) {
try {
String v = s.get();
return v == null || v.isBlank() ? "N/A" : v;
} catch (Exception ignored) {
return "N/A";
}
}
@Override
public void dispose() {
unbindLoader();
super.dispose();
}
private void buildUi() {
setLayout(new BorderLayout(10, 10));
OACPanel root = new OACPanel(new BorderLayout(10, 10));
root.setBorder(javax.swing.BorderFactory.createEmptyBorder(12, 12, 12, 12));
addonList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
addonList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) updateDetails();
});
details.setEditable(false);
details.setLineWrap(true);
details.setWrapStyleWord(true);
OACPanel center = new OACPanel(new GridLayout(1, 2, 10, 0));
center.add(new OACScrollPane(addonList));
center.add(new OACScrollPane(details));
root.add(center, BorderLayout.CENTER);
OACPanel buttons = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
OACButton refresh = new OACButton("Refresh");
refresh.addActionListener(e -> refreshAddons());
OACButton loadDir = new OACButton("Load Dir");
loadDir.addActionListener(e -> loadFromAddonsDirectory());
OACButton loadJar = new OACButton("Load Jar");
loadJar.addActionListener(e -> loadSingleJar());
OACButton enable = new OACButton("Enable");
enable.addActionListener(e -> setSelectedEnabled(true));
OACButton disable = new OACButton("Disable");
disable.addActionListener(e -> setSelectedEnabled(false));
OACButton close = new OACButton("Close");
close.addActionListener(e -> dispose());
buttons.add(new OACLabel("Current tab addons"));
buttons.add(refresh);
buttons.add(loadDir);
buttons.add(loadJar);
buttons.add(enable);
buttons.add(disable);
buttons.add(close);
root.add(buttons, BorderLayout.SOUTH);
add(root, BorderLayout.CENTER);
}
private void bindLoader() {
unbindLoader();
currentLoader = browserUi == null ? null : browserUi.getCurrentAddonLoader();
if (currentLoader != null) {
currentLoader.getEventManager().registerListener(addonListener);
}
}
private void unbindLoader() {
if (currentLoader != null) {
try {
currentLoader.getEventManager().unregisterListener(addonListener);
} catch (Exception ignored) {
// best effort
}
}
currentLoader = null;
}
private void refreshAddons() {
bindLoader();
addonModel.clear();
if (currentLoader == null) {
details.setText("No active tab or no addon loader for current tab.");
return;
}
for (Addon addon : currentLoader.getLoadedAddons()) {
addonModel.addElement(addon);
}
if (addonModel.size() > 0) {
addonList.setSelectedIndex(0);
}
updateDetails();
}
private void loadFromAddonsDirectory() {
if (currentLoader == null) {
OACOptionPane.showMessageDialog(this, "No addon loader for current tab.", "Addons", OACOptionPane.WARNING_MESSAGE);
return;
}
try {
File addonsFolder = new File("addons");
if (!addonsFolder.exists() && !addonsFolder.mkdirs()) {
throw new IllegalStateException("Cannot create addons directory.");
}
currentLoader.loadAddonsFromDirectory(addonsFolder);
refreshAddons();
} catch (Exception ex) {
OACOptionPane.showMessageDialog(this, "Failed to load from directory:\n" + ex.getMessage(), "Addons", OACOptionPane.ERROR_MESSAGE);
}
}
private void loadSingleJar() {
if (currentLoader == null) {
OACOptionPane.showMessageDialog(this, "No addon loader for current tab.", "Addons", OACOptionPane.WARNING_MESSAGE);
return;
}
FileDialog fd = new FileDialog(this, "Select addon jar", FileDialog.LOAD);
fd.setFile("*.jar");
fd.setVisible(true);
String file = fd.getFile();
String dir = fd.getDirectory();
if (file == null || dir == null) return;
try {
currentLoader.loadAddonFromJar(new File(dir, file));
refreshAddons();
} catch (Exception ex) {
OACOptionPane.showMessageDialog(this, "Failed to load jar:\n" + ex.getMessage(), "Addons", OACOptionPane.ERROR_MESSAGE);
}
}
private void setSelectedEnabled(boolean enabled) {
if (currentLoader == null) {
OACOptionPane.showMessageDialog(this, "No addon loader for current tab.", "Addons", OACOptionPane.WARNING_MESSAGE);
return;
}
Addon addon = addonList.getSelectedValue();
if (addon == null) return;
try {
if (enabled) {
currentLoader.enableAddon(addon);
} else {
currentLoader.disableAddon(addon);
}
addonList.repaint();
updateDetails();
} catch (Exception ex) {
OACOptionPane.showMessageDialog(this, "Operation failed:\n" + ex.getMessage(), "Addons", OACOptionPane.ERROR_MESSAGE);
}
}
private void updateDetails() {
Addon addon = addonList.getSelectedValue();
if (addon == null) {
details.setText(addonModel.isEmpty() ? "No addons loaded." : "Select an addon.");
return;
}
String name = safe(() -> addon.getAddonInfo().name());
String version = safe(() -> addon.getAddonInfo().version());
String author = safe(() -> addon.getAddonInfo().author());
details.setText(
"Name: " + name + "\n" +
"Version: " + version + "\n" +
"Author: " + author + "\n" +
"Enabled: " + addon.isEnabled() + "\n" +
"Class: " + addon.getClass().getName()
);
}
private boolean containsAddon(Addon addon) {
for (int i = 0; i < addonModel.size(); i++) {
if (addonModel.get(i) == addon) return true;
}
return false;
}
@FunctionalInterface
private interface ValueSupplier {
String get();
}
private final class AddonListener extends EventListener {
@Listener
public void onLoaded(AddonLoadedEvent event) {
EventQueue.invokeLater(() -> {
Addon a = event.getAddon();
if (a == null) return;
if (!containsAddon(a)) addonModel.addElement(a);
addonList.repaint();
if (addonList.getSelectedValue() == null && addonModel.size() > 0) {
addonList.setSelectedIndex(0);
}
updateDetails();
});
}
}
}

View File

@@ -0,0 +1,417 @@
package org.openautonomousconnection.webclient.ui.menus;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import org.openautonomousconnection.oacswing.component.*;
import org.openautonomousconnection.webclient.settings.AppSettings;
import org.openautonomousconnection.webclient.settings.InsEndpoint;
import javax.swing.*;
import java.awt.*;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
/**
* Settings dialog for WebClient.
*/
public final class SettingsDialog extends OACDialog {
private final AppSettings settings;
private final Runnable onSaved;
private final OACTextField startPageField = new OACTextField();
private final OACCheckBox sslEnabled = new OACCheckBox("Enable SSL");
private final OACCheckBox luaEnabled = new OACCheckBox("Enable Lua");
private final OACCheckBox historyEnabled = new OACCheckBox("Enable History");
private final DefaultListModel<InsEndpoint> insModel = new DefaultListModel<>();
private final OACList<InsEndpoint> insList = new OACList<>(insModel);
private final OACTextField insHost = new OACTextField();
private final OACTextField insPort = new OACTextField();
private final DefaultListModel<String> favModel = new DefaultListModel<>();
private final OACList<String> favList = new OACList<>(favModel);
private final OACTextField favUrl = new OACTextField();
private final OACTextField luaTimeoutMs = new OACTextField();
private final OACTextField luaInstructionLimit = new OACTextField();
private final OACTextField luaHookStep = new OACTextField();
/**
* Creates settings dialog.
*
* @param owner owner frame
* @param settings settings (mutable)
* @param onSaved callback invoked after saving
*/
public SettingsDialog(Frame owner, AppSettings settings, Runnable onSaved) {
super(owner, "Settings", true);
this.settings = Objects.requireNonNull(settings, "settings");
this.onSaved = Objects.requireNonNull(onSaved, "onSaved");
buildUi();
loadFromSettings();
pack();
setMinimumSize(new Dimension(760, 560));
setLocationRelativeTo(owner);
}
private void buildUi() {
setLayout(new BorderLayout());
OACTabbedPane tabs = new OACTabbedPane();
tabs.addTab("General", buildGeneral());
tabs.addTab("INS", buildIns());
tabs.addTab("Favorites", buildFavorites());
tabs.addTab("Lua Policy", buildLuaPolicy());
add(tabs, BorderLayout.CENTER);
OACPanel south = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
OACButton save = new OACButton("Save");
OACButton cancel = new OACButton("Cancel");
save.addActionListener(e -> onSave());
cancel.addActionListener(e -> dispose());
south.add(cancel);
south.add(save);
add(south, BorderLayout.SOUTH);
}
private JComponent buildGeneral() {
OACPanel p = new OACPanel(new GridBagLayout());
p.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(6, 6, 6, 6);
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
int y = 0;
c.gridx = 0;
c.gridy = y;
c.weightx = 0;
p.add(new OACLabel("Start Page"), c);
c.gridx = 1;
c.gridy = y;
c.weightx = 1;
p.add(startPageField, c);
y++;
c.gridx = 0;
c.gridy = y;
c.gridwidth = 2;
OACPanel checks = new OACPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
checks.add(sslEnabled);
checks.add(luaEnabled);
checks.add(historyEnabled);
p.add(checks, c);
return p;
}
private JComponent buildIns() {
OACPanel root = new OACPanel(new BorderLayout(10, 10));
root.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
insList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
root.add(new OACScrollPane(insList), BorderLayout.CENTER);
OACPanel editor = new OACPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(6, 6, 6, 6);
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
c.gridx = 0;
c.gridy = 0;
c.weightx = 0;
editor.add(new OACLabel("Host"), c);
c.gridx = 1;
c.gridy = 0;
c.weightx = 1;
editor.add(insHost, c);
c.gridx = 0;
c.gridy = 1;
c.weightx = 0;
editor.add(new OACLabel("Port"), c);
c.gridx = 1;
c.gridy = 1;
c.weightx = 1;
insPort.setText("1026");
insPort.setToolTipText("Default: 1026");
editor.add(insPort, c);
OACPanel buttons = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
OACButton add = new OACButton("Add");
OACButton remove = new OACButton("Remove");
OACButton select = new OACButton("Select");
add.addActionListener(e -> onAddIns());
remove.addActionListener(e -> onRemoveIns());
select.addActionListener(e -> onSelectIns());
buttons.add(add);
buttons.add(remove);
buttons.add(select);
OACPanel south = new OACPanel(new BorderLayout());
south.add(editor, BorderLayout.CENTER);
south.add(buttons, BorderLayout.SOUTH);
root.add(south, BorderLayout.SOUTH);
return root;
}
private JComponent buildFavorites() {
OACPanel root = new OACPanel(new BorderLayout(10, 10));
root.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
favList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
root.add(new OACScrollPane(favList), BorderLayout.CENTER);
OACPanel addRow = new OACPanel(new BorderLayout(8, 0));
addRow.add(new OACLabel("URL"), BorderLayout.WEST);
addRow.add(favUrl, BorderLayout.CENTER);
OACPanel buttons = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
OACButton add = new OACButton("Add");
OACButton remove = new OACButton("Remove");
add.addActionListener(e -> onAddFavorite());
remove.addActionListener(e -> onRemoveFavorite());
buttons.add(add);
buttons.add(remove);
OACPanel south = new OACPanel(new BorderLayout(8, 8));
south.add(addRow, BorderLayout.CENTER);
south.add(buttons, BorderLayout.SOUTH);
root.add(south, BorderLayout.SOUTH);
return root;
}
private JComponent buildLuaPolicy() {
OACPanel p = new OACPanel(new GridBagLayout());
p.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(6, 6, 6, 6);
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
int y = 0;
c.gridx = 0;
c.gridy = y;
c.weightx = 0;
p.add(new OACLabel("Timeout (ms)"), c);
c.gridx = 1;
c.gridy = y;
c.weightx = 1;
p.add(luaTimeoutMs, c);
y++;
c.gridx = 0;
c.gridy = y;
c.weightx = 0;
p.add(new OACLabel("Instruction Limit"), c);
c.gridx = 1;
c.gridy = y;
c.weightx = 1;
p.add(luaInstructionLimit, c);
y++;
c.gridx = 0;
c.gridy = y;
c.weightx = 0;
p.add(new OACLabel("Hook Step"), c);
c.gridx = 1;
c.gridy = y;
c.weightx = 1;
p.add(luaHookStep, c);
y++;
OACPanel buttons = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
OACButton reset = new OACButton("Reset to uiDefault");
reset.addActionListener(e -> {
settings.resetLuaPolicyToUiDefault();
loadLuaPolicy();
});
buttons.add(reset);
c.gridx = 0;
c.gridy = y;
c.gridwidth = 2;
p.add(buttons, c);
return p;
}
private void loadFromSettings() {
startPageField.setText(settings.getStartPageUrl());
sslEnabled.setSelected(settings.isSslEnabled());
luaEnabled.setSelected(settings.isLuaEnabled());
historyEnabled.setSelected(settings.isHistoryEnabled());
insModel.clear();
for (InsEndpoint ep : settings.getInsEndpoints()) insModel.addElement(ep);
// Select current INS in list if present
InsEndpoint selected = settings.getSelectedIns();
if (selected != null) {
for (int i = 0; i < insModel.size(); i++) {
if (selected.equals(insModel.get(i))) {
insList.setSelectedIndex(i);
break;
}
}
}
favModel.clear();
for (String u : settings.getFavorites()) favModel.addElement(u);
loadLuaPolicy();
}
private void loadLuaPolicy() {
LuaExecutionPolicy pol = settings.getLuaPolicy();
luaTimeoutMs.setText(Long.toString(pol.timeout().toMillis()));
luaInstructionLimit.setText(Long.toString(pol.instructionLimit()));
luaHookStep.setText(Integer.toString(pol.hookStep()));
}
private void onAddIns() {
String host = insHost.getText() == null ? "" : insHost.getText().trim();
String portS = insPort.getText() == null ? "" : insPort.getText().trim();
if (host.isEmpty() || portS.isEmpty()) {
OACOptionPane.showMessageDialog(this, "Host/Port required.", "INS", OACOptionPane.WARNING_MESSAGE);
return;
}
int port;
try {
port = Integer.parseInt(portS);
} catch (Exception e) {
OACOptionPane.showMessageDialog(this, "Invalid port.", "INS", OACOptionPane.WARNING_MESSAGE);
return;
}
InsEndpoint ep;
try {
ep = new InsEndpoint(host, port);
} catch (Exception e) {
OACOptionPane.showMessageDialog(this, e.getMessage(), "INS", OACOptionPane.WARNING_MESSAGE);
return;
}
// Add if not exists
for (int i = 0; i < insModel.size(); i++) {
if (ep.equals(insModel.get(i))) {
insList.setSelectedIndex(i);
return;
}
}
insModel.addElement(ep);
insList.setSelectedIndex(insModel.size() - 1);
}
private void onRemoveIns() {
int idx = insList.getSelectedIndex();
if (idx < 0) return;
InsEndpoint ep = insModel.get(idx);
int confirm = OACOptionPane.showConfirmDialog(this, "Remove " + ep + "?", "INS", OACOptionPane.YES_NO_OPTION);
if (confirm != OACOptionPane.YES_OPTION) return;
insModel.remove(idx);
if (insModel.isEmpty()) {
// Always keep at least one
insModel.addElement(new InsEndpoint("open-autonomous-connection.org", 1026));
}
insList.setSelectedIndex(Math.min(idx, insModel.size() - 1));
}
private void onSelectIns() {
int idx = insList.getSelectedIndex();
if (idx < 0) return;
InsEndpoint ep = insModel.get(idx);
OACOptionPane.showMessageDialog(this,
"Selected INS will be used on next (re)connect:\n" + ep,
"INS", OACOptionPane.INFORMATION_MESSAGE);
}
private void onAddFavorite() {
String url = favUrl.getText() == null ? "" : favUrl.getText().trim();
if (url.isEmpty()) return;
for (int i = 0; i < favModel.size(); i++) {
if (url.equals(favModel.get(i))) return;
}
favModel.addElement(url);
favUrl.setText("");
}
private void onRemoveFavorite() {
int idx = favList.getSelectedIndex();
if (idx < 0) return;
favModel.remove(idx);
}
private void onSave() {
// Validate + apply
String start = startPageField.getText() == null ? "" : startPageField.getText().trim();
if (start.isEmpty()) {
OACOptionPane.showMessageDialog(this, "Start page required.", "Settings", OACOptionPane.WARNING_MESSAGE);
return;
}
try {
settings.setStartPageUrl(start);
settings.setSslEnabled(sslEnabled.isSelected());
settings.setLuaEnabled(luaEnabled.isSelected());
settings.setHistoryEnabled(historyEnabled.isSelected());
// INS list + selection
List<InsEndpoint> list = settings.getInsEndpointsMutable();
list.clear();
for (int i = 0; i < insModel.size(); i++) list.add(insModel.get(i));
int selIdx = insList.getSelectedIndex();
if (selIdx >= 0 && selIdx < insModel.size()) {
settings.setSelectedIns(insModel.get(selIdx));
} else if (!list.isEmpty()) {
settings.setSelectedIns(list.get(0));
}
// Favorites
List<String> favs = settings.getFavoritesMutable();
favs.clear();
for (int i = 0; i < favModel.size(); i++) {
String u = favModel.get(i);
if (u != null && !u.isBlank()) favs.add(u.trim());
}
// Lua policy
long timeoutMs = parseLongOrFail(luaTimeoutMs.getText(), "Timeout (ms)");
long instr = parseLongOrFail(luaInstructionLimit.getText(), "Instruction Limit");
int hook = (int) parseLongOrFail(luaHookStep.getText(), "Hook Step");
settings.setLuaPolicy(new LuaExecutionPolicy(Duration.ofMillis(timeoutMs), instr, hook));
} catch (Exception e) {
OACOptionPane.showMessageDialog(this, e.getMessage(), "Settings", OACOptionPane.ERROR_MESSAGE);
return;
}
onSaved.run();
dispose();
}
private long parseLongOrFail(String text, String field) {
String s = text == null ? "" : text.trim();
if (s.isEmpty()) throw new IllegalArgumentException(field + " is required.");
try {
return Long.parseLong(s);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid number for " + field + ".");
}
}
}