diff --git a/.gitignore b/.gitignore
index 48b701855..04312448a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,6 @@ ComponentsGenerated.wxs
!dist/javafx/**/lib
!dist/javafx/**/bin
dev.properties
+xcuserdata/
+*.dylib
+project.xcworkspace
diff --git a/README.md b/README.md
index accfd3480..15960dcda 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ It currently supports:
- Quickly perform various commonly used actions like starting/stopping containers, establishing tunnels, and more
- Create desktop shortcuts that automatically open remote connections in your terminal without having to open any GUI
-![connections](https://github.com/xpipe-io/xpipe/assets/72509152/5df3169a-4150-4478-a3de-ae1f9748c3c8)
+![connections](https://github.com/user-attachments/assets/07312929-1792-4589-b139-aa10bbcdc649)
## Powerful file management
@@ -39,8 +39,8 @@ It currently supports:
- Dynamically elevate sessions with sudo when required without having to restart the session
- Seamlessly transfer files from and to your system desktop environment
- Work and perform transfers on multiple systems at the same time with the built-in tabbed multitasking
-
-![browser](https://github.com/xpipe-io/xpipe/assets/72509152/4d4e4e54-17c1-4ebe-acf8-f615cfce8b3f)
+
+![browser](https://github.com/user-attachments/assets/7e5d8b3b-8cd7-4b71-ad79-9afb385de3fd)
## Terminal launcher
@@ -52,7 +52,7 @@ It currently supports:
-
+
@@ -63,7 +63,7 @@ It currently supports:
- Setup shell init environments for connections to fully customize your work environment for every purpose
- Open custom shells and custom remote connections by providing your own commands
-![scripts](https://github.com/xpipe-io/xpipe/assets/72509152/56533f22-b689-4201-b58a-eebe0a6d517a)
+![scripts](https://github.com/user-attachments/assets/cf39afaf-638d-48fc-9247-4c8d847d4ed4)
## Secure vault
@@ -72,7 +72,7 @@ It currently supports:
- There are no servers involved, all your information stays on your systems. The XPipe application does not send any personal or sensitive information to outside services.
- Vault changes can be pushed and pulled from your own remote git repository by multiple team members across many systems
-## API
+## Programmatic connection control via the API
- The XPipe API provides programmatic access to XPipe’s features via an HTTP interface
- Manage all your remote systems and access their file systems in your own favorite programming language
@@ -197,6 +197,7 @@ The distributed XPipe application consists out of two parts:
- The closed-source extensions, mostly for professional edition features, which are not included in this repository
Additional features are available in the professional edition. For more details see https://xpipe.io/pricing.
+If your enterprise puts great emphasis on having access to the full source code, there are also full source-available enterprise options available.
## More links
diff --git a/app/build.gradle b/app/build.gradle
index 613c5fe18..a3cf49751 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -23,8 +23,8 @@ dependencies {
api project(':beacon')
compileOnly 'org.hamcrest:hamcrest:2.2'
- compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.2'
- compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.2'
+ compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.3'
+ compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.3'
api 'com.vladsch.flexmark:flexmark:0.64.8'
api 'com.vladsch.flexmark:flexmark-util:0.64.8'
@@ -50,22 +50,23 @@ dependencies {
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
+ api 'org.bouncycastle:bcprov-jdk18on:1.78.1'
api 'info.picocli:picocli:4.7.6'
- api ('org.kohsuke:github-api:1.322') {
+ api ('org.kohsuke:github-api:1.323') {
exclude group: 'org.apache.commons', module: 'commons-lang3'
}
- api 'org.apache.commons:commons-lang3:3.14.0'
- api 'io.sentry:sentry:7.10.0'
+ api 'org.apache.commons:commons-lang3:3.16.0'
+ api 'io.sentry:sentry:7.13.0'
api 'commons-io:commons-io:2.16.1'
- api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.1"
- api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.1"
+ api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.2"
+ api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.2"
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0"
- api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.13'
- api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.13'
+ api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.15'
+ api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.15'
api 'io.xpipe:modulefs:0.1.5'
api 'net.synedra:validatorfx:0.4.2'
api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar")
@@ -93,6 +94,7 @@ run {
systemProperty 'io.xpipe.app.logLevel', "trace"
systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion
systemProperty 'io.xpipe.app.staging', isStage
+ // systemProperty 'io.xpipe.beacon.port', "30000"
// Apply passed xpipe properties
for (final def e in System.getProperties().entrySet()) {
diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java
index 93af376e5..be4e4d8cd 100644
--- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java
+++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java
@@ -1,22 +1,23 @@
package io.xpipe.app.beacon;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.util.MarkdownHelper;
import io.xpipe.beacon.BeaconConfig;
import io.xpipe.beacon.BeaconInterface;
+import io.xpipe.core.process.OsType;
import io.xpipe.core.util.XPipeInstallation;
-
-import com.sun.net.httpserver.HttpExchange;
-import com.sun.net.httpserver.HttpServer;
import lombok.Getter;
import java.io.IOException;
-import java.net.InetAddress;
+import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermissions;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
@@ -84,6 +85,7 @@ public class AppBeaconServer {
public static void reset() {
if (INSTANCE != null) {
INSTANCE.stop();
+ INSTANCE.deleteAuthSecret();
INSTANCE = null;
}
}
@@ -109,11 +111,22 @@ public class AppBeaconServer {
var file = XPipeInstallation.getLocalBeaconAuthFile();
var id = UUID.randomUUID().toString();
Files.writeString(file, id);
+ if (OsType.getLocal() != OsType.WINDOWS) {
+ Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----"));
+ }
localAuthSecret = id;
}
+ private void deleteAuthSecret() {
+ var file = XPipeInstallation.getLocalBeaconAuthFile();
+ try {
+ Files.delete(file);
+ } catch (IOException ignored) {
+ }
+ }
+
private void start() throws IOException {
- server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), 10);
+ server = HttpServer.create(new InetSocketAddress(Inet4Address.getByAddress(new byte[]{ 0x7f,0x00,0x00,0x01 }), port), 10);
BeaconInterface.getAll().forEach(beaconInterface -> {
server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface));
});
diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java
index d231c96a0..019b70096 100644
--- a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java
+++ b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java
@@ -28,7 +28,8 @@ public class BeaconRequestHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
- if (OperationMode.isInShutdown()) {
+ if (OperationMode.isInShutdown() && !beaconInterface.acceptInShutdown()) {
+ writeError(exchange, new BeaconClientErrorResponse("Daemon is currently in shutdown"), 400);
return;
}
@@ -108,7 +109,7 @@ public class BeaconRequestHandler implements HttpHandler {
// Make deserialization error message more readable
var message = ex.getMessage()
.replace("$RequestBuilder", "")
- .replace("Exchange$Request","Request")
+ .replace("Exchange$Request", "Request")
.replace("at [Source: UNKNOWN; byte offset: #UNKNOWN]", "")
.replaceAll("(\\w+) is marked non-null but is null", "field $1 is missing from object")
.trim();
@@ -124,10 +125,13 @@ public class BeaconRequestHandler implements HttpHandler {
try {
var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0;
if (!emptyResponseClass && response != null) {
- TrackEvent.trace("Sending response:\n" + object);
- var tree = JacksonMapper.getDefault().valueToTree(response);
- TrackEvent.trace("Sending raw response:\n" + tree.toPrettyString());
- var bytes = tree.toPrettyString().getBytes(StandardCharsets.UTF_8);
+ TrackEvent.trace("Sending response:\n" + response);
+ TrackEvent.trace("Sending raw response:\n"
+ + JacksonMapper.getCensored().valueToTree(response).toPrettyString());
+ var bytes = JacksonMapper.getDefault()
+ .valueToTree(response)
+ .toPrettyString()
+ .getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
diff --git a/app/src/main/java/io/xpipe/app/beacon/BlobManager.java b/app/src/main/java/io/xpipe/app/beacon/BlobManager.java
index 466ce056d..fc12a1b42 100644
--- a/app/src/main/java/io/xpipe/app/beacon/BlobManager.java
+++ b/app/src/main/java/io/xpipe/app/beacon/BlobManager.java
@@ -50,7 +50,7 @@ public class BlobManager {
public Path newBlobFile() throws IOException {
var file = TEMP.resolve(UUID.randomUUID().toString());
- Files.createDirectories(file.getParent());
+ FileUtils.forceMkdir(file.getParent().toFile());
return file;
}
diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java
index e5704ecad..7cf0c747f 100644
--- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java
+++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java
@@ -1,12 +1,13 @@
package io.xpipe.app.beacon.impl;
-import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.api.ConnectionAddExchange;
import io.xpipe.core.util.ValidationException;
+import com.sun.net.httpserver.HttpExchange;
+
public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
@Override
diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java
index 250e65eec..1fb2f1d2a 100644
--- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java
+++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java
@@ -1,6 +1,5 @@
package io.xpipe.app.beacon.impl;
-import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.storage.DataStorage;
@@ -8,6 +7,8 @@ import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionBrowseExchange;
import io.xpipe.core.store.FileSystemStore;
+import com.sun.net.httpserver.HttpExchange;
+
public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
@Override
@@ -18,7 +19,8 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
if (!(e.getStore() instanceof FileSystemStore)) {
throw new BeaconClientException("Not a file system connection");
}
- BrowserSessionModel.DEFAULT.openFileSystemSync(e.ref(),msg.getDirectory() != null ? ignored -> msg.getDirectory() : null,null);
+ BrowserSessionModel.DEFAULT.openFileSystemSync(
+ e.ref(), msg.getDirectory() != null ? ignored -> msg.getDirectory() : null, null);
AppLayoutModel.get().selectBrowser();
return Response.builder().build();
}
diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java
index 2efaf15b3..a4c454b63 100644
--- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java
+++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java
@@ -28,9 +28,16 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange {
.orElseThrow())
.getNames();
var cat = new StorePath(names.subList(1, names.size()));
- var cache = e.getStoreCache().entrySet().stream().filter(stringObjectEntry -> {
- return stringObjectEntry.getValue() != null && (ClassUtils.isPrimitiveOrWrapper(stringObjectEntry.getValue().getClass()) || stringObjectEntry.getValue() instanceof String);
- }).collect(Collectors.toMap(stringObjectEntry -> stringObjectEntry.getKey(),stringObjectEntry -> stringObjectEntry.getValue()));
+ var cache = e.getStoreCache().entrySet().stream()
+ .filter(stringObjectEntry -> {
+ return stringObjectEntry.getValue() != null
+ && (ClassUtils.isPrimitiveOrWrapper(
+ stringObjectEntry.getValue().getClass())
+ || stringObjectEntry.getValue() instanceof String);
+ })
+ .collect(Collectors.toMap(
+ stringObjectEntry -> stringObjectEntry.getKey(),
+ stringObjectEntry -> stringObjectEntry.getValue()));
var apply = InfoResponse.builder()
.lastModified(e.getLastModified())
@@ -50,27 +57,17 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange {
}
private Class> toWrapper(Class> clazz) {
- if (!clazz.isPrimitive())
- return clazz;
+ if (!clazz.isPrimitive()) return clazz;
- if (clazz == Integer.TYPE)
- return Integer.class;
- if (clazz == Long.TYPE)
- return Long.class;
- if (clazz == Boolean.TYPE)
- return Boolean.class;
- if (clazz == Byte.TYPE)
- return Byte.class;
- if (clazz == Character.TYPE)
- return Character.class;
- if (clazz == Float.TYPE)
- return Float.class;
- if (clazz == Double.TYPE)
- return Double.class;
- if (clazz == Short.TYPE)
- return Short.class;
- if (clazz == Void.TYPE)
- return Void.class;
+ if (clazz == Integer.TYPE) return Integer.class;
+ if (clazz == Long.TYPE) return Long.class;
+ if (clazz == Boolean.TYPE) return Boolean.class;
+ if (clazz == Byte.TYPE) return Byte.class;
+ if (clazz == Character.TYPE) return Character.class;
+ if (clazz == Float.TYPE) return Float.class;
+ if (clazz == Double.TYPE) return Double.class;
+ if (clazz == Short.TYPE) return Short.class;
+ if (clazz == Void.TYPE) return Void.class;
return clazz;
}
diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java
index dfd656d96..5fa336528 100644
--- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java
+++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java
@@ -1,11 +1,12 @@
package io.xpipe.app.beacon.impl;
-import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionRefreshExchange;
+import com.sun.net.httpserver.HttpExchange;
+
public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {
@Override
diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java
index 88f3850cb..ed7c65907 100644
--- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java
+++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java
@@ -1,11 +1,12 @@
package io.xpipe.app.beacon.impl;
-import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionRemoveExchange;
+import com.sun.net.httpserver.HttpExchange;
+
import java.util.ArrayList;
import java.util.UUID;
diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java
index 799be53b1..0717de7f4 100644
--- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java
+++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java
@@ -1,12 +1,13 @@
package io.xpipe.app.beacon.impl;
-import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionTerminalExchange;
import io.xpipe.core.store.ShellStore;
+import com.sun.net.httpserver.HttpExchange;
+
public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
@Override
@@ -18,7 +19,7 @@ public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
throw new BeaconClientException("Not a shell connection");
}
try (var sc = shellStore.control().start()) {
- TerminalLauncher.open(e,e.getName(),msg.getDirectory(),sc);
+ TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc);
}
return Response.builder().build();
}
diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java
index dbecdba4d..7d3f48dd1 100644
--- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java
+++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java
@@ -1,11 +1,12 @@
package io.xpipe.app.beacon.impl;
-import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionToggleExchange;
import io.xpipe.core.store.SingletonSessionStore;
+import com.sun.net.httpserver.HttpExchange;
+
public class ConnectionToggleExchangeImpl extends ConnectionToggleExchange {
@Override
diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java
index 9d05484a6..9ff60fffe 100644
--- a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java
+++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java
@@ -36,6 +36,7 @@ public class ShellStartExchangeImpl extends ShellStartExchange {
.osType(control.getOsType())
.osName(control.getOsName())
.temp(control.getSystemTemporaryDirectory())
+ .ttyState(control.getTtyState())
.build();
}
}
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java
index 2a18d4643..446438b16 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java
@@ -10,10 +10,12 @@ import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
import javafx.css.PseudoClass;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
+import java.util.HashSet;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
@@ -41,13 +43,13 @@ public final class BrowserBookmarkComp extends SimpleComp {
@Override
protected Region createSimple() {
- BooleanProperty busy = new SimpleBooleanProperty(false);
+ var busyEntries = FXCollections.observableSet(new HashSet<>());
BiConsumer>> augment = (s, comp) -> {
comp.disable(Bindings.createBooleanBinding(
() -> {
- return busy.get() || !applicable.test(s.getWrapper());
+ return busyEntries.contains(s) || !applicable.test(s.getWrapper());
},
- busy));
+ busyEntries));
comp.apply(struc -> {
selected.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
@@ -70,7 +72,17 @@ public final class BrowserBookmarkComp extends SimpleComp {
category,
StoreViewState.get().getEntriesListUpdateObservable()),
augment,
- entryWrapper -> action.accept(entryWrapper, busy));
+ selectedAction -> {
+ BooleanProperty busy = new SimpleBooleanProperty(false);
+ action.accept(selectedAction.getWrapper(), busy);
+ busy.addListener((observable, oldValue, newValue) -> {
+ if (newValue) {
+ busyEntries.add(selectedAction);
+ } else {
+ busyEntries.remove(selectedAction);
+ }
+ });
+ });
var r = section.vgrow().createRegion();
r.getStyleClass().add("bookmark-list");
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java
index 2c59091b1..6e67c0a84 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java
@@ -2,6 +2,7 @@ package io.xpipe.app.browser;
import io.xpipe.app.comp.store.StoreCategoryWrapper;
import io.xpipe.app.comp.store.StoreViewState;
+import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
@@ -30,18 +31,26 @@ public final class BrowserBookmarkHeaderComp extends SimpleComp {
StoreViewState.get().getAllConnectionsCategory(),
StoreViewState.get().getActiveCategory(),
this.category)
- .styleClass(Styles.LEFT_PILL);
- var filter = new FilterComp(this.filter).styleClass(Styles.RIGHT_PILL).minWidth(0).hgrow();
+ .styleClass(Styles.LEFT_PILL)
+ .apply(struc -> {
+ AppFont.medium(struc.get());
+ });
+ var filter = new FilterComp(this.filter)
+ .styleClass(Styles.RIGHT_PILL)
+ .minWidth(0)
+ .hgrow()
+ .apply(struc -> {
+ AppFont.medium(struc.get());
+ });
var top = new HorizontalComp(List.of(category, filter))
.apply(struc -> struc.get().setFillHeight(true))
.apply(struc -> {
- ((Region) struc.get().getChildren().get(0))
- .prefHeightProperty()
- .bind(((Region) struc.get().getChildren().get(1)).heightProperty());
- ((Region) struc.get().getChildren().get(0))
- .minWidthProperty()
- .bind(struc.get().widthProperty().divide(2.0));
+ var first = ((Region) struc.get().getChildren().get(0));
+ var second = ((Region) struc.get().getChildren().get(1));
+ first.prefHeightProperty().bind(second.heightProperty());
+ first.minHeightProperty().bind(second.heightProperty());
+ first.maxHeightProperty().bind(second.heightProperty());
})
.styleClass("bookmarks-header")
.createRegion();
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java
index 39290d0e0..f4c4f55f6 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java
@@ -109,7 +109,7 @@ public class BrowserNavBar extends Comp {
new TooltipAugment<>("history", new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN))
.augment(historyButton);
- var breadcrumbs = new BrowserBreadcrumbBar(model).grow(false, true);
+ var breadcrumbs = new BrowserBreadcrumbBar(model);
var pathRegion = pathBar.createStructure().get();
var breadcrumbsRegion = breadcrumbs.createRegion();
@@ -143,7 +143,7 @@ public class BrowserNavBar extends Comp {
topBox.setFillHeight(true);
topBox.setAlignment(Pos.CENTER);
homeButton.minWidthProperty().bind(pathRegion.heightProperty());
- homeButton.maxWidthProperty().bind(pathRegion.heightProperty().multiply(1.3));
+ homeButton.maxWidthProperty().bind(pathRegion.heightProperty());
homeButton.minHeightProperty().bind(pathRegion.heightProperty());
homeButton.maxHeightProperty().bind(pathRegion.heightProperty());
historyButton.minHeightProperty().bind(pathRegion.heightProperty());
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSavedStateImpl.java b/app/src/main/java/io/xpipe/app/browser/BrowserSavedStateImpl.java
index 82528ffd2..cf78e25d7 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserSavedStateImpl.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserSavedStateImpl.java
@@ -29,7 +29,16 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
this.lastSystems = FXCollections.observableArrayList(lastSystems);
}
- public static BrowserSavedStateImpl load() {
+ private static BrowserSavedStateImpl INSTANCE;
+
+ public static BrowserSavedState get() {
+ if (INSTANCE == null) {
+ INSTANCE = load();
+ }
+ return INSTANCE;
+ }
+
+ private static BrowserSavedStateImpl load() {
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
return new BrowserSavedStateImpl(FXCollections.observableArrayList());
});
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java
index 93ae14cec..624d9f618 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java
@@ -7,6 +7,7 @@ import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
+import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
@@ -58,9 +59,15 @@ public class BrowserSelectionListComp extends SimpleComp {
return Comp.of(() -> {
var image = PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 24)
.createRegion();
- var l = new Label(null, image);
+ var t = nameTransformation.apply(entry);
+ var l = new Label(t.getValue(), image);
l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
- l.textProperty().bind(PlatformThread.sync(nameTransformation.apply(entry)));
+ t.addListener((observable, oldValue, newValue) -> {
+ PlatformThread.runLaterIfNeeded(() -> {
+ l.setText(newValue);
+ });
+ });
+ BindingsHelper.preserve(l, t);
return l;
});
},
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java
index f784fe6f3..8d853d5fd 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java
@@ -12,10 +12,12 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.util.HumanReadableFormat;
+
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
+
import lombok.EqualsAndHashCode;
import lombok.Value;
@@ -37,8 +39,7 @@ public class BrowserStatusBarComp extends SimpleComp {
createProgressEstimateStatus(),
Comp.hspacer(),
createClipboardStatus(),
- createSelectionStatus()
- ));
+ createSelectionStatus()));
bar.spacing(15);
bar.styleClass("status-bar");
@@ -58,12 +59,16 @@ public class BrowserStatusBarComp extends SimpleComp {
return null;
} else {
var expected = p.expectedTimeRemaining();
- var show = (p.getTotal() > 50_000_000 && p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0) || expected.toMillis() > 5000;
- var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "...";
+ var show = p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0
+ && (p.getTotal() > 50_000_000 || expected.toMillis() > 5000);
+ var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "";
return time;
}
});
- var progressComp = new LabelComp(text).styleClass("progress").apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)).prefWidth(90);
+ var progressComp = new LabelComp(text)
+ .styleClass("progress")
+ .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
+ .prefWidth(90);
return progressComp;
}
@@ -77,7 +82,10 @@ public class BrowserStatusBarComp extends SimpleComp {
return transferred + " / " + all;
}
});
- var progressComp = new LabelComp(text).styleClass("progress").apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)).prefWidth(150);
+ var progressComp = new LabelComp(text)
+ .styleClass("progress")
+ .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
+ .prefWidth(150);
return progressComp;
}
@@ -89,7 +97,10 @@ public class BrowserStatusBarComp extends SimpleComp {
return p.getName();
}
});
- var progressComp = new LabelComp(text).styleClass("progress").apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)).prefWidth(180);
+ var progressComp = new LabelComp(text)
+ .styleClass("progress")
+ .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
+ .prefWidth(180);
return progressComp;
}
@@ -160,7 +171,6 @@ public class BrowserStatusBarComp extends SimpleComp {
emptyEntry.onDragDone(event);
});
-
// Use status bar as an extension of file list
new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java
index e4bfb2a2d..5c3334684 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java
@@ -1,8 +1,6 @@
package io.xpipe.app.browser;
-import io.xpipe.app.browser.file.BrowserFileTransferMode;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
-import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
@@ -10,15 +8,19 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
-import io.xpipe.app.fxcomps.util.PlatformThread;
+import io.xpipe.app.util.ThreadHelper;
+
import javafx.beans.binding.Bindings;
+import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.image.Image;
+import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region;
+
import org.kordamp.ikonli.javafx.FontIcon;
import java.io.File;
@@ -37,172 +39,151 @@ public class BrowserTransferComp extends SimpleComp {
@Override
protected Region createSimple() {
- var syncItems = PlatformThread.sync(model.getItems());
- var syncDownloaded = PlatformThread.sync(model.getDownloading());
- var syncAllDownloaded = PlatformThread.sync(model.getAllDownloaded());
-
var background = new LabelComp(AppI18n.observable("transferDescription"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.apply(struc -> struc.get().setWrapText(true))
- .visible(Bindings.isEmpty(syncItems));
+ .visible(model.getEmpty());
var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
- var binding = new DerivedObservableList<>(syncItems, true)
+ var binding = new DerivedObservableList<>(model.getItems(), true)
.mapped(item -> item.getBrowserEntry())
.getList();
- var list = new BrowserSelectionListComp(
- binding,
- entry -> Bindings.createStringBinding(
+ var list = new BrowserSelectionListComp(binding, entry -> {
+ var sourceItem = model.getCurrentItems().stream()
+ .filter(item -> item.getBrowserEntry() == entry)
+ .findAny();
+ if (sourceItem.isEmpty()) {
+ return new SimpleStringProperty("?");
+ }
+ synchronized (sourceItem.get().getProgress()) {
+ return Bindings.createStringBinding(
() -> {
- var sourceItem = syncItems.stream()
- .filter(item -> item.getBrowserEntry() == entry)
- .findAny();
- if (sourceItem.isEmpty()) {
- return "?";
- }
- var name = entry.getModel() == null
+ var p = sourceItem.get().getProgress().getValue();
+ var progressSuffix = p == null
|| sourceItem
.get()
.downloadFinished()
.get()
- ? "Local"
- : entry.getModel()
- .getFileSystemModel()
- .getName();
- return entry.getFileName() + " (" + name + ")";
+ ? ""
+ : " " + (p.getTransferred() * 100 / p.getTotal()) + "%";
+ return entry.getFileName() + progressSuffix;
},
- syncAllDownloaded))
+ sourceItem.get().getProgress());
+ }
+ })
.grow(false, true);
- var dragNotice = new LabelComp(syncAllDownloaded.flatMap(
- aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
+ var dragNotice = new LabelComp(AppI18n.observable("dragLocalFiles"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
.apply(struc -> AppFont.medium(struc.get()))
.apply(struc -> struc.get().setWrapText(true))
- .hide(Bindings.isEmpty(syncItems));
+ .hide(model.getEmpty());
- var downloadButton = new IconButtonComp("mdi2d-download", () -> {
- model.download();
- })
- .hide(Bindings.isEmpty(syncItems))
- .disable(syncAllDownloaded)
- .tooltipKey("downloadStageDescription");
var clearButton = new IconButtonComp("mdi2c-close", () -> {
- model.clear(true);
+ ThreadHelper.runAsync(() -> {
+ model.clear(true);
+ });
})
- .hide(Bindings.isEmpty(syncItems))
+ .hide(model.getEmpty())
.tooltipKey("clearTransferDescription");
- var bottom =
- new HorizontalComp(List.of(Comp.hspacer(), dragNotice, Comp.hspacer(), downloadButton, Comp.hspacer(4), clearButton));
+ var downloadButton = new IconButtonComp("mdi2f-folder-move-outline", () -> {
+ ThreadHelper.runFailableAsync(() -> {
+ model.transferToDownloads();
+ });
+ })
+ .hide(model.getEmpty())
+ .tooltipKey("downloadStageDescription");
+
+ var bottom = new HorizontalComp(
+ List.of(Comp.hspacer(), dragNotice, Comp.hspacer(), downloadButton, Comp.hspacer(4), clearButton));
var listBox = new VerticalComp(List.of(list, bottom))
.spacing(5)
.padding(new Insets(10, 10, 5, 10))
.apply(struc -> struc.get().setMinHeight(200))
.apply(struc -> struc.get().setMaxHeight(200));
- var stack = LoadingOverlayComp.noProgress(
- new StackComp(List.of(backgroundStack, listBox))
- .apply(DragOverPseudoClassAugment.create())
- .apply(struc -> {
- struc.get().setOnDragOver(event -> {
- // Accept drops from inside the app window
- if (event.getGestureSource() != null && event.getGestureSource() != struc.get()) {
- event.acceptTransferModes(TransferMode.ANY);
- event.consume();
- }
+ var stack = new StackComp(List.of(backgroundStack, listBox))
+ .apply(DragOverPseudoClassAugment.create())
+ .apply(struc -> {
+ struc.get().setOnDragOver(event -> {
+ // Accept drops from inside the app window
+ if (event.getGestureSource() != null && event.getGestureSource() != struc.get()) {
+ event.acceptTransferModes(TransferMode.ANY);
+ event.consume();
+ }
+ });
+ struc.get().setOnDragDropped(event -> {
+ // Accept drops from inside the app window
+ if (event.getGestureSource() != null) {
+ var drag = BrowserClipboard.retrieveDrag(event.getDragboard());
+ if (drag == null) {
+ return;
+ }
- // Accept drops from outside the app window
- if (event.getGestureSource() == null
- && !event.getDragboard().getFiles().isEmpty()) {
- event.acceptTransferModes(TransferMode.ANY);
- event.consume();
- }
- });
- struc.get().setOnDragDropped(event -> {
- // Accept drops from inside the app window
- if (event.getGestureSource() != null) {
- var drag = BrowserClipboard.retrieveDrag(event.getDragboard());
- if (drag == null) {
- return;
+ if (!(model.getBrowserSessionModel()
+ .getSelectedEntry()
+ .getValue()
+ instanceof OpenFileSystemModel fileSystemModel)) {
+ return;
+ }
+
+ var files = drag.getEntries();
+ model.drop(fileSystemModel, files);
+ event.setDropCompleted(true);
+ event.consume();
+ }
+ });
+ struc.get().setOnDragDetected(event -> {
+ var items = model.getCurrentItems();
+ var selected = items.stream()
+ .map(item -> item.getBrowserEntry())
+ .toList();
+ var files = items.stream()
+ .filter(item -> item.downloadFinished().get())
+ .map(item -> {
+ try {
+ var file = item.getLocalFile();
+ if (!Files.exists(file)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(file.toRealPath().toFile());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
+ })
+ .flatMap(Optional::stream)
+ .toList();
+ if (files.isEmpty()) {
+ return;
+ }
- if (!(model.getBrowserSessionModel()
- .getSelectedEntry()
- .getValue()
- instanceof OpenFileSystemModel fileSystemModel)) {
- return;
- }
+ var cc = new ClipboardContent();
+ cc.putFiles(files);
+ Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
+ db.setContent(cc);
- var files = drag.getEntries();
- model.drop(fileSystemModel, files);
- event.setDropCompleted(true);
- event.consume();
- }
+ Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
+ db.setDragView(image, -20, 15);
- // Accept drops from outside the app window
- if (event.getGestureSource() == null) {
- model.dropLocal(event.getDragboard().getFiles());
- event.setDropCompleted(true);
- event.consume();
- }
- });
- struc.get().setOnDragDetected(event -> {
- if (syncDownloaded.getValue()) {
- return;
- }
+ event.setDragDetect(true);
+ event.consume();
+ });
+ struc.get().setOnDragDone(event -> {
+ if (!event.isAccepted()) {
+ return;
+ }
- var selected = syncItems.stream()
- .map(item -> item.getBrowserEntry())
- .toList();
- Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
-
- var cc = BrowserClipboard.startDrag(null, selected, BrowserFileTransferMode.NORMAL);
- if (cc == null) {
- return;
- }
-
- var files = syncItems.stream()
- .filter(item -> item.downloadFinished().get())
- .map(item -> {
- try {
- var file = item.getLocalFile();
- if (!Files.exists(file)) {
- return Optional.empty();
- }
-
- return Optional.of(
- file.toRealPath().toFile());
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- })
- .flatMap(Optional::stream)
- .toList();
- cc.putFiles(files);
- db.setContent(cc);
-
- Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
- db.setDragView(image, -20, 15);
-
- event.setDragDetect(true);
- event.consume();
- });
- struc.get().setOnDragDone(event -> {
- if (!event.isAccepted()) {
- return;
- }
-
- // The files might not have been transferred yet
- // We can't listen to this, so just don't delete them
- model.clear(false);
- event.consume();
- });
- }),
- syncDownloaded);
+ // The files might not have been transferred yet
+ // We can't listen to this, so just don't delete them
+ model.clear(false);
+ event.consume();
+ });
+ });
stack.apply(struc -> {
model.getBrowserSessionModel().getDraggingFiles().addListener((observable, oldValue, newValue) -> {
- struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("highlighted"),newValue);
+ struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("highlighted"), newValue);
});
});
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java
index bc19c109b..4a89c7e07 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java
@@ -7,13 +7,12 @@ import io.xpipe.app.browser.file.LocalFileSystem;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.issue.ErrorEvent;
-import io.xpipe.app.util.BooleanScope;
+import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.ShellTemp;
+import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
-import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
-import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
@@ -22,136 +21,156 @@ import javafx.collections.ObservableList;
import lombok.Value;
import org.apache.commons.io.FileUtils;
-import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.Optional;
@Value
public class BrowserTransferModel {
private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("download");
- ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
- Thread t = Executors.defaultThreadFactory().newThread(r);
- t.setDaemon(true);
- t.setName("file downloader");
- return t;
- });
BrowserSessionModel browserSessionModel;
ObservableList- items = FXCollections.observableArrayList();
- BooleanProperty downloading = new SimpleBooleanProperty();
- BooleanProperty allDownloaded = new SimpleBooleanProperty();
+ ObservableBooleanValue empty = Bindings.createBooleanBinding(() -> items.isEmpty(), items);
- private void cleanDirectory() {
+ public BrowserTransferModel(BrowserSessionModel browserSessionModel) {
+ this.browserSessionModel = browserSessionModel;
+ var thread = ThreadHelper.createPlatformThread("file downloader", true, () -> {
+ while (true) {
+ Optional
- toDownload;
+ synchronized (items) {
+ toDownload = items.stream()
+ .filter(item -> !item.downloadFinished().get())
+ .findFirst();
+ }
+ if (toDownload.isPresent()) {
+ downloadSingle(toDownload.get());
+ }
+ ThreadHelper.sleep(20);
+ }
+ });
+ thread.start();
+ }
+
+ public List
- getCurrentItems() {
+ synchronized (items) {
+ return new ArrayList<>(items);
+ }
+ }
+
+ private void cleanItem(Item item) {
if (!Files.isDirectory(TEMP)) {
return;
}
- try (var ls = Files.list(TEMP)) {
- var list = ls.toList();
- for (Path path : list) {
- FileUtils.forceDelete(path.toFile());
- }
+ if (!Files.exists(item.getLocalFile())) {
+ return;
+ }
+
+ try {
+ FileUtils.forceDelete(item.getLocalFile().toFile());
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
}
}
public void clear(boolean delete) {
- items.clear();
+ List
- toClear;
+ synchronized (items) {
+ toClear =
+ items.stream().filter(item -> item.downloadFinished().get()).toList();
+ if (toClear.isEmpty()) {
+ return;
+ }
+ items.removeAll(toClear);
+ }
if (delete) {
- executor.submit(() -> {
- cleanDirectory();
- });
+ for (Item item : toClear) {
+ cleanItem(item);
+ }
}
}
public void drop(OpenFileSystemModel model, List entries) {
- entries.forEach(entry -> {
- var name = entry.getFileName();
- if (items.stream().anyMatch(item -> item.getName().equals(name))) {
- return;
- }
-
- Path file = TEMP.resolve(name);
- var item = new Item(model, name, entry, file);
- items.add(item);
- allDownloaded.set(false);
- });
- }
-
- public void dropLocal(List entries) {
- if (entries.isEmpty()) {
- return;
- }
-
- var empty = items.isEmpty();
- try {
- var paths = entries.stream().map(File::toPath).filter(Files::exists).toList();
- for (Path path : paths) {
- var entry = LocalFileSystem.getLocalBrowserEntry(path);
+ synchronized (items) {
+ entries.forEach(entry -> {
var name = entry.getFileName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
return;
}
- var item = new Item(null, name, entry, path);
- item.progress.setValue(BrowserTransferProgress.finished(
- entry.getFileName(), entry.getRawFileEntry().getSize()));
+ Path file = TEMP.resolve(name);
+ var item = new Item(model, name, entry, file);
items.add(item);
- }
- } catch (Exception ex) {
- ErrorEvent.fromThrowable(ex).handle();
- }
- if (empty) {
- allDownloaded.set(true);
+ });
}
}
- public void download() {
- executor.submit(() -> {
- try {
- FileUtils.forceMkdir(TEMP.toFile());
- } catch (IOException e) {
- ErrorEvent.fromThrowable(e).handle();
+ public void downloadSingle(Item item) {
+ try {
+ FileUtils.forceMkdir(TEMP.toFile());
+ } catch (IOException e) {
+ ErrorEvent.fromThrowable(e).handle();
+ return;
+ }
+
+ if (item.downloadFinished().get()) {
+ return;
+ }
+
+ if (item.getOpenFileSystemModel() != null
+ && item.getOpenFileSystemModel().isClosed()) {
+ return;
+ }
+
+ try {
+ var op = new BrowserFileTransferOperation(
+ LocalFileSystem.getLocalFileEntry(TEMP),
+ List.of(item.getBrowserEntry().getRawFileEntry()),
+ BrowserFileTransferMode.COPY,
+ false,
+ progress -> {
+ synchronized (item.getProgress()) {
+ item.getProgress().setValue(progress);
+ }
+ item.getOpenFileSystemModel().getProgress().setValue(progress);
+ });
+ op.execute();
+ } catch (Throwable t) {
+ ErrorEvent.fromThrowable(t).handle();
+ synchronized (items) {
+ items.remove(item);
+ }
+ }
+ }
+
+ public void transferToDownloads() throws Exception {
+ List
- toMove;
+ synchronized (items) {
+ toMove =
+ items.stream().filter(item -> item.downloadFinished().get()).toList();
+ if (toMove.isEmpty()) {
return;
}
+ items.removeAll(toMove);
+ }
- for (Item item : new ArrayList<>(items)) {
- if (item.downloadFinished().get()) {
- continue;
- }
-
- if (item.getOpenFileSystemModel() != null
- && item.getOpenFileSystemModel().isClosed()) {
- continue;
- }
-
- try {
- try (var ignored = new BooleanScope(downloading).start()) {
- var op = new BrowserFileTransferOperation(
- LocalFileSystem.getLocalFileEntry(TEMP),
- List.of(item.getBrowserEntry().getRawFileEntry()),
- BrowserFileTransferMode.COPY,
- false,
- progress -> {
- item.getProgress().setValue(progress);
- item.getOpenFileSystemModel().getProgress().setValue(progress);
- });
- op.execute();
- }
- } catch (Throwable t) {
- ErrorEvent.fromThrowable(t).handle();
- items.remove(item);
- }
+ var files = toMove.stream().map(item -> item.getLocalFile()).toList();
+ var downloads = DesktopHelper.getDownloadsDirectory();
+ for (Path file : files) {
+ var target = downloads.resolve(file.getFileName());
+ // Prevent DirectoryNotEmptyException
+ if (Files.exists(target) && Files.isDirectory(target)) {
+ Files.delete(target);
}
- allDownloaded.set(true);
- });
+ Files.move(file, target, StandardCopyOption.REPLACE_EXISTING);
+ }
+ DesktopHelper.browseFileInDirectory(downloads.resolve(files.getFirst().getFileName()));
}
@Value
@@ -171,12 +190,11 @@ public class BrowserTransferModel {
}
public ObservableBooleanValue downloadFinished() {
- return Bindings.createBooleanBinding(
- () -> {
- return progress.getValue() != null
- && progress.getValue().done();
- },
- progress);
+ synchronized (progress) {
+ return Bindings.createBooleanBinding(() -> {
+ return progress.getValue() != null && progress.getValue().done();
+ }, progress);
+ }
}
}
}
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java
index 5deb42634..bb1e16ec7 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java
@@ -41,6 +41,7 @@ public class BrowserTransferProgress {
var share = (double) transferred / total;
var rest = (1.0 - share) / share;
var restMillis = (long) (elapsed.toMillis() * rest);
- return Duration.of(restMillis, ChronoUnit.MILLIS);
+ var startupAdjustment = (long) (restMillis / (1.0 + Math.max(10000 - elapsed.toMillis(), 0) / 10000.0));
+ return Duration.of(restMillis + startupAdjustment, ChronoUnit.MILLIS);
}
}
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java
index 8350041df..08f4f4d4a 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java
@@ -45,7 +45,7 @@ public class BrowserWelcomeComp extends SimpleComp {
@Override
protected Region createSimple() {
- var state = model.getSavedState();
+ var state = BrowserSavedStateImpl.get();
var welcome = new BrowserGreetingComp().createSimple();
@@ -55,6 +55,7 @@ public class BrowserWelcomeComp extends SimpleComp {
var img = new PrettySvgComp(new SimpleStringProperty("Hips.svg"), 50, 75)
.padding(new Insets(5, 0, 0, 0))
.createRegion();
+
var hbox = new HBox(img, vbox);
hbox.setAlignment(Pos.CENTER_LEFT);
hbox.setSpacing(15);
@@ -139,7 +140,6 @@ public class BrowserWelcomeComp extends SimpleComp {
.hide(empty)
.accessibleTextKey("restoreAllSessions");
layout.getChildren().add(tile.createRegion());
-
return layout;
}
@@ -149,7 +149,7 @@ public class BrowserWelcomeComp extends SimpleComp {
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24);
return new ButtonComp(
- new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())),
+ new SimpleStringProperty(DataStorage.get().getStoreEntryDisplayName(entry.get())),
view.createRegion(),
() -> {
ThreadHelper.runAsync(() -> {
@@ -160,7 +160,7 @@ public class BrowserWelcomeComp extends SimpleComp {
});
})
.minWidth(250)
- .accessibleText(DataStorage.get().getStoreDisplayName(entry.get()))
+ .accessibleText(DataStorage.get().getStoreEntryDisplayName(entry.get()))
.disable(disable)
.styleClass("entry-button")
.styleClass(Styles.LEFT_PILL)
diff --git a/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java b/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java
index 6d77f5a5a..4c722f406 100644
--- a/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java
+++ b/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java
@@ -2,10 +2,40 @@ package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
+import io.xpipe.app.util.LicenseProvider;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public interface BranchAction extends BrowserAction {
- List getBranchingActions(OpenFileSystemModel model, List entries);
+ default MenuItem toMenuItem(OpenFileSystemModel model, List selected) {
+ var m = new Menu(getName(model, selected).getValue() + " ...");
+ for (var sub : getBranchingActions(model, selected)) {
+ var subselected = resolveFilesIfNeeded(selected);
+ if (!sub.isApplicable(model, subselected)) {
+ continue;
+ }
+ m.getItems().add(sub.toMenuItem(model, subselected));
+ }
+ var graphic = getIcon(model, selected);
+ if (graphic != null) {
+ m.setGraphic(graphic);
+ }
+ m.setDisable(!isActive(model, selected));
+
+ if (getProFeatureId() != null
+ && !LicenseProvider.get()
+ .getFeature(getProFeatureId())
+ .isSupported()) {
+ m.setDisable(true);
+ m.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
+ }
+
+ return m;
+ }
+
+ List extends BrowserAction> getBranchingActions(OpenFileSystemModel model, List entries);
}
diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java
index 285959aa1..5387d3c08 100644
--- a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java
+++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java
@@ -7,6 +7,7 @@ import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
+import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCombination;
import java.util.ArrayList;
@@ -19,13 +20,17 @@ public interface BrowserAction {
static List getFlattened(OpenFileSystemModel model, List entries) {
return ALL.stream()
- .map(browserAction -> browserAction instanceof LeafAction
- ? List.of((LeafAction) browserAction)
- : ((BranchAction) browserAction).getBranchingActions(model, entries))
+ .map(browserAction -> getFlattened(browserAction, model, entries))
.flatMap(List::stream)
.toList();
}
+ static List getFlattened(BrowserAction browserAction, OpenFileSystemModel model, List entries) {
+ return browserAction instanceof LeafAction
+ ? List.of((LeafAction) browserAction)
+ : ((BranchAction) browserAction).getBranchingActions(model, entries).stream().map(action -> getFlattened(action, model, entries)).flatMap(List::stream).toList();
+ }
+
static LeafAction byId(String id, OpenFileSystemModel model, List entries) {
return getFlattened(model, entries).stream()
.filter(browserAction -> id.equals(browserAction.getId()))
@@ -33,6 +38,17 @@ public interface BrowserAction {
.orElseThrow();
}
+ default List resolveFilesIfNeeded(List selected) {
+ return automaticallyResolveLinks()
+ ? selected.stream()
+ .map(browserEntry ->
+ new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel()))
+ .toList()
+ : selected;
+ }
+
+ MenuItem toMenuItem(OpenFileSystemModel model, List selected);
+
default void init(OpenFileSystemModel model) throws Exception {}
default String getProFeatureId() {
diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java
index 3700b17eb..3ea4510b9 100644
--- a/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java
+++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java
@@ -80,7 +80,10 @@ public class BrowserAlerts {
private static String getSelectedElementsString(List source) {
var namesHeader = AppI18n.get("selectedElements");
var names = namesHeader + "\n"
- + source.stream().limit(10).map(entry -> "- " + new FilePath(entry.getPath()).getFileName()).collect(Collectors.joining("\n"));
+ + source.stream()
+ .limit(10)
+ .map(entry -> "- " + new FilePath(entry.getPath()).getFileName())
+ .collect(Collectors.joining("\n"));
if (source.size() > 10) {
names += "\n+ " + (source.size() - 10) + " ...";
}
diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java
index 6e33b03b8..bca81c851 100644
--- a/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java
+++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java
@@ -1,19 +1,12 @@
package io.xpipe.app.browser.file;
-import io.xpipe.app.browser.action.BranchAction;
import io.xpipe.app.browser.action.BrowserAction;
-import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.util.InputHelper;
-import io.xpipe.app.util.LicenseProvider;
-
import javafx.scene.control.ContextMenu;
-import javafx.scene.control.Menu;
import javafx.scene.control.SeparatorMenuItem;
-import org.kordamp.ikonli.javafx.FontIcon;
-
import java.util.ArrayList;
import java.util.List;
@@ -30,15 +23,6 @@ public final class BrowserContextMenu extends ContextMenu {
createMenu();
}
- private static List resolveIfNeeded(BrowserAction action, List selected) {
- return action.automaticallyResolveLinks()
- ? selected.stream()
- .map(browserEntry ->
- new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel()))
- .toList()
- : selected;
- }
-
private void createMenu() {
InputHelper.onLeft(this, false, e -> {
hide();
@@ -60,7 +44,7 @@ public final class BrowserContextMenu extends ContextMenu {
var all = BrowserAction.ALL.stream()
.filter(browserAction -> browserAction.getCategory() == cat)
.filter(browserAction -> {
- var used = resolveIfNeeded(browserAction, selected);
+ var used = browserAction.resolveFilesIfNeeded(selected);
if (!browserAction.isApplicable(model, used)) {
return false;
}
@@ -81,36 +65,8 @@ public final class BrowserContextMenu extends ContextMenu {
}
for (BrowserAction a : all) {
- var used = resolveIfNeeded(a, selected);
- if (a instanceof LeafAction la) {
- getItems().add(la.toMenuItem(model, used));
- }
-
- if (a instanceof BranchAction la) {
- var m = new Menu(a.getName(model, used).getValue() + " ...");
- for (LeafAction sub : la.getBranchingActions(model, used)) {
- var subUsed = resolveIfNeeded(sub, selected);
- if (!sub.isApplicable(model, subUsed)) {
- continue;
- }
- m.getItems().add(sub.toMenuItem(model, subUsed));
- }
- var graphic = a.getIcon(model, used);
- if (graphic != null) {
- m.setGraphic(graphic);
- }
- m.setDisable(!a.isActive(model, used));
-
- if (la.getProFeatureId() != null
- && !LicenseProvider.get()
- .getFeature(la.getProFeatureId())
- .isSupported()) {
- m.setDisable(true);
- m.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
- }
-
- getItems().add(m);
- }
+ var used = a.resolveFilesIfNeeded(selected);
+ getItems().add(a.toMenuItem(model, used));
}
}
}
diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java
index 052052b84..091667fc7 100644
--- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java
@@ -4,8 +4,6 @@ import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
-import io.xpipe.app.fxcomps.SimpleCompStructure;
-import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.*;
@@ -29,10 +27,7 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow;
-import javafx.scene.input.DragEvent;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.MouseButton;
-import javafx.scene.input.MouseEvent;
+import javafx.scene.input.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
@@ -40,11 +35,13 @@ import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
+import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
import static io.xpipe.app.util.HumanReadableFormat.byteCount;
import static javafx.scene.control.TableColumn.SortType.ASCENDING;
@@ -60,6 +57,7 @@ public final class BrowserFileListComp extends SimpleComp {
private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current");
private final BrowserFileListModel fileList;
+ private final StringProperty typedSelection = new SimpleStringProperty("");
public BrowserFileListComp(BrowserFileListModel fileList) {
this.fileList = fileList;
@@ -124,16 +122,80 @@ public final class BrowserFileListComp extends SimpleComp {
return true;
});
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
- table.setFixedCellSize(34.0);
+ table.setFixedCellSize(32.0);
prepareTableSelectionModel(table);
prepareTableShortcuts(table);
prepareTableEntries(table);
prepareTableChanges(table, mtimeCol, modeCol);
+ prepareTypedSelectionModel(table);
return table;
}
+ private void prepareTypedSelectionModel(TableView table) {
+ AtomicReference lastFail = new AtomicReference<>();
+ table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
+ updateTypedSelection(table, lastFail, event, false);
+ });
+
+ table.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
+ typedSelection.set("");
+ lastFail.set(null);
+ });
+
+ fileList.getFileSystemModel().getCurrentPath().addListener((observable, oldValue, newValue) -> {
+ typedSelection.set("");
+ lastFail.set(null);
+ });
+
+ table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
+ if (event.getCode() == KeyCode.ESCAPE) {
+ typedSelection.set("");
+ lastFail.set(null);
+ }
+ });
+ }
+
+ private void updateTypedSelection(TableView table, AtomicReference lastType, KeyEvent event, boolean recursive) {
+ var typed = event.getText();
+ if (typed.isEmpty()) {
+ return;
+ }
+
+ var updated = typedSelection.get() + typed;
+ var found = fileList.getShown().getValue().stream()
+ .filter(browserEntry ->
+ browserEntry.getFileName().toLowerCase().startsWith(updated.toLowerCase()))
+ .findFirst();
+ if (found.isEmpty()) {
+ if (typedSelection.get().isEmpty()) {
+ return;
+ }
+
+ var inCooldown = lastType.get() != null && Duration.between(lastType.get(), Instant.now()).toMillis() < 1000;
+ if (inCooldown) {
+ lastType.set(Instant.now());
+ event.consume();
+ return;
+ } else {
+ lastType.set(null);
+ typedSelection.set("");
+ table.getSelectionModel().clearSelection();
+ if (!recursive) {
+ updateTypedSelection(table, lastType, event, true);
+ }
+ return;
+ }
+ }
+
+ lastType.set(Instant.now());
+ typedSelection.set(updated);
+ table.scrollTo(found.get());
+ table.getSelectionModel().clearAndSelect(fileList.getShown().getValue().indexOf(found.get()));
+ event.consume();
+ }
+
private void prepareTableSelectionModel(TableView table) {
if (!fileList.getSelectionMode().isMultiple()) {
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
@@ -167,7 +229,7 @@ public final class BrowserFileListComp extends SimpleComp {
}
private void prepareTableShortcuts(TableView table) {
- table.setOnKeyPressed(event -> {
+ table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
var selected = fileList.getSelection();
var action = BrowserAction.getFlattened(fileList.getFileSystemModel(), selected).stream()
.filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected)
@@ -219,7 +281,6 @@ public final class BrowserFileListComp extends SimpleComp {
emptyEntry.onDragDone(event);
});
-
// Don't let the list view see this event
// otherwise it unselects everything as it doesn't understand shift clicks
table.addEventFilter(MouseEvent.MOUSE_CLICKED, t -> {
@@ -242,38 +303,6 @@ public final class BrowserFileListComp extends SimpleComp {
return row.getItem() != null;
},
row.itemProperty()));
- new ContextMenuAugment<>(
- event -> {
- if (row.getItem() == null) {
- return event.getButton() == MouseButton.SECONDARY;
- }
-
- if (row.getItem() != null
- && row.getItem()
- .getRawFileEntry()
- .resolved()
- .getKind()
- == FileKind.DIRECTORY) {
- return event.getButton() == MouseButton.SECONDARY;
- }
-
- if (row.getItem() != null
- && row.getItem()
- .getRawFileEntry()
- .resolved()
- .getKind()
- != FileKind.DIRECTORY) {
- return event.getButton() == MouseButton.SECONDARY
- || event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2;
- }
-
- return false;
- },
- null,
- () -> {
- return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem(), false);
- })
- .augment(new SimpleCompStructure<>(row));
var listEntry = Bindings.createObjectBinding(
() -> new BrowserFileListCompEntry(table, row, row.getItem(), fileList), row.itemProperty());
@@ -332,7 +361,6 @@ public final class BrowserFileListComp extends SimpleComp {
listEntry.get().onDragDone(event);
});
-
return row;
});
}
@@ -564,7 +592,18 @@ public final class BrowserFileListComp extends SimpleComp {
event.consume();
}
});
- InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, false, event -> {
+ InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, true, event -> {
+ var selection = typedSelection.get() + " ";
+ var found = fileList.getShown().getValue().stream()
+ .filter(browserEntry ->
+ browserEntry.getFileName().toLowerCase().startsWith(selection))
+ .findFirst();
+ // Ugly fix to prevent space from showing the menu when there is a file matching
+ // Due to the table view input map, these events always get sent and consumed, not allowing us to differentiate between these cases
+ if (found.isPresent()) {
+ return;
+ }
+
var selected = fileList.getSelection();
// Only show one menu across all selected entries
if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) {
diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java
index 4ee3fbd4b..7bc1e3485 100644
--- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java
+++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java
@@ -7,6 +7,7 @@ import io.xpipe.core.store.FileKind;
import javafx.geometry.Point2D;
import javafx.scene.Node;
+import javafx.scene.control.ContextMenu;
import javafx.scene.control.TableView;
import javafx.scene.image.Image;
import javafx.scene.input.*;
@@ -31,6 +32,7 @@ public class BrowserFileListCompEntry {
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
+ private ContextMenu lastContextMenu;
public BrowserFileListCompEntry(
TableView tv, Node row, BrowserEntry item, BrowserFileListModel model) {
@@ -41,6 +43,19 @@ public class BrowserFileListCompEntry {
}
public void onMouseClick(MouseEvent t) {
+ if (lastContextMenu != null) {
+ lastContextMenu.hide();
+ lastContextMenu = null;
+ }
+
+ if (showContextMenu(t)) {
+ var cm = new BrowserContextMenu(model.getFileSystemModel(), item, false);
+ cm.show(row, t.getScreenX(), t.getScreenY());
+ lastContextMenu = cm;
+ t.consume();
+ return;
+ }
+
if (item == null) {
// Only clear for normal clicks
if (t.isStillSincePress()) {
@@ -62,6 +77,23 @@ public class BrowserFileListCompEntry {
t.consume();
}
+ private boolean showContextMenu(MouseEvent event) {
+ if (item == null) {
+ return event.getButton() == MouseButton.SECONDARY;
+ }
+
+ if (item.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
+ return event.getButton() == MouseButton.SECONDARY;
+ }
+
+ if (item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
+ return event.getButton() == MouseButton.SECONDARY
+ || event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2;
+ }
+
+ return false;
+ }
+
public void onMouseShiftClick(MouseEvent t) {
if (t.getButton() != MouseButton.PRIMARY) {
return;
diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java
index 03514d0c0..676d11174 100644
--- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java
+++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java
@@ -99,24 +99,32 @@ public final class BrowserFileListModel {
}
public BrowserEntry rename(BrowserEntry old, String newName) {
+ if (fileSystemModel == null || fileSystemModel.isClosed() || fileSystemModel.getCurrentPath().get() == null) {
+ return old;
+ }
+
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), old.getFileName());
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
// This check will fail on case-insensitive file systems when changing the case of the file
// So skip it in this case
- var skipExistCheck = fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS && old.getFileName()
- .equalsIgnoreCase(newName);
+ var skipExistCheck =
+ fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS
+ && old.getFileName().equalsIgnoreCase(newName);
if (!skipExistCheck) {
boolean exists;
try {
- exists = fileSystemModel.getFileSystem().fileExists(newFullPath) || fileSystemModel.getFileSystem().directoryExists(newFullPath);
+ exists = fileSystemModel.getFileSystem().fileExists(newFullPath)
+ || fileSystemModel.getFileSystem().directoryExists(newFullPath);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return old;
}
if (exists) {
- ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").expected().handle();
+ ErrorEvent.fromMessage("Target " + newFullPath + " does already exist")
+ .expected()
+ .handle();
fileSystemModel.refresh();
return old;
}
diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java
index a385bec1e..640d7c868 100644
--- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java
+++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java
@@ -7,9 +7,7 @@ import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
@@ -220,62 +218,85 @@ public class BrowserFileTransferOperation {
continue;
}
- InputStream inputStream = null;
- OutputStream outputStream = null;
- try {
- var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath());
- inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath());
- outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
- transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start);
- inputStream.transferTo(OutputStream.nullOutputStream());
- } catch (Exception ex) {
- // Mark progress as finished to reset any progress display
- updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
-
- if (inputStream != null) {
- try {
- inputStream.close();
- } catch (Exception om) {
- // This is expected as the process control has to be killed
- // When calling close, it will throw an exception when it has to kill
- // ErrorEvent.fromThrowable(om).handle();
- }
- }
- if (outputStream != null) {
- try {
- outputStream.close();
- } catch (Exception om) {
- // This is expected as the process control has to be killed
- // When calling close, it will throw an exception when it has to kill
- // ErrorEvent.fromThrowable(om).handle();
- }
- }
- throw ex;
- }
-
- Exception exception = null;
- try {
- inputStream.close();
- } catch (Exception om) {
- exception = om;
- }
- try {
- outputStream.close();
- } catch (Exception om) {
- if (exception != null) {
- ErrorEvent.fromThrowable(om).handle();
- } else {
- exception = om;
- }
- }
- if (exception != null) {
- throw exception;
- }
+ transfer(sourceFile, targetFile, transferred, totalSize, start);
}
}
updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
}
+ private void transfer(
+ FileSystem.FileEntry sourceFile,
+ String targetFile,
+ AtomicLong transferred,
+ AtomicLong totalSize,
+ Instant start)
+ throws Exception {
+ InputStream inputStream = null;
+ OutputStream outputStream = null;
+ try {
+ var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath());
+
+ // Read the first few bytes to figure out possible command failure early
+ // before creating the output stream
+ inputStream = new BufferedInputStream(sourceFile.getFileSystem().openInput(sourceFile.getPath()), 1024);
+ inputStream.mark(1024);
+ var streamStart = new byte[1024];
+ var streamStartLength = inputStream.read(streamStart, 0, 1024);
+ if (streamStartLength < 1024) {
+ inputStream.close();
+ inputStream = new ByteArrayInputStream(streamStart);
+ } else {
+ inputStream.reset();
+ }
+
+ outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
+ transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start);
+ inputStream.transferTo(OutputStream.nullOutputStream());
+ } catch (Exception ex) {
+ // Mark progress as finished to reset any progress display
+ updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
+
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (Exception om) {
+ // This is expected as the process control has to be killed
+ // When calling close, it will throw an exception when it has to kill
+ // ErrorEvent.fromThrowable(om).handle();
+ }
+ }
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (Exception om) {
+ // This is expected as the process control has to be killed
+ // When calling close, it will throw an exception when it has to kill
+ // ErrorEvent.fromThrowable(om).handle();
+ }
+ }
+ throw ex;
+ }
+
+ Exception exception = null;
+ try {
+ inputStream.close();
+ } catch (Exception om) {
+ exception = om;
+ }
+ try {
+ outputStream.close();
+ } catch (Exception om) {
+ if (exception != null) {
+ ErrorEvent.fromThrowable(om).handle();
+ } else {
+ exception = om;
+ }
+ }
+ if (exception != null) {
+ throw exception;
+ }
+ }
+
private void deleteSingle(FileSystem.FileEntry source) throws Exception {
source.getFileSystem().delete(source.getPath());
}
diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java
index dc4f4b3f1..a1c0d3ba1 100644
--- a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java
+++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java
@@ -79,7 +79,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
getItems().addAll(r.getItems());
// Prevent NPE in show()
- if (getScene() == null) {
+ if (getScene() == null || anchor == null) {
return;
}
show(anchor, Side.RIGHT, 0, 0);
diff --git a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java
index 526699132..145158c02 100644
--- a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java
@@ -9,6 +9,7 @@ import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.file.BrowserFileListComp;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.comp.base.MultiContentComp;
+import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
@@ -84,10 +85,11 @@ public class OpenFileSystemComp extends SimpleComp {
var filter = new BrowserFilterComp(model, model.getFilter()).createStructure();
var topBar = new HBox();
- filter.textField().prefHeightProperty().bind(topBar.heightProperty());
topBar.setAlignment(Pos.CENTER);
topBar.getStyleClass().add("top-bar");
var navBar = new BrowserNavBar(model).createStructure();
+ filter.textField().prefHeightProperty().bind(navBar.get().heightProperty());
+ AppFont.medium(navBar.get());
topBar.getChildren()
.setAll(
overview,
@@ -117,13 +119,13 @@ public class OpenFileSystemComp extends SimpleComp {
});
InputHelper.onKeyCombination(
- root, new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN), true, keyEvent -> {
+ root, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> {
filter.toggleButton().fire();
filter.textField().requestFocus();
keyEvent.consume();
});
InputHelper.onKeyCombination(
- root, new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN), true, keyEvent -> {
+ root, new KeyCodeCombination(KeyCode.L, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> {
navBar.textField().requestFocus();
keyEvent.consume();
});
@@ -140,6 +142,14 @@ public class OpenFileSystemComp extends SimpleComp {
}
keyEvent.consume();
});
+ InputHelper.onKeyCombination(
+ root, new KeyCodeCombination(KeyCode.BACK_SPACE), true, keyEvent -> {
+ var p = model.getCurrentParentDirectory();
+ if (p != null) {
+ model.cdAsync(p.getPath());
+ }
+ keyEvent.consume();
+ });
return root;
}
diff --git a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java
index 7067f8e98..824557bb2 100644
--- a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java
+++ b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java
@@ -1,6 +1,7 @@
package io.xpipe.app.browser.fs;
import io.xpipe.app.browser.BrowserSavedState;
+import io.xpipe.app.browser.BrowserSavedStateImpl;
import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.file.BrowserFileListModel;
@@ -8,7 +9,6 @@ import io.xpipe.app.browser.file.BrowserFileTransferMode;
import io.xpipe.app.browser.file.BrowserFileTransferOperation;
import io.xpipe.app.browser.file.FileSystemHelper;
import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
-import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.browser.session.BrowserSessionTab;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.fxcomps.Comp;
@@ -110,14 +110,13 @@ public final class OpenFileSystemModel extends BrowserSessionTab {
- if (model.isClosed()) {
- return;
- }
- if (Objects.equals(lastDirectory, dir)) {
- updateRecent(dir);
- save();
- }
- });
- }
- },
- 10000);
+ if (delay) {
+ // After 10 seconds
+ TIMEOUT_TIMER.schedule(
+ new TimerTask() {
+ @Override
+ public void run() {
+ // Synchronize with platform thread
+ Platform.runLater(() -> {
+ if (model.isClosed()) {
+ return;
+ }
+
+ if (Objects.equals(lastDirectory, dir)) {
+ updateRecent(dir);
+ save();
+ }
+ });
+ }
+ },
+ 10000);
+ } else {
+ updateRecent(dir);
+ save();
+ }
}
private void updateRecent(String dir) {
diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserAbstractSessionModel.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserAbstractSessionModel.java
index 289d4cf46..980f13871 100644
--- a/app/src/main/java/io/xpipe/app/browser/session/BrowserAbstractSessionModel.java
+++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserAbstractSessionModel.java
@@ -17,6 +17,7 @@ public class BrowserAbstractSessionModel> {
protected final ObservableList sessionEntries = FXCollections.observableArrayList();
protected final Property selectedEntry = new SimpleObjectProperty<>();
+ protected final BooleanProperty busy = new SimpleBooleanProperty();
public void closeAsync(BrowserSessionTab> e) {
ThreadHelper.runAsync(() -> {
diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java
index 9eb4d19ee..8b38c6bb9 100644
--- a/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java
@@ -67,6 +67,10 @@ public class BrowserChooserComp extends SimpleComp {
window.close();
});
window.show();
+ window.setOnHidden(event -> {
+ model.finishWithoutChoice();
+ event.consume();
+ });
ThreadHelper.runAsync(() -> {
model.openFileSystemAsync(store.get(), null, null);
});
diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java
index 6bfbd5ff6..162fe3c6d 100644
--- a/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java
+++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java
@@ -65,6 +65,17 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel {
+ open.close();
+ });
+ }
+ }
+ }
+
public void openFileSystemAsync(
DataStoreEntryRef extends FileSystemStore> store,
FailableFunction path,
diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java
index ae37e2c6b..ed51d4995 100644
--- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java
@@ -3,10 +3,13 @@ package io.xpipe.app.browser.session;
import io.xpipe.app.browser.BrowserBookmarkComp;
import io.xpipe.app.browser.BrowserBookmarkHeaderComp;
import io.xpipe.app.browser.BrowserTransferComp;
+import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.base.SideSplitPaneComp;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppLayoutModel;
+import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
+import io.xpipe.app.fxcomps.impl.AnchorComp;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
@@ -18,6 +21,7 @@ import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
+import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
@@ -100,10 +104,22 @@ public class BrowserSessionComp extends SimpleComp {
new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
var split = new SimpleDoubleProperty();
- var tabs = new BrowserSessionTabsComp(model, split)
- .apply(struc -> struc.get().setViewOrder(1))
- .apply(struc -> struc.get().setPickOnBounds(false));
- var splitPane = new SideSplitPaneComp(vertical, tabs)
+ var tabs = new BrowserSessionTabsComp(model, split).apply(struc -> {
+ struc.get().setViewOrder(1);
+ struc.get().setPickOnBounds(false);
+ AnchorPane.setTopAnchor(struc.get(), 0.0);
+ AnchorPane.setBottomAnchor(struc.get(), 0.0);
+ AnchorPane.setLeftAnchor(struc.get(), 0.0);
+ AnchorPane.setRightAnchor(struc.get(), 0.0);
+ });
+ var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy())
+ .apply(struc -> {
+ AnchorPane.setTopAnchor(struc.get(), 0.0);
+ AnchorPane.setRightAnchor(struc.get(), 0.0);
+ })
+ .styleClass("tab-loading-indicator");
+ var loadingStack = new AnchorComp(List.of(tabs, loadingIndicator));
+ var splitPane = new SideSplitPaneComp(vertical, loadingStack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(d -> {
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);
diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java
index 59fe6101f..ba254d928 100644
--- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java
+++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java
@@ -23,16 +23,11 @@ import java.util.ArrayList;
@Getter
public class BrowserSessionModel extends BrowserAbstractSessionModel> {
- public static final BrowserSessionModel DEFAULT = new BrowserSessionModel(BrowserSavedStateImpl.load());
+ public static final BrowserSessionModel DEFAULT = new BrowserSessionModel();
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
- private final BrowserSavedState savedState;
private final Property draggingFiles = new SimpleBooleanProperty();
- public BrowserSessionModel(BrowserSavedState savedState) {
- this.savedState = savedState;
- }
-
public void restoreState(BrowserSavedState state) {
ThreadHelper.runAsync(() -> {
var l = new ArrayList<>(state.getEntries());
@@ -62,9 +57,7 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel store,
FailableFunction path,
- BooleanProperty externalBusy) throws Exception {
+ BooleanProperty externalBusy)
+ throws Exception {
if (store == null) {
return;
}
OpenFileSystemModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
- model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL);
- model.init();
- // Prevent multiple calls from interfering with each other
- synchronized (BrowserSessionModel.this) {
- sessionEntries.add(model);
- // The tab pane doesn't automatically select new tabs
- selectedEntry.setValue(model);
+ try (var sessionBusy = new BooleanScope(busy).exclusive().start()) {
+ model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL);
+ model.init();
+ // Prevent multiple calls from interfering with each other
+ synchronized (BrowserSessionModel.this) {
+ sessionEntries.add(model);
+ // The tab pane doesn't automatically select new tabs
+ selectedEntry.setValue(model);
+ }
}
}
if (path != null) {
diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java
index 64414f537..09bda38fa 100644
--- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java
+++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java
@@ -22,7 +22,7 @@ public abstract class BrowserSessionTab {
public BrowserSessionTab(BrowserAbstractSessionModel> browserModel, DataStoreEntryRef extends T> entry) {
this.browserModel = browserModel;
this.entry = entry;
- this.name = DataStorage.get().getStoreDisplayName(entry.get());
+ this.name = DataStorage.get().getStoreEntryDisplayName(entry.get());
this.tooltip = DataStorage.get().getStorePath(entry.getEntry()).toString();
}
diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java
index 907ead2c8..8cef777bd 100644
--- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java
@@ -1,16 +1,19 @@
package io.xpipe.app.browser.session;
+import atlantafx.base.controls.RingProgressIndicator;
+import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.BrowserWelcomeComp;
import io.xpipe.app.comp.base.MultiContentComp;
+import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
+import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BooleanScope;
-import io.xpipe.app.util.InputHelper;
-
+import io.xpipe.app.util.ContextMenuHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
@@ -20,20 +23,12 @@ import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
-import javafx.scene.control.Label;
-import javafx.scene.control.Tab;
-import javafx.scene.control.TabPane;
-import javafx.scene.input.DragEvent;
-import javafx.scene.input.KeyCode;
+import javafx.scene.control.*;
+import javafx.scene.input.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
-import atlantafx.base.controls.RingProgressIndicator;
-import atlantafx.base.theme.Styles;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
@@ -50,17 +45,17 @@ public class BrowserSessionTabsComp extends SimpleComp {
}
public Region createSimple() {
- var multi = new MultiContentComp(Map., ObservableValue>of(
- Comp.of(() -> createTabPane()),
- Bindings.isNotEmpty(model.getSessionEntries()),
+ var map = new LinkedHashMap, ObservableValue>();
+ map.put(Comp.hspacer().styleClass("top-spacer"), new SimpleBooleanProperty(true));
+ map.put(Comp.of(() -> createTabPane()), Bindings.isNotEmpty(model.getSessionEntries()));
+ map.put(
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
Bindings.createBooleanBinding(
() -> {
return model.getSessionEntries().size() == 0;
},
- model.getSessionEntries()),
- Comp.hspacer().styleClass("top-spacer"),
- new SimpleBooleanProperty(true)));
+ model.getSessionEntries()));
+ var multi = new MultiContentComp(map);
multi.apply(struc -> ((StackPane) struc.get()).setAlignment(Pos.TOP_CENTER));
return multi.createRegion();
}
@@ -198,28 +193,132 @@ public class BrowserSessionTabsComp extends SimpleComp {
}
});
- InputHelper.onInput(tabs, true, keyEvent -> {
+ tabs.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> {
var current = tabs.getSelectionModel().getSelectedItem();
if (current == null) {
return;
}
- if (keyEvent.getCode() == KeyCode.W && keyEvent.isShortcutDown()) {
+ if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(keyEvent)) {
tabs.getTabs().remove(current);
keyEvent.consume();
+ return;
+ }
+
+ if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN).match(keyEvent)) {
+ tabs.getTabs().clear();
+ keyEvent.consume();
}
- if (keyEvent.getCode() == KeyCode.W && keyEvent.isShortcutDown() && keyEvent.isShiftDown()) {
- tabs.getTabs().clear();
+ if (keyEvent.getCode().isFunctionKey()) {
+ var start = KeyCode.F1.getCode();
+ var index = keyEvent.getCode().getCode() - start;
+ if (index < tabs.getTabs().size()) {
+ tabs.getSelectionModel().select(index);
+ keyEvent.consume();
+ return;
+ }
+ }
+
+ var forward = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN);
+ if (forward.match(keyEvent)) {
+ var next = (tabs.getSelectionModel().getSelectedIndex() + 1)
+ % tabs.getTabs().size();
+ tabs.getSelectionModel().select(next);
keyEvent.consume();
+ return;
+ }
+
+ var back = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN);
+ if (back.match(keyEvent)) {
+ var previous = (tabs.getTabs().size() + tabs.getSelectionModel().getSelectedIndex() - 1)
+ % tabs.getTabs().size();
+ tabs.getSelectionModel().select(previous);
+ keyEvent.consume();
+ return;
}
});
return tabs;
}
+ private ContextMenu createContextMenu(TabPane tabs, Tab tab) {
+ var cm = ContextMenuHelper.create();
+
+ var select = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("selectTab"));
+ select.acceleratorProperty()
+ .bind(Bindings.createObjectBinding(
+ () -> {
+ var start = KeyCode.F1.getCode();
+ var index = tabs.getTabs().indexOf(tab);
+ var keyCode = Arrays.stream(KeyCode.values())
+ .filter(code -> code.getCode() == start + index)
+ .findAny()
+ .orElse(null);
+ return keyCode != null ? new KeyCodeCombination(keyCode) : null;
+ },
+ tabs.getTabs()));
+ select.setOnAction(event -> {
+ tabs.getSelectionModel().select(tab);
+ event.consume();
+ });
+ cm.getItems().add(select);
+
+ cm.getItems().add(new SeparatorMenuItem());
+
+ var close = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeTab"));
+ close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN));
+ close.setOnAction(event -> {
+ tabs.getTabs().remove(tab);
+ event.consume();
+ });
+ cm.getItems().add(close);
+
+ var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeOtherTabs"));
+ closeOthers.setOnAction(event -> {
+ tabs.getTabs()
+ .removeAll(tabs.getTabs().stream().filter(t -> t != tab).toList());
+ event.consume();
+ });
+ cm.getItems().add(closeOthers);
+
+ var closeLeft = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeLeftTabs"));
+ closeLeft.setOnAction(event -> {
+ var index = tabs.getTabs().indexOf(tab);
+ tabs.getTabs()
+ .removeAll(tabs.getTabs().stream()
+ .filter(t -> tabs.getTabs().indexOf(t) < index)
+ .toList());
+ event.consume();
+ });
+ cm.getItems().add(closeLeft);
+
+ var closeRight = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeRightTabs"));
+ closeRight.setOnAction(event -> {
+ var index = tabs.getTabs().indexOf(tab);
+ tabs.getTabs()
+ .removeAll(tabs.getTabs().stream()
+ .filter(t -> tabs.getTabs().indexOf(t) > index)
+ .toList());
+ event.consume();
+ });
+ cm.getItems().add(closeRight);
+
+ var closeAll = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeAllTabs"));
+ closeAll.setAccelerator(
+ new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN));
+ closeAll.setOnAction(event -> {
+ tabs.getTabs().clear();
+ event.consume();
+ });
+ cm.getItems().add(closeAll);
+
+ return cm;
+ }
+
private Tab createTab(TabPane tabs, BrowserSessionTab> model) {
var tab = new Tab();
+ tab.setContextMenu(createContextMenu(tabs, tab));
var ring = new RingProgressIndicator(0, false);
ring.setMinSize(16, 16);
diff --git a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java
index 8c3529643..32c419d36 100644
--- a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java
@@ -13,10 +13,9 @@ import io.xpipe.app.storage.DataStorage;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
+import javafx.scene.Parent;
import javafx.scene.control.ButtonBase;
-import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
-import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
@@ -63,28 +62,11 @@ public class AppLayoutComp extends Comp> {
sidebarR.getChildrenUnmodifiable().forEach(node -> {
var shortcut = (KeyCodeCombination) node.getProperties().get("shortcut");
if (shortcut != null && shortcut.match(event)) {
- ((ButtonBase) node).fire();
+ ((ButtonBase) ((Parent) node).getChildrenUnmodifiable().get(1)).fire();
event.consume();
return;
}
});
- if (event.isConsumed()) {
- return;
- }
-
- var forward = new KeyCodeCombination(KeyCode.TAB, KeyCombination.CONTROL_DOWN);
- if (forward.match(event)) {
- var next = (model.getEntries().indexOf(model.getSelected().getValue()) + 1) % 3;
- model.getSelected().setValue(model.getEntries().get(next));
- return;
- }
-
- var back = new KeyCodeCombination(KeyCode.TAB, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN);
- if (back.match(event)) {
- var next = (model.getEntries().indexOf(model.getSelected().getValue()) + 2) % 3;
- model.getSelected().setValue(model.getEntries().get(next));
- return;
- }
});
AppFont.normal(pane);
pane.getStyleClass().add("layout");
diff --git a/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java b/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java
index 18ae445ca..cc4921cac 100644
--- a/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java
@@ -70,7 +70,9 @@ public class ListBoxViewComp extends Comp> {
.bind(Bindings.createDoubleBinding(
() -> {
var v = bar.getVisibleAmount();
- return v < 1.0 ? 1.0 : 0.0;
+ // Check for rounding and accuracy issues
+ // It might not be exactly equal to 1.0
+ return v < 0.99 ? 1.0 : 0.0;
},
bar.visibleAmountProperty()));
}
diff --git a/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java b/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java
index c0c5f70ed..6000be622 100644
--- a/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java
@@ -88,6 +88,7 @@ public class ListSelectorComp extends SimpleComp {
var sp = new ScrollPane(vbox);
sp.setFitToWidth(true);
+ sp.getStyleClass().add("list-selector-comp");
return sp;
}
}
diff --git a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java
index 6f43963ca..b977727df 100644
--- a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java
@@ -22,6 +22,7 @@ import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import lombok.SneakyThrows;
+import org.apache.commons.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
@@ -63,7 +64,7 @@ public class MarkdownComp extends Comp> {
var html = MarkdownHelper.toHtml(markdown, s -> s, htmlTransformation, null);
try {
// Workaround for https://bugs.openjdk.org/browse/JDK-8199014
- Files.createDirectories(file.getParent());
+ FileUtils.forceMkdir(file.getParent().toFile());
Files.writeString(file, html);
return file;
} catch (IOException e) {
diff --git a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java
index 0c26d5b09..2eb671cfc 100644
--- a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java
@@ -11,6 +11,7 @@ import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.update.XPipeDistributionType;
+
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
@@ -41,14 +42,14 @@ public class SideMenuBarComp extends Comp> {
var selectedBorder = Bindings.createObjectBinding(
() -> {
var c = Platform.getPreferences().getAccentColor().desaturate();
- return new Background(new BackgroundFill(c,new CornerRadii(8), new Insets(10, 1, 10, 2)));
+ return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(10, 1, 10, 2)));
},
Platform.getPreferences().accentColorProperty());
var hoverBorder = Bindings.createObjectBinding(
() -> {
var c = Platform.getPreferences().getAccentColor().darker().desaturate();
- return new Background(new BackgroundFill(c,new CornerRadii(8), new Insets(10, 1, 10, 2)));
+ return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(10, 1, 10, 2)));
},
Platform.getPreferences().accentColorProperty());
@@ -70,12 +71,9 @@ public class SideMenuBarComp extends Comp> {
value.setValue(e);
});
var shortcut = e.combination();
- if (shortcut != null) {
- b.apply(struc -> struc.get().getProperties().put("shortcut", shortcut));
- }
b.apply(new TooltipAugment<>(e.name(), shortcut));
b.apply(struc -> {
- AppFont.setSize(struc.get(), 2);
+ AppFont.setSize(struc.get(), 1);
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
@@ -86,7 +84,8 @@ public class SideMenuBarComp extends Comp> {
b.accessibleText(e.name());
var indicator = Comp.empty().styleClass("indicator");
- var stack = new StackComp(List.of(indicator, b)).apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
+ var stack = new StackComp(List.of(indicator, b))
+ .apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
stack.apply(struc -> {
var indicatorRegion = (Region) struc.get().getChildren().getFirst();
indicatorRegion.setMaxWidth(7);
@@ -110,6 +109,9 @@ public class SideMenuBarComp extends Comp> {
selectedBorder,
noneBorder));
});
+ if (shortcut != null) {
+ stack.apply(struc -> struc.get().getProperties().put("shortcut", shortcut));
+ }
vbox.getChildren().add(stack.createRegion());
}
@@ -118,7 +120,7 @@ public class SideMenuBarComp extends Comp> {
.tooltipKey("updateAvailableTooltip")
.accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> {
- AppFont.setSize(struc.get(), 2);
+ AppFont.setSize(struc.get(), 1);
});
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
diff --git a/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java b/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java
index 502336500..06f42004a 100644
--- a/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java
@@ -95,7 +95,7 @@ public class StoreToggleComp extends SimpleComp {
v -> {
Platform.runLater(() -> {
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);
- StoreViewState.get().toggleStoreListUpdate();
+ StoreViewState.get().triggerStoreListUpdate();
});
});
t.tooltipKey("showAllChildren");
diff --git a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java
index c9d0cabb2..1a1cc2640 100644
--- a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java
@@ -33,16 +33,27 @@ public class SystemStateComp extends SimpleComp {
PlatformThread.runLaterIfNeeded(() -> fi.setIconLiteral(i));
});
- var border = new FontIcon("mdi2c-circle-outline");
+ var border = new FontIcon("mdi2s-square-rounded-outline");
border.getStyleClass().add("outer-icon");
- border.setOpacity(0.5);
+ border.setOpacity(0.3);
var success = Styles.toDataURI(
- ".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }");
+ """
+ .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }
+ """
+ );
var failure =
- Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }");
+ Styles.toDataURI(
+ """
+ .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }
+ """
+ );
var other =
- Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }");
+ Styles.toDataURI(
+ """
+ .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }
+ """
+ );
var pane = new StackedFontIcon();
pane.getChildren().addAll(fi, border);
@@ -51,7 +62,7 @@ public class SystemStateComp extends SimpleComp {
var dataClass1 =
"""
.stacked-ikonli-font-icon > .outer-icon {
- -fx-icon-size: 22px;
+ -fx-icon-size: 26px;
}
.stacked-ikonli-font-icon > .inner-icon {
-fx-icon-size: 12px;
diff --git a/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java
index a6c5db926..8ace6c03c 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java
@@ -1,10 +1,8 @@
package io.xpipe.app.comp.store;
-import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
-
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.HPos;
@@ -17,8 +15,8 @@ public class DenseStoreEntryComp extends StoreEntryComp {
private final boolean showIcon;
- public DenseStoreEntryComp(StoreEntryWrapper entry, boolean showIcon, Comp> content) {
- super(entry, content);
+ public DenseStoreEntryComp(StoreSection section, boolean showIcon, Comp> content) {
+ super(section, content);
this.showIcon = showIcon;
}
@@ -26,24 +24,27 @@ public class DenseStoreEntryComp extends StoreEntryComp {
var information = new Label();
information.setGraphicTextGap(7);
information.getStyleClass().add("information");
- AppFont.header(information);
- var state = wrapper.getEntry().getProvider() != null
- ? wrapper.getEntry().getProvider().stateDisplay(wrapper)
+ var state = getWrapper().getEntry().getProvider() != null
+ ? getWrapper().getEntry().getProvider().stateDisplay(getWrapper())
: Comp.empty();
information.setGraphic(state.createRegion());
- var info = wrapper.getEntry().getProvider() != null ? wrapper.getEntry().getProvider().informationString(wrapper) : new SimpleStringProperty();
- var summary = wrapper.getSummary();
- if (wrapper.getEntry().getProvider() != null) {
+ var info = getWrapper().getEntry().getProvider() != null
+ ? getWrapper().getEntry().getProvider().informationString(section)
+ : new SimpleStringProperty();
+ var summary = getWrapper().getSummary();
+ if (getWrapper().getEntry().getProvider() != null) {
information
.textProperty()
.bind(PlatformThread.sync(Bindings.createStringBinding(
() -> {
var val = summary.getValue();
- if (val != null
- && grid.isHover()
- && wrapper.getEntry().getProvider().alwaysShowSummary()) {
+ var p = getWrapper().getEntry().getProvider();
+ if (val != null && grid.isHover()
+ && p.alwaysShowSummary()) {
+ return val;
+ } else if (info.getValue() == null && p.alwaysShowSummary()){
return val;
} else {
return info.getValue();
@@ -73,11 +74,11 @@ public class DenseStoreEntryComp extends StoreEntryComp {
return grid.getWidth() / 2.5;
},
grid.widthProperty()));
- var notes = new StoreNotesComp(wrapper).createRegion();
+ var notes = new StoreNotesComp(getWrapper()).createRegion();
if (showIcon) {
- var storeIcon = createIcon(30, 24);
- grid.getColumnConstraints().add(new ColumnConstraints(46));
+ var storeIcon = createIcon(28, 24);
+ grid.getColumnConstraints().add(new ColumnConstraints(38));
grid.add(storeIcon, 0, 0);
GridPane.setHalignment(storeIcon, HPos.CENTER);
}
@@ -95,7 +96,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
nameCC.setHgrow(Priority.ALWAYS);
grid.getColumnConstraints().addAll(nameCC);
var nameBox = new HBox(name, notes);
- nameBox.setSpacing(1);
+ nameBox.setSpacing(6);
nameBox.setAlignment(Pos.CENTER_LEFT);
grid.addRow(0, nameBox);
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java
index 595e7bc56..4d2914f23 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java
@@ -1,16 +1,19 @@
package io.xpipe.app.comp.store;
+import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;
+import io.xpipe.core.process.OsType;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
+import javafx.scene.control.Label;
import javafx.scene.layout.*;
public class StandardStoreEntryComp extends StoreEntryComp {
- public StandardStoreEntryComp(StoreEntryWrapper entry, Comp> content) {
- super(entry, content);
+ public StandardStoreEntryComp(StoreSection section, Comp> content) {
+ super(section, content);
}
@Override
@@ -18,23 +21,37 @@ public class StandardStoreEntryComp extends StoreEntryComp {
return true;
}
+ private Label createSummary() {
+ var summary = new Label();
+ summary.textProperty().bind(getWrapper().getSummary());
+ summary.getStyleClass().add("summary");
+ AppFont.small(summary);
+ return summary;
+ }
+
protected Region createContent() {
var name = createName().createRegion();
- var notes = new StoreNotesComp(wrapper).createRegion();
+ var notes = new StoreNotesComp(getWrapper()).createRegion();
var grid = new GridPane();
- grid.setHgap(7);
- grid.setVgap(0);
+ grid.setHgap(6);
+ grid.setVgap(OsType.getLocal() == OsType.MACOS ? 2 : 0);
- var storeIcon = createIcon(50, 40);
+ var storeIcon = createIcon(46, 40);
grid.add(storeIcon, 0, 0, 1, 2);
- grid.getColumnConstraints().add(new ColumnConstraints(66));
+ grid.getColumnConstraints().add(new ColumnConstraints(56));
var nameAndNotes = new HBox(name, notes);
- nameAndNotes.setSpacing(1);
+ nameAndNotes.setSpacing(6);
nameAndNotes.setAlignment(Pos.CENTER_LEFT);
grid.add(nameAndNotes, 1, 0);
- grid.add(createSummary(), 1, 1);
+ GridPane.setVgrow(nameAndNotes, Priority.ALWAYS);
+
+ var summaryBox = new HBox(createSummary());
+ summaryBox.setAlignment(Pos.TOP_LEFT);
+ GridPane.setVgrow(summaryBox, Priority.ALWAYS);
+ grid.add(summaryBox, 1, 1);
+
var nameCC = new ColumnConstraints();
nameCC.setMinWidth(100);
nameCC.setHgrow(Priority.ALWAYS);
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java
index c5ad100c6..414e8ac48 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java
@@ -112,6 +112,11 @@ public class StoreCategoryWrapper {
}
public void update() {
+ // We are probably in shutdown then
+ if (StoreViewState.get() == null) {
+ return;
+ }
+
// Avoid reupdating name when changed from the name property!
var catName = translatedName(category.getName());
if (!catName.equals(name.getValue())) {
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java
index ffcff4718..08e3458ad 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java
@@ -1,5 +1,6 @@
package io.xpipe.app.comp.store;
+import atlantafx.base.controls.Spacer;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.ErrorOverlayComp;
@@ -21,7 +22,6 @@ import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.ValidationException;
-
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
@@ -33,8 +33,6 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
-
-import atlantafx.base.controls.Spacer;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
import net.synedra.validatorfx.GraphicDecorationStackPane;
@@ -51,7 +49,7 @@ public class StoreCreationComp extends DialogComp {
Stage window;
BiConsumer consumer;
Property provider;
- Property store;
+ ObjectProperty store;
Predicate filter;
BooleanProperty busy = new SimpleBooleanProperty();
Property validator = new SimpleObjectProperty<>(new SimpleValidator());
@@ -60,6 +58,7 @@ public class StoreCreationComp extends DialogComp {
ObservableValue entry;
BooleanProperty changedSinceError = new SimpleBooleanProperty();
BooleanProperty skippable = new SimpleBooleanProperty();
+ BooleanProperty connectable = new SimpleBooleanProperty();
StringProperty name;
DataStoreEntry existingEntry;
boolean staticDisplay;
@@ -68,7 +67,7 @@ public class StoreCreationComp extends DialogComp {
Stage window,
BiConsumer consumer,
Property provider,
- Property store,
+ ObjectProperty store,
Predicate filter,
String initialName,
DataStoreEntry existingEntry,
@@ -96,6 +95,12 @@ public class StoreCreationComp extends DialogComp {
}
});
+ this.provider.subscribe((n) -> {
+ if (n != null) {
+ connectable.setValue(n.canConnectDuringCreation());
+ }
+ });
+
this.apply(r -> {
r.get().setPrefWidth(650);
r.get().setPrefHeight(750);
@@ -163,7 +168,12 @@ public class StoreCreationComp extends DialogComp {
if (!DataStorage.get().getStoreEntries().contains(e)) {
DataStorage.get().addStoreEntryIfNotPresent(newE);
} else {
- DataStorage.get().updateEntry(e, newE);
+ // We didn't change anything
+ if (e.getStore().equals(newE.getStore())) {
+ e.setName(newE.getName());
+ } else {
+ DataStorage.get().updateEntry(e, newE);
+ }
}
});
},
@@ -239,7 +249,16 @@ public class StoreCreationComp extends DialogComp {
finish();
}
})
- .visible(skippable));
+ .visible(skippable),
+ new ButtonComp(AppI18n.observable("connect"), null, () -> {
+ var temp = DataStoreEntry.createTempWrapper(store.getValue());
+ var action = provider.getValue().launchAction(temp);
+ ThreadHelper.runFailableAsync(() -> {
+ action.execute();
+ });
+ }).hide(connectable.not().or(Bindings.createBooleanBinding(() -> {
+ return store.getValue() == null || !store.getValue().isComplete();
+ }, store))));
}
@Override
@@ -393,11 +412,10 @@ public class StoreCreationComp extends DialogComp {
private Region createLayout() {
var layout = new BorderPane();
layout.getStyleClass().add("store-creator");
- layout.setPadding(new Insets(20));
var providerChoice = new StoreProviderChoiceComp(filter, provider, staticDisplay);
- if (staticDisplay) {
- providerChoice.apply(struc -> struc.get().setDisable(true));
- } else {
+ var showProviders = (!staticDisplay && (providerChoice.getProviders().size() > 1 || providerChoice.getProviders().getFirst().showProviderChoice())) ||
+ (staticDisplay && provider.getValue().showProviderChoice());
+ if (showProviders) {
providerChoice.onSceneAssign(struc -> struc.get().requestFocus());
}
providerChoice.apply(GrowAugment.create(true, false));
@@ -422,9 +440,14 @@ public class StoreCreationComp extends DialogComp {
var sep = new Separator();
sep.getStyleClass().add("spacer");
- var top = new VBox(providerChoice.createRegion(), new Spacer(7, Orientation.VERTICAL), sep);
+ var top = new VBox(providerChoice.createRegion(), new Spacer(5, Orientation.VERTICAL), sep);
top.getStyleClass().add("top");
- layout.setTop(top);
+ if (showProviders) {
+ layout.setTop(top);
+ layout.setPadding(new Insets(15, 20, 20, 20));
+ } else {
+ layout.setPadding(new Insets(5, 20, 20, 20));
+ }
var valSp = new GraphicDecorationStackPane();
valSp.getChildren().add(layout);
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java
index b30ec5bb4..036d6cb9b 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java
@@ -40,8 +40,9 @@ public class StoreCreationMenu {
menu.getItems()
.add(category("addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, null));
- menu.getItems()
- .add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, "cmd"));
+ menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, null));
+
+ menu.getItems().add(category("addSerial", "mdi2s-serial-port", DataStoreCreationCategory.SERIAL, "serial"));
menu.getItems().add(category("addDatabase", "mdi2d-database-plus", DataStoreCreationCategory.DATABASE, null));
}
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java
index 045d05579..8f7aadbb5 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java
@@ -14,6 +14,7 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
+import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
@@ -23,6 +24,7 @@ import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.*;
import javafx.beans.binding.Bindings;
+import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.css.PseudoClass;
@@ -51,21 +53,25 @@ public abstract class StoreEntryComp extends SimpleComp {
App.getApp().getStage().widthProperty().divide(2.1).add(-100);
public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH =
App.getApp().getStage().widthProperty().divide(2.1).add(-200);
- protected final StoreEntryWrapper wrapper;
+ protected final StoreSection section;
protected final Comp> content;
- public StoreEntryComp(StoreEntryWrapper wrapper, Comp> content) {
- this.wrapper = wrapper;
+ public StoreEntryComp(StoreSection section, Comp> content) {
+ this.section = section;
this.content = content;
}
- public static StoreEntryComp create(StoreEntryWrapper entry, Comp> content, boolean preferLarge) {
+ public StoreEntryWrapper getWrapper() {
+ return section.getWrapper();
+ }
+
+ public static StoreEntryComp create(StoreSection section, Comp> content, boolean preferLarge) {
var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get();
if (!preferLarge || forceCondensed) {
- return new DenseStoreEntryComp(entry, true, content);
+ return new DenseStoreEntryComp(section, true, content);
} else {
- return new StandardStoreEntryComp(entry, content);
+ return new StandardStoreEntryComp(section, content);
}
}
@@ -76,9 +82,7 @@ public abstract class StoreEntryComp extends SimpleComp {
} else {
var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get();
- return forceCondensed
- ? new DenseStoreEntryComp(e.getWrapper(), true, null)
- : new StandardStoreEntryComp(e.getWrapper(), null);
+ return forceCondensed ? new DenseStoreEntryComp(e, true, null) : new StandardStoreEntryComp(e, null);
}
}
@@ -95,16 +99,16 @@ public abstract class StoreEntryComp extends SimpleComp {
button.setPadding(Insets.EMPTY);
button.setMaxWidth(5000);
button.setFocusTraversable(true);
- button.accessibleTextProperty().bind(wrapper.nameProperty());
+ button.accessibleTextProperty().bind(getWrapper().nameProperty());
button.setOnAction(event -> {
event.consume();
ThreadHelper.runFailableAsync(() -> {
- wrapper.executeDefaultAction();
+ getWrapper().executeDefaultAction();
});
});
button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if (AppPrefs.get().requireDoubleClickForConnections().get()) {
- if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() > 2) {
+ if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
event.consume();
}
} else {
@@ -115,7 +119,7 @@ public abstract class StoreEntryComp extends SimpleComp {
});
button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (AppPrefs.get().requireDoubleClickForConnections().get()) {
- if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() > 2) {
+ if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
event.consume();
}
} else {
@@ -132,9 +136,12 @@ public abstract class StoreEntryComp extends SimpleComp {
var loading = LoadingOverlayComp.noProgress(
Comp.of(() -> button),
- wrapper.getEntry().getValidity().isUsable()
- ? wrapper.getBusy().or(wrapper.getEntry().getProvider().busy(wrapper))
- : wrapper.getBusy());
+ getWrapper().getEntry().getValidity().isUsable()
+ ? getWrapper()
+ .getBusy()
+ .or(getWrapper().getEntry().getProvider().busy(getWrapper()))
+ : getWrapper().getBusy());
+ AppFont.normal(button);
return loading.createRegion();
}
@@ -146,31 +153,22 @@ public abstract class StoreEntryComp extends SimpleComp {
information
.textProperty()
.bind(
- wrapper.getEntry().getProvider() != null
+ getWrapper().getEntry().getProvider() != null
? PlatformThread.sync(
- wrapper.getEntry().getProvider().informationString(wrapper))
+ getWrapper().getEntry().getProvider().informationString(section))
: new SimpleStringProperty());
information.getStyleClass().add("information");
- AppFont.header(information);
- var state = wrapper.getEntry().getProvider() != null
- ? wrapper.getEntry().getProvider().stateDisplay(wrapper)
+ var state = getWrapper().getEntry().getProvider() != null
+ ? getWrapper().getEntry().getProvider().stateDisplay(getWrapper())
: Comp.empty();
information.setGraphic(state.createRegion());
return information;
}
- protected Label createSummary() {
- var summary = new Label();
- summary.textProperty().bind(wrapper.getSummary());
- summary.getStyleClass().add("summary");
- AppFont.small(summary);
- return summary;
- }
-
protected void applyState(Node node) {
- PlatformThread.sync(wrapper.getValidity()).subscribe(val -> {
+ PlatformThread.sync(getWrapper().getValidity()).subscribe(val -> {
switch (val) {
case LOAD_FAILED -> {
node.pseudoClassStateChanged(FAILED, true);
@@ -189,24 +187,23 @@ public abstract class StoreEntryComp extends SimpleComp {
}
protected Comp> createName() {
- LabelComp name = new LabelComp(wrapper.nameProperty());
- name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS))
- .apply(struc -> struc.get().setPadding(new Insets(5, 5, 5, 0)));
- name.apply(s -> AppFont.header(s.get()));
+ LabelComp name = new LabelComp(getWrapper().nameProperty());
+ name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS));
name.styleClass("name");
return name;
}
protected Node createIcon(int w, int h) {
- var img = wrapper.disabledProperty().get()
+ var img = getWrapper().disabledProperty().get()
? "disabled_icon.png"
- : wrapper.getEntry()
+ : getWrapper()
+ .getEntry()
.getProvider()
- .getDisplayIconFileName(wrapper.getEntry().getStore());
+ .getDisplayIconFileName(getWrapper().getEntry().getStore());
var imageComp = PrettyImageHelper.ofFixedSize(img, w, h);
var storeIcon = imageComp.createRegion();
- if (wrapper.getValidity().getValue().isUsable()) {
- new TooltipAugment<>(wrapper.getEntry().getProvider().displayName(), null).augment(storeIcon);
+ if (getWrapper().getValidity().getValue().isUsable()) {
+ new TooltipAugment<>(getWrapper().getEntry().getProvider().displayName(), null).augment(storeIcon);
}
var stack = new StackPane(storeIcon);
@@ -220,7 +217,7 @@ public abstract class StoreEntryComp extends SimpleComp {
}
protected Region createButtonBar() {
- var list = new DerivedObservableList<>(wrapper.getActionProviders(), false);
+ var list = new DerivedObservableList<>(getWrapper().getActionProviders(), false);
var buttons = list.mapped(actionProvider -> {
var button = buildButton(actionProvider);
return button != null ? button.createRegion() : null;
@@ -239,8 +236,8 @@ public abstract class StoreEntryComp extends SimpleComp {
buttons.subscribe(update);
update.run();
ig.setAlignment(Pos.CENTER_RIGHT);
- ig.setPadding(new Insets(5));
ig.getStyleClass().add("button-bar");
+ AppFont.medium(ig);
return ig;
}
@@ -249,17 +246,20 @@ public abstract class StoreEntryComp extends SimpleComp {
var branch = p.getBranchDataStoreCallSite();
var cs = leaf != null ? leaf : branch;
- if (cs == null || !cs.isMajor(wrapper.getEntry().ref())) {
+ if (cs == null || !cs.isMajor(getWrapper().getEntry().ref())) {
return null;
}
+ var icon = new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(cs.getIcon(getWrapper().getEntry().ref())));
var button = new IconButtonComp(
- cs.getIcon(wrapper.getEntry().ref()),
+ icon,
leaf != null
? () -> {
ThreadHelper.runFailableAsync(() -> {
- wrapper.runAction(
- leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy());
+ getWrapper().runAction(
+ leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy());
+ // Update icon in case action changed it
+ icon.set(new LabelGraphic.IconGraphic(cs.getIcon(getWrapper().getEntry().ref())));
});
}
: null);
@@ -267,7 +267,7 @@ public abstract class StoreEntryComp extends SimpleComp {
button.apply(new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {
var cm = ContextMenuHelper.create();
- branch.getChildren().forEach(childProvider -> {
+ branch.getChildren(getWrapper().getEntry().ref()).forEach(childProvider -> {
var menu = buildMenuItemForAction(childProvider);
if (menu != null) {
cm.getItems().add(menu);
@@ -276,13 +276,13 @@ public abstract class StoreEntryComp extends SimpleComp {
return cm;
}));
}
- button.accessibleText(cs.getName(wrapper.getEntry().ref()).getValue());
- button.apply(new TooltipAugment<>(cs.getName(wrapper.getEntry().ref()), null));
+ button.accessibleText(cs.getName(getWrapper().getEntry().ref()).getValue());
+ button.apply(new TooltipAugment<>(cs.getName(getWrapper().getEntry().ref()), null));
return button;
}
protected Comp> createSettingsButton() {
- var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline", null);
+ var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline");
settingsButton.styleClass("settings");
settingsButton.accessibleText("More");
settingsButton.apply(new ContextMenuAugment<>(
@@ -298,7 +298,7 @@ public abstract class StoreEntryComp extends SimpleComp {
AppFont.normal(contextMenu.getStyleableNode());
var hasSep = false;
- for (var p : wrapper.getActionProviders()) {
+ for (var p : getWrapper().getActionProviders()) {
var item = buildMenuItemForAction(p);
if (item == null) {
continue;
@@ -321,36 +321,36 @@ public abstract class StoreEntryComp extends SimpleComp {
var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text"));
notes.setOnAction(event -> {
- wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
+ getWrapper().getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
event.consume();
});
- notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null));
+ notes.visibleProperty().bind(BindingsHelper.map(getWrapper().getNotes(), s -> s.getCommited() == null));
contextMenu.getItems().add(notes);
if (AppPrefs.get().developerMode().getValue()) {
var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline"));
- browse.setOnAction(
- event -> DesktopHelper.browsePathLocal(wrapper.getEntry().getDirectory()));
+ browse.setOnAction(event ->
+ DesktopHelper.browsePathLocal(getWrapper().getEntry().getDirectory()));
contextMenu.getItems().add(browse);
var copyId = new MenuItem(AppI18n.get("copyId"), new FontIcon("mdi2c-content-copy"));
copyId.setOnAction(event ->
- ClipboardHelper.copyText(wrapper.getEntry().getUuid().toString()));
+ ClipboardHelper.copyText(getWrapper().getEntry().getUuid().toString()));
contextMenu.getItems().add(copyId);
}
- if (DataStorage.get().isRootEntry(wrapper.getEntry())) {
+ if (DataStorage.get().isRootEntry(getWrapper().getEntry())) {
var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill"));
var none = new MenuItem("None");
none.setOnAction(event -> {
- wrapper.getEntry().setColor(null);
+ getWrapper().getEntry().setColor(null);
event.consume();
});
color.getItems().add(none);
Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> {
MenuItem m = new MenuItem(DataStoreFormatter.capitalize(dataStoreColor.getId()));
m.setOnAction(event -> {
- wrapper.getEntry().setColor(dataStoreColor);
+ getWrapper().getEntry().setColor(dataStoreColor);
event.consume();
});
color.getItems().add(m);
@@ -358,10 +358,10 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(color);
}
- if (wrapper.getEntry().getProvider() != null) {
+ if (getWrapper().getEntry().getProvider() != null) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get()
- .getSortedCategories(wrapper.getCategory().getValue().getRoot())
+ .getSortedCategories(getWrapper().getCategory().getValue().getRoot())
.getList()
.forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem();
@@ -369,12 +369,12 @@ public abstract class StoreEntryComp extends SimpleComp {
.setValue(" ".repeat(storeCategoryWrapper.getDepth())
+ storeCategoryWrapper.getName().getValue());
m.setOnAction(event -> {
- wrapper.moveTo(storeCategoryWrapper.getCategory());
+ getWrapper().moveTo(storeCategoryWrapper.getCategory());
event.consume();
});
if (storeCategoryWrapper.getParent() == null
|| storeCategoryWrapper.equals(
- wrapper.getCategory().getValue())) {
+ getWrapper().getCategory().getValue())) {
m.setDisable(true);
}
@@ -386,10 +386,10 @@ public abstract class StoreEntryComp extends SimpleComp {
var order = new Menu(AppI18n.get("order"), new FontIcon("mdal-bookmarks"));
var noOrder = new MenuItem(AppI18n.get("none"), new FontIcon("mdi2r-reorder-horizontal"));
noOrder.setOnAction(event -> {
- wrapper.setOrder(null);
+ getWrapper().setOrder(null);
event.consume();
});
- if (wrapper.getEntry().getExplicitOrder() == null) {
+ if (getWrapper().getEntry().getExplicitOrder() == null) {
noOrder.setDisable(true);
}
order.getItems().add(noOrder);
@@ -397,20 +397,20 @@ public abstract class StoreEntryComp extends SimpleComp {
var top = new MenuItem(AppI18n.get("stickToTop"), new FontIcon("mdi2o-order-bool-descending"));
top.setOnAction(event -> {
- wrapper.setOrder(DataStoreEntry.Order.TOP);
+ getWrapper().setOrder(DataStoreEntry.Order.TOP);
event.consume();
});
- if (DataStoreEntry.Order.TOP.equals(wrapper.getEntry().getExplicitOrder())) {
+ if (DataStoreEntry.Order.TOP.equals(getWrapper().getEntry().getExplicitOrder())) {
top.setDisable(true);
}
order.getItems().add(top);
var bottom = new MenuItem(AppI18n.get("stickToBottom"), new FontIcon("mdi2o-order-bool-ascending"));
bottom.setOnAction(event -> {
- wrapper.setOrder(DataStoreEntry.Order.BOTTOM);
+ getWrapper().setOrder(DataStoreEntry.Order.BOTTOM);
event.consume();
});
- if (DataStoreEntry.Order.BOTTOM.equals(wrapper.getEntry().getExplicitOrder())) {
+ if (DataStoreEntry.Order.BOTTOM.equals(getWrapper().getEntry().getExplicitOrder())) {
bottom.setDisable(true);
}
order.getItems().add(bottom);
@@ -423,14 +423,14 @@ public abstract class StoreEntryComp extends SimpleComp {
del.disableProperty()
.bind(Bindings.createBooleanBinding(
() -> {
- return !wrapper.getDeletable().get()
+ return !getWrapper().getDeletable().get()
&& !AppPrefs.get()
.developerDisableGuiRestrictions()
.get();
},
- wrapper.getDeletable(),
+ getWrapper().getDeletable(),
AppPrefs.get().developerDisableGuiRestrictions()));
- del.setOnAction(event -> wrapper.delete());
+ del.setOnAction(event -> getWrapper().delete());
contextMenu.getItems().add(del);
return contextMenu;
@@ -441,12 +441,12 @@ public abstract class StoreEntryComp extends SimpleComp {
var branch = p.getBranchDataStoreCallSite();
var cs = leaf != null ? leaf : branch;
- if (cs == null || cs.isMajor(wrapper.getEntry().ref())) {
+ if (cs == null || cs.isMajor(getWrapper().getEntry().ref())) {
return null;
}
- var name = cs.getName(wrapper.getEntry().ref());
- var icon = cs.getIcon(wrapper.getEntry().ref());
+ var name = cs.getName(getWrapper().getEntry().ref());
+ var icon = cs.getIcon(getWrapper().getEntry().ref());
var item = (leaf != null && leaf.canLinkTo()) || branch != null
? new Menu(null, new FontIcon(icon))
: new MenuItem(null, new FontIcon(icon));
@@ -462,7 +462,7 @@ public abstract class StoreEntryComp extends SimpleComp {
Menu menu = item instanceof Menu m ? m : null;
if (branch != null) {
- var items = branch.getChildren().stream()
+ var items = branch.getChildren(getWrapper().getEntry().ref()).stream()
.map(c -> buildMenuItemForAction(c))
.toList();
menu.getItems().addAll(items);
@@ -472,21 +472,25 @@ public abstract class StoreEntryComp extends SimpleComp {
run.textProperty().bind(AppI18n.observable("base.execute"));
run.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
- wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy());
+ getWrapper()
+ .runAction(leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy());
});
});
menu.getItems().add(run);
var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than"));
- var url = "xpipe://action/" + p.getId() + "/" + wrapper.getEntry().getUuid();
+ var url = "xpipe://action/" + p.getId() + "/"
+ + getWrapper().getEntry().getUuid();
sc.textProperty().bind(AppI18n.observable("base.createShortcut"));
sc.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
- DesktopShortcuts.create(
+ DesktopShortcuts.createCliOpen(
url,
- wrapper.nameProperty().getValue() + " ("
+ DataStorage.get()
+ .getStoreEntryDisplayName(
+ getWrapper().getEntry()) + " ("
+ p.getLeafDataStoreCallSite()
- .getName(wrapper.getEntry().ref())
+ .getName(getWrapper().getEntry().ref())
.getValue() + ")");
});
});
@@ -516,7 +520,7 @@ public abstract class StoreEntryComp extends SimpleComp {
event.consume();
ThreadHelper.runFailableAsync(() -> {
- wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy());
+ getWrapper().runAction(leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy());
});
});
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListOverviewComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListOverviewComp.java
index 4c7755431..60823dd6a 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListOverviewComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListOverviewComp.java
@@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
+import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
@@ -95,8 +96,8 @@ public class StoreEntryListOverviewComp extends SimpleComp {
createDateSortButton().createRegion(),
Comp.hspacer(2).createRegion(),
createAlphabeticalSortButton().createRegion());
- AppFont.setSize(label, 3);
- AppFont.setSize(c, 3);
+ AppFont.setSize(label, 2);
+ AppFont.setSize(c, 2);
topBar.setAlignment(Pos.CENTER);
topBar.getStyleClass().add("top");
return topBar;
@@ -111,9 +112,11 @@ public class StoreEntryListOverviewComp extends SimpleComp {
});
var filter = new FilterComp(StoreViewState.get().getFilterString());
var f = filter.createRegion();
- var buttons = createAddButton();
- var hbox = new HBox(buttons, f);
- f.prefHeightProperty().bind(buttons.heightProperty());
+ var button = createAddButton();
+ var hbox = new HBox(button, f);
+ f.minHeightProperty().bind(button.heightProperty());
+ f.prefHeightProperty().bind(button.heightProperty());
+ f.maxHeightProperty().bind(button.heightProperty());
hbox.setSpacing(8);
hbox.setAlignment(Pos.CENTER);
HBox.setHgrow(f, Priority.ALWAYS);
@@ -136,22 +139,22 @@ public class StoreEntryListOverviewComp extends SimpleComp {
if (OsType.getLocal().equals(OsType.MACOS)) {
menu.setPadding(new Insets(-2, 0, -2, 0));
} else {
- menu.setPadding(new Insets(-3, 0, -3, 0));
+ menu.setPadding(new Insets(-4, 0, -4, 0));
}
return menu;
}
private Comp> createAlphabeticalSortButton() {
- var icon = Bindings.createStringBinding(
+ var icon = Bindings.createObjectBinding(
() -> {
if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_ASC) {
- return "mdi2s-sort-alphabetical-descending";
+ return new LabelGraphic.IconGraphic("mdi2s-sort-alphabetical-descending");
}
if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_DESC) {
- return "mdi2s-sort-alphabetical-ascending";
+ return new LabelGraphic.IconGraphic("mdi2s-sort-alphabetical-ascending");
}
- return "mdi2s-sort-alphabetical-descending";
+ return new LabelGraphic.IconGraphic("mdi2s-sort-alphabetical-descending");
},
sortMode);
var alphabetical = new IconButtonComp(icon, () -> {
@@ -164,6 +167,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
}
});
alphabetical.apply(alphabeticalR -> {
+ AppFont.medium(alphabeticalR.get());
alphabeticalR
.get()
.opacityProperty()
@@ -183,15 +187,15 @@ public class StoreEntryListOverviewComp extends SimpleComp {
}
private Comp> createDateSortButton() {
- var icon = Bindings.createStringBinding(
+ var icon = Bindings.createObjectBinding(
() -> {
if (sortMode.getValue() == StoreSortMode.DATE_ASC) {
- return "mdi2s-sort-clock-ascending-outline";
+ return new LabelGraphic.IconGraphic("mdi2s-sort-clock-ascending-outline");
}
if (sortMode.getValue() == StoreSortMode.DATE_DESC) {
- return "mdi2s-sort-clock-descending-outline";
+ return new LabelGraphic.IconGraphic("mdi2s-sort-clock-descending-outline");
}
- return "mdi2s-sort-clock-ascending-outline";
+ return new LabelGraphic.IconGraphic("mdi2s-sort-clock-ascending-outline");
},
sortMode);
var date = new IconButtonComp(icon, () -> {
@@ -204,6 +208,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
}
});
date.apply(dateR -> {
+ AppFont.medium(dateR.get());
dateR.get()
.opacityProperty()
.bind(Bindings.createDoubleBinding(
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java
index 37b40b2f2..6111c4fdd 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java
@@ -40,12 +40,13 @@ public class StoreEntryWrapper {
private final Property category = new SimpleObjectProperty<>();
private final Property summary = new SimpleObjectProperty<>();
private final Property notes;
+ private final IntegerProperty childrenStateUpdateObservable = new SimpleIntegerProperty();
public StoreEntryWrapper(DataStoreEntry entry) {
this.entry = entry;
this.name = new SimpleStringProperty(entry.getName());
this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500)));
- ActionProvider.ALL.stream()
+ ActionProvider.ALL_STANDALONE.stream()
.filter(dataStoreActionProvider -> {
return !entry.isDisabled()
&& dataStoreActionProvider.getLeafDataStoreCallSite() != null
@@ -63,6 +64,12 @@ public class StoreEntryWrapper {
setupListeners();
}
+ public void triggerChildrenStateUpdate() {
+ PlatformThread.runLaterIfNeeded(() -> {
+ childrenStateUpdateObservable.set(childrenStateUpdateObservable.get() + 1);
+ });
+ }
+
public void applyLastAccess() {
this.lastAccessApplied.setValue(lastAccess.getValue());
}
@@ -151,7 +158,8 @@ public class StoreEntryWrapper {
summary.setValue(null);
} else {
try {
- summary.setValue(entry.getProvider() != null ? entry.getProvider().summaryString(this) : null);
+ summary.setValue(
+ entry.getProvider() != null ? entry.getProvider().summaryString(this) : null);
} catch (Exception ex) {
// Summary creation might fail or have a bug
ErrorEvent.fromThrowable(ex).handle();
@@ -163,18 +171,18 @@ public class StoreEntryWrapper {
defaultActionProvider.setValue(null);
} else {
try {
- var defaultProvider = ActionProvider.ALL.stream()
+ var defaultProvider = ActionProvider.ALL_STANDALONE.stream()
.filter(e -> entry.getStore() != null
&& e.getDefaultDataStoreCallSite() != null
&& e.getDefaultDataStoreCallSite()
- .getApplicableClass()
- .isAssignableFrom(entry.getStore().getClass())
+ .getApplicableClass()
+ .isAssignableFrom(entry.getStore().getClass())
&& e.getDefaultDataStoreCallSite().isApplicable(entry.ref()))
.findFirst()
.orElse(null);
this.defaultActionProvider.setValue(defaultProvider);
- var newProviders = ActionProvider.ALL.stream()
+ var newProviders = ActionProvider.ALL_STANDALONE.stream()
.filter(dataStoreActionProvider -> {
return showActionProvider(dataStoreActionProvider);
})
@@ -203,7 +211,7 @@ public class StoreEntryWrapper {
if (branch != null
&& entry.getStore() != null
&& branch.getApplicableClass().isAssignableFrom(entry.getStore().getClass())) {
- return branch.getChildren().stream().anyMatch(child -> {
+ return branch.getChildren(entry.ref()).stream().anyMatch(child -> {
return showActionProvider(child);
});
}
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java
index 7b1328641..35431fb16 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java
@@ -39,7 +39,6 @@ public class StoreNotesComp extends Comp {
.focusTraversableForAccessibility()
.tooltipKey("notes")
.styleClass("notes-button")
- .grow(false, true)
.hide(BindingsHelper.map(n, s -> s.getCommited() == null && s.getCurrent() == null))
.createStructure()
.get();
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java
index d7b1e4d80..1833e589c 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java
@@ -29,7 +29,7 @@ public class StoreProviderChoiceComp extends Comp provider;
boolean staticDisplay;
- private List getProviders() {
+ public List getProviders() {
return DataStoreProviders.getAll().stream()
.filter(val -> filter == null || filter.test(val))
.toList();
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java
index 57d39e0ef..47a513865 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java
@@ -4,6 +4,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
+import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.util.ContextMenuHelper;
import javafx.geometry.Side;
@@ -18,9 +19,9 @@ import java.util.function.Consumer;
public class StoreQuickAccessButtonComp extends Comp> {
private final StoreSection section;
- private final Consumer action;
+ private final Consumer action;
- public StoreQuickAccessButtonComp(StoreSection section, Consumer action) {
+ public StoreQuickAccessButtonComp(StoreSection section, Consumer action) {
this.section = section;
this.action = action;
}
@@ -44,10 +45,9 @@ public class StoreQuickAccessButtonComp extends Comp> {
w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore());
if (c.getList().isEmpty()) {
var item = ContextMenuHelper.item(
- PrettyImageHelper.ofFixedSizeSquare(graphic, 16),
- w.getName().getValue());
+ new LabelGraphic.ImageGraphic(graphic, 16), w.getName().getValue());
item.setOnAction(event -> {
- action.accept(w);
+ action.accept(section);
contextMenu.hide();
event.consume();
});
@@ -72,7 +72,7 @@ public class StoreQuickAccessButtonComp extends Comp> {
return;
}
- action.accept(w);
+ action.accept(section);
contextMenu.hide();
event.consume();
}
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java
index b2527bf91..1e7875f72 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java
@@ -163,10 +163,10 @@ public class StoreSection {
var allChildren = all.filtered(
other -> {
// Legacy implementation that does not use children caches. Use for testing
- // if (true) return DataStorage.get()
- // .getDisplayParent(other.getEntry())
- // .map(found -> found.equals(e.getEntry()))
- // .orElse(false);
+// if (true) return DataStorage.get()
+// .getDefaultDisplayParent(other.getEntry())
+// .map(found -> found.equals(e.getEntry()))
+// .orElse(false);
// is children. This check is fast as the children are cached in the storage
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry())
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java
index 6553b83c3..3085ff39b 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java
@@ -7,6 +7,7 @@ import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
+import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.util.ThreadHelper;
@@ -44,9 +45,9 @@ public class StoreSectionComp extends Comp> {
return section.getShownChildren().getList().isEmpty();
},
section.getShownChildren().getList());
- Consumer quickAccessAction = w -> {
+ Consumer quickAccessAction = w -> {
ThreadHelper.runFailableAsync(() -> {
- w.executeDefaultAction();
+ w.getWrapper().executeDefaultAction();
});
};
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
@@ -68,11 +69,11 @@ public class StoreSectionComp extends Comp> {
private Comp> createExpandButton() {
var expandButton = new IconButtonComp(
- Bindings.createStringBinding(
- () -> section.getWrapper().getExpanded().get()
+ Bindings.createObjectBinding(
+ () -> new LabelGraphic.IconGraphic(section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0
? "mdal-keyboard_arrow_down"
- : "mdal-keyboard_arrow_right",
+ : "mdal-keyboard_arrow_right"),
section.getWrapper().getExpanded(),
section.getShownChildren().getList()),
() -> {
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java
index 58b56ecb1..b041622c2 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java
@@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.VerticalComp;
+import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.storage.DataStoreColor;
import javafx.beans.binding.Bindings;
@@ -34,12 +35,12 @@ public class StoreSectionMiniComp extends Comp> {
private final StoreSection section;
private final BiConsumer>> augment;
- private final Consumer action;
+ private final Consumer action;
public StoreSectionMiniComp(
StoreSection section,
BiConsumer>> augment,
- Consumer action) {
+ Consumer action) {
this.section = section;
this.augment = augment;
this.action = action;
@@ -68,7 +69,7 @@ public class StoreSectionMiniComp extends Comp> {
})
.apply(struc -> {
struc.get().setOnAction(event -> {
- action.accept(section.getWrapper());
+ action.accept(section);
event.consume();
});
})
@@ -81,8 +82,8 @@ public class StoreSectionMiniComp extends Comp> {
new SimpleBooleanProperty(section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0);
var button = new IconButtonComp(
- Bindings.createStringBinding(
- () -> expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right",
+ Bindings.createObjectBinding(
+ () -> new LabelGraphic.IconGraphic(expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right"),
expanded),
() -> {
expanded.set(!expanded.get());
@@ -105,7 +106,7 @@ public class StoreSectionMiniComp extends Comp> {
return section.getShownChildren().getList().isEmpty();
},
section.getShownChildren().getList());
- Consumer quickAccessAction = action;
+ Consumer quickAccessAction = action;
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
.vgrow()
.styleClass("quick-access-button")
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java
index 132de45c0..4ea5cecc1 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java
@@ -65,8 +65,7 @@ public interface StoreSortMode {
.isUsable())
.map(this::representative),
Stream.of(s))
- .max(Comparator.comparing(
- section -> date(section)))
+ .max(Comparator.comparing(section -> date(section)))
.orElseThrow();
}
@@ -103,8 +102,7 @@ public interface StoreSortMode {
.isUsable())
.map(this::representative),
Stream.of(s))
- .max(Comparator.comparing(
- section -> date(section)))
+ .max(Comparator.comparing(section -> date(section)))
.orElseThrow();
}
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java b/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java
index 69506d85b..b8ad517fa 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java
@@ -124,10 +124,10 @@ public class StoreViewState {
public void updateDisplay() {
allEntries.getList().forEach(e -> e.applyLastAccess());
- toggleStoreListUpdate();
+ triggerStoreListUpdate();
}
- public void toggleStoreListUpdate() {
+ public void triggerStoreListUpdate() {
PlatformThread.runLaterIfNeeded(() -> {
entriesListUpdateObservable.set(entriesListUpdateObservable.get() + 1);
});
@@ -152,7 +152,7 @@ public class StoreViewState {
@Override
public void onStoreListUpdate() {
Platform.runLater(() -> {
- toggleStoreListUpdate();
+ triggerStoreListUpdate();
});
}
diff --git a/app/src/main/java/io/xpipe/app/core/App.java b/app/src/main/java/io/xpipe/app/core/App.java
index 6abc9947d..c370764a2 100644
--- a/app/src/main/java/io/xpipe/app/core/App.java
+++ b/app/src/main/java/io/xpipe/app/core/App.java
@@ -7,9 +7,11 @@ import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.LicenseProvider;
+
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.stage.Stage;
+
import lombok.Getter;
import lombok.SneakyThrows;
diff --git a/app/src/main/java/io/xpipe/app/core/AppDataLock.java b/app/src/main/java/io/xpipe/app/core/AppDataLock.java
index 8e3f93154..d84202530 100644
--- a/app/src/main/java/io/xpipe/app/core/AppDataLock.java
+++ b/app/src/main/java/io/xpipe/app/core/AppDataLock.java
@@ -1,6 +1,7 @@
package io.xpipe.app.core;
import io.xpipe.app.issue.ErrorEvent;
+import org.apache.commons.io.FileUtils;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
@@ -21,7 +22,7 @@ public class AppDataLock {
public static boolean lock() {
try {
var file = getLockFile().toFile();
- Files.createDirectories(file.toPath().getParent());
+ FileUtils.forceMkdir(file.getParentFile());
if (!Files.exists(file.toPath())) {
try {
// It is possible that another instance creates the lock at almost the same time
diff --git a/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java b/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java
index a3061cecf..50ca49aaf 100644
--- a/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java
+++ b/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java
@@ -93,7 +93,7 @@ public class AppExtensionManager {
Path p = Path.of(localInstallation);
if (!Files.exists(p)) {
throw new IllegalStateException(
- "Required local XPipe installation was not found but is required for development");
+ "Required local XPipe installation was not found but is required for development. See https://github.com/xpipe-io/xpipe/blob/master/CONTRIBUTING.md#development-setup");
}
var iv = getLocalInstallVersion();
@@ -105,8 +105,9 @@ public class AppExtensionManager {
var sourceVersion = AppVersion.parse(sv)
.orElseThrow(() -> new IllegalArgumentException("Invalid source version: " + sv));
if (AppProperties.get().isLocatorVersionCheck() && !installVersion.equals(sourceVersion)) {
- throw new IllegalStateException("Incompatible development version. Source: " + iv + ", Installation: "
- + sv + "\n\nPlease try to check out the matching release version in the repository.");
+ throw new IllegalStateException(
+ "Incompatible development version. Source: " + iv + ", Installation: " + sv
+ + "\n\nPlease try to check out the matching release version in the repository. See https://github.com/xpipe-io/xpipe/blob/master/CONTRIBUTING.md#development-setup");
}
var extensions = XPipeInstallation.getLocalExtensionsDirectory(p);
diff --git a/app/src/main/java/io/xpipe/app/core/AppGreetings.java b/app/src/main/java/io/xpipe/app/core/AppGreetings.java
index 1d94ed99d..b44ac8813 100644
--- a/app/src/main/java/io/xpipe/app/core/AppGreetings.java
+++ b/app/src/main/java/io/xpipe/app/core/AppGreetings.java
@@ -55,6 +55,12 @@ public class AppGreetings {
if (set || AppProperties.get().isDevelopmentEnvironment()) {
return;
}
+
+ if (AppProperties.get().isAutoAcceptEula()) {
+ AppCache.update("legalAccepted", true);
+ return;
+ }
+
var read = new SimpleBooleanProperty();
var accepted = new SimpleBooleanProperty();
AppWindowHelper.showBlockingAlert(alert -> {
diff --git a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java
index ceb86ffd5..b58fb9e2e 100644
--- a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java
+++ b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java
@@ -78,19 +78,19 @@ public class AppLayoutModel {
"mdi2f-file-cabinet",
new BrowserSessionComp(BrowserSessionModel.DEFAULT),
null,
- new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.CONTROL_DOWN)),
+ new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.SHORTCUT_DOWN)),
new Entry(
AppI18n.observable("connections"),
"mdi2c-connection",
new StoreLayoutComp(),
null,
- new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.CONTROL_DOWN)),
+ new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.SHORTCUT_DOWN)),
new Entry(
AppI18n.observable("settings"),
"mdsmz-miscellaneous_services",
new AppPrefsComp(),
null,
- new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)),
+ new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.SHORTCUT_DOWN)),
new Entry(
AppI18n.observable("explorePlans"),
"mdi2p-professional-hexagon",
@@ -102,20 +102,20 @@ public class AppLayoutModel {
"mdi2g-github",
null,
() -> Hyperlinks.open(Hyperlinks.GITHUB),
- new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)),
+ null),
new Entry(
AppI18n.observable("discord"),
"mdi2d-discord",
null,
() -> Hyperlinks.open(Hyperlinks.DISCORD),
- new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)),
+ null),
new Entry(
AppI18n.observable("api"),
"mdi2c-code-json",
null,
() -> Hyperlinks.open(
"http://localhost:" + AppBeaconServer.get().getPort()),
- new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN))));
+ null)));
return l;
}
@@ -128,5 +128,6 @@ public class AppLayoutModel {
double browserConnectionsWidth;
}
- public record Entry(ObservableValue name, String icon, Comp> comp, Runnable action, KeyCombination combination) {}
+ public record Entry(
+ ObservableValue name, String icon, Comp> comp, Runnable action, KeyCombination combination) {}
}
diff --git a/app/src/main/java/io/xpipe/app/core/AppLogs.java b/app/src/main/java/io/xpipe/app/core/AppLogs.java
index efda9ad5f..b24d2190c 100644
--- a/app/src/main/java/io/xpipe/app/core/AppLogs.java
+++ b/app/src/main/java/io/xpipe/app/core/AppLogs.java
@@ -138,7 +138,7 @@ public class AppLogs {
var shouldLogToFile = shouldWriteLogs();
if (shouldLogToFile) {
try {
- Files.createDirectories(usedLogsDir);
+ FileUtils.forceMkdir(usedLogsDir.toFile());
var file = usedLogsDir.resolve("xpipe.log");
var fos = new FileOutputStream(file.toFile(), true);
var buf = new BufferedOutputStream(fos);
diff --git a/app/src/main/java/io/xpipe/app/core/AppProperties.java b/app/src/main/java/io/xpipe/app/core/AppProperties.java
index 8150cc878..0a18c8d45 100644
--- a/app/src/main/java/io/xpipe/app/core/AppProperties.java
+++ b/app/src/main/java/io/xpipe/app/core/AppProperties.java
@@ -37,11 +37,13 @@ public class AppProperties {
boolean useVirtualThreads;
boolean debugThreads;
Path dataDir;
+ Path defaultDataDir;
boolean showcase;
AppVersion canonicalVersion;
boolean locatePtb;
boolean locatorVersionCheck;
boolean isTest;
+ boolean autoAcceptEula;
public AppProperties() {
var appDir = Path.of(System.getProperty("user.dir")).resolve("app");
@@ -86,6 +88,7 @@ public class AppProperties {
debugThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.debugThreads"))
.map(Boolean::parseBoolean)
.orElse(false);
+ defaultDataDir = Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe-ptb" : ".xpipe");
dataDir = Optional.ofNullable(System.getProperty("io.xpipe.app.dataDir"))
.map(s -> {
var p = Path.of(s);
@@ -94,7 +97,7 @@ public class AppProperties {
}
return p;
})
- .orElse(Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe-ptb" : ".xpipe"));
+ .orElse(defaultDataDir);
showcase = Optional.ofNullable(System.getProperty("io.xpipe.app.showcase"))
.map(Boolean::parseBoolean)
.orElse(false);
@@ -107,6 +110,9 @@ public class AppProperties {
.map(s -> !Boolean.parseBoolean(s))
.orElse(true);
isTest = isJUnitTest();
+ autoAcceptEula = Optional.ofNullable(System.getProperty("io.xpipe.app.acceptEula"))
+ .map(Boolean::parseBoolean)
+ .orElse(false);
}
private static boolean isJUnitTest() {
diff --git a/app/src/main/java/io/xpipe/app/core/AppTheme.java b/app/src/main/java/io/xpipe/app/core/AppTheme.java
index 017c4cf75..05cdb6cb8 100644
--- a/app/src/main/java/io/xpipe/app/core/AppTheme.java
+++ b/app/src/main/java/io/xpipe/app/core/AppTheme.java
@@ -43,6 +43,10 @@ public class AppTheme {
public static void initThemeHandlers(Stage stage) {
Runnable r = () -> {
+ stage.getScene()
+ .getRoot()
+ .pseudoClassStateChanged(
+ PseudoClass.getPseudoClass(OsType.getLocal().getId()), true);
if (AppPrefs.get() == null) {
var def = Theme.getDefaultLightTheme();
stage.getScene().getRoot().getStyleClass().add(def.getCssId());
@@ -109,6 +113,9 @@ public class AppTheme {
}
});
});
+ } catch (UnsupportedOperationException ex) {
+ // The platform preferences are sometimes not initialized yet
+ ErrorEvent.fromThrowable(ex).expected().omit().handle();
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).omit().handle();
}
@@ -132,6 +139,9 @@ public class AppTheme {
} else {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
+ } catch (UnsupportedOperationException ex) {
+ // The platform preferences are sometimes not initialized yet
+ ErrorEvent.fromThrowable(ex).expected().omit().handle();
} catch (Exception ex) {
// The color scheme query can fail if the toolkit is not initialized properly
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
@@ -206,7 +216,6 @@ public class AppTheme {
Application.setUserAgentStylesheet(Styles.toDataURI(builder.toString()));
}
-
public List getAdditionalStylesheets() {
return List.of();
}
diff --git a/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java b/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java
index 5d1318b7a..7a641ccd7 100644
--- a/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java
+++ b/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java
@@ -90,7 +90,8 @@ public class AppTrayIcon {
tray.add(this.trayIcon);
fixBackground();
} catch (Exception e) {
- ErrorEvent.fromThrowable("Unable to add TrayIcon", e).handle();
+ // This can sometimes fail on Linux
+ ErrorEvent.fromThrowable("Unable to add TrayIcon", e).expected().handle();
}
});
}
diff --git a/app/src/main/java/io/xpipe/app/core/check/AppRosettaCheck.java b/app/src/main/java/io/xpipe/app/core/check/AppRosettaCheck.java
new file mode 100644
index 000000000..f044ec126
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/core/check/AppRosettaCheck.java
@@ -0,0 +1,30 @@
+package io.xpipe.app.core.check;
+
+import io.xpipe.app.core.AppProperties;
+import io.xpipe.app.issue.ErrorEvent;
+import io.xpipe.app.util.LocalShell;
+import io.xpipe.core.process.OsType;
+
+public class AppRosettaCheck {
+
+ public static void check() throws Exception {
+ if (OsType.getLocal() != OsType.MACOS) {
+ return;
+ }
+
+ if (!AppProperties.get().getArch().equals("x86_64")) {
+ return;
+ }
+
+ var ret = LocalShell.getShell().command("sysctl -n sysctl.proc_translated").readStdoutIfPossible();
+ if (ret.isEmpty()) {
+ return;
+ }
+
+ if (ret.get().equals("1")) {
+ ErrorEvent.fromMessage("You are running the Intel version of XPipe on an Apple Silicon system."
+ + " There is a native build available that comes with much better performance."
+ + " Please install that one instead.");
+ }
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/core/check/AppShellCheck.java b/app/src/main/java/io/xpipe/app/core/check/AppShellCheck.java
index ac5b58b3a..c3000bc37 100644
--- a/app/src/main/java/io/xpipe/app/core/check/AppShellCheck.java
+++ b/app/src/main/java/io/xpipe/app/core/check/AppShellCheck.java
@@ -6,6 +6,8 @@ import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.process.ProcessOutputException;
+import lombok.Value;
+
import java.util.Optional;
public class AppShellCheck {
@@ -17,15 +19,19 @@ public class AppShellCheck {
.getEffectiveLocalDialect()
.equals(ProcessControlProvider.get().getFallbackDialect());
if (err.isPresent() && canFallback) {
- var msg = formatMessage(err.get());
+ var msg = formatMessage(err.get().getMessage());
ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle();
enableFallback();
err = selfTestErrorCheck();
}
if (err.isPresent()) {
- var msg = formatMessage(err.get());
- ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle();
+ var msg = formatMessage(err.get().getMessage());
+ var event = ErrorEvent.fromThrowable(new IllegalStateException(msg));
+ if (!err.get().isCanContinue()) {
+ event.term();
+ }
+ event.handle();
}
}
@@ -71,17 +77,24 @@ public class AppShellCheck {
LocalShell.init();
}
- private static Optional selfTestErrorCheck() {
+ private static Optional selfTestErrorCheck() {
try (var command = LocalShell.getShell().command("echo test").complex().start()) {
var out = command.readStdoutOrThrow();
if (!out.equals("test")) {
- return Optional.of("Expected \"test\", got \"" + out + "\"");
+ return Optional.of(new FailureResult("Expected \"test\", got \"" + out + "\"", true));
}
} catch (ProcessOutputException ex) {
- return Optional.of(ex.getOutput() != null ? ex.getOutput() : ex.toString());
+ return Optional.of(new FailureResult(ex.getOutput() != null ? ex.getOutput() : ex.toString(), true));
} catch (Throwable t) {
- return Optional.of(t.getMessage() != null ? t.getMessage() : t.toString());
+ return Optional.of(new FailureResult(t.getMessage() != null ? t.getMessage() : t.toString(), false));
}
return Optional.empty();
}
+
+ @Value
+ private static class FailureResult {
+
+ String message;
+ boolean canContinue;
+ }
}
diff --git a/app/src/main/java/io/xpipe/app/core/check/AppUserDirectoryCheck.java b/app/src/main/java/io/xpipe/app/core/check/AppUserDirectoryCheck.java
index cf7684c91..2d974a3b6 100644
--- a/app/src/main/java/io/xpipe/app/core/check/AppUserDirectoryCheck.java
+++ b/app/src/main/java/io/xpipe/app/core/check/AppUserDirectoryCheck.java
@@ -2,6 +2,7 @@ package io.xpipe.app.core.check;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent;
+import org.apache.commons.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
@@ -12,17 +13,18 @@ public class AppUserDirectoryCheck {
var dataDirectory = AppProperties.get().getDataDir();
try {
- Files.createDirectories(dataDirectory);
+ FileUtils.forceMkdir(dataDirectory.toFile());
var testDirectory = dataDirectory.resolve("permissions_check");
- Files.createDirectories(testDirectory);
+ FileUtils.forceMkdir(testDirectory.toFile());
+ if (!Files.exists(testDirectory)) {
+ throw new IOException("Directory creation in user home directory failed silently");
+ }
Files.delete(testDirectory);
// if (true) throw new IOException();
} catch (IOException e) {
- ErrorEvent.fromThrowable(
- new IOException(
- "Unable to access directory " + dataDirectory
- + ". Please make sure that you have the appropriate permissions and no Antivirus program is blocking the access. "
- + "In case you use cloud storage, verify that your cloud storage is working and you are logged in."))
+ ErrorEvent.fromThrowable("Unable to access directory " + dataDirectory
+ + ". Please make sure that you have the appropriate permissions and no Antivirus program is blocking the access. "
+ + "In case you use cloud storage, verify that your cloud storage is working and you are logged in.", e)
.term()
.expected()
.handle();
diff --git a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java
index ecc3e7d68..7e3d12647 100644
--- a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java
+++ b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java
@@ -7,6 +7,7 @@ import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.*;
import io.xpipe.app.core.check.AppAvCheck;
import io.xpipe.app.core.check.AppCertutilCheck;
+import io.xpipe.app.core.check.AppRosettaCheck;
import io.xpipe.app.core.check.AppShellCheck;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.ext.DataStoreProviders;
@@ -51,6 +52,7 @@ public class BaseMode extends OperationMode {
AppSid.init();
LocalShell.init();
AppShellCheck.check();
+ AppRosettaCheck.check();
XPipeDistributionType.init();
AppPrefs.setLocalDefaultsIfNeeded();
// Initialize beacon server as we should be prepared for git askpass commands
diff --git a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java
index 10e6f9950..5d0e33fb4 100644
--- a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java
+++ b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java
@@ -86,6 +86,9 @@ public abstract class OperationMode {
private static void setup(String[] args) {
try {
+ // Register stage theming early to make it apply for any potential early popups
+ ModifiedStage.init();
+
// Only for handling SIGTERM
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
TrackEvent.info("Received SIGTERM externally");
@@ -96,7 +99,9 @@ public abstract class OperationMode {
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
// It seems like a few exceptions are thrown in the quantum renderer
// when in shutdown. We can ignore these
- if (OperationMode.isInShutdown() && Platform.isFxApplicationThread() && ex instanceof NullPointerException) {
+ if (OperationMode.isInShutdown()
+ && Platform.isFxApplicationThread()
+ && ex instanceof NullPointerException) {
return;
}
@@ -117,8 +122,6 @@ public abstract class OperationMode {
AppExtensionManager.init(true);
AppI18n.init();
AppPrefs.initLocal();
- // Register stage theming early to make it apply for any potential early popups
- ModifiedStage.init();
AppBeaconServer.setupPort();
TrackEvent.info("Finished initial setup");
} catch (Throwable ex) {
@@ -224,7 +227,7 @@ public abstract class OperationMode {
CURRENT = null;
r.run();
} catch (Throwable ex) {
- ErrorEvent.fromThrowable(ex).build().handle();
+ ErrorEvent.fromThrowable(ex).handle();
OperationMode.halt(1);
}
diff --git a/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java b/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java
index 0e77b66e1..b5cb69efe 100644
--- a/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java
+++ b/app/src/main/java/io/xpipe/app/core/mode/PlatformMode.java
@@ -8,6 +8,7 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.ThreadHelper;
+
import javafx.application.Application;
public abstract class PlatformMode extends OperationMode {
diff --git a/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java b/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java
index 83bed40c9..fb1b0f799 100644
--- a/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java
+++ b/app/src/main/java/io/xpipe/app/core/mode/TrayMode.java
@@ -11,7 +11,7 @@ public class TrayMode extends PlatformMode {
@Override
public boolean isSupported() {
- return !OsType.getLocal().equals(OsType.MACOS)
+ return OsType.getLocal().equals(OsType.WINDOWS)
&& super.isSupported()
&& Desktop.isDesktopSupported()
&& SystemTray.isSupported();
diff --git a/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java b/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java
index e374c046e..0ed9470c5 100644
--- a/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java
+++ b/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java
@@ -16,6 +16,8 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
@@ -167,8 +169,8 @@ public class AppMainWindow {
e.consume();
});
- stage.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
- if (event.getCode().equals(KeyCode.Q) && event.isShortcutDown()) {
+ stage.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
+ if (new KeyCodeCombination(KeyCode.Q, KeyCombination.SHORTCUT_DOWN).match(event)) {
stage.close();
AppPrefs.get().closeBehaviour().getValue().run();
event.consume();
@@ -184,7 +186,7 @@ public class AppMainWindow {
stage.setY(state.windowY);
stage.setWidth(state.windowWidth);
stage.setHeight(state.windowHeight);
- // stage.setMaximized(state.maximized);
+ stage.setMaximized(state.maximized);
TrackEvent.debug("Window loaded saved bounds");
} else if (!AppProperties.get().isShowcase()) {
@@ -271,8 +273,8 @@ public class AppMainWindow {
contentR.prefHeightProperty().bind(stage.getScene().heightProperty());
if (OsType.getLocal().equals(OsType.LINUX) || OsType.getLocal().equals(OsType.MACOS)) {
- stage.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
- if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
+ stage.getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
+ if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
AppPrefs.get().closeBehaviour().getValue().run();
event.consume();
}
diff --git a/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java b/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java
index 795cd6075..320a05624 100644
--- a/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java
+++ b/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java
@@ -8,6 +8,7 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
+
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
@@ -17,6 +18,8 @@ import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
@@ -25,7 +28,6 @@ import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Modality;
import javafx.stage.Stage;
-import javafx.stage.StageStyle;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
@@ -142,8 +144,8 @@ public class AppWindowHelper {
event.consume();
});
AppWindowBounds.fixInvalidStagePosition(s);
- a.getDialogPane().getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
- if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
+ a.getDialogPane().getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
+ if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
s.close();
event.consume();
return;
@@ -260,8 +262,8 @@ public class AppWindowHelper {
}
});
- scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
- if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
+ scene.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
+ if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
stage.close();
event.consume();
}
diff --git a/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java b/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java
index 960b678a1..171af3d0d 100644
--- a/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java
+++ b/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java
@@ -14,21 +14,16 @@ import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.util.Duration;
-import lombok.SneakyThrows;
import org.apache.commons.lang3.SystemUtils;
public class ModifiedStage extends Stage {
public static boolean mergeFrame() {
- return SystemUtils.IS_OS_WINDOWS_11;
+ return SystemUtils.IS_OS_WINDOWS_11 || SystemUtils.IS_OS_MAC;
}
- @SneakyThrows
- @SuppressWarnings("unchecked")
public static void init() {
- var windowsField = Window.class.getDeclaredField("windows");
- windowsField.setAccessible(true);
- ObservableList list = (ObservableList) windowsField.get(null);
+ ObservableList list = Window.getWindows();
list.addListener((ListChangeListener) c -> {
if (c.next() && c.wasAdded()) {
var added = c.getAddedSubList().getFirst();
@@ -62,24 +57,50 @@ public class ModifiedStage extends Stage {
return;
}
- if (OsType.getLocal() != OsType.WINDOWS || AppPrefs.get() == null || AppPrefs.get().theme.getValue() == null) {
+ var applyToStage = (OsType.getLocal() == OsType.WINDOWS)
+ || (OsType.getLocal() == OsType.MACOS
+ && AppMainWindow.getInstance() != null
+ && AppMainWindow.getInstance().getStage() == stage);
+ if (!applyToStage || AppPrefs.get() == null || AppPrefs.get().theme.getValue() == null) {
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), false);
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), true);
return;
}
- var ctrl = new NativeWinWindowControl(stage);
- ctrl.setWindowAttribute(
- NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
- AppPrefs.get().theme.getValue().isDark());
- boolean seamlessFrame;
- if (AppPrefs.get().performanceMode().get() || !mergeFrame()) {
- seamlessFrame = false;
- } else {
- seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT);
+ switch (OsType.getLocal()) {
+ case OsType.Linux linux -> {}
+ case OsType.MacOs macOs -> {
+ var ctrl = new NativeMacOsWindowControl(stage);
+ var seamlessFrame = !AppPrefs.get().performanceMode().get() && mergeFrame();
+ var seamlessFrameApplied = ctrl.setAppearance(
+ seamlessFrame, AppPrefs.get().theme.getValue().isDark())
+ && seamlessFrame;
+ stage.getScene()
+ .getRoot()
+ .pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied);
+ stage.getScene()
+ .getRoot()
+ .pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied);
+ }
+ case OsType.Windows windows -> {
+ var ctrl = new NativeWinWindowControl(stage);
+ ctrl.setWindowAttribute(
+ NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
+ AppPrefs.get().theme.getValue().isDark());
+ boolean seamlessFrame;
+ if (AppPrefs.get().performanceMode().get() || !mergeFrame()) {
+ seamlessFrame = false;
+ } else {
+ seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT);
+ }
+ stage.getScene()
+ .getRoot()
+ .pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame);
+ stage.getScene()
+ .getRoot()
+ .pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame);
+ }
}
- stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame);
- stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame);
}
private static void updateStage(Stage stage) {
diff --git a/app/src/main/java/io/xpipe/app/core/window/NativeMacOsWindowControl.java b/app/src/main/java/io/xpipe/app/core/window/NativeMacOsWindowControl.java
new file mode 100644
index 000000000..30dbc0c27
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/core/window/NativeMacOsWindowControl.java
@@ -0,0 +1,56 @@
+package io.xpipe.app.core.window;
+
+import io.xpipe.app.core.AppProperties;
+import io.xpipe.app.issue.ErrorEvent;
+import io.xpipe.app.util.NativeBridge;
+import io.xpipe.app.util.ThreadHelper;
+import io.xpipe.core.util.ModuleHelper;
+
+import javafx.stage.Window;
+
+import com.sun.jna.NativeLong;
+import lombok.Getter;
+import lombok.SneakyThrows;
+
+import java.lang.reflect.Method;
+
+@Getter
+public class NativeMacOsWindowControl {
+
+ private final long nsWindow;
+
+ @SneakyThrows
+ public NativeMacOsWindowControl(Window stage) {
+ Method tkStageGetter = Window.class.getDeclaredMethod("getPeer");
+ tkStageGetter.setAccessible(true);
+ Object tkStage = tkStageGetter.invoke(stage);
+ Method getPlatformWindow = tkStage.getClass().getDeclaredMethod("getPlatformWindow");
+ getPlatformWindow.setAccessible(true);
+ Object platformWindow = getPlatformWindow.invoke(tkStage);
+ Method getNativeHandle = platformWindow.getClass().getMethod("getNativeHandle");
+ getNativeHandle.setAccessible(true);
+ Object nativeHandle = getNativeHandle.invoke(platformWindow);
+ this.nsWindow = (long) nativeHandle;
+ }
+
+ public boolean setAppearance(boolean seamlessFrame, boolean darkMode) {
+ if (!ModuleHelper.isImage() || !AppProperties.get().isFullVersion()) {
+ return false;
+ }
+
+ var lib = NativeBridge.getMacOsLibrary();
+ if (lib.isEmpty()) {
+ return false;
+ }
+
+ try {
+ lib.get().setAppearance(new NativeLong(nsWindow), seamlessFrame, darkMode);
+ if (seamlessFrame) {
+ ThreadHelper.sleep(100);
+ }
+ } catch (Throwable e) {
+ ErrorEvent.fromThrowable(e).handle();
+ }
+ return true;
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/ext/ActionProvider.java b/app/src/main/java/io/xpipe/app/ext/ActionProvider.java
index 4e8291b68..117a6db55 100644
--- a/app/src/main/java/io/xpipe/app/ext/ActionProvider.java
+++ b/app/src/main/java/io/xpipe/app/ext/ActionProvider.java
@@ -16,6 +16,7 @@ import java.util.ServiceLoader;
public interface ActionProvider {
List ALL = new ArrayList<>();
+ List ALL_STANDALONE = new ArrayList<>();
static void initProviders() {
for (ActionProvider actionProvider : ALL) {
@@ -111,7 +112,7 @@ public interface ActionProvider {
String getIcon(DataStoreEntryRef store);
- Class getApplicableClass();
+ Class> getApplicableClass();
default boolean showBusy() {
return true;
@@ -120,9 +121,11 @@ public interface ActionProvider {
interface BranchDataStoreCallSite extends DataStoreCallSite {
- default List getChildren() {
- return List.of();
+ default boolean isDynamicallyGenerated(){
+ return false;
}
+
+ List extends ActionProvider> getChildren(DataStoreEntryRef store);
}
interface LeafDataStoreCallSite extends DataStoreCallSite {
@@ -145,6 +148,18 @@ public interface ActionProvider {
ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream()
.map(actionProviderProvider -> actionProviderProvider.get())
.toList());
+
+ var menuProviders = ALL.stream()
+ .map(actionProvider -> actionProvider.getBranchDataStoreCallSite() != null &&
+ !actionProvider.getBranchDataStoreCallSite().isDynamicallyGenerated()
+ ? actionProvider.getBranchDataStoreCallSite().getChildren(null)
+ : List.of())
+ .flatMap(List::stream)
+ .toList();
+ ALL_STANDALONE.addAll(ALL.stream()
+ .filter(actionProvider -> menuProviders.stream()
+ .noneMatch(menuItem -> menuItem.getClass().equals(actionProvider.getClass())))
+ .toList());
}
}
}
diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java b/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java
index 46c304c11..17c863dd9 100644
--- a/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java
+++ b/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java
@@ -9,5 +9,6 @@ public enum DataStoreCreationCategory {
TUNNEL,
SCRIPT,
CLUSTER,
- DESKTOP
+ DESKTOP,
+ SERIAL
}
diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java
index 9f27e842b..393e9fbbb 100644
--- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java
+++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java
@@ -10,7 +10,6 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.issue.ErrorEvent;
-import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonizedValue;
@@ -27,10 +26,16 @@ import java.util.List;
public interface DataStoreProvider {
+ default boolean showProviderChoice() {
+ return true;
+ }
+
default boolean shouldShow(StoreEntryWrapper w) {
return true;
}
+ default void onParentRefresh(DataStoreEntry entry) {}
+
default void onChildrenRefresh(DataStoreEntry entry) {}
default ObservableBooleanValue busy(StoreEntryWrapper wrapper) {
@@ -71,21 +76,16 @@ public interface DataStoreProvider {
return null;
}
- default String browserDisplayName(DataStore store) {
- var e = DataStorage.get().getStoreDisplayName(store);
- return e.orElse("?");
+ default String displayName(DataStoreEntry entry) {
+ return entry.getName();
}
default List getSearchableTerms(DataStore store) {
return List.of();
}
- default boolean shouldEdit() {
- return false;
- }
-
default StoreEntryComp customEntryComp(StoreSection s, boolean preferLarge) {
- return StoreEntryComp.create(s.getWrapper(), null, preferLarge);
+ return StoreEntryComp.create(s, null, preferLarge);
}
default StoreSectionComp customSectionComp(StoreSection section, boolean topLevel) {
@@ -104,6 +104,10 @@ public interface DataStoreProvider {
return Comp.empty();
}
+ default boolean canConnectDuringCreation() {
+ return false;
+ }
+
default Comp> createInsightsComp(ObservableValue store) {
var content = Bindings.createStringBinding(
() -> {
@@ -152,6 +156,10 @@ public interface DataStoreProvider {
return DataStoreUsageCategory.DATABASE;
}
+ if (cc == DataStoreCreationCategory.SERIAL) {
+ return DataStoreUsageCategory.SERIAL;
+ }
+
return null;
}
@@ -191,7 +199,7 @@ public interface DataStoreProvider {
return null;
}
- default ObservableValue informationString(StoreEntryWrapper wrapper) {
+ default ObservableValue informationString(StoreSection section) {
return new SimpleStringProperty(null);
}
diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java
index 85827c093..287253f87 100644
--- a/app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java
+++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java
@@ -68,7 +68,10 @@ public class DataStoreProviders {
throw new IllegalStateException("Not initialized");
}
- return (T) ALL.stream().filter(d -> d.getStoreClasses().contains(store.getClass())).findAny().orElseThrow(() -> new IllegalArgumentException("Unknown store class"));
+ return (T) ALL.stream()
+ .filter(d -> d.getStoreClasses().contains(store.getClass()))
+ .findAny()
+ .orElseThrow(() -> new IllegalArgumentException("Unknown store class"));
}
public static List getAll() {
diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreUsageCategory.java b/app/src/main/java/io/xpipe/app/ext/DataStoreUsageCategory.java
index d88f724df..7257840bf 100644
--- a/app/src/main/java/io/xpipe/app/ext/DataStoreUsageCategory.java
+++ b/app/src/main/java/io/xpipe/app/ext/DataStoreUsageCategory.java
@@ -16,5 +16,7 @@ public enum DataStoreUsageCategory {
@JsonProperty("desktop")
DESKTOP,
@JsonProperty("group")
- GROUP;
+ GROUP,
+ @JsonProperty("serial")
+ SERIAL;
}
diff --git a/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java
index 22e2c1a4d..d1f1e59e8 100644
--- a/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java
+++ b/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java
@@ -14,8 +14,8 @@ public interface EnabledParentStoreProvider extends DataStoreProvider {
@Override
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
- if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) {
- return StoreEntryComp.create(sec.getWrapper(), null, preferLarge);
+ if (sec.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {
+ return StoreEntryComp.create(sec, null, preferLarge);
}
var enabled = StoreToggleComp.>enableToggle(
@@ -35,6 +35,6 @@ public interface EnabledParentStoreProvider extends DataStoreProvider {
}));
}
- return StoreEntryComp.create(sec.getWrapper(), enabled, preferLarge);
+ return StoreEntryComp.create(sec, enabled, preferLarge);
}
}
diff --git a/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java
index cf236e9ad..2a9a4f899 100644
--- a/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java
+++ b/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java
@@ -11,8 +11,8 @@ public interface EnabledStoreProvider extends DataStoreProvider {
@Override
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
- if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) {
- return StoreEntryComp.create(sec.getWrapper(), null, preferLarge);
+ if (sec.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {
+ return StoreEntryComp.create(sec, null, preferLarge);
}
var enabled = StoreToggleComp.>enableToggle(
@@ -20,6 +20,6 @@ public interface EnabledStoreProvider extends DataStoreProvider {
var state = s.getState().toBuilder().enabled(aBoolean).build();
s.setState(state);
});
- return StoreEntryComp.create(sec.getWrapper(), enabled, preferLarge);
+ return StoreEntryComp.create(sec, enabled, preferLarge);
}
}
diff --git a/app/src/main/java/io/xpipe/app/ext/ScanProvider.java b/app/src/main/java/io/xpipe/app/ext/ScanProvider.java
index 3ed8260f4..965f1147e 100644
--- a/app/src/main/java/io/xpipe/app/ext/ScanProvider.java
+++ b/app/src/main/java/io/xpipe/app/ext/ScanProvider.java
@@ -2,10 +2,8 @@ package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellControl;
-import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.ModuleLayerLoader;
-
import lombok.AllArgsConstructor;
import lombok.Value;
@@ -22,10 +20,6 @@ public abstract class ScanProvider {
return ALL;
}
- public ScanOperation create(DataStore store) {
- return null;
- }
-
public ScanOperation create(DataStoreEntry entry, ShellControl sc) throws Exception {
return null;
}
diff --git a/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java
index 0dc04c93d..c856c0121 100644
--- a/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java
+++ b/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java
@@ -30,7 +30,7 @@ public interface SingletonSessionStoreProvider extends DataStoreProvider {
@Override
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
var t = createToggleComp(sec);
- return StoreEntryComp.create(sec.getWrapper(), t, preferLarge);
+ return StoreEntryComp.create(sec, t, preferLarge);
}
default StoreToggleComp createToggleComp(StoreSection sec) {
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/AnchorComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/AnchorComp.java
new file mode 100644
index 000000000..393d30935
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/AnchorComp.java
@@ -0,0 +1,28 @@
+package io.xpipe.app.fxcomps.impl;
+
+import io.xpipe.app.fxcomps.Comp;
+import io.xpipe.app.fxcomps.CompStructure;
+import io.xpipe.app.fxcomps.SimpleCompStructure;
+
+import javafx.scene.layout.AnchorPane;
+
+import java.util.List;
+
+public class AnchorComp extends Comp> {
+
+ private final List> comps;
+
+ public AnchorComp(List> comps) {
+ this.comps = List.copyOf(comps);
+ }
+
+ @Override
+ public CompStructure createBase() {
+ var pane = new AnchorPane();
+ for (var c : comps) {
+ pane.getChildren().add(c.createRegion());
+ }
+ pane.setPickOnBounds(false);
+ return new SimpleCompStructure<>(pane);
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java
index 77d47c38e..50a5b0fdd 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java
@@ -1,5 +1,6 @@
package io.xpipe.app.fxcomps.impl;
+import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.session.BrowserChooserComp;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
@@ -15,39 +16,29 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore;
-
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.value.ObservableValue;
import javafx.scene.control.Alert;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
-
-import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
-import java.util.List;
+import java.util.ArrayList;
public class ContextualFileReferenceChoiceComp extends Comp> {
private final Property> fileSystem;
private final Property filePath;
+ private final boolean allowSync;
public ContextualFileReferenceChoiceComp(
- ObservableValue> fileSystem, Property filePath) {
- this.fileSystem = new SimpleObjectProperty<>();
- fileSystem.subscribe(val -> {
- this.fileSystem.setValue(val);
- });
- this.filePath = filePath;
- }
-
- public ContextualFileReferenceChoiceComp(
- Property> fileSystem, Property filePath) {
+ Property> fileSystem, Property filePath, boolean allowSync
+ ) {
+ this.allowSync = allowSync;
this.fileSystem = new SimpleObjectProperty<>();
fileSystem.subscribe(val -> {
this.fileSystem.setValue(val);
@@ -79,7 +70,7 @@ public class ContextualFileReferenceChoiceComp extends Comp>
},
false);
})
- .styleClass(Styles.CENTER_PILL)
+ .styleClass(allowSync ? Styles.CENTER_PILL : Styles.RIGHT_PILL)
.grow(false, true);
var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> {
@@ -126,7 +117,13 @@ public class ContextualFileReferenceChoiceComp extends Comp>
gitShareButton.tooltipKey("gitShareFileTooltip");
gitShareButton.styleClass(Styles.RIGHT_PILL).grow(false, true);
- var layout = new HorizontalComp(List.of(fileNameComp, fileBrowseButton, gitShareButton))
+ var nodes = new ArrayList>();
+ nodes.add(fileNameComp);
+ nodes.add(fileBrowseButton);
+ if (allowSync) {
+ nodes.add(gitShareButton);
+ }
+ var layout = new HorizontalComp(nodes)
.apply(struc -> struc.get().setFillHeight(true));
layout.apply(struc -> {
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java
index 54560b961..929d87835 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java
@@ -1,7 +1,5 @@
package io.xpipe.app.fxcomps.impl;
-import atlantafx.base.controls.Popover;
-import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.store.*;
import io.xpipe.app.core.AppFont;
@@ -15,6 +13,7 @@ import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.store.ShellStore;
+
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
@@ -26,6 +25,9 @@ import javafx.scene.control.MenuButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
+
+import atlantafx.base.controls.Popover;
+import atlantafx.base.theme.Styles;
import lombok.RequiredArgsConstructor;
import org.kordamp.ikonli.javafx.FontIcon;
@@ -101,9 +103,9 @@ public class DataStoreChoiceComp extends SimpleComp {
comp.disable(new SimpleBooleanProperty(true));
}
},
- storeEntryWrapper -> {
- if (applicable.test(storeEntryWrapper)) {
- selected.setValue(storeEntryWrapper.getEntry().ref());
+ sec -> {
+ if (applicable.test(sec.getWrapper())) {
+ selected.setValue(sec.getWrapper().getEntry().ref());
popover.hide();
}
});
@@ -112,22 +114,31 @@ public class DataStoreChoiceComp extends SimpleComp {
StoreViewState.get().getActiveCategory(),
selectedCategory)
.styleClass(Styles.LEFT_PILL);
- var filter =
- new FilterComp(filterText).styleClass(Styles.CENTER_PILL).hgrow();
+ var filter = new FilterComp(filterText).styleClass(Styles.CENTER_PILL).hgrow();
var addButton = Comp.of(() -> {
MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline"));
+ m.setMaxHeight(100);
+ m.setMinHeight(0);
StoreCreationMenu.addButtons(m);
return m;
})
.accessibleTextKey("addConnection")
- .padding(new Insets(-2))
- .styleClass(Styles.RIGHT_PILL)
- .grow(false, true);
+ .padding(new Insets(-5))
+ .styleClass(Styles.RIGHT_PILL);
- var top = new HorizontalComp(List.of(category, filter.hgrow(), addButton))
+ var top = new HorizontalComp(List.of(category, filter, addButton))
.styleClass("top")
.apply(struc -> struc.get().setFillHeight(true))
+ .apply(struc -> {
+ var first = ((Region) struc.get().getChildren().get(0));
+ var second = ((Region) struc.get().getChildren().get(1));
+ var third = ((Region) struc.get().getChildren().get(1));
+ second.prefHeightProperty().bind(first.heightProperty());
+ second.minHeightProperty().bind(first.heightProperty());
+ second.maxHeightProperty().bind(first.heightProperty());
+ third.prefHeightProperty().bind(first.heightProperty());
+ })
.apply(struc -> {
// Ugly solution to focus the text field
// Somehow this does not work through the normal on shown listeners
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java
index 552ac4ba7..2e901e5e8 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java
@@ -53,7 +53,7 @@ public class DataStoreListChoiceComp extends SimpleComp {
});
return new HorizontalComp(List.of(label, Comp.hspacer(), delete)).styleClass("entry");
},
- true)
+ false)
.padding(new Insets(0))
.apply(struc -> struc.get().setMinHeight(0))
.apply(struc -> ((VBox) struc.get().getContent()).setSpacing(5));
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java
index 4aa431338..8a131e0f2 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java
@@ -1,17 +1,21 @@
package io.xpipe.app.fxcomps.impl;
-import atlantafx.base.controls.CustomTextField;
import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
+
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
-import javafx.geometry.Pos;
import javafx.scene.Cursor;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
+
+import atlantafx.base.controls.CustomTextField;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.Objects;
@@ -36,16 +40,29 @@ public class FilterComp extends Comp> {
}
});
var filter = new CustomTextField();
- filter.alignmentProperty().bind(Bindings.createObjectBinding(() -> {
- return filter.isFocused() || (filter.getText() != null && !filter.getText().isEmpty()) ? Pos.CENTER_LEFT : Pos.CENTER;
- }, filter.textProperty(), filter.focusedProperty()));
+ filter.setMinHeight(0);
filter.setMaxHeight(2000);
filter.getStyleClass().add("filter-comp");
filter.promptTextProperty().bind(AppI18n.observable("searchFilter"));
- filter.setLeft(fi);
- filter.setRight(clear);
+ filter.rightProperty()
+ .bind(Bindings.createObjectBinding(
+ () -> {
+ return filter.isFocused()
+ || (filter.getText() != null
+ && !filter.getText().isEmpty())
+ ? clear
+ : fi;
+ },
+ filter.focusedProperty()));
filter.setAccessibleText("Filter");
+ filter.addEventFilter(KeyEvent.KEY_PRESSED,event -> {
+ if (new KeyCodeCombination(KeyCode.ESCAPE).match(event)) {
+ filter.getScene().getRoot().requestFocus();
+ event.consume();
+ }
+ });
+
filterText.subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
clear.setVisible(val != null);
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/HorizontalComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/HorizontalComp.java
index 1ecb7883f..2ab9f562a 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/HorizontalComp.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/HorizontalComp.java
@@ -4,17 +4,25 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
+import io.xpipe.app.fxcomps.util.DerivedObservableList;
+import io.xpipe.app.fxcomps.util.PlatformThread;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import lombok.AllArgsConstructor;
import java.util.List;
+@AllArgsConstructor
public class HorizontalComp extends Comp> {
- private final List> entries;
+ private final ObservableList> entries;
public HorizontalComp(List> comps) {
- entries = List.copyOf(comps);
+ entries = FXCollections.observableList(List.copyOf(comps));
}
public Comp> spacing(double spacing) {
@@ -25,9 +33,13 @@ public class HorizontalComp extends Comp> {
public CompStructure createBase() {
HBox b = new HBox();
b.getStyleClass().add("horizontal-comp");
- for (var entry : entries) {
- b.getChildren().add(entry.createRegion());
- }
+ var map = new DerivedObservableList<>(entries, false).mapped(comp -> comp.createRegion()).getList();
+ b.getChildren().setAll(map);
+ map.addListener((ListChangeListener super Region>) c -> {
+ PlatformThread.runLaterIfNeeded(() -> {
+ b.getChildren().setAll(c.getList());
+ });
+ });
b.setAlignment(Pos.CENTER);
return new SimpleCompStructure<>(b);
}
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java
index 438247b0e..2bc0961b0 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java
@@ -1,38 +1,42 @@
package io.xpipe.app.fxcomps.impl;
+import atlantafx.base.theme.Styles;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
+import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.fxcomps.util.PlatformThread;
-
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
-import javafx.css.Size;
-import javafx.css.SizeUnits;
import javafx.scene.control.Button;
-import atlantafx.base.theme.Styles;
-import org.kordamp.ikonli.javafx.FontIcon;
-
public class IconButtonComp extends Comp> {
- private final ObservableValue icon;
+ private final ObservableValue extends LabelGraphic> icon;
private final Runnable listener;
public IconButtonComp(String defaultVal) {
+ this(new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(defaultVal)), null);
+ }
+
+ public IconButtonComp(String defaultVal, Runnable listener) {
+ this(new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(defaultVal)), listener);
+ }
+
+ public IconButtonComp(LabelGraphic defaultVal) {
this(new SimpleObjectProperty<>(defaultVal), null);
}
- public IconButtonComp(ObservableValue icon) {
+ public IconButtonComp(ObservableValue extends LabelGraphic> icon) {
this.icon = icon;
this.listener = null;
}
- public IconButtonComp(String defaultVal, Runnable listener) {
+ public IconButtonComp(LabelGraphic defaultVal, Runnable listener) {
this(new SimpleObjectProperty<>(defaultVal), listener);
}
- public IconButtonComp(ObservableValue icon, Runnable listener) {
+ public IconButtonComp(ObservableValue extends LabelGraphic> icon, Runnable listener) {
this.icon = PlatformThread.sync(icon);
this.listener = listener;
}
@@ -42,17 +46,20 @@ public class IconButtonComp extends Comp> {
var button = new Button();
button.getStyleClass().add(Styles.FLAT);
- var fi = new FontIcon(icon.getValue());
- fi.setFocusTraversable(false);
- icon.addListener((c, o, n) -> {
- fi.setIconLiteral(n);
+// var fi = new FontIcon(icon.getValue());
+// fi.setFocusTraversable(false);
+// icon.addListener((c, o, n) -> {
+// fi.setIconLiteral(n);
+// });
+// fi.setIconSize((int) new Size(fi.getFont().getSize(), SizeUnits.PT).pixels());
+// button.fontProperty().addListener((c, o, n) -> {
+// fi.setIconSize((int) new Size(n.getSize(), SizeUnits.PT).pixels());
+// });
+// // fi.iconColorProperty().bind(button.textFillProperty());
+// button.setGraphic(fi);
+ icon.subscribe(labelGraphic -> {
+ button.setGraphic(labelGraphic.createGraphicNode());
});
- fi.setIconSize((int) new Size(fi.getFont().getSize(), SizeUnits.PT).pixels());
- button.fontProperty().addListener((c, o, n) -> {
- fi.setIconSize((int) new Size(n.getSize(), SizeUnits.PT).pixels());
- });
- // fi.iconColorProperty().bind(button.textFillProperty());
- button.setGraphic(fi);
if (listener != null) {
button.setOnAction(e -> {
listener.run();
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/IntComboFieldComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/IntComboFieldComp.java
new file mode 100644
index 000000000..318888a82
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/IntComboFieldComp.java
@@ -0,0 +1,79 @@
+package io.xpipe.app.fxcomps.impl;
+
+import io.xpipe.app.fxcomps.Comp;
+import io.xpipe.app.fxcomps.CompStructure;
+import io.xpipe.app.fxcomps.SimpleCompStructure;
+import io.xpipe.app.fxcomps.util.PlatformThread;
+import javafx.beans.property.Property;
+import javafx.beans.value.ChangeListener;
+import javafx.collections.FXCollections;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.skin.ComboBoxListViewSkin;
+import javafx.scene.input.KeyEvent;
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+import java.util.List;
+
+@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
+public class IntComboFieldComp extends Comp>> {
+
+ Property value;
+ List predefined;
+ boolean allowNegative;
+
+ public IntComboFieldComp(Property value, List predefined, boolean allowNegative) {
+ this.value = value;
+ this.predefined = predefined;
+ this.allowNegative = allowNegative;
+ }
+
+ @Override
+ public CompStructure> createBase() {
+ var text = new ComboBox();
+ text.setEditable(true);
+ text.setValue(value.getValue() != null ? value.getValue().toString() : null);
+ text.setItems(FXCollections.observableList(predefined.stream().map(integer -> "" + integer).toList()));
+ text.setMaxWidth(2000);
+ text.getStyleClass().add("int-combo-field-comp");
+ text.setSkin(new ComboBoxListViewSkin<>(text));
+ text.setVisibleRowCount(Math.min(10, predefined.size()));
+
+ value.addListener((ChangeListener) (observableValue, oldValue, newValue) -> {
+ PlatformThread.runLaterIfNeeded(() -> {
+ if (newValue == null) {
+ text.setValue("");
+ } else {
+ text.setValue(newValue.toString());
+ }
+ });
+ });
+
+ text.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {
+ if (allowNegative) {
+ if (!"-0123456789".contains(keyEvent.getCharacter())) {
+ keyEvent.consume();
+ }
+ } else {
+ if (!"0123456789".contains(keyEvent.getCharacter())) {
+ keyEvent.consume();
+ }
+ }
+ });
+
+ text.valueProperty().addListener((observableValue, oldValue, newValue) -> {
+ if (newValue == null
+ || newValue.isEmpty()
+ || (allowNegative && "-".equals(newValue))
+ || !newValue.matches("-?\\d+")) {
+ value.setValue(null);
+ return;
+ }
+
+ int intValue = Integer.parseInt(newValue);
+ value.setValue(intValue);
+ });
+
+ return new SimpleCompStructure<>(text);
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/IntFieldComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/IntFieldComp.java
index 6c6670015..0beffef90 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/IntFieldComp.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/IntFieldComp.java
@@ -4,12 +4,10 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
-
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
-
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java
index 223d0db1f..573da420c 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java
@@ -33,13 +33,6 @@ public class OptionsComp extends Comp> {
this.entries = entries;
}
- public OptionsComp.Entry queryEntry(String key) {
- return entries.stream()
- .filter(entry -> entry.key != null && entry.key.equals(key))
- .findAny()
- .orElseThrow();
- }
-
@Override
public CompStructure createBase() {
Pane pane;
@@ -70,6 +63,7 @@ public class OptionsComp extends Comp> {
name.getStyleClass().add("name");
name.textProperty().bind(entry.name());
name.setMinWidth(Region.USE_PREF_SIZE);
+ name.setMinHeight(Region.USE_PREF_SIZE);
name.setAlignment(Pos.CENTER_LEFT);
if (compRegion != null) {
name.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty()));
@@ -82,6 +76,7 @@ public class OptionsComp extends Comp> {
description.getStyleClass().add("description");
description.textProperty().bind(entry.description());
description.setAlignment(Pos.CENTER_LEFT);
+ description.setMinHeight(Region.USE_PREF_SIZE);
if (compRegion != null) {
description.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty()));
description.managedProperty().bind(PlatformThread.sync(compRegion.managedProperty()));
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java
index c5919a83d..5312414c6 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java
@@ -12,6 +12,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.DerivedObservableList;
+import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.util.ContextMenuHelper;
@@ -45,14 +46,14 @@ public class StoreCategoryComp extends SimpleComp {
@Override
protected Region createSimple() {
- var i = Bindings.createStringBinding(
+ var i = Bindings.createObjectBinding(
() -> {
if (!DataStorage.get().supportsSharing()
|| !category.getCategory().canShare()) {
- return "mdal-keyboard_arrow_right";
+ return new LabelGraphic.IconGraphic("mdal-keyboard_arrow_right");
}
- return category.getShare().getValue() ? "mdi2a-account-convert" : "mdi2a-account-cancel";
+ return new LabelGraphic.IconGraphic(category.getShare().getValue() ? "mdi2a-account-convert" : "mdi2a-account-cancel");
},
category.getShare());
var icon = new IconButtonComp(i)
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/StringSourceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/StringSourceComp.java
deleted file mode 100644
index 08252bb2c..000000000
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/StringSourceComp.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package io.xpipe.app.fxcomps.impl;
-
-import io.xpipe.app.fxcomps.SimpleComp;
-import io.xpipe.app.storage.DataStoreEntryRef;
-import io.xpipe.app.util.StringSource;
-import io.xpipe.core.store.ShellStore;
-
-import javafx.beans.property.Property;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.value.ObservableValue;
-import javafx.scene.layout.AnchorPane;
-import javafx.scene.layout.Region;
-import javafx.scene.layout.StackPane;
-
-public class StringSourceComp extends SimpleComp {
-
- private final Property> fileSystem;
- private final Property stringSource;
-
- public StringSourceComp(
- ObservableValue> fileSystem, Property stringSource) {
- this.stringSource = stringSource;
- this.fileSystem = new SimpleObjectProperty<>();
- fileSystem.subscribe(val -> {
- this.fileSystem.setValue(val.get().ref());
- });
- }
-
- @Override
- protected Region createSimple() {
- var inPlace =
- new SimpleObjectProperty<>(stringSource.getValue() instanceof StringSource.InPlace i ? i.get() : null);
- var fs = stringSource.getValue() instanceof StringSource.File f ? f.getFile() : null;
- var file = new SimpleObjectProperty<>(
- stringSource.getValue() instanceof StringSource.File f
- ? f.getFile().serialize()
- : null);
- var showText = new SimpleBooleanProperty(inPlace.get() != null);
-
- var stringField = new TextAreaComp(inPlace);
- stringField.hide(showText.not());
- var fileComp = new ContextualFileReferenceChoiceComp(fileSystem, file);
- fileComp.hide(showText);
-
- var tr = stringField.createRegion();
- var button = new IconButtonComp("mdi2c-checkbox-marked-outline", () -> {
- showText.set(!showText.getValue());
- })
- .createRegion();
- AnchorPane.setBottomAnchor(button, 10.0);
- AnchorPane.setRightAnchor(button, 10.0);
- var anchorPane = new AnchorPane(tr, button);
- AnchorPane.setBottomAnchor(tr, 0.0);
- AnchorPane.setTopAnchor(tr, 0.0);
- AnchorPane.setLeftAnchor(tr, 0.0);
- AnchorPane.setRightAnchor(tr, 0.0);
-
- var fr = fileComp.createRegion();
-
- return new StackPane(tr, fr);
- }
-}
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java
index e7c7aeabf..67d820806 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java
@@ -10,6 +10,7 @@ import javafx.beans.value.ObservableValue;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCombination;
import javafx.stage.Window;
+import javafx.util.Duration;
public class TooltipAugment
> implements Augment {
@@ -45,6 +46,7 @@ public class TooltipAugment> implements Augment {
tt.setWrapText(true);
tt.setMaxWidth(400);
tt.getStyleClass().add("fancy-tooltip");
+ tt.setHideDelay(Duration.INDEFINITE);
Tooltip.install(struc.get(), tt);
}
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/LabelGraphic.java b/app/src/main/java/io/xpipe/app/fxcomps/util/LabelGraphic.java
index c16e46484..ab04874b9 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/util/LabelGraphic.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/util/LabelGraphic.java
@@ -1,9 +1,8 @@
package io.xpipe.app.fxcomps.util;
import io.xpipe.app.fxcomps.Comp;
+import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import lombok.EqualsAndHashCode;
@@ -12,8 +11,14 @@ import org.kordamp.ikonli.javafx.FontIcon;
public abstract class LabelGraphic {
- public static ObservableValue fixedIcon(String icon) {
- return new SimpleObjectProperty<>(new IconGraphic(icon));
+ public static LabelGraphic none() {
+ return new LabelGraphic() {
+
+ @Override
+ public Node createGraphicNode() {
+ return null;
+ }
+ };
}
public abstract Node createGraphicNode();
@@ -30,6 +35,19 @@ public abstract class LabelGraphic {
}
}
+ @Value
+ @EqualsAndHashCode(callSuper = true)
+ public static class ImageGraphic extends LabelGraphic {
+
+ String file;
+ int size;
+
+ @Override
+ public Node createGraphicNode() {
+ return PrettyImageHelper.ofFixedSizeSquare(file, size).createRegion();
+ }
+ }
+
@Value
@EqualsAndHashCode(callSuper = true)
public static class CompGraphic extends LabelGraphic {
diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java b/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java
index 6ffc3d887..c05a554bc 100644
--- a/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java
+++ b/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java
@@ -3,6 +3,7 @@ package io.xpipe.app.fxcomps.util;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.PlatformState;
+
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -11,6 +12,7 @@ import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
+
import lombok.NonNull;
import java.util.*;
diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java
index b6c983abe..210afe012 100644
--- a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java
+++ b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java
@@ -57,7 +57,7 @@ public class ErrorEvent {
return EVENT_BASES.remove(t).description(msg);
}
- return builder().throwable(t).description(msg + (t.getMessage() != null ? "\n\n" + t.getMessage() : ""));
+ return builder().throwable(t).description(msg + (t.getMessage() != null ? "\n\n" + t.getMessage().trim() : ""));
}
public static ErrorEventBuilder fromMessage(String msg) {
diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java
index 9f5caae2a..d5054accc 100644
--- a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java
+++ b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java
@@ -196,6 +196,7 @@ public class ErrorHandlerComp extends SimpleComp {
if (desc == null) {
desc = AppI18n.get("errorNoDetail");
}
+ desc = desc.trim();
var graphic = new FontIcon("mdomz-warning");
graphic.setIconColor(Color.RED);
@@ -204,7 +205,7 @@ public class ErrorHandlerComp extends SimpleComp {
header.setGraphicTextGap(6);
AppFont.setSize(header, 3);
var descriptionField = new TextArea(desc);
- descriptionField.setPrefRowCount(6);
+ descriptionField.setPrefRowCount(Math.max(5, Math.min((int) desc.lines().count(), 14)));
descriptionField.setWrapText(true);
descriptionField.setEditable(false);
descriptionField.setPadding(Insets.EMPTY);
diff --git a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java
index 9099461b9..3359ce6cb 100644
--- a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java
+++ b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java
@@ -82,7 +82,7 @@ public class SentryErrorHandler implements ErrorHandler {
causeField.set(copy, adjustCopy(throwable.getCause(), true));
return copy;
- } catch (Exception e) {
+ } catch (Throwable e) {
// This can fail for example when the underlying exception is not serializable
// and comes from some third party library
if (AppLogs.get() != null) {
diff --git a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java
index 689125423..3c4d9f8f7 100644
--- a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java
+++ b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java
@@ -98,9 +98,11 @@ public class LauncherCommand implements Callable {
}
} catch (Exception ex) {
var cli = XPipeInstallation.getLocalDefaultCliExecutable();
- ErrorEvent.fromThrowable("Unable to connect to existing running daemon instance as it did not respond."
- + " Either try to kill the process xpiped manually or use the command \"" + cli
- + "\" daemon stop --force.", ex)
+ ErrorEvent.fromThrowable(
+ "Unable to connect to existing running daemon instance as it did not respond."
+ + " Either try to kill the process xpiped manually or use the command \"" + cli
+ + "\" daemon stop --force.",
+ ex)
.term()
.expected()
.handle();
@@ -127,9 +129,16 @@ public class LauncherCommand implements Callable {
// there might be another instance running, for example
// starting up or listening on another port
if (!AppDataLock.lock()) {
- TrackEvent.info(
- "Data directory " + AppProperties.get().getDataDir().toString()
- + " is already locked. Is another instance running?");
+ TrackEvent.info("Data directory " + AppProperties.get().getDataDir().toString()
+ + " is already locked. Is another instance running?");
+ OperationMode.halt(1);
+ }
+
+ // If an instance is running as another user, we cannot connect to it as the xpipe_auth file is inaccessible
+ // Therefore the beacon client is not present.
+ // We still should check whether it is somehow occupied, otherwise beacon server startup will fail
+ if (BeaconClient.isOccupied(port)) {
+ TrackEvent.info("Another instance is already running on this port as another user. Quitting ...");
OperationMode.halt(1);
}
}
diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java
index 3e94b72b9..c54aaff58 100644
--- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java
+++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java
@@ -14,18 +14,16 @@ import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.PasswordLockSecretValue;
import io.xpipe.core.util.InPlaceSecretValue;
import io.xpipe.core.util.ModuleHelper;
-
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableDoubleValue;
import javafx.beans.value.ObservableStringValue;
import javafx.beans.value.ObservableValue;
-
import lombok.Getter;
import lombok.Value;
+import org.apache.commons.io.FileUtils;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;
@@ -33,7 +31,7 @@ import java.util.stream.Stream;
public class AppPrefs {
public static final Path DEFAULT_STORAGE_DIR =
- AppProperties.get().getDataDir().resolve("storage");
+ AppProperties.get() != null ? AppProperties.get().getDataDir().resolve("storage") : null;
private static final String DEVELOPER_MODE_PROP = "io.xpipe.app.developerMode";
private static AppPrefs INSTANCE;
private final List> mapping = new ArrayList<>();
@@ -111,6 +109,9 @@ public class AppPrefs {
map(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class);
private final ObservableBooleanValue developerDisableGuiRestrictionsEffective =
bindDeveloperTrue(developerDisableGuiRestrictions);
+ final BooleanProperty developerForceSshTty =
+ map(new SimpleBooleanProperty(false), "developerForceSshTty", Boolean.class);
+
final ObjectProperty language =
map(new SimpleObjectProperty<>(SupportedLocale.getEnglish()), "language", SupportedLocale.class);
@@ -175,6 +176,7 @@ public class AppPrefs {
new SecurityCategory(),
new HttpApiCategory(),
new WorkflowCategory(),
+ new WorkspacesCategory(),
new TroubleshootCategory(),
new DeveloperCategory())
.filter(appPrefsCategory -> appPrefsCategory.show())
@@ -436,6 +438,10 @@ public class AppPrefs {
return developerDisableGuiRestrictionsEffective;
}
+ public ObservableBooleanValue developerForceSshTty() {
+ return bindDeveloperTrue(developerForceSshTty);
+ }
+
@SuppressWarnings("unchecked")
private T map(T o, String name, Class> clazz) {
mapping.add(new Mapping<>(name, (Property) o, (Class) clazz));
@@ -489,7 +495,7 @@ public class AppPrefs {
}
try {
- Files.createDirectories(storageDirectory.get());
+ FileUtils.forceMkdir(storageDirectory.getValue().toFile());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().build().handle();
storageDirectory.setValue(DEFAULT_STORAGE_DIR);
diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java
index bd8ea3c1f..26332908a 100644
--- a/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java
+++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java
@@ -41,9 +41,9 @@ public class AppPrefsComp extends SimpleComp {
pfxLimit.setAlignment(Pos.TOP_LEFT);
var sidebar = new AppPrefsSidebarComp().createRegion();
- sidebar.setMinWidth(350);
- sidebar.setPrefWidth(350);
- sidebar.setMaxWidth(350);
+ sidebar.setMinWidth(280);
+ sidebar.setPrefWidth(280);
+ sidebar.setMaxWidth(280);
var split = new HBox(sidebar, pfxLimit);
HBox.setHgrow(pfxLimit, Priority.ALWAYS);
diff --git a/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java b/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java
index 3876d4cf4..11dde276b 100644
--- a/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java
+++ b/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java
@@ -61,6 +61,8 @@ public class DeveloperCategory extends AppPrefsCategory {
.sub(new OptionsBuilder()
.nameAndDescription("developerDisableUpdateVersionCheck")
.addToggle(prefs.developerDisableUpdateVersionCheck)
+ .nameAndDescription("developerForceSshTty")
+ .addToggle(prefs.developerForceSshTty)
.nameAndDescription("developerDisableGuiRestrictions")
.addToggle(prefs.developerDisableGuiRestrictions)
.nameAndDescription("shellCommandTest")
diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java
index 82153394e..c32dd4254 100644
--- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java
+++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java
@@ -114,14 +114,12 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
protected Optional determineFromPath() {
// Try to locate if it is in the Path
- try (var cc = LocalShell.getShell()
- .command(CommandBuilder.ofFunction(
- var1 -> var1.getShellDialect().getWhichCommand(executable)))
+ try (var sc = LocalShell.getShell()
.start()) {
- var out = cc.readStdoutDiscardErr();
- var exit = cc.getExitCode();
- if (exit == 0) {
- var first = out.lines().findFirst();
+ var out = sc.command(CommandBuilder.ofFunction(
+ var1 -> var1.getShellDialect().getWhichCommand(executable))).readStdoutIfPossible();
+ if (out.isPresent()) {
+ var first = out.get().lines().findFirst();
if (first.isPresent()) {
return first.map(String::trim).map(Path::of);
}
diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java
index 52ee01170..c5a3d5b74 100644
--- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java
+++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java
@@ -78,6 +78,10 @@ public interface ExternalEditorType extends PrefsChoiceValue {
LinuxPathType VSCODE_LINUX = new LinuxPathType("app.vscode", "code");
+ LinuxPathType ZED_LINUX = new LinuxPathType("app.zed", "zed");
+
+ ExternalEditorType ZED_MACOS = new MacOsEditor("app.zed", "Zed");
+
LinuxPathType VSCODIUM_LINUX = new LinuxPathType("app.vscodium", "codium");
LinuxPathType GNOME = new LinuxPathType("app.gnomeTextEditor", "gnome-text-editor");
@@ -124,8 +128,9 @@ public interface ExternalEditorType extends PrefsChoiceValue {
List WINDOWS_EDITORS =
List.of(VSCODIUM_WINDOWS, VSCODE_INSIDERS_WINDOWS, VSCODE_WINDOWS, NOTEPADPLUSPLUS, NOTEPAD);
List LINUX_EDITORS =
- List.of(VSCODIUM_LINUX, VSCODE_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME);
- List MACOS_EDITORS = List.of(BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, TEXT_EDIT);
+ List.of(VSCODIUM_LINUX, VSCODE_LINUX, ZED_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME);
+ List MACOS_EDITORS =
+ List.of(BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, ZED_MACOS, TEXT_EDIT);
List CROSS_PLATFORM_EDITORS = List.of(FLEET, INTELLIJ, PYCHARM, WEBSTORM, CLION);
@SuppressWarnings("TrivialFunctionalExpressionUsage")
diff --git a/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java
index 407c38928..da3bf2c0f 100644
--- a/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java
+++ b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java
@@ -2,7 +2,6 @@ package io.xpipe.app.prefs;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
-import io.xpipe.app.core.AppProperties;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.DesktopHelper;
@@ -21,10 +20,8 @@ public class SyncCategory extends AppPrefsCategory {
builder.addTitle("sync")
.sub(new OptionsBuilder()
.name("enableGitStorage")
- .description(
- AppProperties.get().isStaging() && !prefs.developerMode().getValue() ? "enableGitStoragePtbDisabled" : "enableGitStorage")
+ .description("enableGitStorageDescription")
.addToggle(prefs.enableGitStorage)
- .disable(AppProperties.get().isStaging() && !prefs.developerMode().getValue())
.nameAndDescription("storageGitRemote")
.addString(prefs.storageGitRemote, true)
.disable(prefs.enableGitStorage.not())
diff --git a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java
index 86e98c7d4..cba1d6179 100644
--- a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java
+++ b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java
@@ -36,8 +36,6 @@ public class VaultCategory extends AppPrefsCategory {
}
builder.addTitle("vaultSecurity")
.sub(new OptionsBuilder()
- .nameAndDescription("encryptAllVaultData")
- .addToggle(prefs.encryptAllVaultData)
.nameAndDescription("workspaceLock")
.addComp(
new ButtonComp(
@@ -57,7 +55,9 @@ public class VaultCategory extends AppPrefsCategory {
.addToggle(prefs.lockVaultOnHibernation)
.hide(prefs.getLockCrypt()
.isNull()
- .or(prefs.getLockCrypt().isEmpty())));
+ .or(prefs.getLockCrypt().isEmpty()))
+ .nameAndDescription("encryptAllVaultData")
+ .addToggle(prefs.encryptAllVaultData));
return builder.buildComp();
}
}
diff --git a/app/src/main/java/io/xpipe/app/prefs/WorkspaceCreationAlert.java b/app/src/main/java/io/xpipe/app/prefs/WorkspaceCreationAlert.java
new file mode 100644
index 000000000..3ff605f35
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/prefs/WorkspaceCreationAlert.java
@@ -0,0 +1,75 @@
+package io.xpipe.app.prefs;
+
+import io.xpipe.app.core.AppFont;
+import io.xpipe.app.core.AppI18n;
+import io.xpipe.app.core.AppProperties;
+import io.xpipe.app.core.mode.OperationMode;
+import io.xpipe.app.core.window.AppWindowHelper;
+import io.xpipe.app.issue.ErrorEvent;
+import io.xpipe.app.util.DesktopHelper;
+import io.xpipe.app.util.DesktopShortcuts;
+import io.xpipe.app.util.OptionsBuilder;
+import io.xpipe.app.util.ThreadHelper;
+import io.xpipe.core.process.OsType;
+import io.xpipe.core.util.XPipeInstallation;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.geometry.Insets;
+import javafx.scene.control.ButtonType;
+import org.apache.commons.io.FileUtils;
+
+import java.nio.file.Files;
+
+public class WorkspaceCreationAlert {
+
+ public static void showAsync() {
+ ThreadHelper.runFailableAsync(() -> {
+ show();
+ });
+ }
+
+ private static void show() throws Exception {
+ var name = new SimpleObjectProperty<>("New workspace");
+ var path = new SimpleObjectProperty<>(AppProperties.get().getDataDir());
+ var show = AppWindowHelper.showBlockingAlert(alert -> {
+ alert.setTitle(AppI18n.get("workspaceCreationAlertTitle"));
+ var content = new OptionsBuilder()
+ .nameAndDescription("workspaceName")
+ .addString(name)
+ .nameAndDescription("workspacePath")
+ .addPath(path)
+ .buildComp()
+ .minWidth(500)
+ .padding(new Insets(5, 20, 20, 20))
+ .apply(struc -> AppFont.small(struc.get()))
+ .createRegion();
+ alert.getButtonTypes().add(ButtonType.CANCEL);
+ alert.getButtonTypes().add(ButtonType.OK);
+ alert.getDialogPane().setContent(content);
+ })
+ .map(b -> b.getButtonData().isDefaultButton())
+ .orElse(false);
+
+ if (!show || name.get() == null || path.get() == null) {
+ return;
+ }
+
+ if (Files.exists(path.get()) && !FileUtils.isEmptyDirectory(path.get().toFile())) {
+ ErrorEvent.fromMessage("New workspace directory is not empty").expected().handle();
+ return;
+ }
+
+ var shortcutName = (AppProperties.get().isStaging() ? "XPipe PTB" : "XPipe") + " (" + name.get() + ")";
+ var file = switch (OsType.getLocal()) {
+ case OsType.Windows w -> {
+ var exec = XPipeInstallation.getCurrentInstallationBasePath().resolve(XPipeInstallation.getDaemonExecutablePath(w)).toString();
+ yield DesktopShortcuts.create(exec, "-Dio.xpipe.app.dataDir=\"" + path.get().toString() + "\" -Dio.xpipe.app.acceptEula=true", shortcutName);
+ }
+ default -> {
+ var exec = XPipeInstallation.getCurrentInstallationBasePath().resolve(XPipeInstallation.getRelativeCliExecutablePath(OsType.getLocal())).toString();
+ yield DesktopShortcuts.create(exec, "-d \"" + path.get().toString() + "\" --accept-eula", shortcutName);
+ }
+ };
+ DesktopHelper.browseFileInDirectory(file);
+ OperationMode.close();
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/prefs/WorkspacesCategory.java b/app/src/main/java/io/xpipe/app/prefs/WorkspacesCategory.java
new file mode 100644
index 000000000..2c186e008
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/prefs/WorkspacesCategory.java
@@ -0,0 +1,26 @@
+package io.xpipe.app.prefs;
+
+import io.xpipe.app.comp.base.ButtonComp;
+import io.xpipe.app.core.AppI18n;
+import io.xpipe.app.fxcomps.Comp;
+import io.xpipe.app.util.OptionsBuilder;
+
+public class WorkspacesCategory extends AppPrefsCategory {
+
+ @Override
+ protected String getId() {
+ return "workspaces";
+ }
+
+ @Override
+ protected Comp> create() {
+ return new OptionsBuilder()
+ .addTitle("manageWorkspaces")
+ .sub(new OptionsBuilder()
+ .nameAndDescription("workspaceAdd")
+ .addComp(
+ new ButtonComp(AppI18n.observable("addWorkspace"),
+ WorkspaceCreationAlert::showAsync)))
+ .buildComp();
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/storage/ContextualFileReference.java b/app/src/main/java/io/xpipe/app/storage/ContextualFileReference.java
index 063d4c8e3..e12bb513b 100644
--- a/app/src/main/java/io/xpipe/app/storage/ContextualFileReference.java
+++ b/app/src/main/java/io/xpipe/app/storage/ContextualFileReference.java
@@ -23,7 +23,7 @@ public class ContextualFileReference {
private static String getDataDir() {
if (DataStorage.get() == null) {
- return lastDataDir != null ? lastDataDir : normalized(AppPrefs.DEFAULT_STORAGE_DIR);
+ return lastDataDir != null ? lastDataDir : normalized(AppPrefs.DEFAULT_STORAGE_DIR.resolve("data"));
}
return lastDataDir = normalized(DataStorage.get().getDataDir());
diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java
index 2573e747c..4cc3d1106 100644
--- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java
+++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java
@@ -273,6 +273,13 @@ public abstract class DataStorage {
}
public void updateEntry(DataStoreEntry entry, DataStoreEntry newEntry) {
+ var state = entry.getStorePersistentState();
+ var nState = newEntry.getStorePersistentState();
+ if (state != null && nState != null) {
+ var updatedState = state.mergeCopy(nState);
+ newEntry.setStorePersistentState(updatedState);
+ }
+
var oldParent = DataStorage.get().getDefaultDisplayParent(entry);
var newParent = DataStorage.get().getDefaultDisplayParent(newEntry);
var sameParent = Objects.equals(oldParent, newParent);
@@ -341,18 +348,20 @@ public abstract class DataStorage {
@SneakyThrows
public boolean refreshChildren(DataStoreEntry e) {
- return refreshChildren(e,false);
+ return refreshChildren(e, false);
}
public boolean refreshChildren(DataStoreEntry e, boolean throwOnFail) throws Exception {
- if (!(e.getStore() instanceof FixedHierarchyStore)) {
+ if (!(e.getStore() instanceof FixedHierarchyStore h)) {
return false;
}
e.incrementBusyCounter();
List extends DataStoreEntryRef extends FixedChildStore>> newChildren;
try {
- newChildren = ((FixedHierarchyStore) (e.getStore())).listChildren(e).stream().filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null).toList();
+ newChildren = h.listChildren(e).stream()
+ .filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null)
+ .toList();
} catch (Exception ex) {
if (throwOnFail) {
throw ex;
@@ -368,6 +377,10 @@ public abstract class DataStorage {
var toRemove = oldChildren.stream()
.filter(oc -> oc.getStore() instanceof FixedChildStore)
.filter(oc -> {
+ if (!oc.getValidity().isUsable()) {
+ return true;
+ }
+
var oid = ((FixedChildStore) oc.getStore()).getFixedId();
if (oid.isEmpty()) {
return false;
@@ -394,6 +407,7 @@ public abstract class DataStorage {
return oldChildren.stream()
.filter(oc -> oc.getStore() instanceof FixedChildStore)
+ .filter(oc -> oc.getValidity().isUsable())
.filter(oc -> ((FixedChildStore) oc.getStore())
.getFixedId()
.isPresent())
@@ -407,6 +421,7 @@ public abstract class DataStorage {
.toList();
var toUpdate = oldChildren.stream()
.filter(oc -> oc.getStore() instanceof FixedChildStore)
+ .filter(oc -> oc.getValidity().isUsable())
.map(oc -> {
var oid = ((FixedChildStore) oc.getStore()).getFixedId();
if (oid.isEmpty()) {
@@ -433,7 +448,9 @@ public abstract class DataStorage {
nc.get().notifyUpdate(false, true);
});
- deleteWithChildren(toRemove.toArray(DataStoreEntry[]::new));
+ if (h.removeLeftovers()) {
+ deleteWithChildren(toRemove.toArray(DataStoreEntry[]::new));
+ }
addStoreEntriesIfNotPresent(toAdd.stream().map(DataStoreEntryRef::get).toArray(DataStoreEntry[]::new));
toUpdate.forEach(pair -> {
// Update state by merging
@@ -460,10 +477,11 @@ public abstract class DataStorage {
});
refreshEntries();
saveAsync();
+ e.getProvider().onChildrenRefresh(e);
toAdd.forEach(dataStoreEntryRef ->
- dataStoreEntryRef.get().getProvider().onChildrenRefresh(dataStoreEntryRef.getEntry()));
+ dataStoreEntryRef.get().getProvider().onParentRefresh(dataStoreEntryRef.getEntry()));
toUpdate.forEach(dataStoreEntryRef ->
- dataStoreEntryRef.getKey().getProvider().onChildrenRefresh(dataStoreEntryRef.getKey()));
+ dataStoreEntryRef.getKey().getProvider().onParentRefresh(dataStoreEntryRef.getKey()));
return !newChildren.isEmpty();
}
@@ -865,24 +883,16 @@ public abstract class DataStorage {
.findFirst();
}
- public Optional getStoreDisplayName(DataStore store) {
- if (store == null) {
- return Optional.empty();
- }
-
- return getStoreEntryIfPresent(store, true).map(dataStoreEntry -> dataStoreEntry.getName());
- }
-
- public String getStoreDisplayName(DataStoreEntry store) {
- if (store == null) {
+ public String getStoreEntryDisplayName(DataStoreEntry entry) {
+ if (entry == null) {
return "?";
}
- if (!store.getValidity().isUsable()) {
+ if (!entry.getValidity().isUsable()) {
return "?";
}
- return store.getProvider().browserDisplayName(store.getStore());
+ return entry.getProvider().displayName(entry);
}
public Optional getStoreEntryIfPresent(UUID id) {
diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java
index 35ceb6fee..9e108efcf 100644
--- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java
+++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java
@@ -101,9 +101,7 @@ public class DataStoreEntry extends StorageElement {
this.expanded = expanded;
this.color = color;
this.explicitOrder = explicitOrder;
- this.provider = store != null
- ? DataStoreProviders.byStore(store)
- : null;
+ this.provider = store != null ? DataStoreProviders.byStore(store) : null;
this.storePersistentStateNode = storePersistentState;
this.notes = notes;
}
@@ -159,7 +157,7 @@ public class DataStoreEntry extends StorageElement {
null,
uuid,
categoryUuid,
- name,
+ name.trim(),
Instant.now(),
Instant.now(),
storeFromNode,
@@ -196,7 +194,7 @@ public class DataStoreEntry extends StorageElement {
var categoryUuid = Optional.ofNullable(json.get("categoryUuid"))
.map(jsonNode -> UUID.fromString(jsonNode.textValue()))
.orElse(DataStorage.DEFAULT_CATEGORY_UUID);
- var name = json.required("name").textValue();
+ var name = json.required("name").textValue().trim();
var persistentState = stateJson.get("persistentState");
var lastUsed = Optional.ofNullable(stateJson.get("lastUsed"))
@@ -259,7 +257,7 @@ public class DataStoreEntry extends StorageElement {
store,
storeNode,
false,
- Validity.INCOMPLETE,
+ store == null ? Validity.LOAD_FAILED : Validity.INCOMPLETE,
configuration,
persistentState,
expanded,
@@ -452,6 +450,7 @@ public class DataStoreEntry extends StorageElement {
this.store = store;
this.storeNode = JacksonMapper.getDefault().valueToTree(store);
+ this.provider = DataStoreProviders.byStore(store);
if (updateTime) {
lastModified = Instant.now();
}
@@ -500,6 +499,8 @@ public class DataStoreEntry extends StorageElement {
DataStore newStore;
try {
newStore = JacksonMapper.getDefault().treeToValue(storeNode, DataStore.class);
+ // Check whether we have a provider as well
+ DataStoreProviders.byStore(newStore);
} catch (Throwable e) {
ErrorEvent.fromThrowable(e).handle();
newStore = null;
diff --git a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java
index 0a2d46f4a..9191c2f91 100644
--- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java
+++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java
@@ -1,14 +1,14 @@
package io.xpipe.app.storage;
-import com.fasterxml.jackson.core.JacksonException;
import io.xpipe.app.ext.DataStorageExtensionProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.LocalStore;
-
import io.xpipe.core.util.JacksonMapper;
+
+import com.fasterxml.jackson.core.JacksonException;
import lombok.Getter;
import org.apache.commons.io.FileUtils;
@@ -53,19 +53,27 @@ public class StandardStorage extends DataStorage {
try {
FileUtils.forceMkdir(dir.toFile());
} catch (Exception e) {
- ErrorEvent.fromThrowable("Unable to create vault directory", e).terminal(true).build().handle();
+ ErrorEvent.fromThrowable("Unable to create vault directory", e)
+ .terminal(true)
+ .build()
+ .handle();
}
try {
initSystemInfo();
} catch (Exception e) {
- ErrorEvent.fromThrowable("Unable to load vault system info", e).build().handle();
+ ErrorEvent.fromThrowable("Unable to load vault system info", e)
+ .build()
+ .handle();
}
try {
initVaultKey();
} catch (Exception e) {
- ErrorEvent.fromThrowable("Unable to load vault key file", e).terminal(true).build().handle();
+ ErrorEvent.fromThrowable("Unable to load vault key file", e)
+ .terminal(true)
+ .build()
+ .handle();
}
var storesDir = getStoresDir();
@@ -76,7 +84,10 @@ public class StandardStorage extends DataStorage {
FileUtils.forceMkdir(categoriesDir.toFile());
FileUtils.forceMkdir(dataDir.toFile());
} catch (Exception e) {
- ErrorEvent.fromThrowable("Unable to create vault directory", e).terminal(true).build().handle();
+ ErrorEvent.fromThrowable("Unable to create vault directory", e)
+ .terminal(true)
+ .build()
+ .handle();
}
try {
@@ -215,17 +226,26 @@ public class StandardStorage extends DataStorage {
local.setColor(DataStoreColor.BLUE);
}
- callProviders();
+ // Reload stores, this time with all entry refs present
+ // These do however not have a completed validity yet
refreshEntries();
+ // Bring entries into completed validity if possible
+ // Needed for chained stores
+ refreshEntries();
+ // Let providers work on complete stores
+ callProviders();
+ // Update validaties after any possible changes
+ refreshEntries();
+ // Add any possible missing synthetic parents
storeEntriesSet.forEach(entry -> {
var syntheticParent = getSyntheticParent(entry);
syntheticParent.ifPresent(entry1 -> {
addStoreEntryIfNotPresent(entry1);
});
});
+ // Update validaties from synthetic parent I changes
refreshEntries();
- // Save to apply changes
if (!hasFixedLocal) {
storeEntriesSet.removeIf(dataStoreEntry ->
!dataStoreEntry.getUuid().equals(LOCAL_ID) && dataStoreEntry.getStore() instanceof LocalStore);
@@ -235,6 +255,7 @@ public class StandardStorage extends DataStorage {
entry.dirty = true;
entry.setStoreNode(JacksonMapper.getDefault().valueToTree(entry.getStore()));
});
+ // Save to apply changes
save(false);
}
@@ -416,7 +437,7 @@ public class StandardStorage extends DataStorage {
var s = Files.readString(file);
vaultKey = new String(Base64.getDecoder().decode(s), StandardCharsets.UTF_8);
} else {
- Files.createDirectories(dir);
+ FileUtils.forceMkdir(dir.toFile());
vaultKey = UUID.randomUUID().toString();
Files.writeString(file, Base64.getEncoder().encodeToString(vaultKey.getBytes(StandardCharsets.UTF_8)));
}
@@ -437,7 +458,7 @@ public class StandardStorage extends DataStorage {
Files.writeString(file, s);
}
} else {
- Files.createDirectories(dir);
+ FileUtils.forceMkdir(dir.toFile());
var s = OsType.getLocal().getName();
Files.writeString(file, s);
}
diff --git a/app/src/main/java/io/xpipe/app/update/AppDownloads.java b/app/src/main/java/io/xpipe/app/update/AppDownloads.java
index 27fa9550b..9e5f03064 100644
--- a/app/src/main/java/io/xpipe/app/update/AppDownloads.java
+++ b/app/src/main/java/io/xpipe/app/update/AppDownloads.java
@@ -91,7 +91,7 @@ public class AppDownloads {
var changelog = json.required("changelog").asText();
return Optional.of(changelog);
} catch (Throwable t) {
- ErrorEvent.fromThrowable(t).omit().handle();
+ ErrorEvent.fromThrowable(t).omit().expected().handle();
}
try {
diff --git a/app/src/main/java/io/xpipe/app/update/AppInstaller.java b/app/src/main/java/io/xpipe/app/update/AppInstaller.java
index 3a4497ca0..065904c28 100644
--- a/app/src/main/java/io/xpipe/app/update/AppInstaller.java
+++ b/app/src/main/java/io/xpipe/app/update/AppInstaller.java
@@ -2,13 +2,17 @@ package io.xpipe.app.update;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.core.AppProperties;
+import io.xpipe.app.core.mode.OperationMode;
+import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.TerminalLauncher;
+import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.LocalStore;
+import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.XPipeInstallation;
import com.fasterxml.jackson.annotation.JsonSubTypes;
@@ -21,10 +25,6 @@ import java.nio.file.Path;
public class AppInstaller {
- public static void installFileLocal(InstallerAssetType asset, Path localFile) throws Exception {
- asset.installLocal(localFile.toString());
- }
-
public static InstallerAssetType getSuitablePlatformAsset() {
if (OsType.getLocal().equals(OsType.WINDOWS)) {
return new InstallerAssetType.Msi();
@@ -53,7 +53,18 @@ public class AppInstaller {
})
public abstract static class InstallerAssetType {
- public abstract void installLocal(String file) throws Exception;
+ protected void runAndClose(FailableRunnable r) {
+ OperationMode.executeAfterShutdown(() -> {
+ r.run();
+
+ // In case we perform any operations such as opening a terminal
+ // give it some time to open while this process is still alive
+ // Otherwise it might quit because the parent process is dead already
+ ThreadHelper.sleep(100);
+ });
+ }
+
+ public abstract void installLocal(Path file) throws Exception;
public boolean isCorrectAsset(String name) {
return name.endsWith(getExtension())
@@ -66,7 +77,7 @@ public class AppInstaller {
public static final class Msi extends InstallerAssetType {
@Override
- public void installLocal(String file) throws Exception {
+ public void installLocal(Path file) throws Exception {
var shellProcessControl = new LocalStore().control().start();
var exec = (AppProperties.get().isDevelopmentEnvironment()
? Path.of(XPipeInstallation.getLocalDefaultInstallationBasePath())
@@ -75,15 +86,19 @@ public class AppInstaller {
.toString();
var logsDir =
AppLogs.get().getSessionLogsDirectory().getParent().toString();
- var logFile = FileNames.join(logsDir, "installer_" + FileNames.getFileName(file) + ".log");
+ var logFile = FileNames.join(
+ logsDir, "installer_" + file.getFileName().toString() + ".log");
var command = LocalShell.getShell().getShellDialect().equals(ShellDialects.CMD)
- ? getCmdCommand(file, logFile, exec)
- : getPowershellCommand(file, logFile, exec);
+ ? getCmdCommand(file.toString(), logFile, exec)
+ : getPowershellCommand(file.toString(), logFile, exec);
var toRun = LocalShell.getShell().getShellDialect().equals(ShellDialects.CMD)
? "start \"XPipe Updater\" /min cmd /c \"" + ScriptHelper.createLocalExecScript(command) + "\""
: "Start-Process -WindowStyle Minimized -FilePath powershell -ArgumentList \"-ExecutionPolicy\", \"Bypass\", \"-File\", \"`\""
+ ScriptHelper.createLocalExecScript(command) + "`\"\"";
- shellProcessControl.executeSimpleCommand(toRun);
+
+ runAndClose(() -> {
+ shellProcessControl.executeSimpleCommand(toRun);
+ });
}
@Override
@@ -124,7 +139,14 @@ public class AppInstaller {
public static final class Debian extends InstallerAssetType {
@Override
- public void installLocal(String file) throws Exception {
+ public void installLocal(Path file) throws Exception {
+ var start = AppPrefs.get() != null
+ && AppPrefs.get().terminalType().getValue() != null
+ && AppPrefs.get().terminalType().getValue().isAvailable();
+ if (!start) {
+ return;
+ }
+
var name = AppProperties.get().isStaging() ? "xpipe-ptb" : "xpipe";
var command = String.format(
"""
@@ -139,7 +161,10 @@ public class AppInstaller {
exec || read -rsp "Update failed ..."$'\\n' -n 1 key
""",
file, file, name);
- TerminalLauncher.openDirect("XPipe Updater", sc -> command);
+
+ runAndClose(() -> {
+ TerminalLauncher.openDirect("XPipe Updater", sc -> command);
+ });
}
@Override
@@ -150,8 +175,16 @@ public class AppInstaller {
@JsonTypeName("rpm")
public static final class Rpm extends InstallerAssetType {
+
@Override
- public void installLocal(String file) throws Exception {
+ public void installLocal(Path file) throws Exception {
+ var start = AppPrefs.get() != null
+ && AppPrefs.get().terminalType().getValue() != null
+ && AppPrefs.get().terminalType().getValue().isAvailable();
+ if (!start) {
+ return;
+ }
+
var name = AppProperties.get().isStaging() ? "xpipe-ptb" : "xpipe";
var command = String.format(
"""
@@ -166,7 +199,10 @@ public class AppInstaller {
exec || read -rsp "Update failed ..."$'\\n' -n 1 key
""",
file, file, name);
- TerminalLauncher.openDirect("XPipe Updater", sc -> command);
+
+ runAndClose(() -> {
+ TerminalLauncher.openDirect("XPipe Updater", sc -> command);
+ });
}
@Override
@@ -177,8 +213,16 @@ public class AppInstaller {
@JsonTypeName("pkg")
public static final class Pkg extends InstallerAssetType {
+
@Override
- public void installLocal(String file) throws Exception {
+ public void installLocal(Path file) throws Exception {
+ var start = AppPrefs.get() != null
+ && AppPrefs.get().terminalType().getValue() != null
+ && AppPrefs.get().terminalType().getValue().isAvailable();
+ if (!start) {
+ return;
+ }
+
var name = AppProperties.get().isStaging() ? "xpipe-ptb" : "xpipe";
var command = String.format(
"""
@@ -193,7 +237,10 @@ public class AppInstaller {
exec || echo "Update failed ..." && read -rs -k 1 key
""",
file, file, name);
- TerminalLauncher.openDirect("XPipe Updater", sc -> command);
+
+ runAndClose(() -> {
+ TerminalLauncher.openDirect("XPipe Updater", sc -> command);
+ });
}
@Override
diff --git a/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java b/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java
index 77fd593f3..e256053f1 100644
--- a/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java
+++ b/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java
@@ -1,6 +1,8 @@
package io.xpipe.app.update;
+import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppProperties;
+import io.xpipe.app.issue.ErrorEvent;
import javafx.scene.layout.Region;
@@ -43,13 +45,24 @@ public class GitHubUpdater extends UpdateHandler {
preparedUpdate.setValue(rel);
}
- public void executeUpdateOnCloseImpl() throws Exception {
- var downloadFile = preparedUpdate.getValue().getFile();
+ public void executeUpdate() {
+ var p = preparedUpdate.getValue();
+ var downloadFile = p.getFile();
if (!Files.exists(downloadFile)) {
+ event("Prepared update file does not exist");
return;
}
- AppInstaller.installFileLocal(preparedUpdate.getValue().getAssetType(), downloadFile);
+ try {
+ var performedUpdate = new PerformedUpdate(p.getVersion(), p.getBody(), p.getVersion());
+ AppCache.update("performedUpdate", performedUpdate);
+
+ var a = p.getAssetType();
+ a.installLocal(downloadFile);
+ } catch (Throwable t) {
+ ErrorEvent.fromThrowable(t).handle();
+ preparedUpdate.setValue(null);
+ }
}
public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception {
diff --git a/app/src/main/java/io/xpipe/app/update/PortableUpdater.java b/app/src/main/java/io/xpipe/app/update/PortableUpdater.java
index 26099771a..8aafadc40 100644
--- a/app/src/main/java/io/xpipe/app/update/PortableUpdater.java
+++ b/app/src/main/java/io/xpipe/app/update/PortableUpdater.java
@@ -29,10 +29,6 @@ public class PortableUpdater extends UpdateHandler {
.createRegion();
}
- public void executeUpdateOnCloseImpl() {
- throw new UnsupportedOperationException();
- }
-
public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception {
var rel = AppDownloads.getLatestSuitableRelease();
event("Determined latest suitable release "
diff --git a/app/src/main/java/io/xpipe/app/update/UpdateHandler.java b/app/src/main/java/io/xpipe/app/update/UpdateHandler.java
index 09b36c502..82f1ae293 100644
--- a/app/src/main/java/io/xpipe/app/update/UpdateHandler.java
+++ b/app/src/main/java/io/xpipe/app/update/UpdateHandler.java
@@ -2,7 +2,6 @@ package io.xpipe.app.update;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppProperties;
-import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
@@ -72,6 +71,13 @@ public abstract class UpdateHandler {
preparedUpdate.setValue(null);
}
+ // Check if file has been deleted
+ if (preparedUpdate.getValue() != null
+ && preparedUpdate.getValue().getFile() != null
+ && !Files.exists(preparedUpdate.getValue().getFile())) {
+ preparedUpdate.setValue(null);
+ }
+
preparedUpdate.addListener((c, o, n) -> {
AppCache.update("preparedUpdate", n);
});
@@ -220,27 +226,10 @@ public abstract class UpdateHandler {
}
event("Executing update ...");
- OperationMode.executeAfterShutdown(() -> {
- try {
- var performedUpdate = new PerformedUpdate(
- preparedUpdate.getValue().getVersion(),
- preparedUpdate.getValue().getBody(),
- preparedUpdate.getValue().getVersion());
- AppCache.update("performedUpdate", performedUpdate);
-
- executeUpdateOnCloseImpl();
-
- // In case we perform any operations such as opening a terminal
- // give it some time to open while this process is still alive
- // Otherwise it might quit because the parent process is dead already
- ThreadHelper.sleep(100);
- } catch (Throwable ex) {
- ex.printStackTrace();
- }
- });
+ executeUpdate();
}
- public void executeUpdateOnCloseImpl() throws Exception {
+ public void executeUpdate() {
throw new UnsupportedOperationException();
}
diff --git a/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java b/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java
index 410aa38a1..0d6c8d6e3 100644
--- a/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java
+++ b/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java
@@ -99,16 +99,13 @@ public enum XPipeDistributionType {
// In theory, we can also add && !AppProperties.get().isStaging() here, but we want to replicate the
// production behavior
if (OsType.getLocal().equals(OsType.WINDOWS)) {
- try (var chocoOut =
- sc.command("choco search --local-only -r xpipe").start()) {
- var out = chocoOut.readStdoutDiscardErr();
- if (chocoOut.getExitCode() == 0) {
- var split = out.split("\\|");
- if (split.length == 2) {
- var version = split[1];
- if (AppProperties.get().getVersion().equals(version)) {
- return CHOCO;
- }
+ var out = sc.command("choco search --local-only -r xpipe").readStdoutIfPossible();
+ if (out.isPresent()) {
+ var split = out.get().split("\\|");
+ if (split.length == 2) {
+ var version = split[1];
+ if (AppProperties.get().getVersion().equals(version)) {
+ return CHOCO;
}
}
}
@@ -117,17 +114,15 @@ public enum XPipeDistributionType {
// In theory, we can also add && !AppProperties.get().isStaging() here, but we want to replicate the
// production behavior
if (OsType.getLocal().equals(OsType.MACOS)) {
- try (var brewOut = sc.command("brew list --casks --versions").start()) {
- var out = brewOut.readStdoutDiscardErr();
- if (brewOut.getExitCode() == 0) {
- if (out.lines().anyMatch(s -> {
- var split = s.split(" ");
- return split.length == 2
- && split[0].equals("xpipe")
- && split[1].equals(AppProperties.get().getVersion());
- })) {
- return HOMEBREW;
- }
+ var out = sc.command("brew list --casks --versions").readStdoutIfPossible();
+ if (out.isPresent()) {
+ if (out.get().lines().anyMatch(s -> {
+ var split = s.split(" ");
+ return split.length == 2
+ && split[0].equals("xpipe")
+ && split[1].equals(AppProperties.get().getVersion());
+ })) {
+ return HOMEBREW;
}
}
}
diff --git a/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java
index a47fd4d34..f8493ff42 100644
--- a/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java
+++ b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java
@@ -171,7 +171,11 @@ public class AppJacksonModule extends SimpleModule {
// Compatibility fix for legacy local stores
var toUse = e.getStore() instanceof LocalStore ? DataStorage.get().local() : e;
- return toUse != null ? new DataStoreEntryRef<>(toUse) : null;
+ if (toUse == null) {
+ return null;
+ }
+
+ return new DataStoreEntryRef<>(toUse);
}
}
}
diff --git a/app/src/main/java/io/xpipe/app/util/ContextMenuHelper.java b/app/src/main/java/io/xpipe/app/util/ContextMenuHelper.java
index aad686d37..8fa5dfb6d 100644
--- a/app/src/main/java/io/xpipe/app/util/ContextMenuHelper.java
+++ b/app/src/main/java/io/xpipe/app/util/ContextMenuHelper.java
@@ -1,6 +1,6 @@
package io.xpipe.app.util;
-import io.xpipe.app.fxcomps.Comp;
+import io.xpipe.app.fxcomps.util.LabelGraphic;
import javafx.application.Platform;
import javafx.geometry.Side;
@@ -36,14 +36,14 @@ public class ContextMenuHelper {
return contextMenu;
}
- public static MenuItem item(Comp> graphic, String name) {
- var i = new MenuItem(name, graphic.createRegion());
+ public static MenuItem item(LabelGraphic graphic, String name) {
+ var i = new MenuItem(name, graphic.createGraphicNode());
return i;
}
public static void toggleShow(ContextMenu contextMenu, Node ref, Side side) {
if (!contextMenu.isShowing()) {
- contextMenu.show(ref, Side.RIGHT, 0, 0);
+ contextMenu.show(ref, side, 0, 0);
} else {
contextMenu.hide();
}
diff --git a/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java b/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java
index efbaf5757..5f1cb2782 100644
--- a/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java
+++ b/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java
@@ -2,17 +2,13 @@ package io.xpipe.app.util;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.fxcomps.util.BindingsHelper;
-import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.process.ShellStoreState;
-import io.xpipe.core.store.DataStore;
-import io.xpipe.core.store.ShellStore;
+import io.xpipe.core.process.ShellTtyState;
import javafx.beans.value.ObservableValue;
-import java.util.function.IntFunction;
-
public class DataStoreFormatter {
public static String formattedOsName(String osName) {
@@ -46,7 +42,8 @@ public class DataStoreFormatter {
return s.getShellDialect().getDisplayName();
}
- return s.isRunning() ? formattedOsName(s.getOsName()) : "Connection failed";
+ var prefix = s.getTtyState() != null && s.getTtyState() != ShellTtyState.NONE ? "[PTY] " : "";
+ return s.isRunning() ? prefix + formattedOsName(s.getOsName()) : "Connection failed";
}
return "?";
@@ -61,49 +58,10 @@ public class DataStoreFormatter {
return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
}
- public static String formatSubHost(IntFunction func, DataStore at, int length) {
- var atString = at instanceof ShellStore shellStore && !ShellStore.isLocal(shellStore)
- ? DataStorage.get().getStoreDisplayName(at).orElse(null)
- : null;
- if (atString == null) {
- return func.apply(length);
- }
-
- var fileString = func.apply(length - atString.length() - 1);
- return String.format("%s/%s", atString, fileString);
- }
-
- public static String formatAtHost(IntFunction func, DataStore at, int length) {
- var atString = at instanceof ShellStore shellStore && !ShellStore.isLocal(shellStore)
- ? DataStorage.get().getStoreDisplayName(at).orElse(null)
- : null;
- if (atString == null) {
- return func.apply(length);
- }
-
- var fileString = func.apply(length - atString.length() - 3);
- return String.format("%s @ %s", fileString, atString);
- }
-
- public static String formatViaProxy(IntFunction func, DataStoreEntry at, int length) {
- var atString =
- at.getStore() instanceof ShellStore shellStore && !ShellStore.isLocal(shellStore) ? at.getName() : null;
- if (atString == null) {
- return func.apply(length);
- }
-
- var fileString = func.apply(length - atString.length() - 3);
- return String.format("%s > %s", atString, fileString);
- }
-
public static String toApostropheName(DataStoreEntry input) {
return toName(input, Integer.MAX_VALUE) + "'s";
}
- public static String toName(DataStoreEntry input) {
- return toName(input, Integer.MAX_VALUE);
- }
-
public static String toName(DataStoreEntry input, int length) {
if (input == null) {
return "?";
@@ -146,7 +104,7 @@ public class DataStoreFormatter {
DataStoreFormatter.cut(name, lengthShare), DataStoreFormatter.cut(region, length - lengthShare));
}
- if (input.endsWith(".compute.amazonaws.com")) {
+ if (input.endsWith(".compute.amazonaws.com") || input.endsWith(".compute.internal")) {
var split = input.split("\\.");
var name = split[0];
var region = split[1];
diff --git a/app/src/main/java/io/xpipe/app/util/DesktopHelper.java b/app/src/main/java/io/xpipe/app/util/DesktopHelper.java
index d3adaad33..190c20088 100644
--- a/app/src/main/java/io/xpipe/app/util/DesktopHelper.java
+++ b/app/src/main/java/io/xpipe/app/util/DesktopHelper.java
@@ -16,11 +16,10 @@ public class DesktopHelper {
return Path.of(LocalShell.getLocalPowershell()
.executeSimpleStringCommand("[Environment]::GetFolderPath([Environment+SpecialFolder]::Desktop)"));
} else if (OsType.getLocal() == OsType.LINUX) {
- try (var cmd = LocalShell.getShell().command("xdg-user-dir DESKTOP").start()) {
- var read = cmd.readStdoutDiscardErr();
- var exit = cmd.getExitCode();
- if (exit == 0) {
- return Path.of(read);
+ try (var sc = LocalShell.getShell().start()) {
+ var out = sc.command("xdg-user-dir DESKTOP").readStdoutIfPossible();
+ if (out.isPresent()) {
+ return Path.of(out.get());
}
}
}
@@ -28,6 +27,23 @@ public class DesktopHelper {
return Path.of(System.getProperty("user.home") + "/Desktop");
}
+ public static Path getDownloadsDirectory() throws Exception {
+ if (OsType.getLocal() == OsType.WINDOWS) {
+ return Path.of(LocalShell.getLocalPowershell()
+ .executeSimpleStringCommand(
+ "(New-Object -ComObject Shell.Application).NameSpace('shell:Downloads').Self.Path"));
+ } else if (OsType.getLocal() == OsType.LINUX) {
+ try (var sc = LocalShell.getShell().start()) {
+ var out = sc.command("xdg-user-dir DOWNLOAD").readStdoutIfPossible();
+ if (out.isPresent()) {
+ return Path.of(out.get());
+ }
+ }
+ }
+
+ return Path.of(System.getProperty("user.home") + "/Downloads");
+ }
+
public static void browsePathRemote(ShellControl sc, String path, FileKind kind) throws Exception {
var d = sc.getShellDialect();
switch (sc.getOsType()) {
diff --git a/app/src/main/java/io/xpipe/app/util/DesktopShortcuts.java b/app/src/main/java/io/xpipe/app/util/DesktopShortcuts.java
index e93f1eee1..bde39a45f 100644
--- a/app/src/main/java/io/xpipe/app/util/DesktopShortcuts.java
+++ b/app/src/main/java/io/xpipe/app/util/DesktopShortcuts.java
@@ -4,27 +4,31 @@ import io.xpipe.core.process.OsType;
import io.xpipe.core.util.XPipeInstallation;
import java.nio.file.Files;
+import java.nio.file.Path;
public class DesktopShortcuts {
- private static void createWindowsShortcut(String target, String name) throws Exception {
+ private static Path createWindowsShortcut(String executable, String args, String name) throws Exception {
var icon = XPipeInstallation.getLocalDefaultInstallationIcon();
- var shortcutTarget = XPipeInstallation.getLocalDefaultCliExecutable();
var shortcutPath = DesktopHelper.getDesktopDirectory().resolve(name + ".lnk");
var content = String.format(
"""
- set "TARGET=%s"
- set "SHORTCUT=%s"
- set PWS=powershell.exe -ExecutionPolicy Restricted -NoLogo -NonInteractive -NoProfile
-
- %%PWS%% -Command "$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('%%SHORTCUT%%'); $S.IconLocation='%s'; $S.WindowStyle=7; $S.TargetPath = '%%TARGET%%'; $S.Arguments = 'open %s'; $S.Save()"
+ $TARGET="%s"
+ $SHORTCUT="%s"
+ $ws = New-Object -ComObject WScript.Shell
+ $s = $ws.CreateShortcut("$SHORTCUT")
+ $S.IconLocation='%s'
+ $S.WindowStyle=7
+ $S.TargetPath = "$TARGET"
+ $S.Arguments = '%s'
+ $S.Save()
""",
- shortcutTarget, shortcutPath, icon, target);
- LocalShell.getShell().executeSimpleCommand(content);
+ executable, shortcutPath, icon, args);
+ LocalShell.getLocalPowershell().executeSimpleCommand(content);
+ return shortcutPath;
}
- private static void createLinuxShortcut(String target, String name) throws Exception {
- var exec = XPipeInstallation.getLocalDefaultCliExecutable();
+ private static Path createLinuxShortcut(String executable, String args, String name) throws Exception {
var icon = XPipeInstallation.getLocalDefaultInstallationIcon();
var content = String.format(
"""
@@ -32,19 +36,19 @@ public class DesktopShortcuts {
Type=Application
Name=%s
Comment=Open with XPipe
- Exec="%s" open %s
+ Exec="%s" %s
Icon=%s
Terminal=false
Categories=Utility;Development;
""",
- name, exec, target, icon);
+ name, executable, args, icon);
var file = DesktopHelper.getDesktopDirectory().resolve(name + ".desktop");
Files.writeString(file, content);
file.toFile().setExecutable(true);
+ return file;
}
- private static void createMacOSShortcut(String target, String name) throws Exception {
- var exec = XPipeInstallation.getLocalDefaultCliExecutable();
+ private static Path createMacOSShortcut(String executable, String args, String name) throws Exception {
var icon = XPipeInstallation.getLocalDefaultInstallationIcon();
var base = DesktopHelper.getDesktopDirectory().resolve(name + ".app");
var content = String.format(
@@ -52,18 +56,18 @@ public class DesktopShortcuts {
#!/usr/bin/env sh
"%s" open %s
""",
- exec, target);
+ executable, args);
try (var pc = LocalShell.getShell()) {
pc.getShellDialect().deleteFileOrDirectory(pc, base.toString()).executeAndCheck();
pc.executeSimpleCommand(pc.getShellDialect().getMkdirsCommand(base + "/Contents/MacOS"));
pc.executeSimpleCommand(pc.getShellDialect().getMkdirsCommand(base + "/Contents/Resources"));
- var executable = base + "/Contents/MacOS/" + name;
+ var macExec = base + "/Contents/MacOS/" + name;
pc.getShellDialect()
- .createScriptTextFileWriteCommand(pc, content, executable)
+ .createScriptTextFileWriteCommand(pc, content, macExec)
.execute();
- pc.executeSimpleCommand("chmod ugo+x \"" + executable + "\"");
+ pc.executeSimpleCommand("chmod ugo+x \"" + macExec + "\"");
pc.getShellDialect()
.createTextFileWriteCommand(pc, "APPL????", base + "/Contents/PkgInfo")
@@ -85,15 +89,21 @@ public class DesktopShortcuts {
.execute();
pc.executeSimpleCommand("cp \"" + icon + "\" \"" + base + "/Contents/Resources/icon.icns\"");
}
+ return base;
}
- public static void create(String target, String name) throws Exception {
+ public static Path createCliOpen(String action, String name) throws Exception {
+ var exec = XPipeInstallation.getLocalDefaultCliExecutable();
+ return create(exec, "open " + action, name);
+ }
+
+ public static Path create(String executable, String args, String name) throws Exception {
if (OsType.getLocal().equals(OsType.WINDOWS)) {
- createWindowsShortcut(target, name);
+ return createWindowsShortcut(executable, args, name);
} else if (OsType.getLocal().equals(OsType.LINUX)) {
- createLinuxShortcut(target, name);
+ return createLinuxShortcut(executable, args, name);
} else {
- createMacOSShortcut(target, name);
+ return createMacOSShortcut(executable, args, name);
}
}
}
diff --git a/app/src/main/java/io/xpipe/app/util/DialogHelper.java b/app/src/main/java/io/xpipe/app/util/DialogHelper.java
deleted file mode 100644
index 48a18f4b2..000000000
--- a/app/src/main/java/io/xpipe/app/util/DialogHelper.java
+++ /dev/null
@@ -1,116 +0,0 @@
-package io.xpipe.app.util;
-
-import io.xpipe.app.storage.DataStorage;
-import io.xpipe.core.dialog.Dialog;
-import io.xpipe.core.dialog.QueryConverter;
-import io.xpipe.core.store.*;
-import io.xpipe.core.util.NewLine;
-import io.xpipe.core.util.SecretValue;
-import io.xpipe.core.util.StreamCharset;
-
-import lombok.Value;
-
-public class DialogHelper {
-
- public static Dialog addressQuery(Address address) {
- var hostNameQuery = Dialog.query("Hostname", false, true, false, address.getHostname(), QueryConverter.STRING);
- var portQuery = Dialog.query("Port", false, true, false, address.getPort(), QueryConverter.INTEGER);
- return Dialog.chain(hostNameQuery, portQuery)
- .evaluateTo(() -> new Address(hostNameQuery.getResult(), portQuery.getResult()));
- }
-
- public static Dialog machineQuery(DataStore store) {
- var storeName = DataStorage.get().getStoreDisplayName(store).orElse("localhost");
- return Dialog.query("Machine", false, true, false, storeName, QueryConverter.STRING)
- .map((String name) -> {
- if (name.equals("local") || name.equals("localhost")) {
- return new LocalStore();
- }
-
- var stored = DataStorage.get().getStoreEntryIfPresent(name).map(entry -> entry.getStore());
- if (stored.isEmpty()) {
- throw new IllegalArgumentException(String.format("Store not found: %s", name));
- }
-
- if (!(stored.get() instanceof FileSystem)) {
- throw new IllegalArgumentException(String.format("Store not a machine store: %s", name));
- }
-
- return stored.get();
- });
- }
-
- public static Dialog shellQuery(String displayName, DataStore store) {
- var storeName = DataStorage.get().getStoreDisplayName(store).orElse("localhost");
- return Dialog.query(displayName, false, true, false, storeName, QueryConverter.STRING)
- .map((String name) -> {
- if (name.equals("local") || name.equals("localhost")) {
- return new LocalStore();
- }
-
- var stored = DataStorage.get().getStoreEntryIfPresent(name).map(entry -> entry.getStore());
- if (stored.isEmpty()) {
- throw new IllegalArgumentException(String.format("Store not found: %s", name));
- }
-
- if (!(stored.get() instanceof ShellStore)) {
- throw new IllegalArgumentException(String.format("Store not a shell store: %s", name));
- }
-
- return stored.get();
- });
- }
-
- public static Dialog charsetQuery(StreamCharset c, boolean preferQuiet) {
- return Dialog.query("Charset", false, true, c != null && preferQuiet, c, QueryConverter.CHARSET);
- }
-
- public static Dialog newLineQuery(NewLine n, boolean preferQuiet) {
- return Dialog.query("Newline", false, true, n != null && preferQuiet, n, QueryConverter.NEW_LINE);
- }
-
- public static Dialog query(String desc, T value, boolean required, QueryConverter c, boolean preferQuiet) {
- return Dialog.query(desc, false, required, value != null && preferQuiet, value, c);
- }
-
- public static Dialog booleanChoice(String desc, boolean value, boolean preferQuiet) {
- return Dialog.choice(desc, val -> val.toString(), true, preferQuiet, value, Boolean.TRUE, Boolean.FALSE);
- }
-
- public static Dialog fileQuery(String name) {
- return Dialog.query("File", true, true, false, name, QueryConverter.STRING);
- }
-
- public static Dialog userQuery(String name) {
- return Dialog.query("User", false, true, false, name, QueryConverter.STRING);
- }
-
- public static Dialog namedStoreQuery(DataStore store, Class extends DataStore> filter) {
- var name = DataStorage.get().getStoreDisplayName(store).orElse(null);
- return Dialog.query("Store", false, true, false, name, QueryConverter.STRING)
- .map((String newName) -> {
- var found = DataStorage.get()
- .getStoreEntryIfPresent(newName)
- .map(entry -> entry.getStore())
- .orElseThrow();
- if (!filter.isAssignableFrom(found.getClass())) {
- throw new IllegalArgumentException("Incompatible store type");
- }
- return found;
- });
- }
-
- public static Dialog passwordQuery(SecretValue password) {
- return Dialog.querySecret("Password", false, true, password);
- }
-
- public static Dialog timeoutQuery(Integer timeout) {
- return Dialog.query("Timeout", false, true, false, timeout, QueryConverter.INTEGER);
- }
-
- @Value
- public static class Address {
- String hostname;
- Integer port;
- }
-}
diff --git a/app/src/main/java/io/xpipe/app/util/FileBridge.java b/app/src/main/java/io/xpipe/app/util/FileBridge.java
index f726e764e..654a36c2a 100644
--- a/app/src/main/java/io/xpipe/app/util/FileBridge.java
+++ b/app/src/main/java/io/xpipe/app/util/FileBridge.java
@@ -94,7 +94,7 @@ public class FileBridge {
event("File " + TEMP.relativize(e.file) + " is probably still writing ...");
ThreadHelper.sleep(AppPrefs.get().editorReloadTimeout().getValue());
- // If still no read lock after 500ms, just don't parse it
+ // If still no read lock after some time, just don't parse it
if (!Files.exists(changed)) {
event("Could not obtain read lock even after timeout. Ignoring change ...");
return;
@@ -105,9 +105,8 @@ public class FileBridge {
event("Registering modification for file " + TEMP.relativize(e.file));
event("Last modification for file: " + e.lastModified.toString() + " vs current one: "
+ e.getLastModified());
- if (e.hasChanged()) {
+ if (e.registerChange()) {
event("Registering change for file " + TEMP.relativize(e.file) + " for editor entry " + e.getName());
- e.registerChange();
try (var in = Files.newInputStream(e.file)) {
var actualSize = (long) in.available();
var started = Instant.now();
@@ -219,6 +218,7 @@ public class FileBridge {
private final BooleanScope scope;
private final BiConsumer writer;
private Instant lastModified;
+ private long lastSize;
public Entry(Path file, Object key, String name, BooleanScope scope, BiConsumer writer) {
this.file = file;
@@ -228,15 +228,6 @@ public class FileBridge {
this.writer = writer;
}
- public boolean hasChanged() {
- try {
- var newDate = Files.getLastModifiedTime(file).toInstant();
- return !newDate.equals(lastModified);
- } catch (IOException e) {
- return false;
- }
- }
-
public Instant getLastModified() {
try {
return Files.getLastModifiedTime(file).toInstant();
@@ -245,8 +236,29 @@ public class FileBridge {
}
}
- public void registerChange() {
- lastModified = getLastModified();
+ public long getSize() {
+ try {
+ return Files.size(file);
+ } catch (IOException e) {
+ return 0;
+ }
+ }
+
+ public boolean registerChange() {
+ var newSize = getSize();
+ var newDate = getLastModified();
+ // The size check is intended for cases in which editors first clear a file prior to writing it
+ // In that case, multiple watch events are sent. If these happened very fast, it might be possible that
+ // the modified time is the same for both write operations due to the file system modified time resolution
+ // being limited
+ // We then can't identify changes purely based on the modified time, so the file size is the next best
+ // option
+ // This might result in double change detection in rare cases, but that is irrelevant as it prevents files
+ // from being blanked
+ var changed = !newDate.equals(lastModified) || newSize > lastSize;
+ lastSize = newSize;
+ lastModified = newDate;
+ return changed;
}
}
}
diff --git a/app/src/main/java/io/xpipe/app/util/FixedHierarchyStore.java b/app/src/main/java/io/xpipe/app/util/FixedHierarchyStore.java
index 9191e3149..d06ad3e5f 100644
--- a/app/src/main/java/io/xpipe/app/util/FixedHierarchyStore.java
+++ b/app/src/main/java/io/xpipe/app/util/FixedHierarchyStore.java
@@ -9,5 +9,9 @@ import java.util.List;
public interface FixedHierarchyStore extends DataStore {
+ default boolean removeLeftovers() {
+ return true;
+ }
+
List extends DataStoreEntryRef extends FixedChildStore>> listChildren(DataStoreEntry self) throws Exception;
}
diff --git a/app/src/main/java/io/xpipe/app/util/InputHelper.java b/app/src/main/java/io/xpipe/app/util/InputHelper.java
index af2fecb18..e4c5a96a3 100644
--- a/app/src/main/java/io/xpipe/app/util/InputHelper.java
+++ b/app/src/main/java/io/xpipe/app/util/InputHelper.java
@@ -2,10 +2,7 @@ package io.xpipe.app.util;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.KeyCombination;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.input.MouseEvent;
+import javafx.scene.input.*;
import java.util.List;
import java.util.function.Consumer;
@@ -27,11 +24,7 @@ public class InputHelper {
public static void onExactKeyCode(EventTarget target, KeyCode code, boolean filter, Consumer r) {
EventHandler keyEventEventHandler = event -> {
- if (event.isAltDown() || event.isShiftDown() || event.isShortcutDown()) {
- return;
- }
-
- if (code == event.getCode()) {
+ if (new KeyCodeCombination(code).match(event)) {
r.accept(event);
}
};
@@ -42,20 +35,9 @@ public class InputHelper {
}
}
- public static void onInput(EventTarget target, boolean filter, Consumer r) {
- EventHandler keyEventEventHandler = event -> {
- r.accept(event);
- };
- if (filter) {
- target.addEventFilter(KeyEvent.KEY_PRESSED, keyEventEventHandler);
- } else {
- target.addEventHandler(KeyEvent.KEY_PRESSED, keyEventEventHandler);
- }
- }
-
public static void onLeft(EventTarget target, boolean filter, Consumer r) {
EventHandler e = event -> {
- if (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.NUMPAD4) {
+ if (new KeyCodeCombination(KeyCode.LEFT).match(event) || new KeyCodeCombination(KeyCode.NUMPAD4).match(event)) {
r.accept(event);
}
};
@@ -68,7 +50,7 @@ public class InputHelper {
public static void onRight(EventTarget target, boolean filter, Consumer r) {
EventHandler e = event -> {
- if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.NUMPAD6) {
+ if (new KeyCodeCombination(KeyCode.RIGHT).match(event) || new KeyCodeCombination(KeyCode.NUMPAD6).match(event)) {
r.accept(event);
}
};
diff --git a/app/src/main/java/io/xpipe/app/util/NativeBridge.java b/app/src/main/java/io/xpipe/app/util/NativeBridge.java
new file mode 100644
index 000000000..b28039507
--- /dev/null
+++ b/app/src/main/java/io/xpipe/app/util/NativeBridge.java
@@ -0,0 +1,44 @@
+package io.xpipe.app.util;
+
+import io.xpipe.app.issue.ErrorEvent;
+import io.xpipe.core.util.XPipeInstallation;
+
+import com.sun.jna.Library;
+import com.sun.jna.Native;
+import com.sun.jna.NativeLong;
+
+import java.util.Map;
+import java.util.Optional;
+
+public class NativeBridge {
+
+ private static MacOsLibrary macOsLibrary;
+ private static boolean loadingFailed;
+
+ public static Optional getMacOsLibrary() {
+ if (macOsLibrary == null && !loadingFailed) {
+ try {
+ System.setProperty(
+ "jna.library.path",
+ XPipeInstallation.getCurrentInstallationBasePath()
+ .resolve("Contents")
+ .resolve("runtime")
+ .resolve("Contents")
+ .resolve("Home")
+ .resolve("lib")
+ .toString());
+ var l = Native.load("xpipe_bridge", MacOsLibrary.class, Map.of());
+ macOsLibrary = l;
+ } catch (Throwable t) {
+ ErrorEvent.fromThrowable(t).handle();
+ loadingFailed = true;
+ }
+ }
+ return Optional.ofNullable(macOsLibrary);
+ }
+
+ public static interface MacOsLibrary extends Library {
+
+ public abstract void setAppearance(NativeLong window, boolean seamlessFrame, boolean dark);
+ }
+}
diff --git a/app/src/main/java/io/xpipe/app/util/PlatformState.java b/app/src/main/java/io/xpipe/app/util/PlatformState.java
index 559140ba6..41f1205e3 100644
--- a/app/src/main/java/io/xpipe/app/util/PlatformState.java
+++ b/app/src/main/java/io/xpipe/app/util/PlatformState.java
@@ -6,7 +6,9 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
+
import javafx.application.Platform;
+
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.SystemUtils;
@@ -121,7 +123,8 @@ public enum PlatformState {
}
if (SystemUtils.IS_OS_WINDOWS && ModifiedStage.mergeFrame()) {
- // This is primarily intended to fix Windows unified stage transparency issues (https://bugs.openjdk.org/browse/JDK-8329382)
+ // This is primarily intended to fix Windows unified stage transparency issues
+ // (https://bugs.openjdk.org/browse/JDK-8329382)
System.setProperty("prism.forceUploadingPainter", "true");
}
@@ -152,7 +155,7 @@ public enum PlatformState {
// Platform initialization has failed in this case
PlatformState.setCurrent(PlatformState.EXITED);
TrackEvent.error(t.getMessage());
- lastError =t;
+ lastError = t;
return;
}
}
diff --git a/app/src/main/java/io/xpipe/app/util/ScanAlert.java b/app/src/main/java/io/xpipe/app/util/ScanAlert.java
index ef148f383..c2d8a7f80 100644
--- a/app/src/main/java/io/xpipe/app/util/ScanAlert.java
+++ b/app/src/main/java/io/xpipe/app/util/ScanAlert.java
@@ -40,7 +40,7 @@ public class ScanAlert {
});
}
- private static void showForShellStore(DataStoreEntry initial) {
+ public static void showForShellStore(DataStoreEntry initial) {
show(initial, (DataStoreEntry entry, ShellControl sc) -> {
if (!sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
return null;
@@ -141,6 +141,11 @@ public class ScanAlert {
});
}
+ @Override
+ protected Comp> pane(Comp> content) {
+ return content;
+ }
+
@Override
public Comp> content() {
StackPane stackPane = new StackPane();
@@ -166,7 +171,7 @@ public class ScanAlert {
.apply(struc -> {
VBox.setVgrow(struc.get().getChildren().get(1), ALWAYS);
})
- .padding(new Insets(20));
+ .padding(new Insets(5, 20, 20, 20));
entry.subscribe(newValue -> {
selected.clear();
diff --git a/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java b/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java
index 44882af95..3645b97e1 100644
--- a/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java
+++ b/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java
@@ -180,6 +180,10 @@ public interface SecretRetrievalStrategy {
return new SecretQuery() {
@Override
public SecretQueryResult query(String prompt) {
+ if (command == null || command.isBlank()) {
+ throw ErrorEvent.expected(new IllegalStateException("No custom command specified"));
+ }
+
try (var cc = new LocalStore().control().command(command).start()) {
return new SecretQueryResult(
InPlaceSecretValue.of(cc.readStdoutOrThrow()), SecretQueryState.NORMAL);
diff --git a/app/src/main/java/io/xpipe/app/util/ShellTemp.java b/app/src/main/java/io/xpipe/app/util/ShellTemp.java
index 952de29b5..9b8173fdd 100644
--- a/app/src/main/java/io/xpipe/app/util/ShellTemp.java
+++ b/app/src/main/java/io/xpipe/app/util/ShellTemp.java
@@ -26,6 +26,7 @@ public class ShellTemp {
temp = temp.resolve(user != null ? user : "user");
try {
+ FileUtils.forceMkdir(temp.toFile());
// We did not set this in earlier versions. If we are running as a different user, it might fail
Files.setPosixFilePermissions(temp, PosixFilePermissions.fromString("rwxrwxrwx"));
} catch (Exception e) {
diff --git a/app/src/main/java/io/xpipe/app/util/StringSource.java b/app/src/main/java/io/xpipe/app/util/StringSource.java
deleted file mode 100644
index b92a639be..000000000
--- a/app/src/main/java/io/xpipe/app/util/StringSource.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package io.xpipe.app.util;
-
-import io.xpipe.app.issue.ErrorEvent;
-import io.xpipe.app.storage.ContextualFileReference;
-import io.xpipe.core.store.ShellStore;
-
-import lombok.EqualsAndHashCode;
-import lombok.Value;
-
-public abstract class StringSource {
-
- public abstract String get() throws Exception;
-
- @Value
- @EqualsAndHashCode(callSuper = true)
- public static class InPlace extends StringSource {
-
- String value;
-
- @Override
- public String get() {
- return value;
- }
- }
-
- @Value
- @EqualsAndHashCode(callSuper = true)
- public static class File extends StringSource {
-
- ShellStore host;
- ContextualFileReference file;
-
- @Override
- public String get() throws Exception {
- if (host == null || file == null) {
- return "";
- }
-
- try (var sc = host.control().start()) {
- var path = file.toAbsoluteFilePath(sc);
- if (!sc.getShellDialect().createFileExistsCommand(sc, path).executeAndCheck()) {
- throw ErrorEvent.expected(new IllegalArgumentException("File " + path + " does not exist"));
- }
-
- var abs = file.toAbsoluteFilePath(sc);
- var content = sc.getShellDialect().getFileReadCommand(sc, abs).readStdoutOrThrow();
- return content;
- }
- }
- }
-}
diff --git a/app/src/main/java/io/xpipe/app/util/UnlockAlert.java b/app/src/main/java/io/xpipe/app/util/UnlockAlert.java
index e13427780..3f69f5279 100644
--- a/app/src/main/java/io/xpipe/app/util/UnlockAlert.java
+++ b/app/src/main/java/io/xpipe/app/util/UnlockAlert.java
@@ -1,20 +1,8 @@
package io.xpipe.app.util;
import io.xpipe.app.core.AppI18n;
-import io.xpipe.app.core.AppStyle;
-import io.xpipe.app.core.AppTheme;
-import io.xpipe.app.core.window.AppWindowHelper;
-import io.xpipe.app.fxcomps.impl.SecretFieldComp;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
-import io.xpipe.core.util.InPlaceSecretValue;
-
-import javafx.application.Platform;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.scene.control.Alert;
-import javafx.scene.layout.VBox;
-import javafx.stage.Stage;
public class UnlockAlert {
@@ -28,40 +16,9 @@ public class UnlockAlert {
return;
}
- PlatformState.initPlatformOrThrow();
- AppI18n.init();
- AppStyle.init();
- AppTheme.init();
-
while (true) {
- var pw = new SimpleObjectProperty();
- var canceled = new SimpleBooleanProperty();
- AppWindowHelper.showBlockingAlert(alert -> {
- alert.setTitle(AppI18n.get("unlockAlertTitle"));
- alert.setHeaderText(AppI18n.get("unlockAlertHeader"));
- alert.setAlertType(Alert.AlertType.CONFIRMATION);
-
- var text = new SecretFieldComp(pw, false).createRegion();
- text.setStyle("-fx-border-width: 1px");
-
- var content = new VBox(text);
- content.setSpacing(5);
- alert.getDialogPane().setContent(content);
-
- var stage = (Stage) alert.getDialogPane().getScene().getWindow();
- stage.setAlwaysOnTop(true);
-
- alert.setOnShown(event -> {
- stage.requestFocus();
- // Wait 1 pulse before focus so that the scene can be assigned to text
- Platform.runLater(text::requestFocus);
- event.consume();
- });
- })
- .filter(b -> b.getButtonData().isDefaultButton())
- .ifPresentOrElse(t -> {}, () -> canceled.set(true));
-
- if (canceled.get()) {
+ var r = AskpassAlert.queryRaw(AppI18n.get("unlockAlertHeader"), null);
+ if (r.getState() == SecretQueryState.CANCELLED) {
ErrorEvent.fromMessage("Unlock cancelled")
.expected()
.term()
@@ -70,7 +27,7 @@ public class UnlockAlert {
return;
}
- if (AppPrefs.get().unlock(pw.get())) {
+ if (AppPrefs.get().unlock(r.getSecret().inPlace())) {
return;
}
}
diff --git a/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java b/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java
index ce952d374..a2aca1d11 100644
--- a/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java
+++ b/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java
@@ -145,23 +145,20 @@ public abstract class WindowsRegistry {
.add("/v")
.addQuoted(valueName);
- String output;
- try (var c = shellControl.command(command).start()) {
- output = c.readStdoutDiscardErr();
- if (c.getExitCode() != 0) {
- return Optional.empty();
- }
+ var output = shellControl.command(command).readStdoutIfPossible();
+ if (output.isEmpty()) {
+ return Optional.empty();
}
// Output has the following format:
// \n\n\n\t\t
- if (output.contains("\t")) {
- String[] parsed = output.split("\t");
+ if (output.get().contains("\t")) {
+ String[] parsed = output.get().split("\t");
return Optional.of(parsed[parsed.length - 1]);
}
- if (output.contains(" ")) {
- String[] parsed = output.split(" ");
+ if (output.get().contains(" ")) {
+ String[] parsed = output.get().split(" ");
return Optional.of(parsed[parsed.length - 1]);
}
@@ -176,14 +173,7 @@ public abstract class WindowsRegistry {
.add("/v")
.addQuoted(valueName)
.add("/s");
- try (var c = shellControl.command(command).start()) {
- var output = c.readStdoutDiscardErr();
- if (c.getExitCode() != 0) {
- return Optional.empty();
- } else {
- return Optional.of(output);
- }
- }
+ return shellControl.command(command).readStdoutIfPossible();
}
@Override
@@ -196,22 +186,17 @@ public abstract class WindowsRegistry {
.add("/s")
.add("/e")
.add("/d");
- try (var c = shellControl.command(command).start()) {
- var output = c.readStdoutDiscardErr();
- if (c.getExitCode() != 0) {
+ return shellControl.command(command).readStdoutIfPossible().flatMap(output -> {
+ return output.lines().findFirst().flatMap(s -> {
+ if (s.startsWith("HKEY_CURRENT_USER\\")) {
+ return Optional.of(new Key(HKEY_CURRENT_USER, s.replace("HKEY_CURRENT_USER\\", "")));
+ }
+ if (s.startsWith("HKEY_LOCAL_MACHINE\\")) {
+ return Optional.of(new Key(HKEY_LOCAL_MACHINE, s.replace("HKEY_LOCAL_MACHINE\\", "")));
+ }
return Optional.empty();
- } else {
- return output.lines().findFirst().flatMap(s -> {
- if (s.startsWith("HKEY_CURRENT_USER\\")) {
- return Optional.of(new Key(HKEY_CURRENT_USER, s.replace("HKEY_CURRENT_USER\\", "")));
- }
- if (s.startsWith("HKEY_LOCAL_MACHINE\\")) {
- return Optional.of(new Key(HKEY_LOCAL_MACHINE, s.replace("HKEY_LOCAL_MACHINE\\", "")));
- }
- return Optional.empty();
- });
- }
- }
+ });
+ });
}
}
}
diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/api.md b/app/src/main/resources/io/xpipe/app/resources/misc/api.md
index 032417a5d..0fb4a6f37 100644
--- a/app/src/main/resources/io/xpipe/app/resources/misc/api.md
+++ b/app/src/main/resources/io/xpipe/app/resources/misc/api.md
@@ -1673,6 +1673,7 @@ These errors will be returned with the HTTP return code 500.
"shellDialect": 0,
"osType": "string",
"osName": "string",
+ "ttyState": "string",
"temp": "string"
}
```
@@ -2969,6 +2970,7 @@ undefined
"shellDialect": 0,
"osType": "string",
"osName": "string",
+ "ttyState": "string",
"temp": "string"
}
@@ -2981,6 +2983,7 @@ undefined
|shellDialect|integer|true|none|The shell dialect|
|osType|string|true|none|The general type of operating system|
|osName|string|true|none|The display name of the operating system|
+|ttyState|string|false|none|Whether a tty/pty has been allocated for the connection. If allocated, input and output will be unreliable. It is not recommended to use a shell connection then.|
|temp|string|true|none|The location of the temporary directory|
ShellStopRequest
@@ -3535,7 +3538,7 @@ xor
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
-|*anonymous*|[Local](#schemalocal)|false|none|Authentication method for local applications. Uses file system access as proof of authentication.|
+|*anonymous*|[Local](#schemalocal)|false|none|Authentication method for local applications. Uses file system access as proof of authentication.
You can find the authentication file at:
- %TEMP%\xpipe_auth on Windows
- $TMP/xpipe_auth on Linux
- $TMPDIR/xpipe_auth on macOS
For the PTB releases the file name is changed to xpipe_ptb_auth to prevent collisions.
As the temporary directory on Linux is global, the daemon might run as another user and your current user might not have permissions to access the auth file.|
ApiKey
@@ -3578,12 +3581,21 @@ API key authentication
Authentication method for local applications. Uses file system access as proof of authentication.
+You can find the authentication file at:
+- %TEMP%\xpipe_auth on Windows
+- $TMP/xpipe_auth on Linux
+- $TMPDIR/xpipe_auth on macOS
+
+For the PTB releases the file name is changed to xpipe_ptb_auth to prevent collisions.
+
+As the temporary directory on Linux is global, the daemon might run as another user and your current user might not have permissions to access the auth file.
+
Properties
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
|type|string|true|none|none|
-|authFileContent|string|true|none|The contents of the local file $TEMP/xpipe_auth. This file is automatically generated when XPipe starts.|
+|authFileContent|string|true|none|The contents of the local file /xpipe_auth. This file is automatically generated when XPipe starts.|
diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md b/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md
index 3d99b6073..9b4c4dc32 100644
--- a/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md
+++ b/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md
@@ -1,11 +1,10 @@
## Welcome
-Thank you for using XPipe!
+Welcome to XPipe!
+
You can view the development status, report issues, and more at the following places:
- [GitHub Repository](https://github.com/xpipe-io/xpipe/)
- [Discord Server](https://discord.gg/8y89vS8cRb)
- [Slack Server](https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg)
-- [Email me](mailto://crschnick@xpipe.io)
-
-Note that the XPipe project currently is a one-man show, but I still try to respond to everything in time.
+- [Email us](mailto://hello@xpipe.io)
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css b/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css
index 068cb1a08..94f1783df 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css
@@ -33,8 +33,8 @@
}
.bookmarks-header {
- -fx-min-height: 3.5em;
- -fx-pref-height: 3.5em;
- -fx-max-height: 3.5em;
+ -fx-min-height: 3.3em;
+ -fx-pref-height: 3.3em;
+ -fx-max-height: 3.3em;
-fx-padding: 9 6 9 8;
}
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css
index 6fd8e80b1..2d7e6ee79 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css
@@ -15,7 +15,7 @@
-fx-padding: 0 6 8 8;
}
-.transfer > * {
+.transfer > .download-background {
-fx-border-radius: 4;
-fx-background-radius: 4;
-fx-border-color: -color-border-default;
@@ -23,7 +23,7 @@
-fx-background-color: -color-bg-subtle;
}
-.transfer:highlighted > * {
+.transfer:highlighted > .download-background {
-fx-border-color: -color-accent-emphasis;
-fx-background-color: derive(-color-bg-subtle, 5%);
}
@@ -43,7 +43,7 @@
}
.transfer .button:hover {
- -fx-background-color: -color-bg-subtle;
+ -fx-background-color: -color-accent-subtle;
-fx-opacity: 1.0;
}
@@ -56,7 +56,7 @@
}
.root:seamless-frame .browser .top-spacer {
- -fx-background-radius: 0 10 0 0;
+ -fx-background-radius: 0 6 0 0;
}
.browser .welcome .button:hover {
@@ -95,9 +95,9 @@
}
.browser .top-bar {
- -fx-min-height: 3.5em;
- -fx-pref-height: 3.5em;
- -fx-max-height: 3.5em;
+ -fx-min-height: 3.3em;
+ -fx-pref-height: 3.3em;
+ -fx-max-height: 3.3em;
-fx-padding: 9px 6px;
}
@@ -145,7 +145,7 @@
}
.browser .breadcrumbs {
- -fx-padding: 2px 10px 2px 10px;
+ -fx-padding: 0px 10px 0px 10px;
}
.browser .breadcrumbs {
@@ -162,7 +162,7 @@
}
.browser .path-text, .browser .browser-filter .text-field {
- -fx-padding: 6 12;
+ -fx-padding: 3 12;
}
.browser .path-text:invisible {
@@ -203,7 +203,22 @@
-fx-max-height: 2.65em;
}
-.browser .tab-header-area {
+
+.browser .tab-header-area .control-buttons-tab {
+ -fx-opacity: 0;
+}
+
+.browser .tab-loading-indicator .loading-comp {
+ -fx-min-width: 2.5em;
+ -fx-pref-width: 2.5em;
+ -fx-max-width: 2.5em;
+ -fx-min-height: 2.5em;
+ -fx-pref-height: 2.5em;
+ -fx-max-height: 2.5em;
+ -fx-background-color: transparent;
+}
+
+.browser .tab-pane.floating > .tab-header-area {
-fx-border-width: 0 0 0.05em 0;
-fx-border-color: -color-border-default;
}
@@ -223,13 +238,13 @@
}
.root:seamless-frame .browser .tab-header-area {
- -fx-background-radius: 0 10 0 0;
+ -fx-background-radius: 0 6 0 0;
}
.browser .browser-content {
-fx-padding: 6 0 0 0;
- -fx-border-radius: 10 10 4 4;
- -fx-background-radius: 10 10 4 4;
+ -fx-border-radius: 4;
+ -fx-background-radius: 4;
-fx-background-color: -color-bg-subtle, -color-bg-default;
-fx-background-insets: 0, 7 0 0 0;
-fx-border-width: 1;
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/choice-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/choice-comp.css
index 2127a3e4f..b138acd5f 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/choice-comp.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/choice-comp.css
@@ -3,7 +3,7 @@
}
.choice-comp-content > .top {
- -fx-padding: 0.5em 1em 0.5em 1em;
+ -fx-padding: 0.4em;
-fx-background-color: -color-neutral-subtle;
-fx-border-width: 1 0 1 0;
-fx-border-color: -color-border-default;
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/filter-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/filter-comp.css
deleted file mode 100644
index 331fd8c3c..000000000
--- a/app/src/main/resources/io/xpipe/app/resources/style/filter-comp.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.filter-comp {
- -fx-padding: 0.15em 0.3em 0.15em 0.3em;
- -fx-background-color: transparent;
-}
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/named-choice-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/named-choice-comp.css
deleted file mode 100644
index bf1937c57..000000000
--- a/app/src/main/resources/io/xpipe/app/resources/style/named-choice-comp.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.named-store-choice, .named-source-choice {
- -fx-border-width: 1px;
- -fx-border-color: -color-accent-fg;
- -fx-background-color: transparent;
- -fx-border-radius: 4px;
- -fx-padding: 2px;
- -fx-background-radius: 4px;
-}
\ No newline at end of file
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/prefs.css b/app/src/main/resources/io/xpipe/app/resources/style/prefs.css
index d6ceeff26..f00863e4c 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/prefs.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/prefs.css
@@ -35,31 +35,26 @@
-fx-background-color: -color-bg-subtle;
-fx-border-width: 0 1 0 0;
-fx-border-color: -color-border-default;
- -fx-padding: 0.7em 0 0 0;
+ -fx-padding: 0.2em 0 0 0;
}
.prefs .sidebar .button {
-fx-background-color: transparent;
- -fx-padding: 0.5em 1em 0.5em 1.2em;
- -fx-border-radius: 0;
- -fx-background-radius: 0;
- -fx-border-width: 1;
- -fx-background-insets: 0;
- -fx-border-insets: 0;
+ -fx-padding: 0.6em 1em 0.6em 1em;
+ -fx-background-radius: 0, 4, 4;
+ -fx-background-insets: 0, 2 4 2 4, 3 5 3 5;
}
.prefs .sidebar .button:selected {
- -fx-background-color: -color-accent-subtle;
- -fx-border-color: -color-accent-emphasis;
- -fx-border-width: 1 0 1 0;
+ -fx-background-color: transparent, -color-border-default, -color-bg-default;
}
.prefs .sidebar .button:armed {
- -fx-background-color: derive(-color-neutral-muted, 25%);
+ -fx-background-color: transparent, -color-accent-muted, derive(-color-neutral-muted, 25%);
}
.prefs .sidebar .button:hover, .root:key-navigation .prefs .sidebar .button:focused {
- -fx-background-color: -color-neutral-muted;
+ -fx-background-color: transparent, -color-border-default, -color-bg-overlay;
}
.prefs .theme-switcher .combo-box-popup .list-view {
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/section-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/section-comp.css
index 9bc8f7283..df2abab39 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/section-comp.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/section-comp.css
@@ -18,4 +18,4 @@
.options-comp .long-description {
-fx-padding: 0 6 0 6;
-}
\ No newline at end of file
+}
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/sidebar-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/sidebar-comp.css
index 317444173..855e4f9bc 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/sidebar-comp.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/sidebar-comp.css
@@ -30,7 +30,7 @@
}
.sidebar-comp .icon-button-comp {
- -fx-padding: 1em;
+ -fx-padding: 1.1em;
}
.sidebar-comp .icon-button-comp .vbox {
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css
index 10a77eb29..5710d3738 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css
@@ -25,14 +25,6 @@
-fx-text-fill: #ee4829;
}
-.store-entry-grid:incomplete .summary {
- -fx-text-fill: #ee4829;
-}
-
-.store-entry-grid:incomplete .information {
- -fx-text-fill: #ee4829;
-}
-
.store-entry-grid:incomplete .icon {
-fx-opacity: 0.5;
}
@@ -90,6 +82,14 @@
-fx-opacity: 0.2;
}
+.store-entry-comp .button-bar {
+ -fx-padding: 5;
+}
+
+.store-entry-grid.dense .button-bar {
+ -fx-padding: 3;
+}
+
.store-entry-comp .button-bar .button {
-fx-padding: 6px;
}
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/style.css b/app/src/main/resources/io/xpipe/app/resources/style/style.css
index a19000a00..b9c77be98 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/style.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/style.css
@@ -9,6 +9,10 @@
-fx-background-color: transparent;
}
+.root:macos:seamless-frame {
+ -fx-padding: 0 0 27 0;
+}
+
.root:dark:separate-frame .background {
-fx-background-color: derive(-color-bg-default, 1%);
}
@@ -46,16 +50,21 @@
.root:seamless-frame.layout > .background {
-fx-background-insets: 5 0 0 0;
-fx-border-insets: 5 0 0 0;
- -fx-background-radius: 0 10 0 0;
- -fx-border-radius: 0 10 0 0;
+ -fx-background-radius: 0 6 0 0;
+ -fx-border-radius: 0 6 0 0;
-fx-border-width: 1 1 0 0;
-fx-border-color: -color-border-default;
-fx-padding: 0 0 0 0;
}
+.root:macos:seamless-frame.layout > .background {
+ -fx-background-insets: 0;
+ -fx-border-insets: 0;
+}
+
.root:seamless-frame.layout > .background > * {
- -fx-background-radius: 0 10 0 0;
- -fx-border-radius: 0 10 0 0;
+ -fx-background-radius: 0 6 0 0;
+ -fx-border-radius: 0 6 0 0;
}
.toggle-switch:has-graphic .label {
@@ -63,7 +72,7 @@
}
.toggle-switch:has-graphic {
- -fx-font-size: 0.8em;
+ -fx-font-size: 0.75em;
}
.store-layout .split-pane-divider {
@@ -87,12 +96,16 @@
-fx-background-radius: 4px;
-fx-border-width: 0.05em;
-fx-border-radius: 4px;
- -fx-padding: 1em;
+ -fx-padding: 1px;
-fx-background-color: -color-bg-default;
-fx-border-color: -color-neutral-emphasis;
}
-.text {
+.scan-list .list-content {
+ -fx-padding: 0.7em 1px 1em 1em;
+}
+
+* {
-fx-font-smoothing-type: gray;
}
diff --git a/beacon/build.gradle b/beacon/build.gradle
index 096dd649b..e6a912921 100644
--- a/beacon/build.gradle
+++ b/beacon/build.gradle
@@ -17,8 +17,8 @@ repositories {
dependencies {
compileOnly 'org.hamcrest:hamcrest:2.2'
- compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.2'
- compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.2'
+ compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.3'
+ compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.3'
api project(':core')
}
diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java
index be1e2dae7..16dbbec7e 100644
--- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java
+++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java
@@ -24,6 +24,15 @@ public class BeaconClient {
this.port = port;
}
+ public static boolean isOccupied(int port) {
+ var file = XPipeInstallation.getLocalBeaconAuthFile();
+ var reachable = BeaconServer.isReachable(port);
+ if (!Files.exists(file) && !reachable) {
+ return false;
+ }
+ return reachable;
+ }
+
public static BeaconClient establishConnection(int port, BeaconClientInformation information) throws Exception {
var client = new BeaconClient(port);
var auth = Files.readString(XPipeInstallation.getLocalBeaconAuthFile());
@@ -55,7 +64,8 @@ public class BeaconClient {
var client = HttpClient.newHttpClient();
HttpResponse response;
try {
- var uri = URI.create("http://localhost:" + port + prov.getPath());
+ // Use direct IP to prevent DNS lookups and potential blocks (e.g. portmaster)
+ var uri = URI.create("http://127.0.0.1:" + port + prov.getPath());
var builder = HttpRequest.newBuilder();
if (token != null) {
builder.header("Authorization", "Bearer " + token);
diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java b/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java
index 0e28a2f03..91742971c 100644
--- a/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java
+++ b/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java
@@ -62,6 +62,10 @@ public abstract class BeaconInterface {
return (Class) Class.forName(name);
}
+ public boolean acceptInShutdown() {
+ return false;
+ }
+
public boolean requiresCompletedStartup() {
return true;
}
diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java
index 3de5f910b..838058c45 100644
--- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java
+++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java
@@ -8,7 +8,7 @@ import io.xpipe.core.util.XPipeInstallation;
import java.io.BufferedReader;
import java.io.InputStreamReader;
-import java.net.InetAddress;
+import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.List;
@@ -20,7 +20,7 @@ public class BeaconServer {
public static boolean isReachable(int port) {
try (var socket = new Socket()) {
- socket.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), 5000);
+ socket.connect(new InetSocketAddress(Inet4Address.getByAddress(new byte[]{ 0x7f,0x00,0x00,0x01 }), port), 5000);
return true;
} catch (Exception e) {
return false;
diff --git a/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java
index a2674cc76..eb7587ef1 100644
--- a/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java
+++ b/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java
@@ -12,6 +12,11 @@ import java.util.UUID;
public class AskpassExchange extends BeaconInterface {
+ @Override
+ public boolean acceptInShutdown() {
+ return true;
+ }
+
@Override
public String getPath() {
return "/askpass";
diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionAddExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionAddExchange.java
index 64ab54cd8..fec2a8841 100644
--- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionAddExchange.java
+++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionAddExchange.java
@@ -2,6 +2,7 @@ package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
import io.xpipe.core.store.DataStore;
+
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionBrowseExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionBrowseExchange.java
index d5c1dd249..f10b9dc57 100644
--- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionBrowseExchange.java
+++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionBrowseExchange.java
@@ -1,6 +1,7 @@
package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
+
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRefreshExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRefreshExchange.java
index aea789314..2646f0e6e 100644
--- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRefreshExchange.java
+++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRefreshExchange.java
@@ -1,6 +1,7 @@
package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
+
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRemoveExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRemoveExchange.java
index 600f443d8..530f7b0cd 100644
--- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRemoveExchange.java
+++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRemoveExchange.java
@@ -1,6 +1,7 @@
package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
+
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionTerminalExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionTerminalExchange.java
index 8b74204e8..0fbd1138f 100644
--- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionTerminalExchange.java
+++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionTerminalExchange.java
@@ -1,6 +1,7 @@
package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
+
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionToggleExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionToggleExchange.java
index 5a1bc1829..dcce1da17 100644
--- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionToggleExchange.java
+++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionToggleExchange.java
@@ -1,6 +1,7 @@
package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
+
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
diff --git a/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java
index cd1410acd..aa40a5f2e 100644
--- a/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java
+++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java
@@ -26,12 +26,16 @@ public class DaemonVersionExchange extends BeaconInterface {
+ @Override
+ public boolean acceptInShutdown() {
+ return true;
+ }
+
@Override
public String getPath() {
return "/handshake";
diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java
index 10f32b12b..4d4115e29 100644
--- a/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java
+++ b/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java
@@ -3,6 +3,7 @@ package io.xpipe.beacon.api;
import io.xpipe.beacon.BeaconInterface;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellDialect;
+import io.xpipe.core.process.ShellTtyState;
import io.xpipe.core.store.FilePath;
import lombok.Builder;
@@ -40,6 +41,9 @@ public class ShellStartExchange extends BeaconInterface
}
}
+
+def user = project.hasProperty('sonatypeUsername') ? project.property('sonatypeUsername') : System.getenv('SONATYPE_USERNAME')
+def pass = project.hasProperty('sonatypePassword') ? project.property('sonatypePassword') : System.getenv('SONATYPE_PASSWORD')
+
+tasks.withType(GenerateModuleMetadata) {
+ enabled = false
+}
+
+nexusPublishing {
+ repositories {
+ sonatype {
+ nexusUrl.set(uri('https://s01.oss.sonatype.org/service/local/'))
+ snapshotRepositoryUrl.set(uri('https://s01.oss.sonatype.org/content/repositories/snapshots/'))
+ username = user
+ password = pass
+ }
+ }
+ useStaging = true
+}
+
var devProps = file("$rootDir/app/dev.properties")
if (!devProps.exists()) {
devProps.text = file("$rootDir/gradle/gradle_scripts/dev_default.properties").text
@@ -65,13 +85,13 @@ def getArchName() {
def getPlatformName() {
def currentOS = DefaultNativePlatform.currentOperatingSystem;
- def platform = null
+ def platform
if (currentOS.isWindows()) {
platform = 'windows'
- } else if (currentOS.isLinux()) {
- platform = 'linux'
- } else if (currentOS.isMacOsX()) {
+ } else if (currentOS.isMacOsX()) {
platform = 'osx'
+ } else {
+ platform = 'linux'
}
return platform;
}
@@ -122,11 +142,13 @@ project.ext {
"-Dio.xpipe.app.arch=$rootProject.arch",
"-Dio.xpipe.app.languages=${String.join(",", languages)}",
"-Dfile.encoding=UTF-8",
- // Disable this for now as it requires Windows 10+
- // '-XX:+UseZGC',
"-Dvisualvm.display.name=XPipe",
"-Djavafx.preloader=io.xpipe.app.core.AppPreloader"
]
+ // Disable this on Windows for now as it requires Windows 10+
+ if (org.gradle.internal.os.OperatingSystem.current().isLinux() || org.gradle.internal.os.OperatingSystem.current().isMacOsX()) {
+ jvmRunArgs += ['-XX:+UseZGC']
+ }
if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) {
jvmRunArgs += ["-Dapple.awt.application.appearance=system"]
}
@@ -212,3 +234,6 @@ task testAll(type: DefaultTask) {
}
finalizedBy(testReport)
}
+
+group = 'io.xpipe'
+version = versionString
\ No newline at end of file
diff --git a/core/build.gradle b/core/build.gradle
index df8952576..c8478cb37 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -13,8 +13,8 @@ compileJava {
}
dependencies {
- api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.1"
- implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.1"
+ api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.2"
+ implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.2"
}
version = rootProject.versionString
diff --git a/core/src/main/java/io/xpipe/core/process/CommandBuilder.java b/core/src/main/java/io/xpipe/core/process/CommandBuilder.java
index 3e2904845..fa9b90a41 100644
--- a/core/src/main/java/io/xpipe/core/process/CommandBuilder.java
+++ b/core/src/main/java/io/xpipe/core/process/CommandBuilder.java
@@ -255,6 +255,10 @@ public class CommandBuilder {
}
public String buildFull(ShellControl sc) throws Exception {
+ if (sc == null) {
+ return buildSimple();
+ }
+
var s = buildBase(sc);
LinkedHashMap map = new LinkedHashMap<>();
for (var e : environmentVariables.entrySet()) {
diff --git a/core/src/main/java/io/xpipe/core/process/CommandControl.java b/core/src/main/java/io/xpipe/core/process/CommandControl.java
index 7849cf087..2e76586e0 100644
--- a/core/src/main/java/io/xpipe/core/process/CommandControl.java
+++ b/core/src/main/java/io/xpipe/core/process/CommandControl.java
@@ -1,15 +1,10 @@
package io.xpipe.core.process;
-import io.xpipe.core.util.FailableConsumer;
-
-import com.fasterxml.jackson.databind.JsonNode;
-
import java.io.InputStream;
-import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.Charset;
+import java.time.Duration;
import java.util.Optional;
-import java.util.function.Consumer;
import java.util.function.Function;
public interface CommandControl extends ProcessControl {
@@ -59,6 +54,8 @@ public interface CommandControl extends ProcessControl {
OutputStream startExternalStdin() throws Exception;
+ public void setExitTimeout(Duration duration);
+
boolean waitFor();
CommandControl withCustomCharset(Charset charset);
@@ -67,32 +64,14 @@ public interface CommandControl extends ProcessControl {
CommandControl elevated(ElevationFunction function);
- void withStdoutOrThrow(FailableConsumer c);
-
String[] readStdoutAndStderr() throws Exception;
- String readStdoutDiscardErr() throws Exception;
-
- String readStderrDiscardStdout() throws Exception;
-
void discardOrThrow() throws Exception;
- void accumulateStdout(Consumer con);
-
- void accumulateStderr(Consumer con);
-
byte[] readRawBytesOrThrow() throws Exception;
String readStdoutOrThrow() throws Exception;
- JsonNode readStdoutJsonOrThrow() throws Exception;
-
- String readStderrOrThrow() throws Exception;
-
- String readStdoutAndWait() throws Exception;
-
- String readStderrAndWait() throws Exception;
-
Optional readStdoutIfPossible() throws Exception;
default boolean discardAndCheckExit() throws ProcessOutputException {
@@ -110,10 +89,6 @@ public interface CommandControl extends ProcessControl {
}
}
- void discardOut();
-
- void discardErr();
-
enum TerminalExitMode {
KEEP_OPEN,
CLOSE
diff --git a/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java b/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java
deleted file mode 100644
index 24d569580..000000000
--- a/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package io.xpipe.core.process;
-
-public interface CommandFeedbackPredicate {
-
- boolean test(CommandBuilder command);
-}
diff --git a/core/src/main/java/io/xpipe/core/process/OsType.java b/core/src/main/java/io/xpipe/core/process/OsType.java
index 9882e409e..8d454b044 100644
--- a/core/src/main/java/io/xpipe/core/process/OsType.java
+++ b/core/src/main/java/io/xpipe/core/process/OsType.java
@@ -4,8 +4,6 @@ import io.xpipe.core.store.FileNames;
import java.util.List;
import java.util.Locale;
-import java.util.Map;
-import java.util.stream.Collectors;
public interface OsType {
@@ -21,10 +19,8 @@ public interface OsType {
return MACOS;
} else if (osName.contains("win")) {
return WINDOWS;
- } else if (osName.contains("nux")) {
- return LINUX;
} else {
- throw new UnsupportedOperationException("Unknown operating system");
+ return LINUX;
}
}
@@ -38,14 +34,10 @@ public interface OsType {
String getName();
- String getTempDirectory(ShellControl pc) throws Exception;
-
- Map getProperties(ShellControl pc) throws Exception;
-
- String determineOperatingSystemName(ShellControl pc) throws Exception;
-
sealed interface Local extends OsType permits OsType.Windows, OsType.Linux, OsType.MacOs {
+ String getId();
+
default Any toAny() {
return (Any) this;
}
@@ -88,48 +80,8 @@ public interface OsType {
}
@Override
- public String getTempDirectory(ShellControl pc) throws Exception {
- var def = pc.executeSimpleStringCommand(pc.getShellDialect().getPrintEnvironmentVariableCommand("TEMP"));
- if (!def.isBlank() && pc.getShellDialect().directoryExists(pc, def).executeAndCheck()) {
- return def;
- }
-
- var fallback = pc.executeSimpleStringCommand(
- pc.getShellDialect().getPrintEnvironmentVariableCommand("LOCALAPPDATA"));
- if (!fallback.isBlank()
- && pc.getShellDialect().directoryExists(pc, fallback).executeAndCheck()) {
- return fallback;
- }
-
- return def;
- }
-
- @Override
- public Map getProperties(ShellControl pc) throws Exception {
- try (CommandControl c = pc.command("systeminfo").start()) {
- var text = c.readStdoutOrThrow();
- return PropertiesFormatsParser.parse(text, ":");
- }
- }
-
- @Override
- public String determineOperatingSystemName(ShellControl pc) {
- try {
- return pc.executeSimpleStringCommand("wmic os get Caption")
- .lines()
- .skip(1)
- .collect(Collectors.joining())
- .trim()
- + " "
- + pc.executeSimpleStringCommand("wmic os get Version")
- .lines()
- .skip(1)
- .collect(Collectors.joining())
- .trim();
- } catch (Throwable t) {
- // Just in case this fails somehow
- return "Windows";
- }
+ public String getId() {
+ return "windows";
}
}
@@ -145,7 +97,7 @@ public interface OsType {
public List determineInterestingPaths(ShellControl pc) throws Exception {
var home = getHomeDirectory(pc);
return List.of(
- home, FileNames.join(home, "Downloads"), FileNames.join(home, "Documents"), "/etc", "/tmp", "/var");
+ home, "/home", FileNames.join(home, "Downloads"), FileNames.join(home, "Documents"), "/etc", "/tmp", "/var");
}
@Override
@@ -163,58 +115,15 @@ public interface OsType {
return "Linux";
}
- @Override
- public String getTempDirectory(ShellControl pc) {
- return "/tmp/";
- }
-
- @Override
- public Map getProperties(ShellControl pc) {
- return null;
- }
-
- @Override
- public String determineOperatingSystemName(ShellControl pc) throws Exception {
- String type = "Unknown";
- try (CommandControl c = pc.command("uname -o").start()) {
- var text = c.readStdoutDiscardErr();
- if (c.getExitCode() == 0) {
- type = text.strip();
- }
- }
-
- String version = "?";
- try (CommandControl c = pc.command("uname -r").start()) {
- var text = c.readStdoutDiscardErr();
- if (c.getExitCode() == 0) {
- version = text.strip();
- }
- }
-
- return type + " " + version;
- }
}
final class Linux extends Unix implements OsType, Local, Any {
@Override
- public String determineOperatingSystemName(ShellControl pc) throws Exception {
- try (CommandControl c = pc.command("lsb_release -a").start()) {
- var text = c.readStdoutDiscardErr();
- if (c.getExitCode() == 0) {
- return PropertiesFormatsParser.parse(text, ":").getOrDefault("Description", "Unknown");
- }
- }
-
- try (CommandControl c = pc.command("cat /etc/*release").start()) {
- var text = c.readStdoutDiscardErr();
- if (c.getExitCode() == 0) {
- return PropertiesFormatsParser.parse(text, "=").getOrDefault("PRETTY_NAME", "Unknown");
- }
- }
-
- return super.determineOperatingSystemName(pc);
+ public String getId() {
+ return "linux";
}
+
}
final class Solaris extends Unix implements Any {}
@@ -223,6 +132,11 @@ public interface OsType {
final class MacOs implements OsType, Local, Any {
+ @Override
+ public String getId() {
+ return "macos";
+ }
+
@Override
public String makeFileSystemCompatible(String name) {
// Technically the backslash is supported, but it causes all kinds of troubles, so we also exclude it
@@ -258,34 +172,5 @@ public interface OsType {
return "Mac";
}
- @Override
- public String getTempDirectory(ShellControl pc) throws Exception {
- var found = pc.executeSimpleStringCommand(pc.getShellDialect().getPrintVariableCommand("TMPDIR"));
-
- // This variable is not defined for root users, so manually fix it. Why? ...
- if (found.isBlank()) {
- return "/tmp";
- }
-
- return found;
- }
-
- @Override
- public Map getProperties(ShellControl pc) throws Exception {
- try (CommandControl c = pc.command("sw_vers").start()) {
- var text = c.readStdoutOrThrow();
- return PropertiesFormatsParser.parse(text, ":");
- }
- }
-
- @Override
- public String determineOperatingSystemName(ShellControl pc) throws Exception {
- var properties = getProperties(pc);
- var name = pc.executeSimpleStringCommand(
- "awk '/SOFTWARE LICENSE AGREEMENT FOR macOS/' '/System/Library/CoreServices/Setup "
- + "Assistant.app/Contents/Resources/en.lproj/OSXSoftwareLicense.rtf' | "
- + "awk -F 'macOS ' '{print $NF}' | awk '{print substr($0, 0, length($0)-1)}'");
- return properties.get("ProductName") + " " + name + " " + properties.get("ProductVersion");
- }
}
}
diff --git a/core/src/main/java/io/xpipe/core/process/ShellControl.java b/core/src/main/java/io/xpipe/core/process/ShellControl.java
index 3c1efe058..f0cb5af7d 100644
--- a/core/src/main/java/io/xpipe/core/process/ShellControl.java
+++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java
@@ -18,6 +18,8 @@ import java.util.function.Function;
public interface ShellControl extends ProcessControl {
+ ShellTtyState getTtyState();
+
void setNonInteractive();
boolean isInteractive();
@@ -65,6 +67,7 @@ public interface ShellControl extends ProcessControl {
var s = store.getState().toBuilder()
.osType(shellControl.getOsType())
.shellDialect(shellControl.getOriginalShellDialect())
+ .ttyState(shellControl.getTtyState())
.running(true)
.osName(shellControl.getOsName())
.build();
@@ -113,25 +116,12 @@ public interface ShellControl extends ProcessControl {
script));
}
- default byte[] executeSimpleRawBytesCommand(String command) throws Exception {
- try (CommandControl c = command(command).start()) {
- return c.readRawBytesOrThrow();
- }
- }
-
default String executeSimpleStringCommand(String command) throws Exception {
try (CommandControl c = command(command).start()) {
return c.readStdoutOrThrow();
}
}
- default Optional executeSimpleStringCommandAndCheck(String command) throws Exception {
- try (CommandControl c = command(command).start()) {
- var out = c.readStdoutDiscardErr();
- return c.getExitCode() == 0 ? Optional.of(out) : Optional.empty();
- }
- }
-
default boolean executeSimpleBooleanCommand(String command) throws Exception {
try (CommandControl c = command(command).start()) {
return c.discardAndCheckExit();
@@ -150,20 +140,6 @@ public interface ShellControl extends ProcessControl {
}
}
- default void executeSimpleCommand(String command, String failMessage) throws Exception {
- try (CommandControl c = command(command).start()) {
- c.discardOrThrow();
- } catch (ProcessOutputException out) {
- throw ProcessOutputException.withPrefix(failMessage, out);
- }
- }
-
- default String executeSimpleStringCommand(ShellDialect type, String command) throws Exception {
- try (var sub = subShell(type).start()) {
- return sub.executeSimpleStringCommand(command);
- }
- }
-
ShellControl withSecurityPolicy(ShellSecurityPolicy policy);
ShellSecurityPolicy getEffectiveSecurityPolicy();
@@ -232,10 +208,6 @@ public interface ShellControl extends ProcessControl {
ShellControl singularSubShell(ShellOpenFunction command);
- void writeLineAndReadEcho(String command) throws Exception;
-
- void writeLineAndReadEcho(String command, boolean log) throws Exception;
-
void cd(String directory) throws Exception;
default CommandControl command(String command) {
diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialect.java b/core/src/main/java/io/xpipe/core/process/ShellDialect.java
index 231a7b30f..e4f474ef3 100644
--- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java
+++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java
@@ -9,6 +9,7 @@ import io.xpipe.core.util.StreamCharset;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
@@ -104,8 +105,6 @@ public interface ShellDialect {
String nullStdin(String command);
- String getScriptPermissionsCommand(String file);
-
ShellDialectAskpass getAskpass();
String getSetEnvironmentVariableCommand(String variable, String value);
@@ -118,7 +117,11 @@ public interface ShellDialect {
CommandControl printUsernameCommand(ShellControl shellControl);
- String getPrintExitCodeCommand(String prefix, String suffix);
+ String getPrintStartEchoCommand(String prefix);
+
+ Optional executeRobustBootstrapOutputCommand(ShellControl shellControl, String original) throws Exception;
+
+ String getPrintExitCodeCommand(String id, String prefix, String suffix);
int assignMissingExitCode();
@@ -128,9 +131,7 @@ public interface ShellDialect {
CommandBuilder getOpenScriptCommand(String file);
- default void prepareCommandForShell(CommandBuilder b) {}
-
- String prepareTerminalInitFileOpenCommand(ShellDialect parentDialect, ShellControl sc, String file);
+ String prepareTerminalInitFileOpenCommand(ShellDialect parentDialect, ShellControl sc, String file, boolean exit);
String runScriptCommand(ShellControl parent, String file);
@@ -184,5 +185,5 @@ public interface ShellDialect {
String getDisplayName();
- boolean doesEchoInput();
+ boolean doesEchoInputByDefault();
}
diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialects.java b/core/src/main/java/io/xpipe/core/process/ShellDialects.java
index a471f0d39..68893c8bd 100644
--- a/core/src/main/java/io/xpipe/core/process/ShellDialects.java
+++ b/core/src/main/java/io/xpipe/core/process/ShellDialects.java
@@ -27,6 +27,7 @@ public class ShellDialects {
public static ShellDialect CISCO;
public static ShellDialect MIKROTIK;
public static ShellDialect RBASH;
+ public static ShellDialect CONSTRAINED_POWERSHELL;
public static ShellDialect OVH_BASTION;
public static List getStartableDialects() {
@@ -85,6 +86,7 @@ public class ShellDialects {
CISCO = byId("cisco");
MIKROTIK = byId("mikrotik");
RBASH = byId("rbash");
+ CONSTRAINED_POWERSHELL = byId("constrainedPowershell");
OVH_BASTION = byId("ovhBastion");
}
}
diff --git a/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java b/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java
index 20573ed72..3faa460c2 100644
--- a/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java
+++ b/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java
@@ -8,6 +8,8 @@ public interface ShellDumbMode {
return true;
}
+ default void throwIfUnsupported() {}
+
default ShellDialect getSwitchDialect() {
return null;
}
@@ -25,6 +27,14 @@ public interface ShellDumbMode {
class Unsupported implements ShellDumbMode {
+ private final String message;
+
+ public Unsupported(String message) {this.message = message;}
+
+ public void throwIfUnsupported() {
+ throw new UnsupportedOperationException(message);
+ }
+
@Override
public boolean supportsAnyPossibleInteraction() {
return false;
diff --git a/core/src/main/java/io/xpipe/core/process/ShellNameStoreState.java b/core/src/main/java/io/xpipe/core/process/ShellNameStoreState.java
deleted file mode 100644
index 6042b4c4c..000000000
--- a/core/src/main/java/io/xpipe/core/process/ShellNameStoreState.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.xpipe.core.process;
-
-import io.xpipe.core.store.DataStoreState;
-
-import lombok.EqualsAndHashCode;
-import lombok.Value;
-import lombok.experimental.SuperBuilder;
-import lombok.extern.jackson.Jacksonized;
-
-@Value
-@EqualsAndHashCode(callSuper = true)
-@SuperBuilder(toBuilder = true)
-@Jacksonized
-public class ShellNameStoreState extends ShellStoreState {
-
- String shellName;
-
- @Override
- public DataStoreState mergeCopy(DataStoreState newer) {
- var n = (ShellNameStoreState) newer;
- var b = toBuilder();
- mergeBuilder(n, b);
- return b.shellName(useNewer(shellName, n.shellName)).build();
- }
-}
diff --git a/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java b/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java
index ee620918c..07a4b6de3 100644
--- a/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java
+++ b/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java
@@ -27,7 +27,7 @@ public interface ShellOpenFunction {
@Override
public CommandBuilder prepareWithInitCommand(@NonNull String command) {
- throw new UnsupportedOperationException();
+ return CommandBuilder.of().add(command);
}
};
}
diff --git a/core/src/main/java/io/xpipe/core/process/ShellProperties.java b/core/src/main/java/io/xpipe/core/process/ShellProperties.java
deleted file mode 100644
index b8df6d34d..000000000
--- a/core/src/main/java/io/xpipe/core/process/ShellProperties.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package io.xpipe.core.process;
-
-import lombok.Value;
-
-@Value
-public class ShellProperties {
-
- ShellDialect dialect;
- boolean ansiEscapes;
-}
diff --git a/core/src/main/java/io/xpipe/core/process/ShellStoreState.java b/core/src/main/java/io/xpipe/core/process/ShellStoreState.java
index 72dcc9efe..4f2d1360f 100644
--- a/core/src/main/java/io/xpipe/core/process/ShellStoreState.java
+++ b/core/src/main/java/io/xpipe/core/process/ShellStoreState.java
@@ -19,6 +19,7 @@ public class ShellStoreState extends DataStoreState implements OsNameState {
OsType.Any osType;
String osName;
ShellDialect shellDialect;
+ ShellTtyState ttyState;
Boolean running;
public boolean isRunning() {
@@ -39,6 +40,7 @@ public class ShellStoreState extends DataStoreState implements OsNameState {
b.osType(useNewer(osType, shellStoreState.getOsType()))
.osName(useNewer(osName, shellStoreState.getOsName()))
.shellDialect(useNewer(shellDialect, shellStoreState.getShellDialect()))
+ .ttyState(useNewer(ttyState, shellStoreState.getTtyState()))
.running(useNewer(running, shellStoreState.getRunning()));
}
}
diff --git a/core/src/main/java/io/xpipe/core/process/ShellTtyState.java b/core/src/main/java/io/xpipe/core/process/ShellTtyState.java
new file mode 100644
index 000000000..ef1afe101
--- /dev/null
+++ b/core/src/main/java/io/xpipe/core/process/ShellTtyState.java
@@ -0,0 +1,29 @@
+package io.xpipe.core.process;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+
+@Getter
+public enum ShellTtyState {
+
+ @JsonProperty("none")
+ NONE(true, false, false, true, true),
+ @JsonProperty("merged")
+ MERGED_STDERR(false, false, false, false, true),
+ @JsonProperty("pty")
+ PTY_ALLOCATED(false, true, true, false, false);
+
+ private final boolean hasSeparateStreams;
+ private final boolean hasAnsiEscapes;
+ private final boolean echoesAllInput;
+ private final boolean supportsInput;
+ private final boolean preservesOutput;
+
+ ShellTtyState(boolean hasSeparateStreams, boolean hasAnsiEscapes, boolean echoesAllInput, boolean supportsInput, boolean preservesOutput) {
+ this.hasSeparateStreams = hasSeparateStreams;
+ this.hasAnsiEscapes = hasAnsiEscapes;
+ this.echoesAllInput = echoesAllInput;
+ this.supportsInput = supportsInput;
+ this.preservesOutput = preservesOutput;
+ }
+}
diff --git a/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java b/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java
index 6073bd7be..3fb3e81e7 100644
--- a/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java
+++ b/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java
@@ -1,13 +1,13 @@
package io.xpipe.core.store;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import java.io.InputStream;
import java.io.OutputStream;
+import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@@ -36,10 +36,17 @@ public class ConnectionFileSystem implements FileSystem {
@Override
public FileSystem open() throws Exception {
shellControl.start();
- if (!shellControl.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
+
+ var d = shellControl.getShellDialect().getDumbMode();
+ if (!d.supportsAnyPossibleInteraction()) {
shellControl.close();
- throw new UnsupportedOperationException("System shell does not support file system interaction");
+ d.throwIfUnsupported();
}
+
+ if (!shellControl.getTtyState().isPreservesOutput() || !shellControl.getTtyState().isSupportsInput()) {
+ throw new UnsupportedOperationException("Shell has a PTY allocated and does not support file system operations");
+ }
+
return this;
}
@@ -53,10 +60,9 @@ public class ConnectionFileSystem implements FileSystem {
@Override
public OutputStream openOutput(String file, long totalBytes) throws Exception {
- return shellControl
- .getShellDialect()
- .createStreamFileWriteCommand(shellControl, file, totalBytes)
- .startExternalStdin();
+ var cmd = shellControl.getShellDialect().createStreamFileWriteCommand(shellControl, file, totalBytes);
+ cmd.setExitTimeout(Duration.ofMillis(Long.MAX_VALUE));
+ return cmd.startExternalStdin();
}
@Override
diff --git a/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java b/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java
index b805b7e96..e91e40b84 100644
--- a/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java
+++ b/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java
@@ -19,6 +19,6 @@ public class EnabledStoreState extends DataStoreState {
@Override
public DataStoreState mergeCopy(DataStoreState newer) {
var n = (EnabledStoreState) newer;
- return EnabledStoreState.builder().enabled(n.enabled).build();
+ return EnabledStoreState.builder().enabled(enabled || n.enabled).build();
}
}
diff --git a/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java b/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java
index 4abbab803..7676fc2c3 100644
--- a/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java
+++ b/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java
@@ -60,7 +60,8 @@ public interface NetworkTunnelStore extends DataStore {
default NetworkTunnelSession sessionChain(int local, int remotePort) throws Exception {
if (!isLocallyTunneable()) {
- throw new IllegalStateException("Unable to create tunnel chain as one intermediate system does not support tunneling");
+ throw new IllegalStateException(
+ "Unable to create tunnel chain as one intermediate system does not support tunneling");
}
var running = new AtomicBoolean();
diff --git a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java
index 734e91329..a1da2051c 100644
--- a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java
+++ b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java
@@ -1,13 +1,5 @@
package io.xpipe.core.util;
-import com.fasterxml.jackson.annotation.JsonIdentityInfo;
-import com.fasterxml.jackson.annotation.ObjectIdGenerators;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.*;
-import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-import com.fasterxml.jackson.databind.jsontype.NamedType;
-import com.fasterxml.jackson.databind.module.SimpleModule;
import io.xpipe.core.dialog.BaseQueryElement;
import io.xpipe.core.dialog.BusyElement;
import io.xpipe.core.dialog.ChoiceElement;
@@ -19,6 +11,15 @@ import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.store.StorePath;
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.jsontype.NamedType;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
diff --git a/core/src/main/java/io/xpipe/core/util/JacksonMapper.java b/core/src/main/java/io/xpipe/core/util/JacksonMapper.java
index ccb028425..a2166823e 100644
--- a/core/src/main/java/io/xpipe/core/util/JacksonMapper.java
+++ b/core/src/main/java/io/xpipe/core/util/JacksonMapper.java
@@ -1,13 +1,15 @@
package io.xpipe.core.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.Module;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
+import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.Getter;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
@@ -88,4 +90,36 @@ public class JacksonMapper {
return INSTANCE;
}
+
+ public static ObjectMapper getCensored() {
+ if (!JacksonMapper.isInit()) {
+ return BASE;
+ }
+
+ var c = INSTANCE.copy();
+ c.registerModule(new SimpleModule() {
+ @Override
+ public void setupModule(SetupContext context) {
+ addSerializer(SecretValue.class, new JsonSerializer<>() {
+ @Override
+ public void serialize(SecretValue value, JsonGenerator gen, SerializerProvider serializers)
+ throws IOException {
+ gen.writeString("");
+ }
+
+ @Override
+ public void serializeWithType(
+ SecretValue value,
+ JsonGenerator gen,
+ SerializerProvider serializers,
+ TypeSerializer typeSer)
+ throws IOException {
+ gen.writeString("");
+ }
+ });
+ super.setupModule(context);
+ }
+ });
+ return c;
+ }
}
diff --git a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java
index b5557c39a..09b0e8994 100644
--- a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java
+++ b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java
@@ -32,7 +32,7 @@ public class XPipeInstallation {
}
public static Path getLocalBeaconAuthFile() {
- return Path.of(System.getProperty("java.io.tmpdir"), "xpipe_auth");
+ return Path.of(System.getProperty("java.io.tmpdir"), isStaging() ? "xpipe_ptb_auth" : "xpipe_auth");
}
public static String createExternalAsyncLaunchCommand(
diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java
index 1ccf7cf3f..692574970 100644
--- a/core/src/main/java/module-info.java
+++ b/core/src/main/java/module-info.java
@@ -17,6 +17,7 @@ open module io.xpipe.core {
requires com.fasterxml.jackson.databind;
requires java.net.http;
requires static lombok;
+ requires java.sql;
uses com.fasterxml.jackson.databind.Module;
uses ProcessControlProvider;
diff --git a/dist/base.gradle b/dist/base.gradle
index bb3f0aebd..23a3c3da5 100644
--- a/dist/base.gradle
+++ b/dist/base.gradle
@@ -53,31 +53,10 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
debugAttachArguments + ' ' + debugArguments)
debugAttach.setExecutable(true)
- copy {
- from "$distDir/cli"
- into "$distDir/base/cli/bin"
- }
copy {
from "$distDir/licenses"
into "$distDir/base/licenses"
}
- copy {
- from "$projectDir/bundled_bin/$platformName"
- into "$distDir/base/app/bundled"
- }
- copy {
- from "$distDir/docs/html5"
- into "$distDir/base/cli/docs"
- }
-
- if (rootProject.fullVersion) {
- file("$distDir/base/app/xpiped.exe").writable = true
- exec {
- commandLine "$projectDir\\tools\\sign.bat", "$distDir/base/app/xpiped.exe"
- ignoreExitValue = true
- }
- file("$distDir/base/app/xpiped.exe").writable = false
- }
}
}
} else if (org.gradle.internal.os.OperatingSystem.current().isLinux()) {
@@ -129,26 +108,6 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
from "$distDir/licenses"
into "$distDir/base/licenses"
}
- copy {
- from "$distDir/cli/xpipe"
- into "$distDir/base/cli/bin"
- }
- copy {
- from "$distDir/docs/html5"
- into "$distDir/base/cli/docs"
- }
- copy {
- from "$distDir/cli/xpipe_completion"
- into "$distDir/base/cli"
- }
- copy {
- from "$distDir/docs/manpage"
- into "$distDir/base/cli/man"
- }
- copy {
- from "$projectDir/bundled_bin/$platformName"
- into "$distDir/base/app/bundled"
- }
}
}
} else {
@@ -163,49 +122,19 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
from "$projectDir/logo/logo.icns"
into "$distDir/$app/Contents/Resources/"
}
- copy {
- from "$distDir/cli/xpipe"
- into "$distDir/$app/Contents/MacOS/"
- }
copy {
from "$distDir/licenses"
into "$distDir/$app/Contents/Resources/licenses"
}
- copy {
- from "$distDir/docs/html5"
- into "$distDir/$app/Contents/Resources/cli/docs"
- }
- copy {
- from "$distDir/docs/manpage"
- into "$distDir/$app/Contents/Resources/cli/man"
- }
- copy {
- from "$distDir/cli/xpipe_completion"
- into "$distDir/$app/Contents/Resources/cli/"
- }
copy {
from "$projectDir/fonts"
into "$distDir/$app/Contents/Resources/fonts"
}
- copy {
- from "$projectDir/bundled_bin/$platformName"
- into "$distDir/$app/Contents/Resources/bundled"
- }
copy {
from "$rootDir/lang"
into "$distDir/$app/Contents/Resources/lang"
}
-
- copy {
- from "$projectDir/PkgInstaller/darwin/Resources/uninstall.sh"
- into "$distDir/$app/Contents/Resources/scripts/"
- }
- file("$distDir/$app/Contents/Resources/scripts/uninstall.sh").text = file("$distDir/$app/Contents/Resources/scripts/uninstall.sh").text
- .replaceAll("__PRODUCT__", productName)
- .replaceAll("__PRODUCT_KEBAP__", kebapProductName)
- .replaceAll("__VERSION__", versionString)
-
def debugArguments = file("$projectDir/debug/debug_arguments.txt").text.lines().map(s -> '"' + s + '"').collect(Collectors.joining(
' '))
def debugAttachArguments = file("$projectDir/debug/mac/debug_attach_arguments.txt").text.lines().map(s -> '"' + s + '"').collect(
@@ -223,12 +152,6 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
'JVM-ARGS',
debugAttachArguments + ' ' + debugArguments)
debugAttach.setExecutable(true, false)
-
- if (System.getenv("MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_NAME") != null) {
- exec {
- commandLine "$projectDir/misc/mac/sign_and_notarize.sh", "$projectDir", rootProject.arch.toString(), rootProject.productName
- }
- }
}
}
}
diff --git a/dist/build.gradle b/dist/build.gradle
index 7f5f09b9d..e85d4a8d3 100644
--- a/dist/build.gradle
+++ b/dist/build.gradle
@@ -1,8 +1,8 @@
plugins {
id 'org.beryx.jlink' version '3.0.1'
- id "org.asciidoctor.jvm.convert" version "4.0.2"
- id 'org.jreleaser' version '1.12.0'
+ id "org.asciidoctor.jvm.convert" version "4.0.3"
+ id 'org.jreleaser' version '1.13.1'
id("com.netflix.nebula.ospackage") version "11.9.1"
id 'org.gradle.crypto.checksum' version '1.4.0'
id 'signing'
@@ -40,11 +40,11 @@ task createChecksums(type: Checksum) {
doLast {
def artifactChecksumsSha256Hex = new HashMap()
for (final def file in outputDirectory.get().getAsFileTree().files) {
- if (file.toString().endsWith('mapping.map') || file.toString().endsWith('.asc')) {
+ def name = file.name.lastIndexOf('.').with {it != -1 ? file.name[0.."`
+- Add option to use double clicks to open connections instead of single clicks
+- Add support for foot terminal
+- Fix rare null pointers and freezes in file browser
+- Fix PowerShell remote session file editing not transferring file correctly
+- Fix elementary terminal not launching correctly
+- Fix windows jumping around when created
+- Fix kubernetes not elevating correctly for non-default contexts
+- Fix ohmyzsh update notification freezing shell
+- Fix file browser icons being broken for links
+- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality
+- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues
diff --git a/dist/changelogs/10.1.1_incremental.md b/dist/changelogs/10.1.1_incremental.md
new file mode 100644
index 000000000..b54fcc612
--- /dev/null
+++ b/dist/changelogs/10.1.1_incremental.md
@@ -0,0 +1,5 @@
+- Fix terminal window closing instantly if connection failed, not showing error messages
+- Fix file browser editor sometimes not applying changes
+- Fix updater not doing anything when trying to install an update when downloaded installer had been deleted on a restart
+- Fix xpipe CLI executable missing signature on Windows
+- Fix various smaller bugs
\ No newline at end of file
diff --git a/dist/changelogs/10.1_incremental.md b/dist/changelogs/10.1_incremental.md
index 12ceddf1f..e0b96df2d 100644
--- a/dist/changelogs/10.1_incremental.md
+++ b/dist/changelogs/10.1_incremental.md
@@ -1,3 +1,14 @@
+## Browser improvements
+
+Feedback showed that the file browser transfer pane in the bottom left was confusing and unintuitive to use. Therefore, it has now been changed to be a more straightforward download area. You can drag files into it to automatically download them. From there you can either drag them directly where you want them to be in your local desktop environment or move them into the downloads directory.
+
+There is now the possibility to jump to a file in a directory by typing the first few characters of its name.
+
+There were also a couple of bug fixes:
+- Fix file transfers on Windows systems failing for files > 2GB due to overflow
+- Fix remote file editing sometimes creating blank file when using vscode
+- Fix file transfers failing at the end with a timeout when the connection speed was very slow
+
## API additions
Several new endpoints have been added to widen the capabilities for external clients:
@@ -11,7 +22,10 @@ Several new endpoints have been added to widen the capabilities for external cli
## Other
+- Fix xpipe not starting up when changing user on Linux
+- Fix some editors and terminals not launching when using the fallback sh system shell due to missing disown command
+- Fix csh sudo elevation not working
- Implement various application performance improvements
- Rework sidebar styling
- Improve transparency styling on Windows 11
-- Fix csh sudo elevation not working
\ No newline at end of file
+- Add support for zed editor
\ No newline at end of file
diff --git a/dist/changelogs/10.2.1.md b/dist/changelogs/10.2.1.md
new file mode 100644
index 000000000..25037ee47
--- /dev/null
+++ b/dist/changelogs/10.2.1.md
@@ -0,0 +1,67 @@
+## A new HTTP API
+
+There is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages.
+
+To start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files.
+
+There already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects.
+
+## Service integration
+
+Many systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port.
+
+You can use an unlimited amount of local services and one active tunneled service in the community edition.
+
+## Script rework
+
+The scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types:
+- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently
+- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments.
+- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files
+
+If you have existing scripts, they will have to be manually adjusted by setting their execution types.
+
+## Docker improvements
+
+The docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition.
+
+There's now support for Windows docker containers running on HyperV.
+
+Note that old docker container connections will be removed as they are incompatible with the new version.
+
+## Proxmox improvements
+
+You can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers.
+
+You can now open VNC sessions to Proxmox VMs.
+
+The Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition.
+
+## Better connection organization
+
+The toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general.
+
+You can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left.
+
+The UI has also been streamlined to make common actions and toggles more easily accessible.
+
+## Other
+
+- The title bar on Windows will now follow the appearance theme
+- Several more actions have been added for podman containers
+- Support VMs for tunneling
+- Searching for connections has been improved to show children as well
+- There is now an AppImage portable release
+- The welcome screen will now also contain the option to straight up jump to the synchronization settings
+- You can now launch xpipe in another data directory with `xpipe open -d ""`
+- Add option to use double clicks to open connections instead of single clicks
+- Add support for foot terminal
+- Fix rare null pointers and freezes in file browser
+- Fix PowerShell remote session file editing not transferring file correctly
+- Fix elementary terminal not launching correctly
+- Fix windows jumping around when created
+- Fix kubernetes not elevating correctly for non-default contexts
+- Fix ohmyzsh update notification freezing shell
+- Fix file browser icons being broken for links
+- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality
+- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues
diff --git a/dist/changelogs/10.2.1_incremental.md b/dist/changelogs/10.2.1_incremental.md
new file mode 100644
index 000000000..5d9289d4a
--- /dev/null
+++ b/dist/changelogs/10.2.1_incremental.md
@@ -0,0 +1 @@
+- Fix startup issue on older x86_64 macOS systems
diff --git a/dist/changelogs/10.2.2.md b/dist/changelogs/10.2.2.md
new file mode 100644
index 000000000..25037ee47
--- /dev/null
+++ b/dist/changelogs/10.2.2.md
@@ -0,0 +1,67 @@
+## A new HTTP API
+
+There is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages.
+
+To start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files.
+
+There already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects.
+
+## Service integration
+
+Many systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port.
+
+You can use an unlimited amount of local services and one active tunneled service in the community edition.
+
+## Script rework
+
+The scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types:
+- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently
+- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments.
+- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files
+
+If you have existing scripts, they will have to be manually adjusted by setting their execution types.
+
+## Docker improvements
+
+The docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition.
+
+There's now support for Windows docker containers running on HyperV.
+
+Note that old docker container connections will be removed as they are incompatible with the new version.
+
+## Proxmox improvements
+
+You can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers.
+
+You can now open VNC sessions to Proxmox VMs.
+
+The Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition.
+
+## Better connection organization
+
+The toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general.
+
+You can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left.
+
+The UI has also been streamlined to make common actions and toggles more easily accessible.
+
+## Other
+
+- The title bar on Windows will now follow the appearance theme
+- Several more actions have been added for podman containers
+- Support VMs for tunneling
+- Searching for connections has been improved to show children as well
+- There is now an AppImage portable release
+- The welcome screen will now also contain the option to straight up jump to the synchronization settings
+- You can now launch xpipe in another data directory with `xpipe open -d ""`
+- Add option to use double clicks to open connections instead of single clicks
+- Add support for foot terminal
+- Fix rare null pointers and freezes in file browser
+- Fix PowerShell remote session file editing not transferring file correctly
+- Fix elementary terminal not launching correctly
+- Fix windows jumping around when created
+- Fix kubernetes not elevating correctly for non-default contexts
+- Fix ohmyzsh update notification freezing shell
+- Fix file browser icons being broken for links
+- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality
+- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues
diff --git a/dist/changelogs/10.2.2_incremental.md b/dist/changelogs/10.2.2_incremental.md
new file mode 100644
index 000000000..6fa79e493
--- /dev/null
+++ b/dist/changelogs/10.2.2_incremental.md
@@ -0,0 +1,6 @@
+- Fix Windows installers producing SmartScreen warning
+- Fix setting to use double clicks when launching connections not working
+- Fix potential stack overflow when opening VNC connections
+- Fix file browser shortcuts conflicting with others and intercepting others
+- Fix some broken keyboard shortcuts
+- Fix certain special character key combinations being wrongfully intercepted by window, leading to a window close when typing @ on some european keyboards
diff --git a/dist/changelogs/10.2.md b/dist/changelogs/10.2.md
new file mode 100644
index 000000000..25037ee47
--- /dev/null
+++ b/dist/changelogs/10.2.md
@@ -0,0 +1,67 @@
+## A new HTTP API
+
+There is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages.
+
+To start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files.
+
+There already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects.
+
+## Service integration
+
+Many systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port.
+
+You can use an unlimited amount of local services and one active tunneled service in the community edition.
+
+## Script rework
+
+The scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types:
+- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently
+- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments.
+- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files
+
+If you have existing scripts, they will have to be manually adjusted by setting their execution types.
+
+## Docker improvements
+
+The docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition.
+
+There's now support for Windows docker containers running on HyperV.
+
+Note that old docker container connections will be removed as they are incompatible with the new version.
+
+## Proxmox improvements
+
+You can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers.
+
+You can now open VNC sessions to Proxmox VMs.
+
+The Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition.
+
+## Better connection organization
+
+The toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general.
+
+You can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left.
+
+The UI has also been streamlined to make common actions and toggles more easily accessible.
+
+## Other
+
+- The title bar on Windows will now follow the appearance theme
+- Several more actions have been added for podman containers
+- Support VMs for tunneling
+- Searching for connections has been improved to show children as well
+- There is now an AppImage portable release
+- The welcome screen will now also contain the option to straight up jump to the synchronization settings
+- You can now launch xpipe in another data directory with `xpipe open -d ""`
+- Add option to use double clicks to open connections instead of single clicks
+- Add support for foot terminal
+- Fix rare null pointers and freezes in file browser
+- Fix PowerShell remote session file editing not transferring file correctly
+- Fix elementary terminal not launching correctly
+- Fix windows jumping around when created
+- Fix kubernetes not elevating correctly for non-default contexts
+- Fix ohmyzsh update notification freezing shell
+- Fix file browser icons being broken for links
+- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality
+- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues
diff --git a/dist/changelogs/10.2_incremental.md b/dist/changelogs/10.2_incremental.md
new file mode 100644
index 000000000..d5e19b4ba
--- /dev/null
+++ b/dist/changelogs/10.2_incremental.md
@@ -0,0 +1,31 @@
+## File browser improvements
+
+- Add right click context menu to browser tabs
+- Add ability to select tabs with function keys, e.g. F1, F2, ...
+- Add ability to cycle between tabs with CTRL+TAB and CTRL+SHIFT+TAB
+- Fix some keyboard shortcuts being broken
+- Fix pressing enter on rename also opening file
+- Fix right click not opening context menu in empty directory
+- Fix shell opener in navigation bar being broken, so you can now run programs and shells again from the navigation bar similar to Windows explorer
+- There is now an always visible loading indicator when a tab is being opened
+- Add timeout to file selection when typing a file name that was not found
+- Improve flow of file selection by when typing its name
+- Remove limitation of only being able to open one system at the time while it is loading
+
+## Other
+
+- Rework UI to be more compact and show more connections
+- Implement native window styling on macOS
+- Add support for VNC RSA-AES authentication schemes, allowing to connect to more types of VNC servers
+- Services can now be opened in a browser using either HTTP or HTTPs
+- You can now create shortcuts to automatically forward and open services in a browser
+- Fix docker containers in some cases not persisting, leaving invalid orphan connections behind on the bottom
+- Fix connection failures to proxmox VMs that have additional custom network interfaces
+- Fix window not saving maximized state on restart
+- Don't modify git URLs anymore to fix sync with certain providers like azure
+- Improve git remote connection error messages
+- Replace system tray mode with background mode on Linux
+- Improve description for service groups
+- Publish API libraries to maven central
+- Show warning when launching PowerShell in constrained language mode
+- Fix rare NullPointers when migrating an old vault
diff --git a/dist/changelogs/11.0.md b/dist/changelogs/11.0.md
new file mode 100644
index 000000000..bca13a08c
--- /dev/null
+++ b/dist/changelogs/11.0.md
@@ -0,0 +1,49 @@
+## TTYs and PTYs
+
+Up until now, if you added a connection that always allocated pty, XPipe would complain about a missing stderr.
+In XPipe 11, there has been a ground up rework of the shell initialization code which will in theory allow for better handling of these cases.
+They are not fully supported yet and have some issues, but should work better.
+
+The main concern here is to verify that the existing normal shell implementation still works as before and there were no bugs introduced by this rework.
+
+## Teleport support
+
+There is now support to add your teleport connections that are available via tsh.
+
+## Profiles
+
+You can now create multiple user profiles in the settings menu.
+
+This will create desktop shortcuts that you can use to start XPipe with different profiles active.
+
+## Serial connection support
+
+There is now support to add serial connections.
+
+## Scripting improvements
+
+The scripting system has been reworked in order to make it more intuitive and powerful.
+
+The script execution types have been renamed, the documentation has been improved, and a new execution type has been added.
+The new runnable execution type will allow you to call a script from the connection hub directly in a dropdown for each connection when the script is active.
+This will also replace the current terminal command functionality, which has been removed.
+
+Any file browser scripts are now grouped by the scripts groups they are in, improving the overview when having many file browser scripts.
+Furthermore, you can now launch these scripts in the file browser either in the background if they are quiet or in a terminal if they are intended to be interactive.
+When multiple files are selected, a script is now called only once with all the selected files as arguments.
+
+## Other
+
+- Rework state information display for proxmox VMs
+- Fix git sync freezing when using key with passphrase on modern ssh clients
+- Fix git sync restarting daemon after exit when using key with passphrase
+- Fix terminal exit not working properly in fish
+- Fix renaming a connection clearing all state information
+- Fix script enabled status being wrong after editing an enabled script
+- Fix download move operation failing when moving a directory that already existed in the downloads folder
+- Fix some scrollbars are necessarily showing
+- Automatically fill identity file for ssh config wildcard keys as well
+- Improve error messages when system interaction was disabled for a system
+- Don't show git all compatibility warnings on minor version updates
+- Enable ZGC on Linux and macOS
+- Some small appearance fixes
\ No newline at end of file
diff --git a/dist/jpackage.gradle b/dist/jpackage.gradle
index 62ae673d3..993e15018 100644
--- a/dist/jpackage.gradle
+++ b/dist/jpackage.gradle
@@ -54,7 +54,8 @@ jlink {
'--no-header-files',
'--no-man-pages',
'--include-locales', "${String.join(",", languages)}",
- '--compress', 'zip-9'
+ '--compress', 'zip-9',
+ '--ignore-signing-information'
]
if (org.gradle.internal.os.OperatingSystem.current().isLinux()) {
@@ -118,7 +119,7 @@ task prepareMacOSInfo(type: DefaultTask) {
doLast {
file("${project.layout.buildDirectory.get()}/macos_resources").mkdirs()
copy {
- from replaceVariablesInFile("$projectDir/misc/mac/Info.plist",
+ from replaceVariablesInFile("$projectDir/jpackage/Info.plist",
Map.of('__NAME__',
rootProject.productName,
'__VERSION__',
diff --git a/dist/jpackage/Info.plist b/dist/jpackage/Info.plist
new file mode 100644
index 000000000..5557a5efb
--- /dev/null
+++ b/dist/jpackage/Info.plist
@@ -0,0 +1,48 @@
+
+
+
+