mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-05-29 07:20:35 +00:00
Squash merge branch 19-release into master
This commit is contained in:
+6
-6
@@ -26,16 +26,16 @@ You should therefore always check out the matching version tag for your local re
|
||||
You can find the available version tags at https://github.com/xpipe-io/xpipe/tags.
|
||||
So for example if you currently have XPipe `16.0` installed, you should run `git reset --hard 16.0` first to properly compile against it.
|
||||
|
||||
You need to have JDK for Java 24 installed to compile the project.
|
||||
You need to have JDK for Java 25 installed to compile the project.
|
||||
If you are on Linux or macOS, you can easily accomplish that by running
|
||||
```bash
|
||||
curl -s "https://get.sdkman.io" | bash
|
||||
. "$HOME/.sdkman/bin/sdkman-init.sh"
|
||||
sdk install java 24-graalce
|
||||
sdk default java 24-graalce
|
||||
sdk install java 25-graalce
|
||||
sdk default java 25-graalce
|
||||
```
|
||||
.
|
||||
On Windows, you have to manually install a JDK, e.g. from [Adoptium](https://adoptium.net/temurin/releases/?version=24).
|
||||
On Windows, you have to manually install a JDK, e.g. from [Adoptium](https://adoptium.net/temurin/releases/?version=25).
|
||||
|
||||
You can configure a few development options in the file `app/dev.properties` which will be automatically generated when gradle is first run.
|
||||
|
||||
@@ -51,7 +51,7 @@ You are also able to properly debug the built production application:
|
||||
|
||||
## Modularity and IDEs
|
||||
|
||||
All XPipe components target [Java 24](https://openjdk.java.net/projects/jdk/24/) and make full use of the Java Module System (JPMS).
|
||||
All XPipe components target [Java 25](https://openjdk.java.net/projects/jdk/25/) and make full use of the Java Module System (JPMS).
|
||||
All components are modularized, including all their dependencies.
|
||||
In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
|
||||
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
|
||||
@@ -59,7 +59,7 @@ many IDEs still have problems building this project properly.
|
||||
|
||||
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
|
||||
The tested and recommended IDE is IntelliJ.
|
||||
When setting up the project in IntelliJ, make sure that the correct JDK (Java 24)
|
||||
When setting up the project in IntelliJ, make sure that the correct JDK (Java 25)
|
||||
is selected both for the project and for gradle itself.
|
||||
|
||||
## Contributing guide
|
||||
|
||||
@@ -19,7 +19,7 @@ It currently supports:
|
||||
- [Proxmox PVE](https://docs.xpipe.io/guide/proxmox) virtual machines and containers
|
||||
- [Hyper-V](https://docs.xpipe.io/guide/hyperv), [KVM](https://docs.xpipe.io/guide/kvm), [VMware Player/Workstation/Fusion](https://docs.xpipe.io/guide/vmware) virtual machines
|
||||
- [Kubernetes](https://docs.xpipe.io/guide/kubernetes) clusters, pods, and containers
|
||||
- [Tailscale](https://docs.xpipe.io/guide/tailscale) and [Teleport](https://docs.xpipe.io/guide/teleport) connections
|
||||
- [Tailscale](https://docs.xpipe.io/guide/tailscale), [Netbird](https://docs.xpipe.io/guide/netbird), and [Teleport](https://docs.xpipe.io/guide/teleport) connections
|
||||
- Windows Subsystem for Linux, Cygwin, and MSYS2 environments
|
||||
- [Powershell Remote Sessions](https://docs.xpipe.io/guide/pssession)
|
||||
- [RDP](https://docs.xpipe.io/guide/rdp) and [VNC](https://docs.xpipe.io/guide/vnc) connections
|
||||
|
||||
+20
-12
@@ -48,10 +48,17 @@ dependencies {
|
||||
api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8'
|
||||
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
|
||||
|
||||
api ('io.modelcontextprotocol.sdk:mcp:0.11.2') {
|
||||
api ('io.modelcontextprotocol.sdk:mcp-core:0.14.1') {
|
||||
exclude group: "com.ethlo.time", module: "itu"
|
||||
}
|
||||
api ('io.modelcontextprotocol.sdk:mcp-json:0.14.1') {
|
||||
exclude group: "com.ethlo.time", module: "itu"
|
||||
}
|
||||
api ('io.modelcontextprotocol.sdk:mcp-json-jackson2:0.13.0') {
|
||||
exclude group: "com.ethlo.time", module: "itu"
|
||||
exclude group: "com.fasterxml.jackson.dataformat", module: "jackson-dataformat-yaml"
|
||||
}
|
||||
|
||||
api "io.projectreactor:reactor-core:3.7.9"
|
||||
api "org.reactivestreams:reactive-streams:1.0.4"
|
||||
api ("com.networknt:json-schema-validator:1.5.8") {
|
||||
@@ -59,22 +66,23 @@ dependencies {
|
||||
exclude group: "com.fasterxml.jackson.dataformat", module: "jackson-dataformat-yaml"
|
||||
}
|
||||
|
||||
api "com.github.weisj:jsvg:1.7.1"
|
||||
api 'io.xpipe:vernacular:1.15'
|
||||
api "com.github.weisj:jsvg:1.7.2"
|
||||
api 'io.xpipe:vernacular:1.16'
|
||||
api 'org.bouncycastle:bcprov-jdk18on:1.81'
|
||||
api 'info.picocli:picocli:4.7.7'
|
||||
api 'org.apache.commons:commons-lang3:3.18.0'
|
||||
api 'io.sentry:sentry:8.13.3'
|
||||
api 'io.sentry:sentry:8.20.0'
|
||||
api 'commons-io:commons-io:2.20.0'
|
||||
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.19.2"
|
||||
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.19.2"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.4.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.4.0"
|
||||
api "com.fasterxml.jackson.core:jackson-databind:2.20.0"
|
||||
api "com.fasterxml.jackson.core:jackson-annotations:2.20"
|
||||
api "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.0"
|
||||
api "org.kordamp.ikonli:ikonli-material2-pack:12.4.0"
|
||||
api "org.kordamp.ikonli:ikonli-materialdesign2-pack:12.4.0"
|
||||
api 'org.kordamp.ikonli:ikonli-bootstrapicons-pack:12.4.0'
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.4.0"
|
||||
api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.17'
|
||||
api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.17'
|
||||
api 'io.xpipe:modulefs:0.1.6'
|
||||
api "org.kordamp.ikonli:ikonli-javafx:12.4.0"
|
||||
api "org.slf4j:slf4j-api:2.0.17"
|
||||
api "org.slf4j:slf4j-jdk-platform-logging:2.0.17"
|
||||
api 'io.xpipe:modulefs:0.1.7'
|
||||
api 'net.synedra:validatorfx:0.4.2'
|
||||
api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package io.xpipe.app;
|
||||
|
||||
import io.xpipe.app.core.AppNames;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
|
||||
public class Main {
|
||||
|
||||
@@ -17,13 +18,14 @@ public class Main {
|
||||
if (args.length == 1 && args[0].equals("--help")) {
|
||||
System.out.println(
|
||||
"""
|
||||
The daemon executable xpiped does not accept any command-line arguments.
|
||||
The daemon executable %s does not accept any command-line arguments.
|
||||
|
||||
For a reference on how to use xpipe from the command-line, take a look at https://docs.xpipe.io/cli.
|
||||
""");
|
||||
"""
|
||||
.formatted(AppNames.ofCurrent().getExecutableName()));
|
||||
return;
|
||||
}
|
||||
|
||||
OperationMode.init(args);
|
||||
AppOperationMode.init(args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.util.DataStoreFormatter;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.util.LicensedFeature;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
@@ -40,9 +40,11 @@ public abstract class AbstractAction {
|
||||
|
||||
AppLayoutModel.get().getQueueEntries().add(queueEntry);
|
||||
pick = action -> {
|
||||
cancelPick();
|
||||
var modal = ModalOverlay.of("actionShortcuts", new ActionPickComp(action).prefWidth(600));
|
||||
modal.show();
|
||||
if (action instanceof SerializableAction) {
|
||||
cancelPick();
|
||||
var modal = ModalOverlay.of("actionShortcuts", new ActionPickComp(action).prefWidth(600));
|
||||
modal.show();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import io.xpipe.app.hub.action.StoreAction;
|
||||
import io.xpipe.app.hub.comp.StoreChoiceComp;
|
||||
import io.xpipe.app.hub.comp.StoreListChoiceComp;
|
||||
import io.xpipe.app.hub.comp.StoreViewState;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.*;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -69,7 +69,7 @@ public class ActionConfigComp extends SimpleComp {
|
||||
singleProp.set((DataStoreEntryRef<DataStore>) s);
|
||||
|
||||
singleProp.addListener((obs, o, n) -> {
|
||||
if (action.getValue() instanceof StoreAction<?> sa) {
|
||||
if (action.getValue() instanceof StoreAction<?> sa && n != null) {
|
||||
action.setValue(sa.withRef(n.asNeeded()));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,8 +9,8 @@ import io.xpipe.app.hub.action.MultiStoreAction;
|
||||
import io.xpipe.app.hub.action.StoreAction;
|
||||
import io.xpipe.app.hub.comp.StoreListChoiceComp;
|
||||
import io.xpipe.app.hub.comp.StoreViewState;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
|
||||
@@ -2,7 +2,7 @@ package io.xpipe.app.action;
|
||||
|
||||
import io.xpipe.app.comp.base.ModalOverlayContentComp;
|
||||
import io.xpipe.app.comp.base.ScrollComp;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
@@ -8,9 +8,14 @@ import io.xpipe.app.comp.base.InputGroupComp;
|
||||
import io.xpipe.app.comp.base.TextFieldComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppInstallation;
|
||||
import io.xpipe.app.platform.BindingsHelper;
|
||||
import io.xpipe.app.platform.ClipboardHelper;
|
||||
import io.xpipe.app.platform.OptionsBuilder;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.update.AppDistributionType;
|
||||
import io.xpipe.app.util.*;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.scene.layout.Region;
|
||||
@@ -62,6 +67,7 @@ public class ActionShortcutComp extends SimpleComp {
|
||||
field.apply(struc -> struc.get().setEditable(false));
|
||||
var group = new InputGroupComp(List.of(field, copyButton));
|
||||
group.setHeightReference(copyButton);
|
||||
group.hide(Bindings.isNull(url));
|
||||
return group;
|
||||
}
|
||||
|
||||
@@ -76,7 +82,9 @@ public class ActionShortcutComp extends SimpleComp {
|
||||
});
|
||||
var copyButton = new ButtonComp(null, new FontIcon("mdi2f-file-move-outline"), () -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var exec = AppInstallation.ofCurrent().getCliExecutablePath().toString();
|
||||
var exec = AppInstallation.ofCurrent()
|
||||
.getCliExecutablePath()
|
||||
.toString();
|
||||
var file = DesktopShortcuts.create(exec, "open \"" + url.getValue() + "\"", name.getValue());
|
||||
DesktopHelper.browseFileInDirectory(file);
|
||||
});
|
||||
@@ -87,6 +95,7 @@ public class ActionShortcutComp extends SimpleComp {
|
||||
field.grow(true, false);
|
||||
var group = new InputGroupComp(List.of(field, copyButton));
|
||||
group.setHeightReference(copyButton);
|
||||
group.hide(BindingsHelper.map(action, v -> !(v instanceof SerializableAction)));
|
||||
return group;
|
||||
}
|
||||
|
||||
@@ -108,6 +117,7 @@ public class ActionShortcutComp extends SimpleComp {
|
||||
field.apply(struc -> struc.get().setEditable(false));
|
||||
var group = new InputGroupComp(List.of(field, copyButton));
|
||||
group.setHeightReference(copyButton);
|
||||
group.hide(BindingsHelper.map(action, v -> !(v instanceof SerializableAction)));
|
||||
return group;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ public class AppBeaconServer {
|
||||
// Not terminal!
|
||||
// We can still continue without the running server
|
||||
ErrorEventFactory.fromThrowable("Unable to start local http server on port " + INSTANCE.getPort(), ex)
|
||||
.documentationLink(DocumentationLink.BEACON_PORT_BIND)
|
||||
.build()
|
||||
.handle();
|
||||
}
|
||||
@@ -122,7 +123,7 @@ public class AppBeaconServer {
|
||||
var file = BeaconConfig.getLocalBeaconAuthFile();
|
||||
var id = UUID.randomUUID().toString();
|
||||
Files.writeString(file, id);
|
||||
if (OsType.getLocal() != OsType.WINDOWS) {
|
||||
if (OsType.ofLocal() != OsType.WINDOWS) {
|
||||
Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----"));
|
||||
}
|
||||
localAuthSecret = id;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.xpipe.app.beacon;
|
||||
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
@@ -28,13 +28,13 @@ public class BeaconRequestHandler<T> implements HttpHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) {
|
||||
if (OperationMode.isInShutdown() && !beaconInterface.acceptInShutdown()) {
|
||||
if (AppOperationMode.isInShutdown() && !beaconInterface.acceptInShutdown()) {
|
||||
writeError(exchange, new BeaconClientErrorResponse("Daemon is currently in shutdown"), 400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (beaconInterface.requiresCompletedStartup()) {
|
||||
while (OperationMode.isInStartup()) {
|
||||
while (AppOperationMode.isInStartup()) {
|
||||
ThreadHelper.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.xpipe.app.beacon;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.util.ShellTemp;
|
||||
import io.xpipe.app.process.ShellTemp;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
@@ -2,6 +2,9 @@ package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.secret.SecretManager;
|
||||
import io.xpipe.app.secret.SecretQueryState;
|
||||
import io.xpipe.app.terminal.TerminalView;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
|
||||
@@ -26,7 +26,7 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
|
||||
}
|
||||
|
||||
if (found.isPresent()) {
|
||||
found.get().setStoreInternal(store, true);
|
||||
DataStorage.get().updateEntryStore(found.get(), store);
|
||||
return Response.builder().connection(found.get().getUuid()).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.ext.FixedHierarchyStore;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.FixedHierarchyStore;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.api.ConnectionRefreshExchange;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.beacon.api.DaemonFocusExchange;
|
||||
|
||||
@@ -9,9 +9,12 @@ import com.sun.net.httpserver.HttpExchange;
|
||||
public class DaemonFocusExchangeImpl extends DaemonFocusExchange {
|
||||
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) {
|
||||
OperationMode.switchUp(OperationMode.GUI);
|
||||
var w = AppMainWindow.getInstance();
|
||||
public Object handle(HttpExchange exchange, Request msg) throws Throwable {
|
||||
if (AppOperationMode.GUI.isSupported()) {
|
||||
AppOperationMode.switchToSyncOrThrow(AppOperationMode.GUI);
|
||||
}
|
||||
|
||||
var w = AppMainWindow.get();
|
||||
if (w != null) {
|
||||
w.focus();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.api.DaemonModeExchange;
|
||||
|
||||
@@ -9,19 +9,19 @@ import com.sun.net.httpserver.HttpExchange;
|
||||
public class DaemonModeExchangeImpl extends DaemonModeExchange {
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
|
||||
var mode = OperationMode.map(msg.getMode());
|
||||
var mode = AppOperationMode.map(msg.getMode());
|
||||
if (!mode.isSupported()) {
|
||||
throw new BeaconClientException("Unsupported mode: " + msg.getMode().getDisplayName()
|
||||
+ ". Supported: "
|
||||
+ String.join(
|
||||
", ",
|
||||
OperationMode.getAll().stream()
|
||||
.filter(OperationMode::isSupported)
|
||||
.map(OperationMode::getId)
|
||||
AppOperationMode.getAll().stream()
|
||||
.filter(AppOperationMode::isSupported)
|
||||
.map(AppOperationMode::getId)
|
||||
.toList()));
|
||||
}
|
||||
|
||||
OperationMode.switchToSyncIfPossible(mode);
|
||||
AppOperationMode.switchToSyncIfPossible(mode);
|
||||
return DaemonModeExchange.Response.builder().usedMode(msg.getMode()).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.core.AppOpenArguments;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.util.PlatformInit;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.platform.PlatformInit;
|
||||
import io.xpipe.beacon.BeaconServerException;
|
||||
import io.xpipe.beacon.api.DaemonOpenExchange;
|
||||
import io.xpipe.core.OsType;
|
||||
@@ -31,11 +31,11 @@ public class DaemonOpenExchangeImpl extends DaemonOpenExchange {
|
||||
|
||||
// The open command is used as a default opener on Linux
|
||||
// We don't want to overwrite the default startup mode
|
||||
if (OsType.getLocal() == OsType.LINUX && openCounter++ == 0) {
|
||||
if (OsType.ofLocal() == OsType.LINUX && openCounter++ == 0) {
|
||||
return Response.builder().build();
|
||||
}
|
||||
|
||||
OperationMode.switchToAsync(OperationMode.GUI);
|
||||
AppOperationMode.switchToAsync(AppOperationMode.GUI);
|
||||
} else {
|
||||
AppOpenArguments.handle(msg.getArguments());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.beacon.api.DaemonStatusExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
@@ -15,10 +15,10 @@ public class DaemonStatusExchangeImpl extends DaemonStatusExchange {
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request body) {
|
||||
String mode;
|
||||
if (OperationMode.get() == null) {
|
||||
if (AppOperationMode.get() == null) {
|
||||
mode = "none";
|
||||
} else {
|
||||
mode = OperationMode.get().getId();
|
||||
mode = AppOperationMode.get().getId();
|
||||
}
|
||||
|
||||
return Response.builder().mode(mode).build();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.beacon.api.DaemonStopExchange;
|
||||
|
||||
@@ -17,7 +17,7 @@ public class DaemonStopExchangeImpl extends DaemonStopExchange {
|
||||
public Object handle(HttpExchange exchange, Request msg) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
ThreadHelper.sleep(1000);
|
||||
OperationMode.close();
|
||||
AppOperationMode.close();
|
||||
});
|
||||
return Response.builder().success(true).build();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.beacon.AppBeaconServer;
|
||||
import io.xpipe.app.beacon.BlobManager;
|
||||
import io.xpipe.app.util.ScriptHelper;
|
||||
import io.xpipe.app.process.ScriptHelper;
|
||||
import io.xpipe.beacon.api.FsScriptExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.beacon.AppBeaconServer;
|
||||
import io.xpipe.app.beacon.BeaconSession;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.beacon.BeaconAuthMethod;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
@@ -19,12 +20,16 @@ public class HandshakeExchangeImpl extends HandshakeExchange {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request body) throws BeaconClientException {
|
||||
if (!checkAuth(body.getAuth())) {
|
||||
public Object handle(HttpExchange exchange, Request request) throws BeaconClientException {
|
||||
if (!checkAuth(request.getAuth())) {
|
||||
throw new BeaconClientException("Authentication failed");
|
||||
}
|
||||
|
||||
var session = new BeaconSession(body.getClient(), UUID.randomUUID().toString());
|
||||
TrackEvent.withTrace("Handshake request received")
|
||||
.tag("client", request.getClient().toDisplayString())
|
||||
.handle();
|
||||
|
||||
var session = new BeaconSession(request.getClient(), UUID.randomUUID().toString());
|
||||
AppBeaconServer.get().addSession(session);
|
||||
return Response.builder().sessionToken(session.getToken()).build();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import io.xpipe.app.util.ThreadHelper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import io.modelcontextprotocol.common.McpTransportContext;
|
||||
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.modelcontextprotocol.spec.HttpHeaders;
|
||||
@@ -38,7 +40,11 @@ public class AppMcpServer {
|
||||
@SneakyThrows
|
||||
public static void init() {
|
||||
var transportProvider = new HttpStreamableServerTransportProvider(
|
||||
new ObjectMapper(), "/mcp", false, (req, context) -> context, null);
|
||||
new JacksonMcpJsonMapper(new ObjectMapper()),
|
||||
"/mcp",
|
||||
false,
|
||||
(serverRequest) -> McpTransportContext.EMPTY,
|
||||
null);
|
||||
|
||||
McpSyncServer syncServer = io.modelcontextprotocol.server.McpServer.sync(transportProvider)
|
||||
.serverInfo(AppNames.ofCurrent().getName(), AppProperties.get().getVersion())
|
||||
|
||||
+21
-26
@@ -6,11 +6,10 @@ package io.xpipe.app.beacon.mcp;
|
||||
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.modelcontextprotocol.server.DefaultMcpTransportContext;
|
||||
import io.modelcontextprotocol.server.McpTransportContext;
|
||||
import io.modelcontextprotocol.common.McpTransportContext;
|
||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
import io.modelcontextprotocol.server.McpTransportContextExtractor;
|
||||
import io.modelcontextprotocol.spec.*;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
@@ -32,8 +31,6 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
|
||||
public static final String MESSAGE_EVENT_TYPE = "message";
|
||||
|
||||
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
|
||||
|
||||
public static final String UTF_8 = "UTF-8";
|
||||
public static final String APPLICATION_JSON = "application/json";
|
||||
public static final String TEXT_EVENT_STREAM = "text/event-stream";
|
||||
@@ -46,7 +43,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
|
||||
private final boolean disallowDelete;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final McpJsonMapper jsonMapper;
|
||||
|
||||
private final ConcurrentHashMap<String, McpStreamableServerSession> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -58,16 +55,16 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
private KeepAliveScheduler keepAliveScheduler;
|
||||
|
||||
HttpStreamableServerTransportProvider(
|
||||
ObjectMapper objectMapper,
|
||||
McpJsonMapper jsonMapper,
|
||||
String mcpEndpoint,
|
||||
boolean disallowDelete,
|
||||
McpTransportContextExtractor<HttpExchange> contextExtractor,
|
||||
Duration keepAliveInterval) {
|
||||
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
||||
Assert.notNull(jsonMapper, "ObjectMapper must not be null");
|
||||
Assert.notNull(mcpEndpoint, "MCP endpoint must not be null");
|
||||
Assert.notNull(contextExtractor, "Context extractor must not be null");
|
||||
|
||||
this.objectMapper = objectMapper;
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.mcpEndpoint = mcpEndpoint;
|
||||
this.disallowDelete = disallowDelete;
|
||||
this.contextExtractor = contextExtractor;
|
||||
@@ -85,7 +82,8 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
}
|
||||
|
||||
public List<String> protocolVersions() {
|
||||
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26);
|
||||
return List.of(
|
||||
ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -186,8 +184,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
|
||||
logger.debug("Handling GET request for session: {}", sessionId);
|
||||
|
||||
McpTransportContext transportContext =
|
||||
this.contextExtractor.extract(exchange, new DefaultMcpTransportContext());
|
||||
McpTransportContext transportContext = this.contextExtractor.extract(exchange);
|
||||
|
||||
try {
|
||||
exchange.getResponseHeaders().add("Content-Type", TEXT_EVENT_STREAM);
|
||||
@@ -265,13 +262,12 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
badRequestErrors.add("application/json required in Accept header");
|
||||
}
|
||||
|
||||
McpTransportContext transportContext =
|
||||
this.contextExtractor.extract(exchange, new DefaultMcpTransportContext());
|
||||
McpTransportContext transportContext = this.contextExtractor.extract(exchange);
|
||||
|
||||
try {
|
||||
var body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
|
||||
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body);
|
||||
|
||||
// Handle initialization request
|
||||
if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest
|
||||
@@ -283,7 +279,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
}
|
||||
|
||||
McpSchema.InitializeRequest initializeRequest =
|
||||
objectMapper.convertValue(jsonrpcRequest.params(), new TypeReference<>() {});
|
||||
jsonMapper.convertValue(jsonrpcRequest.params(), new TypeRef<>() {});
|
||||
McpStreamableServerSession.McpStreamableServerSessionInit init =
|
||||
this.sessionFactory.startSession(initializeRequest);
|
||||
this.sessions.put(init.session().getId(), init.session());
|
||||
@@ -291,7 +287,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
try {
|
||||
McpSchema.InitializeResult initResult = init.initResult().block();
|
||||
|
||||
String jsonResponse = objectMapper.writeValueAsString(new McpSchema.JSONRPCResponse(
|
||||
String jsonResponse = jsonMapper.writeValueAsString(new McpSchema.JSONRPCResponse(
|
||||
McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initResult, null));
|
||||
var jsonBytes = jsonResponse.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
@@ -399,8 +395,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
return;
|
||||
}
|
||||
|
||||
McpTransportContext transportContext =
|
||||
this.contextExtractor.extract(exchange, new DefaultMcpTransportContext());
|
||||
McpTransportContext transportContext = this.contextExtractor.extract(exchange);
|
||||
|
||||
if (exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID) == null) {
|
||||
sendError(exchange, 400, "Session ID required in mcp-session-id header");
|
||||
@@ -462,6 +457,11 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
logger.debug("Streamable session transport {} initialized with SSE writer", sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
||||
return jsonMapper.convertValue(data, typeRef);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message, String messageId) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
@@ -477,7 +477,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
return;
|
||||
}
|
||||
|
||||
String jsonText = objectMapper.writeValueAsString(message);
|
||||
String jsonText = jsonMapper.writeValueAsString(message);
|
||||
HttpStreamableServerTransportProvider.this.sendEvent(
|
||||
writer, MESSAGE_EVENT_TYPE, jsonText, messageId != null ? messageId : this.sessionId);
|
||||
logger.debug("Message sent to session {} with ID {}", this.sessionId, messageId);
|
||||
@@ -523,10 +523,5 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
|
||||
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
||||
return sendMessage(message, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
||||
return objectMapper.convertValue(data, typeRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import io.xpipe.app.core.AppExtensionManager;
|
||||
import io.xpipe.app.core.AppNames;
|
||||
import io.xpipe.app.ext.ConnectionFileSystem;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.FileInfo;
|
||||
import io.xpipe.app.ext.SingletonSessionStore;
|
||||
import io.xpipe.app.process.ScriptHelper;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.process.TerminalInitScriptConfig;
|
||||
import io.xpipe.app.process.WorkingDirectoryFunction;
|
||||
@@ -13,13 +15,12 @@ import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStorageQuery;
|
||||
import io.xpipe.app.terminal.TerminalLaunch;
|
||||
import io.xpipe.app.util.CommandDialog;
|
||||
import io.xpipe.app.util.ScriptHelper;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.core.FileInfo;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.JacksonMapper;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import lombok.Builder;
|
||||
@@ -124,7 +125,9 @@ public final class McpTools {
|
||||
object.set("found", json);
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.structuredContent(JacksonMapper.getDefault().writeValueAsString(object))
|
||||
.structuredContent(
|
||||
new JacksonMcpJsonMapper(JacksonMapper.getDefault()),
|
||||
JacksonMapper.getDefault().writeValueAsString(object))
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
@@ -229,13 +232,13 @@ public final class McpTools {
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
var fs = new ConnectionFileSystem(shellSession.getControl());
|
||||
|
||||
if (!fs.fileExists(path)) {
|
||||
throw new BeaconClientException("File " + path + " does not exist");
|
||||
if (!fs.fileExists(path) && !fs.directoryExists(path)) {
|
||||
throw new BeaconClientException("Path " + path + " does not exist");
|
||||
}
|
||||
|
||||
var entry = fs.getFileInfo(path);
|
||||
if (entry.isEmpty()) {
|
||||
throw new BeaconClientException("File " + path + " does not exist");
|
||||
throw new BeaconClientException("Path " + path + " does not exist");
|
||||
}
|
||||
|
||||
var map = new LinkedHashMap<String, Object>();
|
||||
|
||||
@@ -13,11 +13,11 @@ import io.xpipe.app.ext.FileSystemStore;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.hub.comp.StoreEntryWrapper;
|
||||
import io.xpipe.app.hub.comp.StoreViewState;
|
||||
import io.xpipe.app.platform.BindingsHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.FileReference;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
@@ -78,7 +78,7 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
|
||||
modal.addButton(new ModalButton("select", () -> model.finishChooser(), true, true));
|
||||
modal.show();
|
||||
ThreadHelper.runAsync(() -> {
|
||||
model.openFileSystemAsync(store.get(), (sc) -> initialPath.get(), null);
|
||||
model.openFileSystemAsync(store.get(), null, (sc) -> initialPath.get(), null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
|
||||
}
|
||||
|
||||
if (entry.getStore() instanceof ShellStore) {
|
||||
model.openFileSystemAsync(entry.ref(), null, busy);
|
||||
model.openFileSystemAsync(entry.ref(), null, null, busy);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.ext.FileSystem;
|
||||
import io.xpipe.app.ext.FileSystemStore;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
@@ -80,6 +81,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
|
||||
|
||||
public void openFileSystemAsync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> customFileSystemFactory,
|
||||
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
|
||||
BooleanProperty externalBusy) {
|
||||
if (store == null) {
|
||||
@@ -91,7 +93,13 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
|
||||
|
||||
try (var ignored =
|
||||
new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
|
||||
model = new BrowserFileSystemTabModel(this, store, selectionMode);
|
||||
model = new BrowserFileSystemTabModel(
|
||||
this,
|
||||
store,
|
||||
selectionMode,
|
||||
customFileSystemFactory != null
|
||||
? customFileSystemFactory
|
||||
: ref -> ref.getStore().createFileSystem());
|
||||
model.init();
|
||||
// Prevent multiple calls from interfering with each other
|
||||
synchronized (BrowserFileChooserSessionModel.this) {
|
||||
|
||||
@@ -12,8 +12,8 @@ import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.hub.comp.StoreEntryWrapper;
|
||||
import io.xpipe.app.hub.comp.StoreViewState;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.platform.BindingsHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
@@ -75,7 +75,7 @@ public class BrowserFullSessionComp extends SimpleComp {
|
||||
loadingStack.apply(struc -> struc.get().setPickOnBounds(false));
|
||||
var delayedStack = new DelayedInitComp(
|
||||
left, () -> StoreViewState.get() != null && StoreViewState.get().isInitialized());
|
||||
delayedStack.hide(AppMainWindow.getInstance().getStage().widthProperty().lessThan(1000));
|
||||
delayedStack.hide(AppMainWindow.get().getStage().widthProperty().lessThan(1000));
|
||||
var splitPane = new LeftSplitPaneComp(delayedStack, loadingStack)
|
||||
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
|
||||
.withOnDividerChange(d -> {
|
||||
|
||||
@@ -4,6 +4,8 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.browser.file.BrowserHistorySavedState;
|
||||
import io.xpipe.app.browser.file.BrowserHistoryTabModel;
|
||||
import io.xpipe.app.browser.file.BrowserTransferModel;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.ext.FileSystem;
|
||||
import io.xpipe.app.ext.FileSystemStore;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
@@ -61,7 +63,10 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
DEFAULT.openSync(new BrowserHistoryTabModel(DEFAULT), null);
|
||||
if (AppPrefs.get().pinLocalMachineOnStartup().get()) {
|
||||
var tab = new BrowserFileSystemTabModel(
|
||||
DEFAULT, DataStorage.get().local().ref(), BrowserFileSystemTabModel.SelectionMode.ALL);
|
||||
DEFAULT,
|
||||
DataStorage.get().local().ref(),
|
||||
BrowserFileSystemTabModel.SelectionMode.ALL,
|
||||
ref -> ref.getStore().createFileSystem());
|
||||
try {
|
||||
DEFAULT.openSync(tab, null);
|
||||
DEFAULT.pinTab(tab);
|
||||
@@ -150,7 +155,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
}
|
||||
}
|
||||
|
||||
public void unpinTab(BrowserSessionTab tab) {
|
||||
public void unpinTab() {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
globalPinnedTab.setValue(null);
|
||||
});
|
||||
@@ -170,7 +175,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
public void restoreStateAsync(BrowserHistorySavedState.Entry e, BooleanProperty busy) {
|
||||
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
|
||||
storageEntry.ifPresent(entry -> {
|
||||
openFileSystemAsync(entry.ref(), model -> e.getPath(), busy);
|
||||
openFileSystemAsync(entry.ref(), null, model -> e.getPath(), busy);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,6 +207,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
|
||||
public void openFileSystemAsync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> customFileSystemFactory,
|
||||
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
|
||||
BooleanProperty externalBusy) {
|
||||
if (store == null) {
|
||||
@@ -209,12 +215,13 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
}
|
||||
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
openFileSystemSync(store, path, externalBusy, true);
|
||||
openFileSystemSync(store, customFileSystemFactory, path, externalBusy, true);
|
||||
});
|
||||
}
|
||||
|
||||
public BrowserFileSystemTabModel openFileSystemSync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> customFileSystemFactory,
|
||||
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
|
||||
BooleanProperty externalBusy,
|
||||
boolean select)
|
||||
@@ -223,12 +230,19 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
try (var ignored =
|
||||
new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
|
||||
try (var ignored2 = new BooleanScope(busy).exclusive().start()) {
|
||||
model = new BrowserFileSystemTabModel(this, store, BrowserFileSystemTabModel.SelectionMode.ALL);
|
||||
model = new BrowserFileSystemTabModel(
|
||||
this,
|
||||
store,
|
||||
BrowserFileSystemTabModel.SelectionMode.ALL,
|
||||
customFileSystemFactory != null
|
||||
? customFileSystemFactory
|
||||
: ref -> ref.getStore().createFileSystem());
|
||||
model.init();
|
||||
// Prevent multiple calls from interfering with each other
|
||||
synchronized (BrowserFullSessionModel.this) {
|
||||
sessionEntries.add(model);
|
||||
if (select) {
|
||||
AppLayoutModel.get().selectBrowser();
|
||||
// The tab pane doesn't automatically select new tabs
|
||||
selectedEntry.setValue(model);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import io.xpipe.app.comp.base.StackComp;
|
||||
import io.xpipe.app.core.App;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.platform.ContextMenuHelper;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ContextMenuHelper;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -211,7 +211,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
|
||||
|
||||
for (var a : c.getAddedSubList()) {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
try (var b = new BooleanScope(addingTab).start()) {
|
||||
try (var ignored = new BooleanScope(addingTab).start()) {
|
||||
var t = createTab(tabs, a);
|
||||
map.put(a, t);
|
||||
tabs.getTabs().add(t);
|
||||
@@ -307,7 +307,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
|
||||
},
|
||||
model.getGlobalPinnedTab())));
|
||||
unpin.setOnAction(event -> {
|
||||
model.unpinTab(tabModel);
|
||||
model.unpinTab();
|
||||
event.consume();
|
||||
});
|
||||
cm.getItems().add(unpin);
|
||||
@@ -511,6 +511,8 @@ public class BrowserSessionTabsComp extends SimpleComp {
|
||||
var color = tabModel.getColor();
|
||||
if (color != null) {
|
||||
c.getStyleClass().add(color.getId());
|
||||
} else {
|
||||
c.getStyleClass().add("gray");
|
||||
}
|
||||
c.addEventHandler(DragEvent.DRAG_ENTERED, de -> {
|
||||
// Prevent switch when dragging local files into app
|
||||
|
||||
@@ -45,6 +45,7 @@ public abstract class BrowserAction extends StoreAction<FileSystemStore> {
|
||||
} else {
|
||||
model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(
|
||||
ref.asNeeded(),
|
||||
null,
|
||||
model -> {
|
||||
return getTargetDirectory(model);
|
||||
},
|
||||
@@ -57,8 +58,8 @@ public abstract class BrowserAction extends StoreAction<FileSystemStore> {
|
||||
|
||||
model.getBusy().set(true);
|
||||
|
||||
// Start shell in case we exited
|
||||
model.getFileSystem().getShell().orElseThrow().start();
|
||||
// Restart in case we exited
|
||||
model.getFileSystem().reinitIfNeeded();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,4 @@ public interface BrowserActionProvider extends ActionProvider {
|
||||
default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return true;
|
||||
}
|
||||
|
||||
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+17
-5
@@ -2,13 +2,14 @@ package io.xpipe.app.browser.action.impl;
|
||||
|
||||
import io.xpipe.app.action.AbstractAction;
|
||||
import io.xpipe.app.action.ActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserFileInput;
|
||||
import io.xpipe.app.browser.file.BrowserFileOutput;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -27,7 +28,7 @@ public class ApplyFileEditActionProvider implements ActionProvider {
|
||||
String target;
|
||||
|
||||
@NonNull
|
||||
InputStream input;
|
||||
BrowserFileInput input;
|
||||
|
||||
@NonNull
|
||||
BrowserFileOutput output;
|
||||
@@ -36,9 +37,13 @@ public class ApplyFileEditActionProvider implements ActionProvider {
|
||||
public void executeImpl() throws Exception {
|
||||
output.beforeTransfer();
|
||||
try (var out = output.open()) {
|
||||
input.transferTo(out);
|
||||
input.open().transferTo(out);
|
||||
}
|
||||
try {
|
||||
output.onFinish();
|
||||
} finally {
|
||||
input.onFinish();
|
||||
}
|
||||
output.onFinish();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,7 +55,14 @@ public class ApplyFileEditActionProvider implements ActionProvider {
|
||||
public Map<String, String> toDisplayMap() {
|
||||
var map = new LinkedHashMap<String, String>();
|
||||
map.put("Action", getDisplayName());
|
||||
map.put("Target", target);
|
||||
|
||||
var system = output.target();
|
||||
if (system.isPresent()) {
|
||||
map.put("System", DataStorage.get().getStoreEntryDisplayName(system.get()));
|
||||
}
|
||||
|
||||
map.put("File", target);
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -4,9 +4,9 @@ import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.process.LocalShell;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.util.DesktopHelper;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
@@ -17,6 +17,10 @@ public class BrowseInNativeManagerActionProvider implements BrowserActionProvide
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
if (model.getFileSystem().getShell().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
@@ -39,10 +43,7 @@ public class BrowseInNativeManagerActionProvider implements BrowserActionProvide
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
var e = entry.getRawFileEntry().getPath();
|
||||
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
|
||||
try (var local = LocalShell.getShell().start()) {
|
||||
DesktopHelper.browsePathRemote(
|
||||
local, localFile, entry.getRawFileEntry().getKind());
|
||||
}
|
||||
DesktopHelper.browseFileInDirectory(localFile.asLocalPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
@@ -17,8 +15,7 @@ public class ChgrpActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
|
||||
return os != OsType.WINDOWS && os != OsType.MACOS;
|
||||
return model.getFileSystem().supportsChgrp();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -37,19 +34,9 @@ public class ChgrpActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("chgrp")
|
||||
.addIf(recursive, "-R")
|
||||
.addLiteral(group)
|
||||
.addFiles(getEntries().stream()
|
||||
.map(browserEntry -> browserEntry
|
||||
.getRawFileEntry()
|
||||
.getPath()
|
||||
.toString())
|
||||
.toList()));
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
model.getFileSystem().chgrp(entry.getRawFileEntry().getPath(), group, recursive);
|
||||
}
|
||||
model.refreshBrowserEntriesSync(getEntries());
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
@@ -17,7 +15,7 @@ public class ChmodActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;
|
||||
return model.getFileSystem().supportsChmod();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -36,19 +34,9 @@ public class ChmodActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("chmod")
|
||||
.addIf(recursive, "-R")
|
||||
.addLiteral(permissions)
|
||||
.addFiles(getEntries().stream()
|
||||
.map(browserEntry -> browserEntry
|
||||
.getRawFileEntry()
|
||||
.getPath()
|
||||
.toString())
|
||||
.toList()));
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
model.getFileSystem().chmod(entry.getRawFileEntry().getPath(), permissions, recursive);
|
||||
}
|
||||
model.refreshBrowserEntriesSync(getEntries());
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
@@ -17,8 +15,7 @@ public class ChownActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
|
||||
return os != OsType.WINDOWS && os != OsType.MACOS;
|
||||
return model.getFileSystem().supportsChown();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -37,19 +34,9 @@ public class ChownActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("chown")
|
||||
.addIf(recursive, "-R")
|
||||
.addLiteral(owner)
|
||||
.addFiles(getEntries().stream()
|
||||
.map(browserEntry -> browserEntry
|
||||
.getRawFileEntry()
|
||||
.getPath()
|
||||
.toString())
|
||||
.toList()));
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
model.getFileSystem().chown(entry.getRawFileEntry().getPath(), owner, recursive);
|
||||
}
|
||||
model.refreshBrowserEntriesSync(getEntries());
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -3,11 +3,14 @@ package io.xpipe.app.browser.action.impl;
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ComputeDirectorySizesActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
@@ -15,6 +18,11 @@ public class ComputeDirectorySizesActionProvider implements BrowserActionProvide
|
||||
return "computeDirectorySizes";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileSystem().supportsDirectorySizes();
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
@@ -3,7 +3,7 @@ package io.xpipe.app.browser.action.impl;
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@@ -3,7 +3,7 @@ package io.xpipe.app.browser.action.impl;
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@@ -3,7 +3,7 @@ package io.xpipe.app.browser.action.impl;
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import lombok.NonNull;
|
||||
|
||||
@@ -4,7 +4,7 @@ import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileOpener;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
+9
-4
@@ -4,10 +4,10 @@ import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.LocalShell;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import lombok.experimental.SuperBuilder;
|
||||
@@ -24,6 +24,10 @@ public class OpenFileNativeDetailsActionProvider implements BrowserActionProvide
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
if (model.getFileSystem().getShell().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
return sc.getLocalSystemAccess().supportsFileSystemAccess();
|
||||
}
|
||||
@@ -38,7 +42,7 @@ public class OpenFileNativeDetailsActionProvider implements BrowserActionProvide
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
var e = entry.getRawFileEntry().getPath();
|
||||
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
|
||||
switch (OsType.getLocal()) {
|
||||
switch (OsType.ofLocal()) {
|
||||
case OsType.Windows ignored -> {
|
||||
var shell = LocalShell.getLocalPowershell();
|
||||
if (shell.isEmpty()) {
|
||||
@@ -59,7 +63,8 @@ public class OpenFileNativeDetailsActionProvider implements BrowserActionProvide
|
||||
// as
|
||||
// long as the parent process is running.
|
||||
// So let's keep one process running
|
||||
shell.get().command(content).notComplex().execute();
|
||||
// Ignore exit value as this can fail somehow (maybe if the system blocks shell com objects?)
|
||||
shell.get().command(content).notComplex().executeAndCheck();
|
||||
}
|
||||
case OsType.Linux ignored -> {
|
||||
var dbus = String.format(
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package io.xpipe.app.browser.action.impl;
|
||||
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.LocalShell;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.util.DesktopHelper;
|
||||
import io.xpipe.core.OsType;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class OpenFileNativeManagerActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openFileNativeManager";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
if (model.getFileSystem().getShell().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
return sc.getLocalSystemAccess().supportsFileSystemAccess();
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
ShellControl sc = model.getFileSystem().getShell().get();
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
var e = entry.getRawFileEntry().getPath();
|
||||
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
|
||||
DesktopHelper.browseFileInDirectory(localFile.asLocalPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileOpener;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import lombok.experimental.SuperBuilder;
|
||||
@@ -22,7 +22,7 @@ public class OpenFileWithActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return OsType.getLocal() == OsType.WINDOWS
|
||||
return OsType.ofLocal() == OsType.WINDOWS
|
||||
&& entries.size() == 1
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package io.xpipe.app.browser.action.impl;
|
||||
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class OpenTerminalActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openTerminalInDirectory";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var t = AppPrefs.get().terminalType().getValue();
|
||||
return t != null;
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
var entries = getEntries();
|
||||
var dirs = entries.size() > 0
|
||||
? entries.stream()
|
||||
.map(browserEntry -> browserEntry.getRawFileEntry().getPath())
|
||||
.toList()
|
||||
: model.getCurrentDirectory() != null
|
||||
? List.of(model.getCurrentDirectory().getPath())
|
||||
: Collections.singletonList((FilePath) null);
|
||||
for (var dir : dirs) {
|
||||
var name = (dir != null ? dir + " - " : "") + model.getName().getValue();
|
||||
model.openTerminalSync(
|
||||
name, dir, model.getFileSystem().getShell().orElseThrow(), dirs.size() == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -2,6 +2,8 @@ package io.xpipe.app.browser.action.impl;
|
||||
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.process.ProcessOutputException;
|
||||
|
||||
@@ -10,6 +12,7 @@ import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@@ -20,6 +23,11 @@ public class RunCommandInBackgroundActionProvider implements BrowserActionProvid
|
||||
return "runFileInBackground";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileSystem().getShell().isPresent();
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -48,7 +56,7 @@ public class RunCommandInBackgroundActionProvider implements BrowserActionProvid
|
||||
|
||||
// Only throw actual error output
|
||||
if (exitCode != 0) {
|
||||
throw ErrorEventFactory.expected(ProcessOutputException.of(exitCode, out.get(), err.get()));
|
||||
throw ErrorEventFactory.expected(ProcessOutputException.of(command, exitCode, out.get(), err.get()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -2,6 +2,8 @@ package io.xpipe.app.browser.action.impl;
|
||||
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.util.CommandDialog;
|
||||
|
||||
import lombok.NonNull;
|
||||
@@ -9,6 +11,7 @@ import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class RunCommandInBrowserActionProvider implements BrowserActionProvider {
|
||||
@@ -18,6 +21,11 @@ public class RunCommandInBrowserActionProvider implements BrowserActionProvider
|
||||
return "runCommandInBrowser";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileSystem().getShell().isPresent();
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
+8
@@ -2,12 +2,15 @@ package io.xpipe.app.browser.action.impl;
|
||||
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class RunCommandInTerminalActionProvider implements BrowserActionProvider {
|
||||
@@ -17,6 +20,11 @@ public class RunCommandInTerminalActionProvider implements BrowserActionProvider
|
||||
return "runCommandInTerminal";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileSystem().getShell().isPresent();
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.process.OsFileSystem;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.util.Callback;
|
||||
|
||||
import atlantafx.base.controls.Breadcrumbs;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BrowserBreadcrumbBar extends SimpleComp {
|
||||
|
||||
private final BrowserFileSystemTabModel model;
|
||||
private Instant lastHoverUpdate;
|
||||
|
||||
public BrowserBreadcrumbBar(BrowserFileSystemTabModel model) {
|
||||
this.model = model;
|
||||
@@ -27,23 +33,54 @@ public class BrowserBreadcrumbBar extends SimpleComp {
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
Callback<Breadcrumbs.BreadCrumbItem<String>, ButtonBase> crumbFactory = crumb -> {
|
||||
var name = crumb.getValue().equals("/")
|
||||
Callback<Breadcrumbs.BreadCrumbItem<FilePath>, ButtonBase> crumbFactory = crumb -> {
|
||||
var name = crumb.getValue().toString().equals("/")
|
||||
? "/"
|
||||
: FilePath.of(crumb.getValue()).getFileName();
|
||||
: crumb.getValue().getFileName();
|
||||
var btn = new Button(name, null);
|
||||
btn.setMnemonicParsing(false);
|
||||
btn.setFocusTraversable(false);
|
||||
btn.setOnDragEntered(event -> onDragEntered(btn, crumb.getValue()));
|
||||
btn.setOnDragOver(event -> onDragOver(event));
|
||||
btn.setOnDragExited(event -> onDragExited(btn));
|
||||
return btn;
|
||||
};
|
||||
return createBreadcrumbs(crumbFactory, null);
|
||||
}
|
||||
|
||||
private Region createBreadcrumbs(
|
||||
Callback<Breadcrumbs.BreadCrumbItem<String>, ButtonBase> crumbFactory,
|
||||
Callback<Breadcrumbs.BreadCrumbItem<String>, ? extends Node> dividerFactory) {
|
||||
private void onDragEntered(Button button, FilePath path) {
|
||||
button.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
|
||||
|
||||
var breadcrumbs = new Breadcrumbs<String>();
|
||||
var timestamp = Instant.now();
|
||||
lastHoverUpdate = timestamp;
|
||||
// Reduce printed window updates
|
||||
GlobalTimer.delay(
|
||||
() -> {
|
||||
if (!timestamp.equals(lastHoverUpdate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.cdAsync(path);
|
||||
},
|
||||
Duration.ofMillis(500));
|
||||
}
|
||||
|
||||
private void onDragOver(DragEvent event) {
|
||||
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
event.consume();
|
||||
}
|
||||
|
||||
private void onDragExited(Button button) {
|
||||
button.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), false);
|
||||
|
||||
lastHoverUpdate = null;
|
||||
}
|
||||
|
||||
private Region createBreadcrumbs(
|
||||
Callback<Breadcrumbs.BreadCrumbItem<FilePath>, ButtonBase> crumbFactory,
|
||||
Callback<Breadcrumbs.BreadCrumbItem<FilePath>, ? extends Node> dividerFactory) {
|
||||
|
||||
var breadcrumbs = new Breadcrumbs<FilePath>();
|
||||
breadcrumbs.setMinWidth(0);
|
||||
model.getCurrentPath().subscribe(val -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
@@ -52,30 +89,21 @@ public class BrowserBreadcrumbBar extends SimpleComp {
|
||||
return;
|
||||
}
|
||||
|
||||
var sc = model.getFileSystem().getShell();
|
||||
if (sc.isEmpty()) {
|
||||
breadcrumbs.setDividerFactory(item -> item != null && !item.isLast() ? new Label("/") : null);
|
||||
} else {
|
||||
breadcrumbs.setDividerFactory(item -> {
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
breadcrumbs.setDividerFactory(item -> {
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item.isFirst() && item.getValue().equals("/")) {
|
||||
return new Label("");
|
||||
}
|
||||
if (item.isFirst() && item.getValue().toString().equals("/")) {
|
||||
return new Label("");
|
||||
}
|
||||
|
||||
return new Label(OsFileSystem.of(sc.get().getOsType()).getFileSystemSeparator());
|
||||
});
|
||||
}
|
||||
return new Label(model.getFileSystem().getFileSeparator());
|
||||
});
|
||||
|
||||
var elements = createBreadcumbHierarchy(val);
|
||||
var modifiedElements = new ArrayList<>(elements);
|
||||
if (val.toString().startsWith("/")) {
|
||||
modifiedElements.addFirst("/");
|
||||
}
|
||||
Breadcrumbs.BreadCrumbItem<String> items =
|
||||
Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new));
|
||||
var elements = createBreadcrumbHierarchy(val);
|
||||
Breadcrumbs.BreadCrumbItem<FilePath> items =
|
||||
Breadcrumbs.buildTreeModel(elements.toArray(FilePath[]::new));
|
||||
breadcrumbs.setSelectedCrumb(items);
|
||||
});
|
||||
});
|
||||
@@ -94,19 +122,24 @@ public class BrowserBreadcrumbBar extends SimpleComp {
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
private List<String> createBreadcumbHierarchy(FilePath filePath) {
|
||||
var f = filePath.toString() + "/";
|
||||
var list = new ArrayList<String>();
|
||||
private List<FilePath> createBreadcrumbHierarchy(FilePath filePath) {
|
||||
var f = filePath.toDirectory().toString();
|
||||
var list = new ArrayList<FilePath>();
|
||||
int lastElementStart = 0;
|
||||
for (int i = 0; i < f.length(); i++) {
|
||||
if (f.charAt(i) == '\\' || f.charAt(i) == '/') {
|
||||
if (i - lastElementStart > 0) {
|
||||
list.add(f.substring(0, i));
|
||||
list.add(FilePath.of(f.substring(0, i)).toDirectory());
|
||||
}
|
||||
|
||||
lastElementStart = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (filePath.toString().startsWith("/")) {
|
||||
list.addFirst(FilePath.of("/"));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package io.xpipe.app.browser.file;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.util.GlobalClipboard;
|
||||
import io.xpipe.app.util.GlobalObjectProperty;
|
||||
import io.xpipe.app.platform.GlobalClipboard;
|
||||
import io.xpipe.app.platform.GlobalObjectProperty;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
@@ -4,8 +4,8 @@ import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.hub.comp.*;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
|
||||
@@ -4,9 +4,9 @@ import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.FilterComp;
|
||||
import io.xpipe.app.comp.base.HorizontalComp;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.hub.comp.DataStoreCategoryChoiceComp;
|
||||
import io.xpipe.app.hub.comp.StoreCategoryWrapper;
|
||||
import io.xpipe.app.hub.comp.StoreViewState;
|
||||
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
@@ -4,7 +4,7 @@ import io.xpipe.app.action.ActionProvider;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuCategory;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuItemProvider;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.platform.InputHelper;
|
||||
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
|
||||
@@ -10,7 +10,7 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
public class BrowserDialogs {
|
||||
|
||||
public static FileConflictChoice showFileConflictAlert(FilePath file, boolean multiple) {
|
||||
public static FileConflictChoice showFileConflictDialog(FilePath file, boolean multiple) {
|
||||
var choice = new SimpleObjectProperty<FileConflictChoice>();
|
||||
var key = multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent";
|
||||
var w = multiple ? 1050 : 400;
|
||||
|
||||
@@ -3,7 +3,7 @@ package io.xpipe.app.browser.file;
|
||||
import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
|
||||
import io.xpipe.app.browser.icon.BrowserIconFileType;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -64,7 +64,7 @@ public class BrowserEntry {
|
||||
if (fileType != null) {
|
||||
return fileType.getIcon();
|
||||
} else if (directoryType != null) {
|
||||
return directoryType.getIcon(rawFileEntry);
|
||||
return directoryType.getIcon();
|
||||
} else {
|
||||
return rawFileEntry != null && rawFileEntry.resolved().getKind() == FileKind.DIRECTORY
|
||||
? "browser/default_folder.svg"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.ext.FileSystem;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class BrowserFileDuplicates {
|
||||
|
||||
public static FilePath renameFileDuplicate(FileSystem fileSystem, FilePath target, boolean dir) throws Exception {
|
||||
// Who has more than 10 copies?
|
||||
for (int i = 0; i < 10; i++) {
|
||||
target = renameFile(target);
|
||||
if ((dir && !fileSystem.directoryExists(target)) || (!dir && !fileSystem.fileExists(target))) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private static FilePath renameFile(FilePath target) {
|
||||
var name = target.getFileName();
|
||||
var pattern = Pattern.compile("(.+)\\((\\d+)\\)\\.(.+?)");
|
||||
var matcher = pattern.matcher(name);
|
||||
if (matcher.matches()) {
|
||||
try {
|
||||
var number = Integer.parseInt(matcher.group(2));
|
||||
var newFile = target.getParent().join(matcher.group(1) + "(" + (number + 1) + ")." + matcher.group(3));
|
||||
return newFile;
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
var ext = target.getExtension();
|
||||
return FilePath.of(target.getBaseName() + "(" + 1 + ")" + (ext.isPresent() ? "." + ext.get() : ""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.ext.ConnectionFileSystem;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.FileInfo;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ElevationFunction;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface BrowserFileInput {
|
||||
|
||||
static BrowserFileInput openFileInput(BrowserFileSystemTabModel model, FileEntry file) throws Exception {
|
||||
if (model.isClosed()) {
|
||||
return BrowserFileInput.none();
|
||||
}
|
||||
|
||||
var defOutput = createFileInputImpl(model, file, false);
|
||||
if (model.getFileSystem().getShell().isEmpty()) {
|
||||
return defOutput;
|
||||
}
|
||||
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
var requiresSudo =
|
||||
sc.getOsType() != OsType.WINDOWS && requiresSudo(model, (FileInfo.Unix) file.getInfo(), file.getPath());
|
||||
|
||||
if (!requiresSudo) {
|
||||
return defOutput;
|
||||
}
|
||||
|
||||
var elevate = AppDialog.confirm("fileReadSudo");
|
||||
if (!elevate) {
|
||||
return defOutput;
|
||||
}
|
||||
|
||||
var rootOutput = createFileInputImpl(model, file, true);
|
||||
return rootOutput;
|
||||
}
|
||||
|
||||
private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath)
|
||||
throws Exception {
|
||||
if (model.getFileSystem().getShell().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var sc = model.getFileSystem().getShell().get();
|
||||
if (sc.view().isRoot()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info != null) {
|
||||
var otherWrite = info.getPermissions().charAt(6) == 'r';
|
||||
if (otherWrite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var userOwned = info.getUid() != null
|
||||
&& sc.view().getPasswdFile().getUidForUser(sc.view().user()) == info.getUid()
|
||||
|| info.getUser() != null && sc.view().user().equals(info.getUser());
|
||||
var userWrite = info.getPermissions().charAt(0) == 'r';
|
||||
if (userOwned && userWrite) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var test = model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.command(CommandBuilder.of().add("test", "-r").addFile(filePath))
|
||||
.executeAndCheck();
|
||||
return !test;
|
||||
}
|
||||
|
||||
private static BrowserFileInput createFileInputImpl(
|
||||
BrowserFileSystemTabModel model, FileEntry file, boolean elevate) throws Exception {
|
||||
var shell = model.getFileSystem().getShell();
|
||||
var sc = shell.isEmpty()
|
||||
? null
|
||||
: elevate
|
||||
? shell.orElseThrow()
|
||||
.identicalDialectSubShell()
|
||||
.elevated(ElevationFunction.elevated(null))
|
||||
.start()
|
||||
: model.getFileSystem().getShell().orElseThrow().start();
|
||||
var fs = elevate ? new ConnectionFileSystem(sc) : model.getFileSystem();
|
||||
var output = new BrowserFileInput() {
|
||||
|
||||
@Override
|
||||
public InputStream open() throws Exception {
|
||||
try {
|
||||
return fs.openInput(file.getPath());
|
||||
} catch (Exception ex) {
|
||||
if (elevate) {
|
||||
fs.close();
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() throws Exception {
|
||||
if (elevate) {
|
||||
fs.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
return output;
|
||||
}
|
||||
|
||||
static BrowserFileInput none() {
|
||||
return new BrowserFileInput() {
|
||||
|
||||
@Override
|
||||
public InputStream open() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() {}
|
||||
};
|
||||
}
|
||||
|
||||
static BrowserFileInput of(InputStream in) {
|
||||
return new BrowserFileInput() {
|
||||
@Override
|
||||
public InputStream open() {
|
||||
return in;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() {}
|
||||
};
|
||||
}
|
||||
|
||||
InputStream open() throws Exception;
|
||||
|
||||
void onFinish() throws Exception;
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.FileInfo;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.FileInfo;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -24,6 +24,7 @@ import javafx.scene.input.*;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -146,17 +147,21 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
table.setAccessibleText("Directory contents");
|
||||
|
||||
var placeholder = new Label();
|
||||
var placeholderText = Bindings.createStringBinding(() -> {
|
||||
if (fileList.getFileSystemModel().getCurrentPath().get() == null) {
|
||||
return null;
|
||||
}
|
||||
var placeholderText = Bindings.createStringBinding(
|
||||
() -> {
|
||||
if (fileList.getFileSystemModel().getCurrentPath().get() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fileList.getFileSystemModel().getBusy().get()) {
|
||||
return null;
|
||||
}
|
||||
if (fileList.getFileSystemModel().getBusy().get()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AppI18n.get("emptyDirectory");
|
||||
}, AppI18n.activeLanguage(), fileList.getFileSystemModel().getBusy(), fileList.getFileSystemModel().getCurrentPath());
|
||||
return AppI18n.get("emptyDirectory");
|
||||
},
|
||||
AppI18n.activeLanguage(),
|
||||
fileList.getFileSystemModel().getBusy(),
|
||||
fileList.getFileSystemModel().getCurrentPath());
|
||||
placeholder.textProperty().bind(PlatformThread.sync(placeholderText));
|
||||
table.setPlaceholder(placeholder);
|
||||
AppFontSizes.base(placeholder);
|
||||
@@ -189,18 +194,12 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
TableColumn<BrowserEntry, String> modeCol,
|
||||
TableColumn<BrowserEntry, String> ownerCol,
|
||||
TableColumn<BrowserEntry, String> sizeCol) {
|
||||
var os = fileList.getFileSystemModel()
|
||||
.getFileSystem()
|
||||
.getShell()
|
||||
.map(shellControl -> shellControl.getOsType())
|
||||
.orElse(null);
|
||||
table.widthProperty().subscribe((newValue) -> {
|
||||
if (os != OsType.WINDOWS && os != OsType.MACOS) {
|
||||
if (fileList.getFileSystemModel().getFileSystem().supportsOwnerColumn()) {
|
||||
ownerCol.setVisible(newValue.doubleValue() > 1000);
|
||||
}
|
||||
|
||||
var shell = fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow();
|
||||
if (!OsType.WINDOWS.equals(shell.getOsType()) && !OsType.MACOS.equals(shell.getOsType())) {
|
||||
if (fileList.getFileSystemModel().getFileSystem().supportsModeColumn()) {
|
||||
modeCol.setVisible(newValue.doubleValue() > 600);
|
||||
}
|
||||
|
||||
@@ -222,6 +221,7 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
return Math.max(200, tableView.getWidth() - sum);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private String formatOwner(BrowserEntry param) {
|
||||
FileInfo.Unix unix = param.getRawFileEntry().resolved().getInfo() instanceof FileInfo.Unix u ? u : null;
|
||||
if (unix == null) {
|
||||
@@ -233,20 +233,35 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
}
|
||||
|
||||
var m = fileList.getFileSystemModel();
|
||||
var v = m.getFileSystem().getShell().isPresent()
|
||||
? m.getFileSystem().getShell().get().view()
|
||||
: null;
|
||||
|
||||
var user = unix.getUser() != null
|
||||
? unix.getUser()
|
||||
: m.getCache().getUsers().getOrDefault(unix.getUid(), "?");
|
||||
: v != null ? v.getPasswdFile().getUsers().getOrDefault(unix.getUid(), "?") : null;
|
||||
var group = unix.getGroup() != null
|
||||
? unix.getGroup()
|
||||
: m.getCache().getGroups().getOrDefault(unix.getGid(), "?");
|
||||
var uid = String.valueOf(
|
||||
unix.getUid() != null ? unix.getUid() : m.getCache().getUidForUser(user));
|
||||
var gid = String.valueOf(
|
||||
unix.getGid() != null ? unix.getGid() : m.getCache().getGidForGroup(group));
|
||||
if (uid.equals(gid) && user.equals(group)) {
|
||||
return user + " [" + uid + "]";
|
||||
: v != null ? v.getGroupFile().getGroups().getOrDefault(unix.getGid(), "?") : null;
|
||||
var uid = unix.getUid() != null
|
||||
? String.valueOf(unix.getUid())
|
||||
: v != null ? v.getPasswdFile().getUidForUser(user) : null;
|
||||
var gid = unix.getGid() != null
|
||||
? String.valueOf(unix.getGid())
|
||||
: v != null ? v.getGroupFile().getGidForGroup(group) : null;
|
||||
|
||||
var userFormat = user + (uid != null ? " [" + uid + "]" : "");
|
||||
var groupFormat = group + (gid != null ? " [" + gid + "]" : "");
|
||||
|
||||
if (uid != null && uid.equals(gid) && user != null && user.equals(group)) {
|
||||
return userFormat;
|
||||
}
|
||||
return user + " [" + uid + "] / " + group + " [" + gid + "]";
|
||||
|
||||
if (uid == null && gid == null && user != null && user.equals(group)) {
|
||||
return userFormat;
|
||||
}
|
||||
|
||||
return userFormat + " / " + groupFormat;
|
||||
}
|
||||
|
||||
private void prepareTypedSelectionModel(TableView<BrowserEntry> table) {
|
||||
@@ -394,7 +409,7 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
var selected = fileList.getSelection();
|
||||
var action = BrowserMenuProviders.getFlattened(fileList.getFileSystemModel(), selected).stream()
|
||||
.filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected)
|
||||
&& browserAction.isActive(fileList.getFileSystemModel(), selected))
|
||||
&& browserAction.isActive(fileList.getFileSystemModel()))
|
||||
.filter(browserAction -> browserAction.getShortcut() != null)
|
||||
.filter(browserAction -> browserAction.getShortcut().match(event))
|
||||
.findAny();
|
||||
@@ -585,13 +600,15 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
ownerCol.setPrefWidth(0);
|
||||
}
|
||||
|
||||
var shell =
|
||||
fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow();
|
||||
if (OsType.WINDOWS.equals(shell.getOsType()) || OsType.MACOS.equals(shell.getOsType())) {
|
||||
if (!fileList.getFileSystemModel().getFileSystem().supportsModeColumn()) {
|
||||
modeCol.setVisible(false);
|
||||
ownerCol.setVisible(false);
|
||||
} else {
|
||||
modeCol.setVisible(table.getWidth() > 600);
|
||||
}
|
||||
|
||||
if (!fileList.getFileSystemModel().getFileSystem().supportsOwnerColumn()) {
|
||||
ownerCol.setVisible(false);
|
||||
} else {
|
||||
if (table.getWidth() > 1000) {
|
||||
ownerCol.setVisible(hasOwner);
|
||||
} else if (!hasOwner) {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.core.AppSystemInfo;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.TableView;
|
||||
@@ -16,8 +17,10 @@ import javafx.scene.input.*;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -29,8 +32,7 @@ public class BrowserFileListCompEntry {
|
||||
private final BrowserEntry item;
|
||||
private final BrowserFileListModel model;
|
||||
|
||||
private Point2D lastOver = new Point2D(-1, -1);
|
||||
private Runnable activeTask;
|
||||
private Instant lastHoverUpdate;
|
||||
private ContextMenu lastContextMenu;
|
||||
|
||||
public BrowserFileListCompEntry(
|
||||
@@ -148,6 +150,22 @@ public class BrowserFileListCompEntry {
|
||||
private boolean acceptsDrop(DragEvent event) {
|
||||
// Accept drops from outside the app window
|
||||
if (event.getGestureSource() == null) {
|
||||
// Don't accept 7zip temp files
|
||||
if (OsType.ofLocal() == OsType.WINDOWS
|
||||
&& event.getDragboard().getFiles().stream().anyMatch(file -> {
|
||||
try {
|
||||
return file.toPath()
|
||||
.toRealPath()
|
||||
.startsWith(
|
||||
AppSystemInfo.ofWindows().getTemp())
|
||||
&& file.toPath().getFileName().toString().matches("7z[A-Z0-9]+");
|
||||
} catch (IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -237,6 +255,7 @@ public class BrowserFileListCompEntry {
|
||||
} else {
|
||||
model.getDraggedOverEmpty().setValue(false);
|
||||
}
|
||||
lastHoverUpdate = null;
|
||||
event.consume();
|
||||
}
|
||||
|
||||
@@ -273,48 +292,45 @@ public class BrowserFileListCompEntry {
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptDrag(DragEvent event) {
|
||||
model.getDraggedOverEmpty()
|
||||
.setValue(item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY);
|
||||
model.getDraggedOverDirectory().setValue(item);
|
||||
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
}
|
||||
|
||||
private void handleHoverTimer(DragEvent event) {
|
||||
if (item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastOver = (new Point2D(event.getX(), event.getY()));
|
||||
activeTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (activeTask != this) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item != model.getDraggedOverDirectory().getValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.getFileSystemModel()
|
||||
.cdAsync(item.getRawFileEntry().getPath().toString());
|
||||
}
|
||||
};
|
||||
GlobalTimer.delayAsync(activeTask, Duration.ofMillis(1200));
|
||||
}
|
||||
|
||||
public void onDragEntered(DragEvent event) {
|
||||
event.consume();
|
||||
if (!acceptsDrop(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
acceptDrag(event);
|
||||
model.getDraggedOverEmpty()
|
||||
.setValue(item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY);
|
||||
model.getDraggedOverDirectory().setValue(item);
|
||||
|
||||
if (item != null) {
|
||||
var timestamp = Instant.now();
|
||||
lastHoverUpdate = timestamp;
|
||||
// Reduce printed window updates
|
||||
GlobalTimer.delay(
|
||||
() -> {
|
||||
if (!timestamp.equals(lastHoverUpdate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item != model.getDraggedOverDirectory().getValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.getFileSystemModel()
|
||||
.cdAsync(item.getRawFileEntry().getPath());
|
||||
},
|
||||
Duration.ofMillis(500));
|
||||
}
|
||||
}
|
||||
|
||||
public void onDragOver(DragEvent event) {
|
||||
event.consume();
|
||||
if (!acceptsDrop(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
event.consume();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -333,14 +349,4 @@ public class BrowserFileListCompEntry {
|
||||
row.getParent().getParent().getParent().getParent());
|
||||
tv.getSelectionModel().select(item);
|
||||
}
|
||||
|
||||
public void onDragOver(DragEvent event) {
|
||||
event.consume();
|
||||
if (!acceptsDrop(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
acceptDrag(event);
|
||||
handleHoverTimer(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.base.TextFieldComp;
|
||||
import io.xpipe.app.comp.base.TooltipHelper;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.platform.InputHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -51,8 +51,7 @@ public class BrowserFileListFilterComp extends Comp<BrowserFileListFilterComp.St
|
||||
Tooltip.install(
|
||||
button,
|
||||
TooltipHelper.create(
|
||||
AppI18n.observable("app.search"),
|
||||
new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)));
|
||||
AppI18n.observable("search"), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)));
|
||||
text.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue && filterString.getValue() == null) {
|
||||
if (button.isFocused()) {
|
||||
|
||||
@@ -2,11 +2,10 @@ package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.browser.action.impl.MoveFileActionProvider;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -128,10 +127,7 @@ public final class BrowserFileListModel {
|
||||
|
||||
// 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 ||
|
||||
fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.MACOS)
|
||||
&& old.getFileName().equalsIgnoreCase(newName);
|
||||
var skipExistCheck = old.getFileName().equalsIgnoreCase(newName);
|
||||
if (!skipExistCheck) {
|
||||
boolean exists;
|
||||
try {
|
||||
|
||||
@@ -2,12 +2,12 @@ package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.comp.base.LazyTextFieldComp;
|
||||
import io.xpipe.app.comp.base.PrettyImageHelper;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.platform.ContextMenuHelper;
|
||||
import io.xpipe.app.platform.InputHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ContextMenuHelper;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import javafx.application.Platform;
|
||||
|
||||
@@ -2,164 +2,19 @@ package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.ext.ConnectionFileSystem;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ElevationFunction;
|
||||
import io.xpipe.app.process.ProcessOutputException;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.FileBridge;
|
||||
import io.xpipe.app.util.FileOpener;
|
||||
import io.xpipe.core.FileInfo;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.OsType;
|
||||
import io.xpipe.app.util.HumanReadableFormat;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class BrowserFileOpener {
|
||||
|
||||
private static BrowserFileOutput openFileOutput(BrowserFileSystemTabModel model, FileEntry file, long totalBytes)
|
||||
throws Exception {
|
||||
var fileSystem = model.getFileSystem();
|
||||
if (model.isClosed() || fileSystem.getShell().isEmpty()) {
|
||||
return BrowserFileOutput.none();
|
||||
}
|
||||
|
||||
if (totalBytes == 0) {
|
||||
var existingSize = model.getFileSystem().getFileSize(file.getPath());
|
||||
if (existingSize != 0) {
|
||||
var blank = AppDialog.confirm(
|
||||
"fileWriteBlankTitle", AppI18n.observable("fileWriteBlankContent", file.getPath()));
|
||||
if (!blank) {
|
||||
return BrowserFileOutput.none();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
var requiresSudo =
|
||||
sc.getOsType() != OsType.WINDOWS && requiresSudo(model, (FileInfo.Unix) file.getInfo(), file.getPath());
|
||||
|
||||
var defOutput = createFileOutput(model, file, totalBytes, false);
|
||||
if (!requiresSudo) {
|
||||
return defOutput;
|
||||
}
|
||||
|
||||
var elevate = AppDialog.confirm("fileWriteSudo");
|
||||
if (!elevate) {
|
||||
return defOutput;
|
||||
}
|
||||
|
||||
var rootOutput = createFileOutput(model, file, totalBytes, true);
|
||||
return rootOutput;
|
||||
}
|
||||
|
||||
private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath)
|
||||
throws Exception {
|
||||
if (model.getCache().isRoot()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info != null) {
|
||||
var otherWrite = info.getPermissions().charAt(7) == 'w';
|
||||
if (otherWrite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var userOwned = info.getUid() != null
|
||||
&& model.getCache().getUidForUser(model.getCache().getUsername()) == info.getUid()
|
||||
|| info.getUser() != null && model.getCache().getUsername().equals(info.getUser());
|
||||
var userWrite = info.getPermissions().charAt(1) == 'w';
|
||||
if (userOwned && userWrite) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var test = model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.command(CommandBuilder.of().add("test", "-w").addFile(filePath))
|
||||
.executeAndCheck();
|
||||
return !test;
|
||||
}
|
||||
|
||||
private static BrowserFileOutput createFileOutput(
|
||||
BrowserFileSystemTabModel model, FileEntry file, long totalBytes, boolean elevate) throws Exception {
|
||||
var sc = elevate
|
||||
? model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.identicalDialectSubShell()
|
||||
.elevated(ElevationFunction.elevated(null))
|
||||
.start()
|
||||
: model.getFileSystem().getShell().orElseThrow().start();
|
||||
var fs = elevate ? new ConnectionFileSystem(sc) : model.getFileSystem();
|
||||
var isSudoersFile = file.getPath().startsWith("/etc/sudo");
|
||||
var output = new BrowserFileOutput() {
|
||||
|
||||
@Override
|
||||
public Optional<DataStoreEntry> target() {
|
||||
return Optional.of(model.getEntry().get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasOutput() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream open() throws Exception {
|
||||
try {
|
||||
return fs.openOutput(file.getPath(), totalBytes);
|
||||
} catch (Exception ex) {
|
||||
if (elevate) {
|
||||
fs.close();
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTransfer() throws Exception {
|
||||
if (isSudoersFile) {
|
||||
fs.copy(file.getPath(), sc.getSystemTemporaryDirectory().join(file.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() throws Exception {
|
||||
if (isSudoersFile) {
|
||||
if (sc.view().findProgram("visudo").isPresent()) {
|
||||
try {
|
||||
sc.command(CommandBuilder.of()
|
||||
.add("visudo", "-c", "-f")
|
||||
.addFile(file.getPath()))
|
||||
.execute();
|
||||
} catch (ProcessOutputException ex) {
|
||||
ErrorEventFactory.fromThrowable(ex).expected().handle();
|
||||
fs.copy(sc.getSystemTemporaryDirectory().join(file.getName()), file.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (elevate) {
|
||||
fs.close();
|
||||
}
|
||||
|
||||
model.refreshFileEntriesSync(List.of(file));
|
||||
}
|
||||
};
|
||||
return output;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static int calculateKey(BrowserFileSystemTabModel model, FileEntry entry) {
|
||||
// Use different key for empty / non-empty files to prevent any issues from blanked files when transfer fails
|
||||
@@ -168,7 +23,8 @@ public class BrowserFileOpener {
|
||||
}
|
||||
|
||||
public static void openWithAnyApplication(BrowserFileSystemTabModel model, FileEntry entry) {
|
||||
if (model.getFileSystem().getShell().orElseThrow().isLocal()) {
|
||||
if (model.getFileSystem().getShell().isPresent()
|
||||
&& model.getFileSystem().getShell().get().isLocal()) {
|
||||
FileOpener.openWithAnyApplication(entry.getPath().toString());
|
||||
return;
|
||||
}
|
||||
@@ -180,44 +36,14 @@ public class BrowserFileOpener {
|
||||
file.getFileName(),
|
||||
key,
|
||||
new BooleanScope(model.getBusy()).exclusive(),
|
||||
() -> {
|
||||
return entry.getFileSystem().openInput(file);
|
||||
},
|
||||
(size) -> {
|
||||
if (model.isClosed()) {
|
||||
return BrowserFileOutput.none();
|
||||
}
|
||||
|
||||
return new BrowserFileOutput() {
|
||||
@Override
|
||||
public Optional<DataStoreEntry> target() {
|
||||
return Optional.of(model.getEntry().get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasOutput() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream open() throws Exception {
|
||||
return entry.getFileSystem().openOutput(file, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTransfer() {}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
model.refreshFileEntriesSync(List.of(entry));
|
||||
}
|
||||
};
|
||||
},
|
||||
() -> BrowserFileInput.openFileInput(model, entry),
|
||||
(size) -> BrowserFileOutput.openFileOutput(model, entry, size),
|
||||
s -> FileOpener.openWithAnyApplication(s));
|
||||
}
|
||||
|
||||
public static void openInDefaultApplication(BrowserFileSystemTabModel model, FileEntry entry) {
|
||||
if (model.getFileSystem().getShell().orElseThrow().isLocal()) {
|
||||
if (model.getFileSystem().getShell().isPresent()
|
||||
&& model.getFileSystem().getShell().get().isLocal()) {
|
||||
FileOpener.openInDefaultApplication(entry.getPath().toString());
|
||||
return;
|
||||
}
|
||||
@@ -229,39 +55,8 @@ public class BrowserFileOpener {
|
||||
file.getFileName(),
|
||||
key,
|
||||
new BooleanScope(model.getBusy()).exclusive(),
|
||||
() -> {
|
||||
return entry.getFileSystem().openInput(file);
|
||||
},
|
||||
(size) -> {
|
||||
if (model.isClosed()) {
|
||||
return BrowserFileOutput.none();
|
||||
}
|
||||
|
||||
return new BrowserFileOutput() {
|
||||
@Override
|
||||
public Optional<DataStoreEntry> target() {
|
||||
return Optional.of(model.getEntry().get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasOutput() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream open() throws Exception {
|
||||
return entry.getFileSystem().openOutput(file, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTransfer() {}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
model.refreshFileEntriesSync(List.of(entry));
|
||||
}
|
||||
};
|
||||
},
|
||||
() -> BrowserFileInput.openFileInput(model, entry),
|
||||
(size) -> BrowserFileOutput.openFileOutput(model, entry, size),
|
||||
s -> FileOpener.openInDefaultApplication(s));
|
||||
}
|
||||
|
||||
@@ -270,11 +65,23 @@ public class BrowserFileOpener {
|
||||
if (editor == null) {
|
||||
return;
|
||||
}
|
||||
if (model.getFileSystem().getShell().orElseThrow().isLocal()) {
|
||||
|
||||
if (model.getFileSystem().getShell().isPresent()
|
||||
&& model.getFileSystem().getShell().get().isLocal()) {
|
||||
FileOpener.openInTextEditor(entry.getPath().toString());
|
||||
return;
|
||||
}
|
||||
|
||||
var size = entry.getFileSizeLong().orElse(0L);
|
||||
if (size > 1_000_000) {
|
||||
var confirm = AppDialog.confirm(
|
||||
"largeFileWarningTitle",
|
||||
AppI18n.observable("largeFileWarningContent", HumanReadableFormat.byteCount(size)));
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var file = entry.getPath();
|
||||
var key = calculateKey(model, entry);
|
||||
FileBridge.get()
|
||||
@@ -282,11 +89,9 @@ public class BrowserFileOpener {
|
||||
file.getFileName(),
|
||||
key,
|
||||
new BooleanScope(model.getBusy()).exclusive(),
|
||||
() -> {
|
||||
return entry.getFileSystem().openInput(file);
|
||||
},
|
||||
(size) -> {
|
||||
return openFileOutput(model, entry, size);
|
||||
() -> BrowserFileInput.openFileInput(model, entry),
|
||||
(os) -> {
|
||||
return BrowserFileOutput.openFileOutput(model, entry, os);
|
||||
},
|
||||
FileOpener::openInTextEditor);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,167 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.ext.ConnectionFileSystem;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.FileInfo;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ElevationFunction;
|
||||
import io.xpipe.app.process.ProcessOutputException;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BrowserFileOutput {
|
||||
|
||||
static BrowserFileOutput openFileOutput(BrowserFileSystemTabModel model, FileEntry file, long totalBytes)
|
||||
throws Exception {
|
||||
if (model.isClosed()) {
|
||||
return BrowserFileOutput.none();
|
||||
}
|
||||
|
||||
if (totalBytes == 0) {
|
||||
var existingSize = model.getFileSystem().getFileSize(file.getPath());
|
||||
if (existingSize != 0) {
|
||||
var blank = AppDialog.confirm(
|
||||
"fileWriteBlankTitle", AppI18n.observable("fileWriteBlankContent", file.getPath()));
|
||||
if (!blank) {
|
||||
return BrowserFileOutput.none();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var defOutput = createFileOutputImpl(model, file, totalBytes, false);
|
||||
if (model.getFileSystem().getShell().isEmpty()) {
|
||||
return defOutput;
|
||||
}
|
||||
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
var requiresSudo =
|
||||
sc.getOsType() != OsType.WINDOWS && requiresSudo(model, (FileInfo.Unix) file.getInfo(), file.getPath());
|
||||
|
||||
if (!requiresSudo) {
|
||||
return defOutput;
|
||||
}
|
||||
|
||||
var elevate = AppDialog.confirm("fileWriteSudo");
|
||||
if (!elevate) {
|
||||
return defOutput;
|
||||
}
|
||||
|
||||
var rootOutput = createFileOutputImpl(model, file, totalBytes, true);
|
||||
return rootOutput;
|
||||
}
|
||||
|
||||
private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath)
|
||||
throws Exception {
|
||||
if (model.getFileSystem().getShell().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var sc = model.getFileSystem().getShell().get();
|
||||
if (sc.view().isRoot()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info != null) {
|
||||
var otherWrite = info.getPermissions().charAt(7) == 'w';
|
||||
if (otherWrite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var userOwned = info.getUid() != null
|
||||
&& sc.view().getPasswdFile().getUidForUser(sc.view().user()) == info.getUid()
|
||||
|| info.getUser() != null && sc.view().user().equals(info.getUser());
|
||||
var userWrite = info.getPermissions().charAt(1) == 'w';
|
||||
if (userOwned && userWrite) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var test = model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.command(CommandBuilder.of().add("test", "-w").addFile(filePath))
|
||||
.executeAndCheck();
|
||||
return !test;
|
||||
}
|
||||
|
||||
private static BrowserFileOutput createFileOutputImpl(
|
||||
BrowserFileSystemTabModel model, FileEntry file, long totalBytes, boolean elevate) throws Exception {
|
||||
var shell = model.getFileSystem().getShell();
|
||||
var sc = shell.isEmpty()
|
||||
? null
|
||||
: elevate
|
||||
? shell.orElseThrow()
|
||||
.identicalDialectSubShell()
|
||||
.elevated(ElevationFunction.elevated(null))
|
||||
.start()
|
||||
: model.getFileSystem().getShell().orElseThrow().start();
|
||||
var fs = elevate ? new ConnectionFileSystem(sc) : model.getFileSystem();
|
||||
var checkSudoersFile = shell.isPresent() && file.getPath().startsWith("/etc/sudo");
|
||||
var output = new BrowserFileOutput() {
|
||||
|
||||
@Override
|
||||
public Optional<DataStoreEntry> target() {
|
||||
return Optional.of(model.getEntry().get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasOutput() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream open() throws Exception {
|
||||
try {
|
||||
return fs.openOutput(file.getPath(), totalBytes);
|
||||
} catch (Exception ex) {
|
||||
if (elevate) {
|
||||
fs.close();
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTransfer() throws Exception {
|
||||
if (checkSudoersFile) {
|
||||
fs.copy(file.getPath(), sc.getSystemTemporaryDirectory().join(file.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() throws Exception {
|
||||
if (checkSudoersFile) {
|
||||
if (sc.view().findProgram("visudo").isPresent()) {
|
||||
try {
|
||||
sc.command(CommandBuilder.of()
|
||||
.add("visudo", "-c", "-f")
|
||||
.addFile(file.getPath()))
|
||||
.execute();
|
||||
} catch (ProcessOutputException ex) {
|
||||
ErrorEventFactory.fromThrowable(ex).expected().handle();
|
||||
fs.copy(sc.getSystemTemporaryDirectory().join(file.getName()), file.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (elevate) {
|
||||
fs.close();
|
||||
}
|
||||
|
||||
model.refreshFileEntriesSync(List.of(file));
|
||||
}
|
||||
};
|
||||
return output;
|
||||
}
|
||||
|
||||
static BrowserFileOutput none() {
|
||||
return new BrowserFileOutput() {
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package io.xpipe.app.browser.file;
|
||||
import io.xpipe.app.browser.icon.BrowserIcons;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.comp.base.HorizontalComp;
|
||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
@@ -42,7 +41,7 @@ public class BrowserFileOverviewComp extends SimpleComp {
|
||||
event.consume();
|
||||
});
|
||||
l.setAlignment(Pos.CENTER_LEFT);
|
||||
GrowAugment.create(true, false).augment(l);
|
||||
l.setMaxWidth(10000);
|
||||
return l;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,9 +5,9 @@ import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||
import io.xpipe.app.comp.base.PrettyImageHelper;
|
||||
import io.xpipe.app.core.AppStyle;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.core.window.AppWindowStyle;
|
||||
import io.xpipe.app.platform.BindingsHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -42,7 +42,7 @@ public class BrowserFileSelectionListComp extends SimpleComp {
|
||||
public static Image snapshot(ObservableList<BrowserEntry> list) {
|
||||
var r = new BrowserFileSelectionListComp(list).styleClass("drag").createRegion();
|
||||
var scene = new Scene(r);
|
||||
AppWindowHelper.setupStylesheets(scene);
|
||||
AppWindowStyle.addStylesheets(scene);
|
||||
AppStyle.addStylesheets(scene);
|
||||
SnapshotParameters parameters = new SnapshotParameters();
|
||||
parameters.setFill(Color.TRANSPARENT);
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.process.ShellDialect;
|
||||
import io.xpipe.app.util.PasswdFile;
|
||||
import io.xpipe.app.util.ShellControlCache;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
public class BrowserFileSystemCache extends ShellControlCache {
|
||||
|
||||
private final BrowserFileSystemTabModel model;
|
||||
private final String username;
|
||||
private final PasswdFile passwdFile;
|
||||
private final Map<Integer, String> groups = new LinkedHashMap<>();
|
||||
|
||||
public BrowserFileSystemCache(BrowserFileSystemTabModel model) throws Exception {
|
||||
super(model.getFileSystem().getShell().orElseThrow());
|
||||
this.model = model;
|
||||
|
||||
ShellControl sc = model.getFileSystem().getShell().get();
|
||||
ShellDialect d = sc.getShellDialect();
|
||||
// If there is no id command, we should still be fine with just assuming root
|
||||
username = d.printUsernameCommand(sc).readStdoutIfPossible().orElse("root");
|
||||
passwdFile = PasswdFile.parse(sc);
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
public Map<Integer, String> getUsers() {
|
||||
return passwdFile.getUsers();
|
||||
}
|
||||
|
||||
public int getUidForUser(String name) {
|
||||
return passwdFile.getUidForUser(name);
|
||||
}
|
||||
|
||||
public int getGidForGroup(String name) {
|
||||
return groups.entrySet().stream()
|
||||
.filter(e -> e.getValue().equals(name))
|
||||
.findFirst()
|
||||
.map(e -> e.getKey())
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
private void loadGroups() throws Exception {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
if (sc.getOsType() == OsType.WINDOWS || sc.getOsType() == OsType.MACOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = sc.command(CommandBuilder.of().add("cat").addFile("/etc/group"))
|
||||
.sensitive()
|
||||
.readStdoutIfPossible()
|
||||
.orElse("");
|
||||
lines.lines().forEach(s -> {
|
||||
var split = s.split(":");
|
||||
try {
|
||||
groups.putIfAbsent(Integer.parseInt(split[2]), split[0]);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
|
||||
if (groups.isEmpty()) {
|
||||
groups.put(0, "root");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRoot() {
|
||||
return username.equals("root");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.ext.FileSystem;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
@@ -94,24 +94,18 @@ public class BrowserFileSystemHelper {
|
||||
return resolved.toDirectory();
|
||||
}
|
||||
|
||||
public static void validateDirectoryPath(BrowserFileSystemTabModel model, FilePath path, boolean verifyExists)
|
||||
throws Exception {
|
||||
public static void validateDirectoryPath(FileSystem fs, FilePath path, boolean verifyExists) throws Exception {
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var shell = model.getFileSystem().getShell();
|
||||
if (shell.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (verifyExists && !model.getFileSystem().directoryExists(path)) {
|
||||
if (verifyExists && !fs.directoryExists(path)) {
|
||||
throw ErrorEventFactory.expected(new IllegalArgumentException(
|
||||
String.format("Directory %s does not exist or is not accessible", path)));
|
||||
}
|
||||
|
||||
try {
|
||||
model.getFileSystem().directoryAccessible(path);
|
||||
fs.directoryAccessible(path);
|
||||
} catch (Exception ex) {
|
||||
ErrorEventFactory.expected(ex);
|
||||
throw ex;
|
||||
|
||||
@@ -9,12 +9,13 @@ import io.xpipe.app.comp.augment.ContextMenuAugment;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.platform.InputHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
@@ -93,8 +94,13 @@ public class BrowserFileSystemTabComp extends SimpleComp {
|
||||
|
||||
refreshBtn.managedProperty().bind(smallWidth.not());
|
||||
refreshBtn.visibleProperty().bind(refreshBtn.managedProperty());
|
||||
terminalBtn.managedProperty().bind(smallWidth.not());
|
||||
terminalBtn.visibleProperty().bind(terminalBtn.managedProperty());
|
||||
|
||||
var terminalSupported =
|
||||
BrowserMenuProviders.byId("openInTerminal", model, List.of()).isApplicable(model, List.of());
|
||||
terminalBtn.managedProperty().bind(smallWidth.not().and(new ReadOnlyBooleanWrapper(terminalSupported)));
|
||||
terminalBtn
|
||||
.visibleProperty()
|
||||
.bind(terminalBtn.managedProperty().and(new ReadOnlyBooleanWrapper(terminalSupported)));
|
||||
|
||||
var filter = new BrowserFileListFilterComp(model, model.getFilter())
|
||||
.hide(smallWidth)
|
||||
@@ -135,7 +141,7 @@ public class BrowserFileSystemTabComp extends SimpleComp {
|
||||
if (fullSessionModel.getGlobalPinnedTab().getValue() != model) {
|
||||
fullSessionModel.pinTab(model);
|
||||
} else {
|
||||
fullSessionModel.unpinTab(model);
|
||||
fullSessionModel.unpinTab();
|
||||
}
|
||||
e.consume();
|
||||
});
|
||||
@@ -215,6 +221,8 @@ public class BrowserFileSystemTabComp extends SimpleComp {
|
||||
}
|
||||
var fileList = new VerticalComp(fileListElements)
|
||||
.styleClass("browser-content")
|
||||
.styleClass("color-box")
|
||||
.styleClass("gray")
|
||||
.apply(struc -> {
|
||||
struc.get().focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
|
||||
@@ -9,10 +9,10 @@ import io.xpipe.app.browser.menu.BrowserMenuItemProvider;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.ext.FileSystem;
|
||||
import io.xpipe.app.ext.FileSystemStore;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.ext.WrapperFileSystem;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.process.*;
|
||||
@@ -20,12 +20,13 @@ import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.terminal.*;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.FailableFunction;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -48,7 +49,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
private final BrowserFileListModel fileList;
|
||||
private final ReadOnlyObjectWrapper<FilePath> currentPath = new ReadOnlyObjectWrapper<>();
|
||||
private final BrowserFileSystemHistory history = new BrowserFileSystemHistory();
|
||||
private final BooleanProperty inOverview = new SimpleBooleanProperty();
|
||||
private final ObservableBooleanValue inOverview = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return currentPath.get() == null;
|
||||
},
|
||||
currentPath);
|
||||
private final ObservableList<UUID> terminalRequests = FXCollections.observableArrayList();
|
||||
private final BooleanProperty transferCancelled = new SimpleBooleanProperty();
|
||||
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
|
||||
@@ -56,21 +61,18 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
FXCollections.observableArrayList();
|
||||
private final LongProperty progressTransferSpeed = new SimpleLongProperty();
|
||||
private final Property<Duration> progressRemaining = new SimpleObjectProperty<>();
|
||||
private final FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> fileSystemFactory;
|
||||
private FileSystem fileSystem;
|
||||
private BrowserFileSystemSavedState savedState;
|
||||
private BrowserFileSystemCache cache;
|
||||
|
||||
public BrowserFileSystemTabModel(
|
||||
BrowserAbstractSessionModel<?> model,
|
||||
DataStoreEntryRef<? extends FileSystemStore> entry,
|
||||
SelectionMode selectionMode) {
|
||||
SelectionMode selectionMode,
|
||||
FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> fileSystemFactory) {
|
||||
super(model, entry);
|
||||
this.inOverview.bind(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return currentPath.get() == null;
|
||||
},
|
||||
currentPath));
|
||||
fileList = new BrowserFileListModel(selectionMode, this);
|
||||
this.fileList = new BrowserFileListModel(selectionMode, this);
|
||||
this.fileSystemFactory = fileSystemFactory;
|
||||
}
|
||||
|
||||
public void updateProgress(BrowserTransferProgress n) {
|
||||
@@ -148,23 +150,26 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
var fs = entry.getStore().createFileSystem();
|
||||
var fs = fileSystemFactory.apply(getEntry().asNeeded());
|
||||
if (fs.getShell().isPresent()) {
|
||||
ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());
|
||||
var originalFs = fs;
|
||||
fs = new WrapperFileSystem(
|
||||
originalFs, () -> originalFs.getShell().get().isRunning(true));
|
||||
}
|
||||
fs.open();
|
||||
|
||||
// Listen to kill after init as the shell might get killed during init for certain reasons
|
||||
if (fs.getShell().isPresent()) {
|
||||
fs.getShell().get().onKill(() -> {
|
||||
if (fs.getRawShellControl().isPresent()) {
|
||||
fs.getRawShellControl().get().onKill(() -> {
|
||||
browserModel.closeAsync(this);
|
||||
});
|
||||
}
|
||||
this.fileSystem = fs;
|
||||
|
||||
this.cache = new BrowserFileSystemCache(this);
|
||||
// Cache for later usage
|
||||
if (fs.getShell().isPresent()) {
|
||||
fs.getShell().get().view().getPasswdFile();
|
||||
fs.getShell().get().view().getGroupFile();
|
||||
}
|
||||
|
||||
for (var a : ActionProvider.ALL) {
|
||||
if (a instanceof BrowserMenuItemProvider ba) {
|
||||
ba.init(this);
|
||||
@@ -198,13 +203,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
}
|
||||
|
||||
public void startIfNeeded() throws Exception {
|
||||
var s = fileSystem.getShell();
|
||||
if (s.isPresent()) {
|
||||
s.get().start();
|
||||
if (s.get().isAnyStreamClosed()) {
|
||||
s.get().restart();
|
||||
}
|
||||
}
|
||||
fileSystem.reinitIfNeeded();
|
||||
}
|
||||
|
||||
public void killTransfer() {
|
||||
@@ -300,11 +299,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
return false;
|
||||
}
|
||||
|
||||
if (OsType.getLocal() != OsType.WINDOWS) {
|
||||
if (OsType.ofLocal() != OsType.WINDOWS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AppMainWindow.getInstance().getStage().getWidth() <= 1380) {
|
||||
if (AppMainWindow.get().getStage().getWidth() <= 1380) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -330,6 +329,10 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
}
|
||||
|
||||
public Optional<String> cdSyncOrRetry(String path, boolean customInput) {
|
||||
if (!fileSystem.isRunning()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
var cps = currentPath.get() != null ? currentPath.get().toString() : null;
|
||||
if (Objects.equals(path, cps)) {
|
||||
return Optional.empty();
|
||||
@@ -421,7 +424,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
}
|
||||
|
||||
try {
|
||||
BrowserFileSystemHelper.validateDirectoryPath(this, resolvedPath, true);
|
||||
BrowserFileSystemHelper.validateDirectoryPath(fileSystem, resolvedPath, true);
|
||||
cdSyncWithoutCheck(resolvedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEventFactory.fromThrowable(ex).handle();
|
||||
@@ -499,6 +502,20 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
});
|
||||
}
|
||||
|
||||
public void duplicateFile(FileEntry entry) {
|
||||
// Technically we would have to create an action to allow confirmations for this
|
||||
// But in practice, this is almost a non mutable action, so we will save the effort
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
startIfNeeded();
|
||||
var adjusted = BrowserFileDuplicates.renameFileDuplicate(
|
||||
fileSystem, entry.getPath(), entry.getKind() == FileKind.DIRECTORY);
|
||||
fileSystem.copy(entry.getPath(), adjusted);
|
||||
refreshSync();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return false;
|
||||
}
|
||||
@@ -559,6 +576,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
}
|
||||
|
||||
@Getter
|
||||
@SuppressWarnings("unused")
|
||||
public enum SelectionMode {
|
||||
SINGLE_FILE(false, true, false),
|
||||
MULTIPLE_FILE(true, true, false),
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.ext.FileSystem;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.process.OsFileSystem;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -25,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class BrowserFileTransferOperation {
|
||||
|
||||
@@ -82,17 +80,9 @@ public class BrowserFileTransferOperation {
|
||||
return new BrowserFileTransferOperation(target, entries, transferMode, checkConflicts, progress, cancelled);
|
||||
}
|
||||
|
||||
private void restartShellIfNeeded() throws Exception {
|
||||
var source = getFiles().getFirst().getFileSystem().getShell().orElseThrow();
|
||||
var target = getTarget().getFileSystem().getShell().orElseThrow();
|
||||
|
||||
if (source.isAnyStreamClosed()) {
|
||||
source.restart();
|
||||
}
|
||||
|
||||
if (target.isAnyStreamClosed()) {
|
||||
target.restart();
|
||||
}
|
||||
private void reinitFileSystemsIfNeeded() throws Exception {
|
||||
getFiles().getFirst().getFileSystem().reinitIfNeeded();
|
||||
getTarget().getFileSystem().reinitIfNeeded();
|
||||
}
|
||||
|
||||
private void updateProgress(BrowserTransferProgress progress) {
|
||||
@@ -118,7 +108,7 @@ public class BrowserFileTransferOperation {
|
||||
return BrowserDialogs.FileConflictChoice.SKIP;
|
||||
}
|
||||
|
||||
var choice = BrowserDialogs.showFileConflictAlert(target, multiple);
|
||||
var choice = BrowserDialogs.showFileConflictDialog(target, multiple);
|
||||
if (choice == BrowserDialogs.FileConflictChoice.CANCEL) {
|
||||
lastConflictChoice = BrowserDialogs.FileConflictChoice.CANCEL;
|
||||
return BrowserDialogs.FileConflictChoice.CANCEL;
|
||||
@@ -149,7 +139,7 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
|
||||
private boolean cancelled() {
|
||||
return cancelled.get() || OperationMode.isInShutdown();
|
||||
return cancelled.get() || AppOperationMode.isInShutdown();
|
||||
}
|
||||
|
||||
public boolean isMove() {
|
||||
@@ -169,7 +159,7 @@ public class BrowserFileTransferOperation {
|
||||
return;
|
||||
}
|
||||
|
||||
restartShellIfNeeded();
|
||||
reinitFileSystemsIfNeeded();
|
||||
|
||||
cancelled.set(false);
|
||||
|
||||
@@ -186,13 +176,12 @@ public class BrowserFileTransferOperation {
|
||||
handleSingleOnSameFileSystem(file);
|
||||
} else {
|
||||
// Transfers might change the working directory
|
||||
var currentDir =
|
||||
file.getFileSystem().getShell().orElseThrow().view().pwd();
|
||||
var currentDir = file.getFileSystem().pwd();
|
||||
handleSingleAcrossFileSystems(file);
|
||||
|
||||
// Expect a kill
|
||||
if (!file.getFileSystem().getShell().orElseThrow().isAnyStreamClosed()) {
|
||||
file.getFileSystem().getShell().orElseThrow().view().cd(currentDir);
|
||||
if (currentDir.isPresent() && !file.getFileSystem().requiresReinit()) {
|
||||
file.getFileSystem().cd(currentDir.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +213,8 @@ public class BrowserFileTransferOperation {
|
||||
|
||||
if (sourceFile.equals(targetFile)) {
|
||||
// Duplicate file by renaming it
|
||||
targetFile = renameFileLoop(target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);
|
||||
targetFile = BrowserFileDuplicates.renameFileDuplicate(
|
||||
target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) {
|
||||
@@ -240,7 +230,8 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
|
||||
if (fileConflictChoice == BrowserDialogs.FileConflictChoice.RENAME) {
|
||||
targetFile = renameFileLoop(target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);
|
||||
targetFile = BrowserFileDuplicates.renameFileDuplicate(
|
||||
target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,40 +243,14 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
}
|
||||
|
||||
private FilePath renameFileLoop(FileSystem fileSystem, FilePath target, boolean dir) throws Exception {
|
||||
// Who has more than 10 copies?
|
||||
for (int i = 0; i < 10; i++) {
|
||||
target = renameFile(target);
|
||||
if ((dir && !fileSystem.directoryExists(target)) || (!dir && !fileSystem.fileExists(target))) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private FilePath renameFile(FilePath target) {
|
||||
var name = target.getFileName();
|
||||
var pattern = Pattern.compile("(.+) \\((\\d+)\\)\\.(.+?)");
|
||||
var matcher = pattern.matcher(name);
|
||||
if (matcher.matches()) {
|
||||
try {
|
||||
var number = Integer.parseInt(matcher.group(2));
|
||||
var newFile = target.getParent().join(matcher.group(1) + " (" + (number + 1) + ")." + matcher.group(3));
|
||||
return newFile;
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
var ext = target.getExtension();
|
||||
return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (ext.isPresent() ? "." + ext.get() : ""));
|
||||
}
|
||||
|
||||
private void handleSingleAcrossFileSystems(FileEntry source) throws Exception {
|
||||
if (target.getKind() != FileKind.DIRECTORY) {
|
||||
throw new IllegalStateException("Target " + target.getPath() + " is not a directory");
|
||||
}
|
||||
|
||||
var flatFiles = new LinkedHashMap<FileEntry, String>();
|
||||
BrowserFileSystemHelper.validateDirectoryPath(target.getFileSystem(), target.getPath(), true);
|
||||
|
||||
var flatFiles = new LinkedHashMap<FileEntry, FilePath>();
|
||||
|
||||
// Prevent dropping directory into itself
|
||||
if (source.getFileSystem().equals(target.getFileSystem())
|
||||
@@ -302,7 +267,7 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
|
||||
var directoryName = source.getPath().getFileName();
|
||||
flatFiles.put(source, directoryName);
|
||||
flatFiles.put(source, FilePath.of(directoryName));
|
||||
|
||||
var baseRelative = source.getPath().getParent().toDirectory();
|
||||
var list = new ArrayList<FileEntry>();
|
||||
@@ -312,7 +277,7 @@ public class BrowserFileTransferOperation {
|
||||
return false;
|
||||
}
|
||||
|
||||
var rel = fileEntry.getPath().relativize(baseRelative).toUnix().toString();
|
||||
var rel = fileEntry.getPath().relativize(baseRelative).toUnix();
|
||||
flatFiles.put(fileEntry, rel);
|
||||
if (fileEntry.getKind() == FileKind.FILE) {
|
||||
// This one is up-to-date and does not need to be recalculated
|
||||
@@ -329,7 +294,7 @@ public class BrowserFileTransferOperation {
|
||||
return;
|
||||
}
|
||||
|
||||
var rel = fileEntry.getPath().relativize(baseRelative).toUnix().toString();
|
||||
var rel = fileEntry.getPath().relativize(baseRelative).toUnix();
|
||||
flatFiles.put(fileEntry, rel);
|
||||
if (fileEntry.getKind() == FileKind.FILE) {
|
||||
// This one is up-to-date and does not need to be recalculated
|
||||
@@ -345,7 +310,7 @@ public class BrowserFileTransferOperation {
|
||||
return;
|
||||
}
|
||||
|
||||
flatFiles.put(source, source.getPath().getFileName());
|
||||
flatFiles.put(source, FilePath.of(source.getPath().getFileName()));
|
||||
// If we don't have a size, it doesn't matter that much as the total size is only for display
|
||||
totalSize.addAndGet(source.getFileSizeLong().orElse(0));
|
||||
}
|
||||
@@ -367,8 +332,7 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
|
||||
var sourceFile = e.getKey();
|
||||
var os = targetFs.getShell().orElseThrow().getOsType();
|
||||
var fixedRelPath = OsFileSystem.of(os).makeFileSystemCompatible(FilePath.of(e.getValue()));
|
||||
var fixedRelPath = targetFs.makeFileSystemCompatible(e.getValue());
|
||||
var targetFile = target.getPath().join(fixedRelPath.toString());
|
||||
if (sourceFile.getFileSystem().equals(targetFs)) {
|
||||
throw new IllegalStateException();
|
||||
@@ -386,7 +350,7 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
|
||||
if (fileConflictChoice == BrowserDialogs.FileConflictChoice.RENAME) {
|
||||
targetFile = renameFileLoop(targetFs, targetFile, false);
|
||||
targetFile = BrowserFileDuplicates.renameFileDuplicate(targetFs, targetFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,8 +434,8 @@ public class BrowserFileTransferOperation {
|
||||
return;
|
||||
}
|
||||
|
||||
sourceFs.getShell().orElseThrow().killExternal();
|
||||
targetFs.getShell().orElseThrow().killExternal();
|
||||
sourceFs.kill();
|
||||
targetFs.kill();
|
||||
};
|
||||
cancelled.addListener(closeCancelListener);
|
||||
|
||||
@@ -545,9 +509,10 @@ public class BrowserFileTransferOperation {
|
||||
outputStream.flush();
|
||||
inputStream.transferTo(OutputStream.nullOutputStream());
|
||||
|
||||
var incomplete = readCount.get() < expectedFileSize;
|
||||
var incomplete = !killStreams.get() && readCount.get() < expectedFileSize;
|
||||
if (incomplete) {
|
||||
throw new IOException("Source file " + sourceFile + " input did end prematurely");
|
||||
throw new IOException("Source file " + sourceFile + " input size mismatch: Expected "
|
||||
+ expectedFileSize + " but got " + readCount.get() + ". Did the source file get updated?");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
exception.set(ex);
|
||||
@@ -588,13 +553,7 @@ public class BrowserFileTransferOperation {
|
||||
var targetFs = target.getFileSystem();
|
||||
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
|
||||
if (!same) {
|
||||
var sourceShell = sourceFs.getShell().orElseThrow();
|
||||
var targetShell = targetFs.getShell().orElseThrow();
|
||||
// Check for null on shell reset
|
||||
return sourceShell.getStdout() != null
|
||||
&& !sourceShell.getStdout().isClosed()
|
||||
&& targetShell.getStdin() != null
|
||||
&& !targetShell.getStdin().isClosed();
|
||||
return !sourceFs.requiresReinit() && !targetFs.requiresReinit();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
@@ -611,20 +570,26 @@ public class BrowserFileTransferOperation {
|
||||
var nowTransferred = transferred.get();
|
||||
var stuck = initialTransferred == nowTransferred;
|
||||
if (stuck) {
|
||||
sourceFs.getShell().orElseThrow().killExternal();
|
||||
targetFs.getShell().orElseThrow().killExternal();
|
||||
sourceFs.kill();
|
||||
targetFs.kill();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!same) {
|
||||
var sourceShell = sourceFs.getShell().orElseThrow();
|
||||
var targetShell = targetFs.getShell().orElseThrow();
|
||||
try {
|
||||
sourceShell.closeStdout();
|
||||
} finally {
|
||||
targetShell.closeStdin();
|
||||
if (sourceFs.getShell().isPresent()) {
|
||||
try {
|
||||
sourceFs.getShell().get().closeStdout();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (targetFs.getShell().isPresent()) {
|
||||
try {
|
||||
targetFs.getShell().get().closeStdin();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
@@ -5,11 +5,11 @@ import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.platform.DerivedObservableList;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -127,13 +127,11 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
||||
open.setButtonGraphic(new LabelGraphic.IconGraphic("mdi2f-folder-open-outline"));
|
||||
open.setButtonAction(() -> {
|
||||
BrowserFullSessionModel.DEFAULT.openFileSystemAsync(
|
||||
DataStorage.get().local().ref(), null, null);
|
||||
DataStorage.get().local().ref(), null, null, null);
|
||||
});
|
||||
|
||||
var v = new VerticalComp(List.of(docs, open));
|
||||
v.spacing(70);
|
||||
v.apply(struc -> struc.get().setAlignment(Pos.CENTER));
|
||||
return v;
|
||||
var list = new IntroListComp(List.of(docs, open));
|
||||
return list;
|
||||
}
|
||||
|
||||
private Comp<?> entryButton(BrowserHistorySavedState.Entry e, BooleanProperty disable) {
|
||||
@@ -150,7 +148,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
|
||||
if (storageEntry.isPresent()) {
|
||||
model.openFileSystemAsync(storageEntry.get().ref(), null, disable);
|
||||
model.openFileSystemAsync(storageEntry.get().ref(), null, null, disable);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.ext.FileSystem;
|
||||
import io.xpipe.app.ext.LocalStore;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import java.nio.file.Files;
|
||||
@@ -17,8 +17,8 @@ public class BrowserLocalFileSystem {
|
||||
if (localFileSystem == null) {
|
||||
localFileSystem = new LocalStore().createFileSystem();
|
||||
localFileSystem.open();
|
||||
} else if (localFileSystem.getShell().orElseThrow().isAnyStreamClosed()) {
|
||||
localFileSystem.getShell().orElseThrow().restart();
|
||||
} else {
|
||||
localFileSystem.reinitIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import io.xpipe.app.comp.base.PrettyImageHelper;
|
||||
import io.xpipe.app.comp.base.TextFieldComp;
|
||||
import io.xpipe.app.comp.base.TooltipHelper;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.platform.ContextMenuHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ContextMenuHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.SimpleTitledPaneComp;
|
||||
import io.xpipe.app.comp.base.VerticalComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.process.OsFileSystem;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.platform.DerivedObservableList;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
@@ -19,7 +19,7 @@ import javafx.scene.layout.VBox;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class BrowserOverviewComp extends SimpleComp {
|
||||
|
||||
@@ -34,11 +34,19 @@ public class BrowserOverviewComp extends SimpleComp {
|
||||
protected Region createSimple() {
|
||||
// The open file system might have already been closed
|
||||
|
||||
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
|
||||
var list = new ArrayList<Comp<?>>();
|
||||
|
||||
var recent = DerivedObservableList.wrap(model.getSavedState().getRecentDirectories(), true)
|
||||
.mapped(s -> FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()))
|
||||
.getList();
|
||||
var recentOverview = new BrowserFileOverviewComp(model, recent, true);
|
||||
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview, false);
|
||||
recentPane.hide(Bindings.isEmpty(recent));
|
||||
list.add(recentPane);
|
||||
|
||||
var commonPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var common = OsFileSystem.of(sc.getOsType()).determineInterestingPaths(sc).stream()
|
||||
var common = model.getFileSystem().listCommonDirectories().stream()
|
||||
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
|
||||
.filter(entry -> {
|
||||
var fs = model.getFileSystem();
|
||||
@@ -58,20 +66,24 @@ public class BrowserOverviewComp extends SimpleComp {
|
||||
var commonOverview = new BrowserFileOverviewComp(model, commonPlatform, false);
|
||||
var commonPane = new SimpleTitledPaneComp(AppI18n.observable("common"), commonOverview, false)
|
||||
.apply(struc -> VBox.setVgrow(struc.get(), Priority.NEVER));
|
||||
commonPane.hide(Bindings.isEmpty(commonPlatform));
|
||||
list.add(commonPane);
|
||||
|
||||
var roots = model.getFileSystem().listRoots().stream()
|
||||
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
|
||||
.toList();
|
||||
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
|
||||
var rootPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var roots = model.getFileSystem().listRoots().stream()
|
||||
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
|
||||
.toList();
|
||||
Platform.runLater(() -> {
|
||||
rootPlatform.setAll(roots);
|
||||
});
|
||||
});
|
||||
var rootsOverview = new BrowserFileOverviewComp(model, rootPlatform, false);
|
||||
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview, false);
|
||||
rootsPane.hide(Bindings.isEmpty(rootPlatform));
|
||||
list.add(rootsPane);
|
||||
|
||||
var recent = DerivedObservableList.wrap(model.getSavedState().getRecentDirectories(), true)
|
||||
.mapped(s -> FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()))
|
||||
.getList();
|
||||
var recentOverview = new BrowserFileOverviewComp(model, recent, true);
|
||||
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview, false);
|
||||
|
||||
var vbox = new VerticalComp(List.of(recentPane, commonPane, rootsPane)).styleClass("overview");
|
||||
var vbox = new VerticalComp(list).styleClass("overview");
|
||||
var r = vbox.createRegion();
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.IconButtonComp;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.platform.InputHelper;
|
||||
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import io.xpipe.app.browser.icon.BrowserIconManager;
|
||||
import io.xpipe.app.comp.base.PrettyImageHelper;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.util.BooleanAnimationTimer;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.platform.BooleanAnimationTimer;
|
||||
import io.xpipe.app.platform.InputHelper;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FileKind;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
||||
@@ -8,9 +8,9 @@ import io.xpipe.app.comp.base.HorizontalComp;
|
||||
import io.xpipe.app.comp.base.IconButtonComp;
|
||||
import io.xpipe.app.comp.base.LabelComp;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.platform.BindingsHelper;
|
||||
import io.xpipe.app.platform.PlatformThread;
|
||||
import io.xpipe.app.util.HumanReadableFormat;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -100,6 +100,10 @@ public class BrowserStatusBarComp extends SimpleComp {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (p.getTotal() == 0) {
|
||||
return HumanReadableFormat.byteCount(p.getTransferred());
|
||||
}
|
||||
|
||||
var elapsed = (p.getTotal() - p.getTransferred() / (double) p.getTotal()) * expected.toMillis();
|
||||
var show = elapsed > 3000;
|
||||
if (!show) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.platform.DerivedObservableList;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -62,7 +62,7 @@ public class BrowserTransferComp extends SimpleComp {
|
||||
return Bindings.createStringBinding(
|
||||
() -> {
|
||||
var p = sourceItem.get().getProgress().getValue();
|
||||
if (p == null) {
|
||||
if (p == null || p.getTotal() == 0) {
|
||||
return entry.getFileName();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ package io.xpipe.app.browser.file;
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.browser.action.impl.TransferFilesActionProvider;
|
||||
import io.xpipe.app.core.AppSystemInfo;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.mode.AppOperationMode;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.process.OsFileSystem;
|
||||
import io.xpipe.app.process.ShellTemp;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.DesktopHelper;
|
||||
import io.xpipe.app.util.ShellTemp;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -137,7 +137,7 @@ public class BrowserTransferModel {
|
||||
return;
|
||||
}
|
||||
|
||||
if (OperationMode.isInShutdown()) {
|
||||
if (AppOperationMode.isInShutdown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ package io.xpipe.app.browser.icon;
|
||||
|
||||
import io.xpipe.app.core.AppResources;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.core.FileKind;
|
||||
|
||||
import lombok.Getter;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -23,11 +21,6 @@ public abstract class BrowserIconDirectoryType {
|
||||
public static synchronized void loadDefinitions() {
|
||||
ALL.add(new BrowserIconDirectoryType() {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "root";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(FileEntry entry) {
|
||||
return entry.getPath().toString().equals("/")
|
||||
@@ -35,7 +28,7 @@ public abstract class BrowserIconDirectoryType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(FileEntry entry) {
|
||||
public String getIcon() {
|
||||
return "browser/default_root_folder.svg";
|
||||
}
|
||||
});
|
||||
@@ -46,7 +39,6 @@ public abstract class BrowserIconDirectoryType {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
var split = line.split("\\|");
|
||||
var id = split[0].strip();
|
||||
var filter = Arrays.stream(split[1].split(","))
|
||||
.map(s -> {
|
||||
return s.strip();
|
||||
@@ -56,7 +48,7 @@ public abstract class BrowserIconDirectoryType {
|
||||
var closedIcon = "browser/" + split[2].strip();
|
||||
var lightClosedIcon = split.length > 4 ? "browser/" + split[4].strip() : closedIcon;
|
||||
|
||||
ALL.add(new Simple(id, new BrowserIconVariant(lightClosedIcon, closedIcon), filter));
|
||||
ALL.add(new Simple(new BrowserIconVariant(lightClosedIcon, closedIcon), filter));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -66,22 +58,16 @@ public abstract class BrowserIconDirectoryType {
|
||||
return ALL;
|
||||
}
|
||||
|
||||
public abstract String getId();
|
||||
|
||||
public abstract boolean matches(FileEntry entry);
|
||||
|
||||
public abstract String getIcon(FileEntry entry);
|
||||
public abstract String getIcon();
|
||||
|
||||
public static class Simple extends BrowserIconDirectoryType {
|
||||
|
||||
@Getter
|
||||
private final String id;
|
||||
|
||||
private final BrowserIconVariant closed;
|
||||
private final Set<String> names;
|
||||
|
||||
public Simple(String id, BrowserIconVariant closed, Set<String> names) {
|
||||
this.id = id;
|
||||
public Simple(BrowserIconVariant closed, Set<String> names) {
|
||||
this.closed = closed;
|
||||
this.names = names;
|
||||
}
|
||||
@@ -97,7 +83,7 @@ public abstract class BrowserIconDirectoryType {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(FileEntry entry) {
|
||||
public String getIcon() {
|
||||
return this.closed.getIcon();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package io.xpipe.app.browser.icon;
|
||||
|
||||
import io.xpipe.app.core.AppResources;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.xpipe.app.browser.icon;
|
||||
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
|
||||
public class BrowserIconManager {
|
||||
|
||||
@@ -30,7 +30,7 @@ public class BrowserIconManager {
|
||||
} else {
|
||||
for (var f : BrowserIconDirectoryType.getAll()) {
|
||||
if (f.matches(r)) {
|
||||
return f.getIcon(r);
|
||||
return f.getIcon();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-6
@@ -1,22 +1,27 @@
|
||||
package io.xpipe.app.browser.menu;
|
||||
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
public interface BrowserApplicationPathMenuProvider extends BrowserMenuItemProvider {
|
||||
|
||||
String getExecutable();
|
||||
|
||||
@Override
|
||||
default void init(BrowserFileSystemTabModel model) {
|
||||
default void init(BrowserFileSystemTabModel model) throws Exception {
|
||||
if (model.getFileSystem().getShell().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache result for later calls
|
||||
model.getCache().isApplicationInPath(getExecutable());
|
||||
model.getFileSystem().getShell().get().view().isInPath(getExecutable(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getCache().isApplicationInPath(getExecutable());
|
||||
@SneakyThrows
|
||||
default boolean isActive(BrowserFileSystemTabModel model) {
|
||||
// This will always return without an exception as it is cached
|
||||
return model.getFileSystem().getShell().orElseThrow().view().isInPath(getExecutable(), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ public interface BrowserMenuBranchProvider extends BrowserMenuItemProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
var graphic = getIcon(model, selected);
|
||||
var graphic = getIcon();
|
||||
if (graphic != null) {
|
||||
m.setGraphic(graphic.createGraphicNode());
|
||||
}
|
||||
m.setDisable(!isActive(model, selected));
|
||||
m.setDisable(!isActive(model));
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package io.xpipe.app.browser.menu;
|
||||
import io.xpipe.app.action.ActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.MenuItem;
|
||||
@@ -30,7 +30,7 @@ public interface BrowserMenuItemProvider extends ActionProvider {
|
||||
: selected;
|
||||
}
|
||||
|
||||
default LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
default LabelGraphic getIcon() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public interface BrowserMenuItemProvider extends ActionProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
default boolean isActive(BrowserFileSystemTabModel model) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {
|
||||
});
|
||||
var name = getName(model, selected);
|
||||
Tooltip.install(b, TooltipHelper.create(name, getShortcut()));
|
||||
var graphic = getIcon(model, selected);
|
||||
var graphic = getIcon();
|
||||
if (graphic != null) {
|
||||
b.setGraphic(graphic.createGraphicNode());
|
||||
}
|
||||
@@ -88,9 +88,9 @@ public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {
|
||||
}
|
||||
});
|
||||
|
||||
b.setDisable(!isActive(model, selected));
|
||||
b.setDisable(!isActive(model));
|
||||
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
|
||||
b.setDisable(!isActive(model, selected));
|
||||
b.setDisable(!isActive(model));
|
||||
});
|
||||
|
||||
return b;
|
||||
@@ -111,12 +111,12 @@ public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {
|
||||
if (getShortcut() != null) {
|
||||
mi.setAccelerator(getShortcut());
|
||||
}
|
||||
var graphic = getIcon(model, selected);
|
||||
var graphic = getIcon();
|
||||
if (graphic != null) {
|
||||
mi.setGraphic(graphic.createGraphicNode());
|
||||
}
|
||||
mi.setMnemonicParsing(false);
|
||||
mi.setDisable(!isActive(model, selected));
|
||||
mi.setDisable(!isActive(model));
|
||||
|
||||
return mi;
|
||||
}
|
||||
|
||||
@@ -22,11 +22,13 @@ public class BrowserMenuProviders {
|
||||
BrowserMenuItemProvider browserAction, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return browserAction instanceof BrowserMenuLeafProvider
|
||||
? List.of((BrowserMenuLeafProvider) browserAction)
|
||||
: ((BrowserMenuBranchProvider) browserAction)
|
||||
.getBranchingActions(model, entries).stream()
|
||||
.map(action -> getFlattened(action, model, entries))
|
||||
.flatMap(List::stream)
|
||||
.toList();
|
||||
: browserAction.isApplicable(model, entries)
|
||||
? ((BrowserMenuBranchProvider) browserAction)
|
||||
.getBranchingActions(model, entries).stream()
|
||||
.map(action -> getFlattened(action, model, entries))
|
||||
.flatMap(List::stream)
|
||||
.toList()
|
||||
: List.of();
|
||||
}
|
||||
|
||||
public static BrowserMenuLeafProvider byId(String id, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
|
||||
@@ -4,14 +4,14 @@ import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.browser.icon.BrowserIconFileType;
|
||||
import io.xpipe.app.browser.icon.BrowserIcons;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface FileTypeMenuProvider extends BrowserMenuItemProvider {
|
||||
|
||||
@Override
|
||||
default LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
default LabelGraphic getIcon() {
|
||||
return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(getType()));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -19,7 +20,9 @@ public class BackMenuProvider implements BrowserMenuLeafProvider {
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
model.backSync(1);
|
||||
BooleanScope.executeExclusive(model.getBusy(), () -> {
|
||||
model.backSync(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +36,7 @@ public class BackMenuProvider implements BrowserMenuLeafProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2a-arrow-left");
|
||||
}
|
||||
|
||||
@@ -48,7 +51,7 @@ public class BackMenuProvider implements BrowserMenuLeafProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public boolean isActive(BrowserFileSystemTabModel model) {
|
||||
return model.getHistory().canGoBackProperty().get();
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -7,6 +7,7 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuCategory;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -27,7 +28,7 @@ public class BrowseInNativeManagerMenuProvider implements BrowserMenuLeafProvide
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return switch (OsType.getLocal()) {
|
||||
return switch (OsType.ofLocal()) {
|
||||
case OsType.Windows ignored -> AppI18n.observable("browseInWindowsExplorer");
|
||||
case OsType.Linux ignored -> AppI18n.observable("browseInDefaultFileManager");
|
||||
case OsType.MacOs ignored -> AppI18n.observable("browseInFinder");
|
||||
@@ -38,4 +39,9 @@ public class BrowseInNativeManagerMenuProvider implements BrowserMenuLeafProvide
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2f-folder-eye-outline");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,22 +10,28 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.OsType;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.TextField;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@SneakyThrows
|
||||
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
|
||||
if (model.getFileSystem().getShell().isEmpty()) {
|
||||
return List.of(new CustomProvider(recursive));
|
||||
}
|
||||
|
||||
List<BrowserMenuItemProvider> actions = Stream.<BrowserMenuItemProvider>concat(
|
||||
model.getCache().getGroups().entrySet().stream()
|
||||
model.getFileSystem().getShell().get().view().getGroupFile().getGroups().entrySet().stream()
|
||||
.filter(e -> !e.getValue().equals("nohome")
|
||||
&& !e.getValue().equals("nogroup")
|
||||
&& !e.getValue().equals("nobody")
|
||||
@@ -38,7 +44,7 @@ public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2a-account-group-outline");
|
||||
}
|
||||
|
||||
@@ -54,8 +60,7 @@ public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
|
||||
return os != OsType.WINDOWS && os != OsType.MACOS;
|
||||
return model.getFileSystem().supportsChgrp();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -72,7 +77,7 @@ public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
|
||||
private static class FlatProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2f-file-outline");
|
||||
}
|
||||
|
||||
@@ -91,7 +96,7 @@ public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
|
||||
private static class RecursiveProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2f-file-tree");
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,8 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.OsType;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -22,7 +21,7 @@ import java.util.List;
|
||||
|
||||
public class ChmodMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
|
||||
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel ignored, boolean recursive) {
|
||||
var custom = new CustomProvider(recursive);
|
||||
return List.of(
|
||||
new FixedProvider("400", recursive),
|
||||
@@ -37,7 +36,7 @@ public class ChmodMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2w-wrench-outline");
|
||||
}
|
||||
|
||||
@@ -53,7 +52,7 @@ public class ChmodMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;
|
||||
return model.getFileSystem().supportsChmod();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,7 +69,7 @@ public class ChmodMenuProvider implements BrowserMenuBranchProvider {
|
||||
private static class FlatProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2f-file-outline");
|
||||
}
|
||||
|
||||
@@ -89,7 +88,7 @@ public class ChmodMenuProvider implements BrowserMenuBranchProvider {
|
||||
private static class RecursiveProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2f-file-tree");
|
||||
}
|
||||
|
||||
|
||||
@@ -10,22 +10,28 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.OsType;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.TextField;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ChownMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@SneakyThrows
|
||||
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
|
||||
if (model.getFileSystem().getShell().isEmpty()) {
|
||||
return List.of(new CustomProvider(recursive));
|
||||
}
|
||||
|
||||
var actions = Stream.<BrowserMenuItemProvider>concat(
|
||||
model.getCache().getUsers().entrySet().stream()
|
||||
model.getFileSystem().getShell().get().view().getPasswdFile().getUsers().entrySet().stream()
|
||||
.filter(e -> !e.getValue().equals("nohome")
|
||||
&& !e.getValue().equals("nobody")
|
||||
&& (e.getKey().equals(0) || e.getKey() >= 900))
|
||||
@@ -37,7 +43,7 @@ public class ChownMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2a-account-edit");
|
||||
}
|
||||
|
||||
@@ -53,8 +59,7 @@ public class ChownMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
|
||||
return os != OsType.WINDOWS && os != OsType.MACOS;
|
||||
return model.getFileSystem().supportsChown();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,7 +76,7 @@ public class ChownMenuProvider implements BrowserMenuBranchProvider {
|
||||
private static class FlatProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2f-file-outline");
|
||||
}
|
||||
|
||||
@@ -90,7 +95,7 @@ public class ChownMenuProvider implements BrowserMenuBranchProvider {
|
||||
private static class RecursiveProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2f-file-tree");
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -7,8 +7,8 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuCategory;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvide
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2f-format-list-text");
|
||||
}
|
||||
|
||||
@@ -44,8 +44,9 @@ public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvide
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
.allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
|
||||
return model.getFileSystem().supportsDirectorySizes()
|
||||
&& entries.stream()
|
||||
.allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,7 +6,7 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuCategory;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -23,7 +23,7 @@ public class CopyMenuProvider implements BrowserMenuLeafProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdoal-file_copy");
|
||||
}
|
||||
|
||||
@@ -41,9 +41,4 @@ public class CopyMenuProvider implements BrowserMenuLeafProvider {
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("copy");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import io.xpipe.app.browser.menu.BrowserMenuBranchProvider;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuCategory;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.ClipboardHelper;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.app.ext.FileKind;
|
||||
import io.xpipe.app.platform.ClipboardHelper;
|
||||
import io.xpipe.app.platform.LabelGraphic;
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -35,7 +35,7 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
public LabelGraphic getIcon() {
|
||||
return new LabelGraphic.IconGraphic("mdi2c-content-copy");
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user