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();
+ }
+}
diff --git a/src/main/java/org/openautonomousconnection/webclient/ui/GlassPanel.java b/src/main/java/org/openautonomousconnection/webclient/ui/GlassPanel.java
new file mode 100644
index 0000000..18f1562
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/webclient/ui/GlassPanel.java
@@ -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);
+ }
+}
diff --git a/src/main/java/org/openautonomousconnection/webclient/ui/PlusTabSupport.java b/src/main/java/org/openautonomousconnection/webclient/ui/PlusTabSupport.java
new file mode 100644
index 0000000..04081ff
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/webclient/ui/PlusTabSupport.java
@@ -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;
+ }
+}
diff --git a/src/main/java/org/openautonomousconnection/webclient/ui/TabButton.java b/src/main/java/org/openautonomousconnection/webclient/ui/TabButton.java
new file mode 100644
index 0000000..669ac0f
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/webclient/ui/TabButton.java
@@ -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();
+ }
+}
diff --git a/src/main/java/org/openautonomousconnection/webclient/ui/TabView.java b/src/main/java/org/openautonomousconnection/webclient/ui/TabView.java
deleted file mode 100644
index 0a9fb82..0000000
--- a/src/main/java/org/openautonomousconnection/webclient/ui/TabView.java
+++ /dev/null
@@ -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).
- *
- * Loads "web://" URLs so JavaFX can request subresources (CSS, images, href navigations)
- * through URLConnection (mapped to OAC WebRequestPacket).
- */
-public final class TabView extends OACPanel {
-
- private final AtomicBoolean initialized = new AtomicBoolean(false);
- private final Consumer 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 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);
- }
- });
- }
-}
\ No newline at end of file
diff --git a/src/main/java/org/openautonomousconnection/webclient/ui/menus/AboutDialog.java b/src/main/java/org/openautonomousconnection/webclient/ui/menus/AboutDialog.java
new file mode 100644
index 0000000..26e1b82
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/webclient/ui/menus/AboutDialog.java
@@ -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.
+ *
+ * Adjust this method once you confirm where your web-server id is exposed.
+ *
+ * @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);
+ }
+ }
+}
diff --git a/src/main/java/org/openautonomousconnection/webclient/ui/menus/AddonsDialog.java b/src/main/java/org/openautonomousconnection/webclient/ui/menus/AddonsDialog.java
new file mode 100644
index 0000000..7dca316
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/webclient/ui/menus/AddonsDialog.java
@@ -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 addonModel = new DefaultListModel<>();
+ private final OACList 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();
+ });
+ }
+ }
+}
diff --git a/src/main/java/org/openautonomousconnection/webclient/ui/menus/SettingsDialog.java b/src/main/java/org/openautonomousconnection/webclient/ui/menus/SettingsDialog.java
new file mode 100644
index 0000000..4ed78b9
--- /dev/null
+++ b/src/main/java/org/openautonomousconnection/webclient/ui/menus/SettingsDialog.java
@@ -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 insModel = new DefaultListModel<>();
+ private final OACList insList = new OACList<>(insModel);
+ private final OACTextField insHost = new OACTextField();
+ private final OACTextField insPort = new OACTextField();
+
+ private final DefaultListModel favModel = new DefaultListModel<>();
+ private final OACList 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 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 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 + ".");
+ }
+ }
+}