Compare commits
32 Commits
classic
...
1.0.1-BETA
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3457ee133 | ||
|
|
b6f7110d16 | ||
|
|
4376fe6daa | ||
|
|
69be55cac0 | ||
|
|
54ff2c449d | ||
|
|
016051e2ed | ||
|
|
c525fd3ae6 | ||
|
|
278871d937 | ||
|
|
8721c58e44 | ||
|
|
ede26281a2 | ||
|
|
82e2938294 | ||
|
|
9e478afd9d | ||
|
|
083d29caa5 | ||
|
|
59f40e9371 | ||
|
|
f5e97403f7 | ||
|
|
252c1519a8 | ||
|
|
d4d2baa3cf | ||
|
|
c4c5f42922 | ||
|
|
2bb9d522f3 | ||
|
|
0e289f7913 | ||
|
|
35e3ec954b | ||
|
|
3fcf6a66a2 | ||
|
|
8b34405594 | ||
| 82c889448b | |||
| 5a0044f59c | |||
|
|
2b7d30810b | ||
| 52f243f3a2 | |||
|
|
1403138089 | ||
|
|
a80eb2ec74 | ||
|
|
4dd9cde494 | ||
|
|
1c6105dda5 | ||
|
|
69438cdf9d |
7
.idea/discord.xml
generated
Normal file
7
.idea/discord.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/misc.xml
generated
6
.idea/misc.xml
generated
@@ -1,9 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
@@ -12,7 +8,7 @@
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="25" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
2
LICENSE
Normal file
2
LICENSE
Normal file
@@ -0,0 +1,2 @@
|
||||
Please read the license here: https://open-autonomous-connection.org/license.html
|
||||
Download all third parties licenses here: https://open-autonomous-connection.org/assets/licenses.zip
|
||||
13
README.MD
13
README.MD
@@ -3,17 +3,16 @@
|
||||
This is the Protocol for our Open Autonomous Connection project.<br />
|
||||
Feel free to join our Discord.
|
||||
<br />
|
||||
|
||||
## License Notice
|
||||
|
||||
This project (OAC) is licensed under the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.org/license.html).
|
||||
This project (OAC) is licensed under
|
||||
the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.org/license.html).
|
||||
|
||||
**Third-party components:**
|
||||
<br />
|
||||
Download all license here: https://open-autonomous-connection.org/assets/licenses.zip
|
||||
- *UnlegitLibrary* is authored by the same copyright holder and is used here under a special agreement:
|
||||
While [UnlegitLibrary](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/) is generally distributed under the [GNU GPLv3](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master/LICENSE),
|
||||
While [UnlegitLibrary](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/) is generally distributed under
|
||||
the [GNU GPLv3](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master/LICENSE),
|
||||
it is additionally licensed under OAPL **exclusively for the OAC project**.
|
||||
Therefore, within OAC, the OAPL terms apply to UnlegitLibrary as well.
|
||||
|
||||
# Bugs/Problems
|
||||
# In progress
|
||||
# TODO
|
||||
128
dependency-reduced-pom.xml
Normal file
128
dependency-reduced-pom.xml
Normal file
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>WebClient</artifactId>
|
||||
<version>1.0.1-BETA.0.1</version>
|
||||
<description>The default WebClient</description>
|
||||
<url>https://open-autonomous-connection.org/</url>
|
||||
<issueManagement>
|
||||
<system>Issue Tracker</system>
|
||||
<url>https://repo.open-autonomous-connection.org/open-autonomous-connection/WebClient/issues</url>
|
||||
</issueManagement>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>UnlegitDqrk</name>
|
||||
<url>https://unlegitdqrk.dev/</url>
|
||||
<organization>Open Autonomous Connection</organization>
|
||||
<organizationUrl>https://open-autonomous-connection.org/</organizationUrl>
|
||||
<roles>
|
||||
<role>Owner</role>
|
||||
<role>Head Developer</role>
|
||||
</roles>
|
||||
</developer>
|
||||
<developer>
|
||||
<name>Maple</name>
|
||||
<url>https://niumaple.carrd.co/</url>
|
||||
<organization>Open Autonomous Connection</organization>
|
||||
<organizationUrl>https://open-autonomous-connection.org/</organizationUrl>
|
||||
<roles>
|
||||
<role>Owner</role>
|
||||
<role>Head Developer</role>
|
||||
</roles>
|
||||
</developer>
|
||||
</developers>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Open Autonomous Public License (OAPL)</name>
|
||||
<url>https://open-autonomous-connection.org/license.html</url>
|
||||
</license>
|
||||
</licenses>
|
||||
<organization>
|
||||
<name>Open Autonomous Connection</name>
|
||||
<url>https://open-autonomous-connection.org/</url>
|
||||
</organization>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer>
|
||||
<mainClass>org.openautonomousconnection.webclient.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.42</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs>
|
||||
<arg>--add-exports</arg>
|
||||
<arg>java.base/sun.security.x509=ALL-UNNAMED</arg>
|
||||
<arg>--add-exports</arg>
|
||||
<arg>java.base/sun.security.util=ALL-UNNAMED</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<failOnError>false</failOnError>
|
||||
<failOnWarnings>false</failOnWarnings>
|
||||
<doclint>none</doclint>
|
||||
<locale>en_US</locale>
|
||||
<encoding>UTF-8</encoding>
|
||||
<docencoding>UTF-8</docencoding>
|
||||
<charset>UTF-8</charset>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<repositories>
|
||||
<repository>
|
||||
<snapshots />
|
||||
<id>oac</id>
|
||||
<url>https://repo.open-autonomous-connection.org/api/packages/open-autonomous-connection/maven</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.42</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.target>25</maven.compiler.target>
|
||||
<maven.compiler.source>25</maven.compiler.source>
|
||||
</properties>
|
||||
</project>
|
||||
122
pom.xml
122
pom.xml
@@ -1,22 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>WebClient</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<version>1.0.1-BETA.0.2</version>
|
||||
<organization>
|
||||
<name>Open Autonomous Connection</name>
|
||||
<url>https://open-autonomous-connection.org/</url>
|
||||
</organization>
|
||||
<url>https://open-autonomous-connection.org/</url>
|
||||
<description>The default DNS-Server</description>
|
||||
<description>The default WebClient</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>23</maven.compiler.source>
|
||||
<maven.compiler.target>23</maven.compiler.target>
|
||||
<maven.compiler.source>25</maven.compiler.source>
|
||||
<maven.compiler.target>25</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
@@ -45,14 +45,13 @@
|
||||
|
||||
<issueManagement>
|
||||
<system>Issue Tracker</system>
|
||||
<url>https://repo.open-autonomous-connection.org/open-autonomous-connection/DNSServer/issues</url>
|
||||
<url>https://repo.open-autonomous-connection.org/open-autonomous-connection/WebClient/issues</url>
|
||||
</issueManagement>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Open Autonomous Public License</name>
|
||||
<url>https://repo.open-autonomous-connection.org/Open-Autonomous-Connection/OAPL/</url>
|
||||
<distribution>repo</distribution>
|
||||
<name>Open Autonomous Public License (OAPL)</name>
|
||||
<url>https://open-autonomous-connection.org/license.html</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
@@ -69,14 +68,111 @@
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>protocol</artifactId>
|
||||
<version>1.0.0-BETA.1</version>
|
||||
<artifactId>OACSwing</artifactId>
|
||||
<version>0.0.0-STABLE.1.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>LuaScript</artifactId>
|
||||
<version>1.0.0-BETA.1.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openautonomousconnection</groupId>
|
||||
<artifactId>Protocol</artifactId>
|
||||
<version>1.0.1-BETA.0.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.38</version>
|
||||
<version>1.18.42</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-base</artifactId>
|
||||
<version>26-ea+22</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-fxml</artifactId>
|
||||
<version>26-ea+22</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-swing</artifactId>
|
||||
<version>26-ea+22</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.openautonomousconnection.webclient.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.42</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
|
||||
<compilerArgs>
|
||||
<arg>--add-exports</arg>
|
||||
<arg>java.base/sun.security.x509=ALL-UNNAMED</arg>
|
||||
<arg>--add-exports</arg>
|
||||
<arg>java.base/sun.security.util=ALL-UNNAMED</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<configuration>
|
||||
<failOnError>false</failOnError>
|
||||
<failOnWarnings>false</failOnWarnings>
|
||||
<doclint>none</doclint>
|
||||
<locale>en_US</locale>
|
||||
<encoding>UTF-8</encoding>
|
||||
<docencoding>UTF-8</docencoding>
|
||||
<charset>UTF-8</charset>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,249 @@
|
||||
package org.openautonomousconnection.webclient;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.Listener;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.C_PacketReadEvent;
|
||||
import lombok.Getter;
|
||||
import org.openautonomousconnection.oacswing.component.OACOptionPane;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.WebPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.navigate.WebNavigateAckPacket;
|
||||
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
|
||||
import org.openautonomousconnection.protocol.side.client.ProtocolClient;
|
||||
import org.openautonomousconnection.protocol.side.client.ProtocolWebClient;
|
||||
import org.openautonomousconnection.protocol.side.client.events.ConnectedToProtocolINSServerEvent;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.LibClientImpl_v1_0_1_B;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebFlagInspector;
|
||||
import org.openautonomousconnection.protocol.urlhandler.v1_0_1.beta.web.WebRequestContextProvider;
|
||||
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
||||
import org.openautonomousconnection.webclient.ui.BrowserTab;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* WebClient Protocol implementation (v1.0.1).
|
||||
*
|
||||
* <p>Implements full stream assembly with strict correlation via:
|
||||
* requestId + tabId + pageId + frameId.</p>
|
||||
*/
|
||||
public final class ClientImpl extends ProtocolWebClient {
|
||||
|
||||
private static final long MAX_STREAM_BYTES = 64L * 1024L * 1024L; // 64MB safety cap
|
||||
private static final int MAX_CONCURRENT_STREAMS = 256;
|
||||
|
||||
@Getter
|
||||
private final LibImpl libImpl = new LibImpl();
|
||||
|
||||
private final AtomicBoolean serverConnectionInitialized = 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean trustINS(String caFingerprint) {
|
||||
Object[] options = {"Continue", "Cancel"};
|
||||
return OACOptionPane.showOptionDialog(
|
||||
dialogParent,
|
||||
"Fingerprint: " + caFingerprint + "\nContinue?",
|
||||
"INS Connection",
|
||||
OACOptionPane.YES_NO_OPTION,
|
||||
OACOptionPane.INFORMATION_MESSAGE,
|
||||
null,
|
||||
options,
|
||||
options[0]
|
||||
) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean trustNewINSFingerprint(String oldCAFingerprint, String newCAFingerprint) {
|
||||
Object[] options = {"Continue", "Cancel"};
|
||||
return OACOptionPane.showOptionDialog(
|
||||
dialogParent,
|
||||
"Saved: " + oldCAFingerprint + "\nNew: " + newCAFingerprint + "\nContinue?",
|
||||
"INS Connection",
|
||||
OACOptionPane.YES_NO_OPTION,
|
||||
OACOptionPane.INFORMATION_MESSAGE,
|
||||
null,
|
||||
options,
|
||||
options[0]
|
||||
) == 0;
|
||||
}
|
||||
|
||||
@Listener
|
||||
public void onConnected(ConnectedToProtocolINSServerEvent event) {
|
||||
try {
|
||||
if (serverConnectionInitialized.compareAndSet(false, true)) {
|
||||
buildServerConnection(null, getProtocolBridge().getProtocolValues().ssl);
|
||||
onServerReady.run();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
serverConnectionInitialized.set(false);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private record StreamKey(long requestId, long tabId, long pageId, long frameId) {
|
||||
|
||||
StreamKey(WebPacketHeader h) {
|
||||
this(h.getRequestId(), h.getTabId(), h.getPageId(), h.getFrameId());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StreamState {
|
||||
|
||||
private final int statusCode;
|
||||
private final String contentType;
|
||||
private final Map<String, String> headers;
|
||||
private final long declaredLength;
|
||||
|
||||
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
private int expectedSeq = 0;
|
||||
private long written = 0;
|
||||
private boolean ended = false;
|
||||
private boolean ok = true;
|
||||
|
||||
StreamState(int statusCode,
|
||||
String contentType,
|
||||
Map<String, String> headers,
|
||||
long declaredLength) {
|
||||
|
||||
this.statusCode = statusCode;
|
||||
this.contentType = contentType == null ? "application/octet-stream" : contentType;
|
||||
this.headers = headers == null ? Map.of() : Map.copyOf(headers);
|
||||
this.declaredLength = declaredLength;
|
||||
}
|
||||
|
||||
void append(int seq, byte[] data) {
|
||||
|
||||
if (ended) throw new IllegalStateException("Chunk after end");
|
||||
if (seq != expectedSeq) throw new IllegalStateException("Out-of-order chunk");
|
||||
|
||||
expectedSeq++;
|
||||
|
||||
if (data == null || data.length == 0) return;
|
||||
|
||||
written += data.length;
|
||||
if (written > MAX_STREAM_BYTES)
|
||||
throw new IllegalStateException("Stream exceeds limit");
|
||||
|
||||
buffer.writeBytes(data);
|
||||
}
|
||||
|
||||
void markEnd(boolean ok, String error) {
|
||||
this.ended = true;
|
||||
this.ok = ok;
|
||||
}
|
||||
|
||||
byte[] finish() {
|
||||
if (!ok) return new byte[0];
|
||||
byte[] data = buffer.toByteArray();
|
||||
|
||||
if (declaredLength > 0 && data.length != declaredLength) {
|
||||
// tolerated but can log if needed
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public final class LibImpl extends LibClientImpl_v1_0_1_B {
|
||||
|
||||
private final WebRequestContextProvider provider = new WebRequestContextProvider.Default();
|
||||
private final WebFlagInspector inspector = new WebFlagInspector.Default();
|
||||
private final ConcurrentHashMap<StreamKey, StreamState> streams = new ConcurrentHashMap<>();
|
||||
|
||||
private BrowserTab currentTab;
|
||||
|
||||
@Override
|
||||
public void serverConnectionFailed(Exception exception) {
|
||||
getProtocolBridge().getProtocolValues().logger.exception("Failed to connect to server", exception);
|
||||
OACOptionPane.showMessageDialog(
|
||||
dialogParent,
|
||||
"Failed to connect to Server:\n" + exception.getMessage(),
|
||||
"Server Connection",
|
||||
OACOptionPane.ERROR_MESSAGE
|
||||
);
|
||||
}
|
||||
|
||||
public void bindTab(BrowserTab tab) {
|
||||
this.currentTab = tab;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStream(WebPacketHeader header) {
|
||||
return inspector.isStream(header);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebRequestContext contextFor(URL url) {
|
||||
return provider.contextFor(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void streamStart(WebPacketHeader header,
|
||||
int statusCode,
|
||||
String contentType,
|
||||
Map<String, String> headers,
|
||||
long totalLength) {
|
||||
|
||||
if (streams.size() >= MAX_CONCURRENT_STREAMS) {
|
||||
throw new IllegalStateException("Too many concurrent streams");
|
||||
}
|
||||
|
||||
StreamKey key = new StreamKey(header);
|
||||
streams.put(key, new StreamState(statusCode, contentType, headers, totalLength));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void streamChunk(WebPacketHeader header, int seq, byte[] data) {
|
||||
|
||||
StreamState state = streams.get(new StreamKey(header));
|
||||
if (state == null) {
|
||||
throw new IllegalStateException("Chunk without streamStart");
|
||||
}
|
||||
|
||||
state.append(seq, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void streamEnd(WebPacketHeader header, boolean ok, String error) {
|
||||
|
||||
StreamState state = streams.get(new StreamKey(header));
|
||||
if (state != null) {
|
||||
state.markEnd(ok, error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void streamFinish(WebPacketHeader header, byte[] ignored) {
|
||||
|
||||
StreamKey key = new StreamKey(header);
|
||||
StreamState state = streams.remove(key);
|
||||
if (state == null) return;
|
||||
|
||||
byte[] content = state.finish();
|
||||
|
||||
if (currentTab != null) {
|
||||
currentTab.handleStreamFinished(
|
||||
header.getRequestId(),
|
||||
header.getTabId(),
|
||||
header.getPageId(),
|
||||
header.getFrameId(),
|
||||
state.statusCode,
|
||||
state.contentType,
|
||||
state.headers,
|
||||
content
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.openautonomousconnection.webclient;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Initializes the JavaFX Toolkit exactly once for Swing embedding.
|
||||
*/
|
||||
public final class FxBootstrap {
|
||||
|
||||
private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);
|
||||
|
||||
private FxBootstrap() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures JavaFX Toolkit is initialized.
|
||||
* Must be called before any Platform.runLater() usage.
|
||||
*/
|
||||
public static void ensureInitialized() {
|
||||
if (!INITIALIZED.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Creating a JFXPanel initializes the JavaFX toolkit in Swing apps.
|
||||
new JFXPanel();
|
||||
|
||||
// Keep JavaFX runtime alive even if last window closes.
|
||||
Platform.setImplicitExit(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.openautonomousconnection.webclient;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
|
||||
import org.openautonomousconnection.oacswing.component.design.Design;
|
||||
import org.openautonomousconnection.oacswing.component.design.DesignManager;
|
||||
import org.openautonomousconnection.webclient.settings.AppSettings;
|
||||
import org.openautonomousconnection.webclient.settings.SettingsManager;
|
||||
import org.openautonomousconnection.webclient.ui.BrowserUI;
|
||||
|
||||
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 {
|
||||
|
||||
private static BrowserUI ui;
|
||||
|
||||
private static AppSettings settings;
|
||||
|
||||
private static AddonLoader addonLoader;
|
||||
private static Logger logger;
|
||||
private static EventManager eventManager;
|
||||
|
||||
public static BrowserUI getUi() {
|
||||
return ui;
|
||||
}
|
||||
|
||||
public static AppSettings getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static AddonLoader getAddonLoader() {
|
||||
return addonLoader;
|
||||
}
|
||||
|
||||
public static Logger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
private static void installDefaultCookieManager() {
|
||||
if (CookieHandler.getDefault() != null) return;
|
||||
|
||||
CookieManager cm = new CookieManager();
|
||||
cm.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
|
||||
CookieHandler.setDefault(cm);
|
||||
}
|
||||
|
||||
static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
|
||||
File logsFolder = new File("logs");
|
||||
if (!logsFolder.exists()) logsFolder.mkdir();
|
||||
|
||||
eventManager = new EventManager();
|
||||
logger = new Logger(logsFolder, false, true);
|
||||
addonLoader = new AddonLoader(eventManager, logger);
|
||||
settings = SettingsManager.load();
|
||||
|
||||
FxBootstrap.ensureInitialized();
|
||||
installDefaultCookieManager();
|
||||
DesignManager.setGlobalDesign(Design.DARK);
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
ui = new BrowserUI(settings);
|
||||
ui.setSize(1200, 800);
|
||||
ui.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
|
||||
ui.setLocationRelativeTo(null);
|
||||
ui.setVisible(true);
|
||||
ui.openNewTab(settings.getStartPageUrl());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.openautonomousconnection.webclient.lua;
|
||||
|
||||
import org.openautonomousconnection.webclient.ClientImpl;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class WebLogger {
|
||||
|
||||
private final String host;
|
||||
private final ClientImpl client;
|
||||
|
||||
public WebLogger(String host, ClientImpl client) {
|
||||
this.host = host;
|
||||
this.client = Objects.requireNonNull(client, "client");
|
||||
}
|
||||
|
||||
public void log(String string) {
|
||||
client.getProtocolBridge().getProtocolValues().logger.log(host + ": " + string);
|
||||
}
|
||||
|
||||
public void info(String info) {
|
||||
client.getProtocolBridge().getProtocolValues().logger.info(host + ": " + info);
|
||||
}
|
||||
|
||||
public void warn(String warn) {
|
||||
client.getProtocolBridge().getProtocolValues().logger.warn(host + ": " + warn);
|
||||
}
|
||||
|
||||
public void error(String error) {
|
||||
client.getProtocolBridge().getProtocolValues().logger.error(host + ": " + error);
|
||||
}
|
||||
|
||||
public void exception(String infoLine, Exception exception) {
|
||||
client.getProtocolBridge().getProtocolValues().logger.exception(host + ": " + infoLine, exception);
|
||||
}
|
||||
|
||||
public void debug(String debug) {
|
||||
client.getProtocolBridge().getProtocolValues().logger.debug(host + ": " + debug);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.openautonomousconnection.webclient.lua.hosts;
|
||||
|
||||
import org.openautonomousconnection.luascript.hosts.ConsoleHost;
|
||||
import org.openautonomousconnection.webclient.lua.WebLogger;
|
||||
|
||||
public class ConsoleHostImpl implements ConsoleHost {
|
||||
private final WebLogger logger;
|
||||
|
||||
public ConsoleHostImpl(WebLogger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String message) {
|
||||
logger.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message) {
|
||||
logger.log(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(String message) {
|
||||
logger.warn(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String message) {
|
||||
logger.error(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exception(String message) {
|
||||
logger.exception("", new RuntimeException(message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package org.openautonomousconnection.webclient.lua.hosts;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import org.openautonomousconnection.luascript.fx.FxDomHost;
|
||||
import org.openautonomousconnection.luascript.fx.FxThreadBridge;
|
||||
import org.openautonomousconnection.luascript.hosts.UiHost;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* UiHost implementation for JavaFX WebView (no JavaScript).
|
||||
*
|
||||
* <p>Operations are implemented via W3C DOM attributes/text, and best-effort behavior for
|
||||
* value/style/class using standard attributes.</p>
|
||||
*/
|
||||
public final class UiHostImpl implements UiHost {
|
||||
|
||||
private final WebEngine engine;
|
||||
private final WebView view;
|
||||
private final FxDomHost dom;
|
||||
|
||||
/**
|
||||
* Creates a new UI host.
|
||||
*
|
||||
* @param engine web engine
|
||||
* @param view web view
|
||||
* @param dom dom host
|
||||
*/
|
||||
public UiHostImpl(WebEngine engine, WebView view, FxDomHost dom) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.view = view;
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
}
|
||||
|
||||
private static boolean hasClassToken(String classAttr, String cls) {
|
||||
String[] parts = classAttr.trim().split("\\s+");
|
||||
for (String p : parts) {
|
||||
if (p.equals(cls)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String removeCssProp(String style, String propLower) {
|
||||
if (style == null || style.isBlank()) return "";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (String part : style.split(";")) {
|
||||
String p = part.trim();
|
||||
if (p.isEmpty()) continue;
|
||||
int idx = p.indexOf(':');
|
||||
if (idx <= 0) continue;
|
||||
|
||||
String k = p.substring(0, idx).trim().toLowerCase();
|
||||
if (k.equals(propLower)) continue;
|
||||
|
||||
if (!sb.isEmpty()) sb.append(';');
|
||||
sb.append(p);
|
||||
}
|
||||
|
||||
String out = sb.toString().trim();
|
||||
if (!out.isEmpty() && !out.endsWith(";")) out += ";";
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void alert(String message) {
|
||||
// No JS: use simple JavaFX dialog-less fallback (log-style). You can replace with real Dialogs later.
|
||||
// Keeping it deterministic and non-blocking for now.
|
||||
System.out.println("[ui.alert] " + (message == null ? "" : message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirm(String message) {
|
||||
// No JS: deterministic default (false). Replace with JavaFX dialogs if you want UI interaction.
|
||||
System.out.println("[ui.confirm] " + (message == null ? "" : message));
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String prompt(String message, String defaultValue) {
|
||||
// No JS: deterministic default.
|
||||
System.out.println("[ui.prompt] " + (message == null ? "" : message));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setText(String elementId, String text) {
|
||||
dom.setTextContent(elementId, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText(String elementId) {
|
||||
return dom.getTextContent(elementId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHtml(String elementId, String html) {
|
||||
// Without JS, safest is to set textContent (prevents HTML parsing).
|
||||
// If you need real HTML injection, we must extend DomHost with fragment parsing (not in current API).
|
||||
dom.setTextContent(elementId, html);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHtml(String elementId) {
|
||||
// Without JS, best-effort: return textContent.
|
||||
return dom.getTextContent(elementId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(String elementId, String value) {
|
||||
// Input/textarea value is commonly reflected as attribute "value".
|
||||
dom.setAttribute(elementId, "value", value == null ? "" : value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(String elementId) {
|
||||
String v = dom.getAttribute(elementId, "value");
|
||||
return v == null ? "" : v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(String elementId, boolean enabled) {
|
||||
if (enabled) dom.removeAttribute(elementId, "disabled");
|
||||
else dom.setAttribute(elementId, "disabled", "disabled");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(String elementId, boolean visible) {
|
||||
// Best-effort via style attribute
|
||||
String style = dom.getAttribute(elementId, "style");
|
||||
style = style == null ? "" : style;
|
||||
|
||||
style = removeCssProp(style, "display");
|
||||
if (!visible) {
|
||||
style = style.trim();
|
||||
if (!style.isEmpty() && !style.endsWith(";")) style += ";";
|
||||
style += "display:none;";
|
||||
}
|
||||
|
||||
dom.setAttribute(elementId, "style", style);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addClass(String elementId, String className) {
|
||||
String cls = Objects.requireNonNull(className, "className").trim();
|
||||
if (cls.isEmpty()) return;
|
||||
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element el = dom.byId(elementId);
|
||||
String c = el.getAttribute("class");
|
||||
c = (c == null) ? "" : c.trim();
|
||||
|
||||
if (c.isEmpty()) {
|
||||
el.setAttribute("class", cls);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasClassToken(c, cls)) {
|
||||
el.setAttribute("class", c + " " + cls);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeClass(String elementId, String className) {
|
||||
String cls = Objects.requireNonNull(className, "className").trim();
|
||||
if (cls.isEmpty()) return;
|
||||
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element el = dom.byId(elementId);
|
||||
String c = el.getAttribute("class");
|
||||
c = (c == null) ? "" : c.trim();
|
||||
if (c.isEmpty()) return;
|
||||
|
||||
String[] parts = c.split("\\s+");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String p : parts) {
|
||||
if (p.equals(cls)) continue;
|
||||
if (!sb.isEmpty()) sb.append(' ');
|
||||
sb.append(p);
|
||||
}
|
||||
el.setAttribute("class", sb.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean toggleClass(String elementId, String className) {
|
||||
if (hasClass(elementId, className)) {
|
||||
removeClass(elementId, className);
|
||||
return false;
|
||||
}
|
||||
addClass(elementId, className);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasClass(String elementId, String className) {
|
||||
String cls = Objects.requireNonNull(className, "className").trim();
|
||||
if (cls.isEmpty()) return false;
|
||||
|
||||
return FxThreadBridge.callAndWait(() -> {
|
||||
Element el = dom.byId(elementId);
|
||||
String c = el.getAttribute("class");
|
||||
c = (c == null) ? "" : c.trim();
|
||||
return !c.isEmpty() && hasClassToken(c, cls);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStyle(String elementId, String property, String value) {
|
||||
String prop = Objects.requireNonNull(property, "property").trim().toLowerCase();
|
||||
if (prop.isEmpty()) return;
|
||||
|
||||
String style = dom.getAttribute(elementId, "style");
|
||||
style = style == null ? "" : style;
|
||||
|
||||
style = removeCssProp(style, prop);
|
||||
String v = value == null ? "" : value.trim();
|
||||
|
||||
if (!v.isEmpty()) {
|
||||
style = style.trim();
|
||||
if (!style.isEmpty() && !style.endsWith(";")) style += ";";
|
||||
style += prop + ":" + v + ";";
|
||||
}
|
||||
dom.setAttribute(elementId, "style", style);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStyle(String elementId, String property) {
|
||||
// Best-effort parsing from style attribute.
|
||||
String prop = Objects.requireNonNull(property, "property").trim().toLowerCase();
|
||||
if (prop.isEmpty()) return "";
|
||||
|
||||
String style = dom.getAttribute(elementId, "style");
|
||||
style = style == null ? "" : style;
|
||||
|
||||
for (String part : style.split(";")) {
|
||||
String p = part.trim();
|
||||
if (p.isEmpty()) continue;
|
||||
int idx = p.indexOf(':');
|
||||
if (idx <= 0) continue;
|
||||
|
||||
String k = p.substring(0, idx).trim().toLowerCase();
|
||||
if (k.equals(prop)) return p.substring(idx + 1).trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String elementId, String name, String value) {
|
||||
dom.setAttribute(elementId, name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttribute(String elementId, String name) {
|
||||
return dom.getAttribute(elementId, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String elementId, String name) {
|
||||
dom.removeAttribute(elementId, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void focus(String elementId) {
|
||||
engine.setJavaScriptEnabled(true);
|
||||
engine.executeScript(
|
||||
"document.getElementById('" + elementId + "').focus();"
|
||||
);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blur(String elementId) {
|
||||
engine.setJavaScriptEnabled(true);
|
||||
engine.executeScript(
|
||||
"document.getElementById('" + elementId + "').blur();"
|
||||
);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollIntoView(String elementId) {
|
||||
engine.setJavaScriptEnabled(true);
|
||||
engine.executeScript(
|
||||
"document.getElementById('" + elementId + "').scrollIntoView();"
|
||||
);
|
||||
engine.setJavaScriptEnabled(false);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int viewportWidth() {
|
||||
return FxThreadBridge.callAndWait(() ->
|
||||
(int) Math.round(view.getWidth())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int viewportHeight() {
|
||||
return FxThreadBridge.callAndWait(() ->
|
||||
(int) Math.round(view.getHeight())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nowMillis() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package org.openautonomousconnection.webclient.settings;
|
||||
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import org.luaj.vm2.Globals;
|
||||
import org.openautonomousconnection.luascript.fx.FxDomHost;
|
||||
import org.openautonomousconnection.luascript.fx.FxEventHost;
|
||||
import org.openautonomousconnection.luascript.fx.FxWebViewResourceHost;
|
||||
import org.openautonomousconnection.luascript.hosts.HostServices;
|
||||
import org.openautonomousconnection.luascript.runtime.LuaRuntime;
|
||||
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
|
||||
import org.openautonomousconnection.luascript.utils.LuaGlobalsFactory;
|
||||
import org.openautonomousconnection.webclient.lua.WebLogger;
|
||||
import org.openautonomousconnection.webclient.lua.hosts.ConsoleHostImpl;
|
||||
import org.openautonomousconnection.webclient.lua.hosts.UiHostImpl;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* JavaFX WebView integration entry point for LuaScript (no JavaScript).
|
||||
*
|
||||
* <p>Hard rule: every HTML script tag is treated as Lua.</p>
|
||||
*/
|
||||
public final class FxEngine implements AutoCloseable {
|
||||
|
||||
private final WebEngine engine;
|
||||
private final WebView webView;
|
||||
private final LuaExecutionPolicy policy;
|
||||
private final WebLogger logger;
|
||||
|
||||
private final AtomicBoolean bootstrapped = new AtomicBoolean(false);
|
||||
|
||||
private LuaRuntime runtime;
|
||||
|
||||
/**
|
||||
* Creates an integration engine with default UI execution policy.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an integration engine with a custom execution policy.
|
||||
*
|
||||
* @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 = Objects.requireNonNull(webView, "webView");
|
||||
this.policy = Objects.requireNonNull(policy, "policy");
|
||||
this.logger = Objects.requireNonNull(logger, "logger");
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a load hook that bootstraps Lua when a page finished loading.
|
||||
*/
|
||||
public void install() {
|
||||
engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
|
||||
if (newState == Worker.State.SUCCEEDED) {
|
||||
bootstrapped.set(false);
|
||||
bootstrap();
|
||||
} else if (newState == Worker.State.CANCELLED || newState == Worker.State.FAILED) {
|
||||
bootstrapped.set(false);
|
||||
closeRuntimeQuietly();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstraps Lua for the currently loaded document.
|
||||
*/
|
||||
public void bootstrap() {
|
||||
if (!bootstrapped.compareAndSet(false, true)) return;
|
||||
|
||||
closeRuntimeQuietly();
|
||||
|
||||
FxDomHost dom = new FxDomHost(engine);
|
||||
dom.ensureAllElementsHaveId();
|
||||
|
||||
Globals globals = LuaGlobalsFactory.create(
|
||||
new LuaGlobalsFactory.Options()
|
||||
.enableDebug(false)
|
||||
.sandbox(true)
|
||||
);
|
||||
|
||||
ConsoleHostImpl console = new ConsoleHostImpl(logger);
|
||||
UiHostImpl uiHost = new UiHostImpl(engine, webView, dom);
|
||||
FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine);
|
||||
|
||||
LuaRuntime rt = new LuaRuntime(globals, new HostServices.Default(uiHost, dom, null, resourceHost, console), policy);
|
||||
|
||||
FxEventHost eventHost = new FxEventHost(dom, rt.eventRouter());
|
||||
HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console);
|
||||
|
||||
rt.close();
|
||||
rt = new LuaRuntime(globals, services, policy);
|
||||
|
||||
rt.installStdTables(true);
|
||||
rt.bootstrapFromDom();
|
||||
|
||||
this.runtime = rt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns active runtime or null if not bootstrapped.
|
||||
*
|
||||
* @return runtime or null
|
||||
*/
|
||||
public LuaRuntime runtimeOrNull() {
|
||||
return runtime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closeRuntimeQuietly();
|
||||
}
|
||||
|
||||
private void closeRuntimeQuietly() {
|
||||
LuaRuntime rt = this.runtime;
|
||||
this.runtime = null;
|
||||
if (rt != null) {
|
||||
try {
|
||||
rt.close();
|
||||
} catch (Exception ignored) {
|
||||
// Best-effort shutdown.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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(String host1, int port1))) return false;
|
||||
return host.equalsIgnoreCase(host1) && port == port1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return host.toLowerCase().hashCode() * 31 + port;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
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.File;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Logical browser tab: stable UI key + embedded JavaFX WebView.
|
||||
*
|
||||
* <p>This class merges the previous {@code BrowserTab} + {@code TabView} into one component.</p>
|
||||
*/
|
||||
public final class BrowserTab extends OACPanel {
|
||||
|
||||
private final String key;
|
||||
|
||||
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 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
|
||||
* @param protocolClient protocol client used for OAC network requests
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
private static String normalizeMime(String contentType) {
|
||||
String ct = (contentType == null || contentType.isBlank()) ? "application/octet-stream" : contentType.trim();
|
||||
int semi = ct.indexOf(';');
|
||||
String base = (semi >= 0 ? ct.substring(0, semi) : ct).trim();
|
||||
return base.isEmpty() ? "application/octet-stream" : base;
|
||||
}
|
||||
|
||||
private static boolean isHtml(String contentType) {
|
||||
String ct = normalizeMime(contentType).toLowerCase(Locale.ROOT);
|
||||
return ct.equals("text/html") || ct.equals("application/xhtml+xml");
|
||||
}
|
||||
|
||||
private static boolean isText(String contentType) {
|
||||
String ct = normalizeMime(contentType).toLowerCase(Locale.ROOT);
|
||||
return ct.startsWith("text/") || ct.equals("application/json") || ct.equals("application/xml") || ct.endsWith("+json") || ct.endsWith("+xml");
|
||||
}
|
||||
|
||||
private static boolean isImage(String contentType) {
|
||||
String ct = normalizeMime(contentType).toLowerCase(Locale.ROOT);
|
||||
return ct.startsWith("image/");
|
||||
}
|
||||
|
||||
private static boolean isPdf(String contentType) {
|
||||
String ct = normalizeMime(contentType).toLowerCase(Locale.ROOT);
|
||||
return ct.equals("application/pdf");
|
||||
}
|
||||
|
||||
private static Charset charsetFromContentType(String contentType, Charset def) {
|
||||
if (contentType == null) return def;
|
||||
String[] parts = contentType.split(";");
|
||||
for (String p : parts) {
|
||||
String s = p.trim();
|
||||
if (s.toLowerCase(Locale.ROOT).startsWith("charset=")) {
|
||||
String name = s.substring("charset=".length()).trim();
|
||||
try {
|
||||
return Charset.forName(name);
|
||||
} catch (Exception ignored) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
private static String extractFilenameFromContentDisposition(Map<String, String> headers) {
|
||||
if (headers == null || headers.isEmpty()) return null;
|
||||
|
||||
String cd = null;
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
if (e.getKey() != null && e.getKey().equalsIgnoreCase("content-disposition")) {
|
||||
cd = e.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cd == null || cd.isBlank()) return null;
|
||||
|
||||
String lower = cd.toLowerCase(Locale.ROOT);
|
||||
int fn = lower.indexOf("filename=");
|
||||
if (fn < 0) return null;
|
||||
|
||||
String v = cd.substring(fn + "filename=".length()).trim();
|
||||
if (v.startsWith("\"")) {
|
||||
int end = v.indexOf('"', 1);
|
||||
if (end > 1) return v.substring(1, end);
|
||||
return null;
|
||||
}
|
||||
int semi = v.indexOf(';');
|
||||
if (semi >= 0) v = v.substring(0, semi).trim();
|
||||
return v.isBlank() ? null : v;
|
||||
}
|
||||
|
||||
private static String sanitizeFilename(String name) {
|
||||
String s = name.replace('\\', '_').replace('/', '_');
|
||||
s = s.replace("..", "_");
|
||||
s = s.replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_')
|
||||
.replace('<', '_').replace('>', '_').replace('|', '_');
|
||||
return s.isBlank() ? "download.bin" : s;
|
||||
}
|
||||
|
||||
private static String extensionFromContentType(String contentType) {
|
||||
String ct = normalizeMime(contentType).toLowerCase(Locale.ROOT);
|
||||
|
||||
if (ct.equals("application/pdf")) return ".pdf";
|
||||
if (ct.equals("application/zip")) return ".zip";
|
||||
if (ct.equals("application/x-7z-compressed")) return ".7z";
|
||||
if (ct.equals("application/x-rar-compressed")) return ".rar";
|
||||
if (ct.equals("application/gzip")) return ".gz";
|
||||
if (ct.equals("application/json")) return ".json";
|
||||
if (ct.equals("application/xml") || ct.endsWith("+xml")) return ".xml";
|
||||
|
||||
if (ct.startsWith("image/")) {
|
||||
int slash = ct.indexOf('/');
|
||||
if (slash > 0 && slash < ct.length() - 1) {
|
||||
String ext = ct.substring(slash + 1).trim();
|
||||
if (!ext.isEmpty()) return "." + ext;
|
||||
}
|
||||
}
|
||||
|
||||
if (ct.startsWith("text/")) return ".txt";
|
||||
return ".bin";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stable tab key.
|
||||
*
|
||||
* @return key
|
||||
*/
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the protocol client used by this tab.
|
||||
*
|
||||
* @return protocol client
|
||||
*/
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a URL.
|
||||
*
|
||||
* @param url URL
|
||||
*/
|
||||
public void loadUrl(String 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() {
|
||||
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() {
|
||||
if (peekHistoryTarget(+1) == null) return;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
WebEngine e = engine;
|
||||
if (e == null) return;
|
||||
WebHistory h = e.getHistory();
|
||||
if (h.getCurrentIndex() < h.getEntries().size() - 1) h.go(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current location if known.
|
||||
*
|
||||
* @return URL or null
|
||||
*/
|
||||
public String getLocationUrl() {
|
||||
return getEngineLocation();
|
||||
}
|
||||
|
||||
// -------------------- Stream render/save helpers --------------------
|
||||
|
||||
/**
|
||||
* 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 "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a fully assembled stream payload from the protocol layer and renders or saves it.
|
||||
*
|
||||
* <p>Rules:
|
||||
* <ul>
|
||||
* <li>Renderable: HTML/text/images/pdf are rendered directly (no wrapper pages for non-html).</li>
|
||||
* <li>Not renderable: opens "Save As" dialog.</li>
|
||||
* <li>If user cancels "Save As": content is shown raw in the browser via data: URL.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param requestId request correlation id
|
||||
* @param tabId protocol tab id
|
||||
* @param pageId protocol page id
|
||||
* @param frameId protocol frame id
|
||||
* @param statusCode http-like status code
|
||||
* @param contentType mime type
|
||||
* @param headers response headers
|
||||
* @param content full payload bytes
|
||||
*/
|
||||
public void handleStreamFinished(long requestId,
|
||||
long tabId,
|
||||
long pageId,
|
||||
long frameId,
|
||||
int statusCode,
|
||||
String contentType,
|
||||
Map<String, String> headers,
|
||||
byte[] content) {
|
||||
|
||||
String ct = (contentType == null || contentType.isBlank())
|
||||
? "application/octet-stream"
|
||||
: contentType.trim();
|
||||
|
||||
byte[] data = (content == null) ? new byte[0] : content;
|
||||
|
||||
// ---- Renderable types -> render without extra wrapper pages ----
|
||||
if (isHtml(ct)) {
|
||||
Charset cs = charsetFromContentType(ct, StandardCharsets.UTF_8);
|
||||
String html = new String(data, cs);
|
||||
Platform.runLater(() -> {
|
||||
WebEngine e = engine;
|
||||
if (e != null) e.loadContent(html, "text/html");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isText(ct)) {
|
||||
Charset cs = charsetFromContentType(ct, StandardCharsets.UTF_8);
|
||||
String text = new String(data, cs);
|
||||
Platform.runLater(() -> {
|
||||
WebEngine e = engine;
|
||||
if (e != null) e.loadContent(text, "text/plain");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isImage(ct) || isPdf(ct)) {
|
||||
renderRawDataUrl(ct, data);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Not renderable -> Save As; if cancelled -> raw data: URL ----
|
||||
String suggested = extractFilenameFromContentDisposition(headers);
|
||||
if (suggested == null || suggested.isBlank()) {
|
||||
String ext = extensionFromContentType(ct);
|
||||
suggested = "download_" + requestId + "_" + Instant.now().toEpochMilli() + ext;
|
||||
} else {
|
||||
suggested = sanitizeFilename(suggested);
|
||||
}
|
||||
|
||||
showSaveAsDialogAndWriteBytes(suggested, ct, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources.
|
||||
*/
|
||||
public void 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();
|
||||
}
|
||||
|
||||
private void fireLocationChanged(String location) {
|
||||
if (location == null) return;
|
||||
String s = location.trim();
|
||||
if (s.isEmpty()) return;
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
private void showSaveAsDialogAndWriteBytes(String suggestedFilename, String contentType, byte[] data) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
Window parent = SwingUtilities.getWindowAncestor(this);
|
||||
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Save As");
|
||||
chooser.setSelectedFile(new File(suggestedFilename));
|
||||
|
||||
int result = chooser.showSaveDialog(parent);
|
||||
if (result != JFileChooser.APPROVE_OPTION) {
|
||||
// Cancel -> show raw in browser
|
||||
renderRawDataUrl(contentType, data);
|
||||
return;
|
||||
}
|
||||
|
||||
File file = chooser.getSelectedFile();
|
||||
if (file == null) {
|
||||
renderRawDataUrl(contentType, data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.isDirectory()) {
|
||||
JOptionPane.showMessageDialog(parent, "Please choose a file, not a directory.", "Save As", JOptionPane.WARNING_MESSAGE);
|
||||
renderRawDataUrl(contentType, data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.exists()) {
|
||||
int overwrite = JOptionPane.showConfirmDialog(
|
||||
parent,
|
||||
"File already exists. Overwrite?\n" + file.getAbsolutePath(),
|
||||
"Confirm Overwrite",
|
||||
JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE
|
||||
);
|
||||
if (overwrite != JOptionPane.YES_OPTION) {
|
||||
renderRawDataUrl(contentType, data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Files.write(file.toPath(), data);
|
||||
} catch (IOException ex) {
|
||||
JOptionPane.showMessageDialog(
|
||||
parent,
|
||||
"Failed to save file:\n" + ex.getMessage(),
|
||||
"Save As",
|
||||
JOptionPane.ERROR_MESSAGE
|
||||
);
|
||||
// On failure, still show raw so user can at least see bytes
|
||||
renderRawDataUrl(contentType, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void renderRawDataUrl(String contentType, byte[] data) {
|
||||
String mime = normalizeMime(contentType);
|
||||
String b64 = Base64.getEncoder().encodeToString(data == null ? new byte[0] : data);
|
||||
String dataUrl = "data:" + mime + ";base64," + b64;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
WebEngine e = engine;
|
||||
if (e != null) e.load(dataUrl);
|
||||
});
|
||||
}
|
||||
|
||||
public void bindProtocolClient() {
|
||||
protocolClient.getLibImpl().bindTab(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
package org.openautonomousconnection.webclient.ui;
|
||||
|
||||
import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader;
|
||||
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
|
||||
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||
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.Main;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Multi-tab browser UI.
|
||||
*/
|
||||
public class BrowserUI extends OACFrame {
|
||||
|
||||
private static final int TITLE_BAR_HEIGHT = 42;
|
||||
|
||||
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;
|
||||
|
||||
public BrowserUI(AppSettings settings) {
|
||||
super("OAC Browser");
|
||||
setBackground(DesignManager.getGlobalDesign().getElements().get(OACFrame.class).background().getColor());
|
||||
this.settings = Objects.requireNonNull(settings, "settings");
|
||||
|
||||
OACPanel content = (OACPanel) getContentPane();
|
||||
content.setLayout(new BorderLayout());
|
||||
content.setBorder(BorderFactory.createEmptyBorder(TITLE_BAR_HEIGHT, 0, 0, 0));
|
||||
|
||||
browser = new BrowserDesign();
|
||||
content.add(browser, BorderLayout.NORTH);
|
||||
|
||||
cardLayout = new CardLayout();
|
||||
pageHost = new OACPanel(cardLayout);
|
||||
content.add(pageHost, BorderLayout.CENTER);
|
||||
|
||||
plusTabSupport = new PlusTabSupport(getTitleBar().getTabs(), () -> openNewTab(settings.getStartPageUrl()));
|
||||
plusTabSupport.ensurePlusTab();
|
||||
|
||||
getTitleBar().getTabs().addChangeListener(e -> onHeaderTabChanged());
|
||||
|
||||
browser.addressField().addActionListener(e -> navigateCurrent(browser.addressField().getText()));
|
||||
browser.goButton().addActionListener(e -> navigateCurrent(browser.addressField().getText()));
|
||||
|
||||
browser.backButton().addActionListener(e -> {
|
||||
BrowserTab tab = getCurrentTab();
|
||||
if (tab != null) tab.goBack();
|
||||
});
|
||||
browser.forwardButton().addActionListener(e -> {
|
||||
BrowserTab tab = getCurrentTab();
|
||||
if (tab != null) tab.goForward();
|
||||
});
|
||||
browser.reloadButton().addActionListener(e -> {
|
||||
BrowserTab tab = getCurrentTab();
|
||||
if (tab != null) tab.reload();
|
||||
});
|
||||
|
||||
browser.starButton().addActionListener(e -> addCurrentToFavorites());
|
||||
browser.menuButton().addActionListener(e -> showAppMenu(browser.menuButton()));
|
||||
|
||||
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://")) {
|
||||
String rest = s.substring("web://".length());
|
||||
if (!rest.contains("/")) return s + "/";
|
||||
return s;
|
||||
}
|
||||
return "web://" + s + (s.contains("/") ? "" : "/");
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
}));
|
||||
|
||||
connectTabClient(key, client);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
// Bind stream callbacks to this tab (v1.0.1 stream assembly -> tab render).
|
||||
tab.bindProtocolClient();
|
||||
|
||||
tabRef.set(tab);
|
||||
tab.setOpenInNewTabCallback(() -> openNewTab(
|
||||
tab.getEngineLocation() == null ? settings.getStartPageUrl() : tab.getEngineLocation()
|
||||
));
|
||||
|
||||
tabsByKey.put(key, tab);
|
||||
pageHost.add(tab, key);
|
||||
|
||||
OACTabbedPane tabs = getTitleBar().getTabs();
|
||||
suppressPlusAutoOpen = true;
|
||||
try {
|
||||
plusTabSupport.ensurePlusTab();
|
||||
|
||||
int insertIndex = findPlusIndex(tabs);
|
||||
if (insertIndex < 0) insertIndex = tabs.getTabCount();
|
||||
|
||||
tabs.insertTab(key, null, new OACPanel(), null, insertIndex);
|
||||
tabs.setTabComponentAt(insertIndex, new TabButton(displayTitleFromUrl(normalized), () -> closeTabByKey(key)));
|
||||
tabs.setSelectedIndex(insertIndex);
|
||||
} finally {
|
||||
suppressPlusAutoOpen = false;
|
||||
}
|
||||
|
||||
cardLayout.show(pageHost, key);
|
||||
browser.addressField().setText(normalized);
|
||||
}
|
||||
|
||||
private void connectTabClient(String key, ClientImpl client) {
|
||||
try {
|
||||
ProtocolValues values = new ProtocolValues();
|
||||
values.packetHandler = new PacketHandler();
|
||||
values.eventManager = new EventManager();
|
||||
values.ssl = settings.isSslEnabled();
|
||||
values.logger = Main.getLogger();
|
||||
values.addonLoader = Main.getAddonLoader();
|
||||
values.protocolVersion = ProtocolVersion.PV_1_0_1_BETA;
|
||||
AddonLoader addonLoader = values.addonLoader;
|
||||
|
||||
ProtocolBridge bridge = new ProtocolBridge(
|
||||
client,
|
||||
client.getLibImpl(),
|
||||
values
|
||||
);
|
||||
|
||||
protocolByKey.put(key, bridge);
|
||||
addonLoaderByKey.put(key, addonLoader);
|
||||
|
||||
File addonsFolder = new File("addons");
|
||||
if (!addonsFolder.exists()) addonsFolder.mkdir();
|
||||
addonLoader.loadAddonsFromDirectory(addonsFolder);
|
||||
|
||||
client.buildINSConnection();
|
||||
bridge.getProtocolValues().eventManager.registerListener(client);
|
||||
|
||||
InsEndpoint ep = Objects.requireNonNull(settings.getSelectedIns(), "selectedIns");
|
||||
client.getClientINSConnection().connect(ep.host(), ep.port());
|
||||
} catch (Exception e) {
|
||||
OACOptionPane.showMessageDialog(
|
||||
this,
|
||||
"Tab connection failed:\n" + e.getMessage(),
|
||||
"Connection",
|
||||
OACOptionPane.ERROR_MESSAGE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently active tab (excluding the plus-tab).
|
||||
*
|
||||
* @return current tab or null
|
||||
*/
|
||||
public BrowserTab getCurrentTab() {
|
||||
String key = getSelectedTabKey();
|
||||
if (key == null) return null;
|
||||
return tabsByKey.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the addon loader associated with the current tab.
|
||||
*
|
||||
* @return addon loader or null
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user