diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8662917d..565d44f1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index 068c2f6fa..d1b4709bd 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 1c6330904..535a042d9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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") } diff --git a/app/src/main/java/io/xpipe/app/Main.java b/app/src/main/java/io/xpipe/app/Main.java index 0943ff784..467d56b2a 100644 --- a/app/src/main/java/io/xpipe/app/Main.java +++ b/app/src/main/java/io/xpipe/app/Main.java @@ -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); } } diff --git a/app/src/main/java/io/xpipe/app/action/AbstractAction.java b/app/src/main/java/io/xpipe/app/action/AbstractAction.java index 768150d11..587df2080 100644 --- a/app/src/main/java/io/xpipe/app/action/AbstractAction.java +++ b/app/src/main/java/io/xpipe/app/action/AbstractAction.java @@ -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(); + } }; } diff --git a/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java b/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java index 4057e905f..54cfa3a51 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java @@ -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) 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())); } }); diff --git a/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java b/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java index 3a73e589c..2245028f3 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/action/ActionPickComp.java b/app/src/main/java/io/xpipe/app/action/ActionPickComp.java index c602553b5..e13bdc3d3 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionPickComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionPickComp.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java b/app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java index 70d65ebab..97d70731d 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java index 5bea468a3..c5be488e2 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java index aaa0284d4..ccb261dfa 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java @@ -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 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); } } diff --git a/app/src/main/java/io/xpipe/app/beacon/BlobManager.java b/app/src/main/java/io/xpipe/app/beacon/BlobManager.java index 571ec7e8d..fa0bb4a02 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BlobManager.java +++ b/app/src/main/java/io/xpipe/app/beacon/BlobManager.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java index 1dc7afdb7..1f88f8a66 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java index 15b28154f..01459cfde 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java @@ -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(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java index 1d83efda1..4fabec9c8 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java @@ -1,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; diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java index 02ea966c2..ff86617e4 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java @@ -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(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java index 3c8d0b17b..341354f54 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java @@ -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(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java index 787347c29..207421e52 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java @@ -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()); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java index f001dd18e..74854d1d3 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java @@ -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(); diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java index 40cfe5c29..ef5ffa7de 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java @@ -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(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java index 92b7b9dd4..eb46b4edf 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java index 0284be1bb..629ea5370 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java @@ -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(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java b/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java index 2f5c77ea7..8b225909d 100644 --- a/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java @@ -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()) diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/HttpStreamableServerTransportProvider.java b/app/src/main/java/io/xpipe/app/beacon/mcp/HttpStreamableServerTransportProvider.java index 0129dbe51..e20b759d2 100644 --- a/app/src/main/java/io/xpipe/app/beacon/mcp/HttpStreamableServerTransportProvider.java +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/HttpStreamableServerTransportProvider.java @@ -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 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 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 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 unmarshalFrom(Object data, TypeRef typeRef) { + return jsonMapper.convertValue(data, typeRef); + } + @Override public Mono 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 sendMessage(McpSchema.JSONRPCMessage message) { return sendMessage(message, null); } - - @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return objectMapper.convertValue(data, typeRef); - } } } diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java b/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java index 651bc6d39..1e3f8e792 100644 --- a/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java @@ -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(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java index 90e3ae960..d33950d62 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java @@ -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); } }); }; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java index 1c98f04f6..cdc8bd742 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java @@ -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 store, + FailableFunction, FileSystem, Exception> customFileSystemFactory, FailableFunction 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) { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java index e25145bdd..ceaa21c53 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java @@ -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 -> { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java index b7b850470..00b051c6d 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java @@ -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 ref.getStore().createFileSystem()); try { DEFAULT.openSync(tab, null); DEFAULT.pinTab(tab); @@ -150,7 +155,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel { globalPinnedTab.setValue(null); }); @@ -170,7 +175,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel { - openFileSystemAsync(entry.ref(), model -> e.getPath(), busy); + openFileSystemAsync(entry.ref(), null, model -> e.getPath(), busy); }); } @@ -202,6 +207,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel store, + FailableFunction, FileSystem, Exception> customFileSystemFactory, FailableFunction path, BooleanProperty externalBusy) { if (store == null) { @@ -209,12 +215,13 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel { - openFileSystemSync(store, path, externalBusy, true); + openFileSystemSync(store, customFileSystemFactory, path, externalBusy, true); }); } public BrowserFileSystemTabModel openFileSystemSync( DataStoreEntryRef store, + FailableFunction, FileSystem, Exception> customFileSystemFactory, FailableFunction path, BooleanProperty externalBusy, boolean select) @@ -223,12 +230,19 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel 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); } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java index a761d37e9..308e95699 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java @@ -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 diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java index 13a151663..1a2c66faf 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java @@ -45,6 +45,7 @@ public abstract class BrowserAction extends StoreAction { } else { model = BrowserFullSessionModel.DEFAULT.openFileSystemSync( ref.asNeeded(), + null, model -> { return getTargetDirectory(model); }, @@ -57,8 +58,8 @@ public abstract class BrowserAction extends StoreAction { 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; } diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionProvider.java index 0a3b5e732..db4930275 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionProvider.java @@ -11,8 +11,4 @@ public interface BrowserActionProvider extends ActionProvider { default boolean isApplicable(BrowserFileSystemTabModel model, List entries) { return true; } - - default boolean isActive(BrowserFileSystemTabModel model, List entries) { - return true; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java index f16e9899a..13012e67f 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java @@ -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 toDisplayMap() { var map = new LinkedHashMap(); 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; } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java index 684f1b8cd..59b685db9 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java @@ -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 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()); } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java index 93a1b580c..12bbf796d 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java @@ -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 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()); } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java index a7475aef6..31a3cf827 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java @@ -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 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()); } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java index 0cf7e5128..4b0901a3a 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java @@ -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 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()); } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java index 257b5038a..019247b1a 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java @@ -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 entries) { + return model.getFileSystem().supportsDirectorySizes(); + } + @Jacksonized @SuperBuilder public static class Action extends BrowserAction { diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java index f95053bca..27ffffb6f 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java index 89b2b5409..791fade85 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java index 2d60fbe2b..cf7bc834b 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java index 8be25a34b..622fa01cb 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java index 7db1195c4..ef86d60ed 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java index 73920dfe5..5dc716703 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java @@ -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 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( diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeManagerActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeManagerActionProvider.java new file mode 100644 index 000000000..b59cdb2b0 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeManagerActionProvider.java @@ -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 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()); + } + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java index 8f05e42a8..b2a6cd233 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java @@ -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 entries) { - return OsType.getLocal() == OsType.WINDOWS + return OsType.ofLocal() == OsType.WINDOWS && entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java deleted file mode 100644 index 34967c1a7..000000000 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java +++ /dev/null @@ -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 entries) { - return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); - } - - @Override - public boolean isActive(BrowserFileSystemTabModel model, List 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); - } - } - } -} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java index 43777cd13..085d61fbf 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java @@ -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 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())); } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java index 8058d769f..1837684f3 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java @@ -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 entries) { + return model.getFileSystem().getShell().isPresent(); + } + @Jacksonized @SuperBuilder public static class Action extends BrowserAction { diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java index 7e4057424..74ca84e52 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java @@ -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 entries) { + return model.getFileSystem().getShell().isPresent(); + } + @Jacksonized @SuperBuilder public static class Action extends BrowserAction { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserBreadcrumbBar.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserBreadcrumbBar.java index afed4d173..c95e9d8d2 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserBreadcrumbBar.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserBreadcrumbBar.java @@ -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, ButtonBase> crumbFactory = crumb -> { - var name = crumb.getValue().equals("/") + Callback, 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, ButtonBase> crumbFactory, - Callback, ? extends Node> dividerFactory) { + private void onDragEntered(Button button, FilePath path) { + button.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true); - var breadcrumbs = new Breadcrumbs(); + 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, ButtonBase> crumbFactory, + Callback, ? extends Node> dividerFactory) { + + var breadcrumbs = new Breadcrumbs(); 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 items = - Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new)); + var elements = createBreadcrumbHierarchy(val); + Breadcrumbs.BreadCrumbItem items = + Breadcrumbs.buildTreeModel(elements.toArray(FilePath[]::new)); breadcrumbs.setSelectedCrumb(items); }); }); @@ -94,19 +122,24 @@ public class BrowserBreadcrumbBar extends SimpleComp { return breadcrumbs; } - private List createBreadcumbHierarchy(FilePath filePath) { - var f = filePath.toString() + "/"; - var list = new ArrayList(); + private List createBreadcrumbHierarchy(FilePath filePath) { + var f = filePath.toDirectory().toString(); + var list = new ArrayList(); 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; } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java index a313e38da..6c503215e 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java index d0b09206e..6373bd159 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java @@ -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.*; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java index f5b65673d..d8505ea68 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java index 9241a809e..19c18a25e 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserDialogs.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserDialogs.java index 814a7091b..2689c67f2 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserDialogs.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserDialogs.java @@ -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(); var key = multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent"; var w = multiple ? 1050 : 400; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java index 092e2f6de..c580cf429 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java @@ -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" diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileDuplicates.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileDuplicates.java new file mode 100644 index 000000000..72cb73b15 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileDuplicates.java @@ -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() : "")); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileInput.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileInput.java new file mode 100644 index 000000000..4f8c54803 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileInput.java @@ -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; +} diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java index ec1acdf9e..03663e345 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java @@ -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 modeCol, TableColumn ownerCol, TableColumn 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 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) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java index 011f51a5b..1e467854b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java @@ -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); - } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListFilterComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListFilterComp.java index 282edcdfb..1a4e169b5 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListFilterComp.java @@ -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 { if (!newValue && filterString.getValue() == null) { if (button.isFocused()) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java index 9ac8be561..38c030fa1 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java @@ -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 { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java index 699211d36..c67ea7c1f 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java index 36ccd0d64..b559aeb41 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java @@ -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 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 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 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); } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOutput.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOutput.java index 0202edd19..0d44408b2 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOutput.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOutput.java @@ -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 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() { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java index 2426b98cd..8b4102045 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java @@ -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; }); }; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSelectionListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSelectionListComp.java index a47efd4b2..4dfb88690 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSelectionListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSelectionListComp.java @@ -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 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); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemCache.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemCache.java deleted file mode 100644 index bbd41b4bc..000000000 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemCache.java +++ /dev/null @@ -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 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 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"); - } -} diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java index 0a8cefa44..c570c8d4b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java index 3cc2395c6..38f9b4983 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java @@ -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) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java index 1fd561521..31e564f6a 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java @@ -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 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 terminalRequests = FXCollections.observableArrayList(); private final BooleanProperty transferCancelled = new SimpleBooleanProperty(); private final Property progress = new SimpleObjectProperty<>(); @@ -56,21 +61,18 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab progressRemaining = new SimpleObjectProperty<>(); + private final FailableFunction, FileSystem, Exception> fileSystemFactory; private FileSystem fileSystem; private BrowserFileSystemSavedState savedState; - private BrowserFileSystemCache cache; public BrowserFileSystemTabModel( BrowserAbstractSessionModel model, DataStoreEntryRef entry, - SelectionMode selectionMode) { + SelectionMode selectionMode, + FailableFunction, 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 { - 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 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 { + 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(); + BrowserFileSystemHelper.validateDirectoryPath(target.getFileSystem(), target.getPath(), true); + + var flatFiles = new LinkedHashMap(); // 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(); @@ -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) { + } } } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserGreetingComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserGreetingComp.java index cae3eeefc..57a99de0b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserGreetingComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserGreetingComp.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabComp.java index 4ab7cd5bd..d76c46098 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistoryTabComp.java @@ -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); } }); }) diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserLocalFileSystem.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserLocalFileSystem.java index 6b4976f7b..08e157e63 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserLocalFileSystem.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserLocalFileSystem.java @@ -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(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java index ed6110eb2..8f3271b0d 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java index 91b64b6d2..558a87b84 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java @@ -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>(); + + 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.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.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; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessButtonComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessButtonComp.java index d8382b5aa..0fd9f9dba 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessButtonComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessButtonComp.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java index 8a1a74e7a..1061f157b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java index b687f126e..58d9781f9 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java @@ -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) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java index 66658cfb1..5bf626248 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java @@ -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(); } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java index f93674429..d36485f4d 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java index 28cdcac30..c00044e22 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java @@ -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 names; - public Simple(String id, BrowserIconVariant closed, Set names) { - this.id = id; + public Simple(BrowserIconVariant closed, Set 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(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java index 9ed544783..21ffa9073 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconManager.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconManager.java index 98b54061f..1509c47e6 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconManager.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconManager.java @@ -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(); } } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserApplicationPathMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserApplicationPathMenuProvider.java index 7d53c6076..652b5fa0e 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/BrowserApplicationPathMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserApplicationPathMenuProvider.java @@ -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 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); } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java index 757cffda0..a5f9fe5a3 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuItemProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuItemProvider.java index 50c2e7a8f..43d7bba43 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuItemProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuItemProvider.java @@ -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 entries) { + default LabelGraphic getIcon() { return null; } @@ -52,7 +52,7 @@ public interface BrowserMenuItemProvider extends ActionProvider { return true; } - default boolean isActive(BrowserFileSystemTabModel model, List entries) { + default boolean isActive(BrowserFileSystemTabModel model) { return true; } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java index 588e56b67..bf1401dd9 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuProviders.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuProviders.java index c97fa544c..abbd34edb 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuProviders.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuProviders.java @@ -22,11 +22,13 @@ public class BrowserMenuProviders { BrowserMenuItemProvider browserAction, BrowserFileSystemTabModel model, List 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 entries) { diff --git a/app/src/main/java/io/xpipe/app/browser/menu/FileTypeMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/FileTypeMenuProvider.java index 489df88b1..3fc40f03c 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/FileTypeMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/FileTypeMenuProvider.java @@ -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 entries) { + default LabelGraphic getIcon() { return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(getType())); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java index ee3575794..0cde8496c 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java @@ -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 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 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 entries) { + public boolean isActive(BrowserFileSystemTabModel model) { return model.getHistory().canGoBackProperty().get(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java index da5af2645..9921cc458 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java @@ -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 getName(BrowserFileSystemTabModel model, List 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"); + } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java index a80de6791..cc1b45b8c 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java @@ -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 getLeafActions(BrowserFileSystemTabModel model, boolean recursive) { + if (model.getFileSystem().getShell().isEmpty()) { + return List.of(new CustomProvider(recursive)); + } + List actions = Stream.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 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 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 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 entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2f-file-tree"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java index 3b08f98c1..a663b6642 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java @@ -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 getLeafActions(BrowserFileSystemTabModel model, boolean recursive) { + private static List 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 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 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 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 entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2f-file-tree"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java index 20d08941d..d13cda973 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java @@ -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 getLeafActions(BrowserFileSystemTabModel model, boolean recursive) { + if (model.getFileSystem().getShell().isEmpty()) { + return List.of(new CustomProvider(recursive)); + } + var actions = Stream.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 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 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 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 entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2f-file-tree"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java index a9bbbe712..68ef93f9c 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java @@ -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 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 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 diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java index 050c5639c..c356f63a9 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java @@ -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 entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdoal-file_copy"); } @@ -41,9 +41,4 @@ public class CopyMenuProvider implements BrowserMenuLeafProvider { public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { return AppI18n.observable("copy"); } - - @Override - public boolean acceptsEmptySelection() { - return true; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java index 784280eb9..da6546531 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java @@ -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 entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2c-content-copy"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java index ed8281c29..d053a8656 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java @@ -1,6 +1,5 @@ package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.action.AbstractAction; import io.xpipe.app.browser.action.BrowserActionProvider; import io.xpipe.app.browser.action.impl.DeleteActionProvider; import io.xpipe.app.browser.file.BrowserEntry; @@ -8,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; import javafx.scene.input.KeyCode; @@ -31,7 +30,7 @@ public class DeleteMenuProvider implements BrowserMenuLeafProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2d-delete"); } @@ -50,8 +49,8 @@ public class DeleteMenuProvider implements BrowserMenuLeafProvider { return AppI18n.observable( "deleteFile", entries.stream() - .anyMatch(browserEntry -> - browserEntry.getRawFileEntry().getKind() == FileKind.LINK) + .anyMatch(browserEntry -> + browserEntry.getRawFileEntry().getKind() == FileKind.LINK) ? "link" : ""); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java index e791f1024..27c76bd39 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java @@ -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; @@ -41,7 +41,7 @@ public class DownloadMenuProvider implements BrowserMenuLeafProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2d-download"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java index c2fd07744..7d1bc7d8b 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java @@ -6,10 +6,10 @@ 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.ext.FileKind; +import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.app.util.LabelGraphic; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.FileKind; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; @@ -35,7 +35,7 @@ public class EditFileMenuProvider implements BrowserMenuLeafProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2p-pencil"); } @@ -57,7 +57,7 @@ public class EditFileMenuProvider implements BrowserMenuLeafProvider { } @Override - public boolean isActive(BrowserFileSystemTabModel model, List entries) { + public boolean isActive(BrowserFileSystemTabModel model) { var e = AppPrefs.get().externalEditor().getValue(); return e != null; } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java index 9c94170c5..2fbd54a1b 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java @@ -5,8 +5,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; @@ -34,7 +34,7 @@ public class FollowLinkMenuProvider implements BrowserMenuLeafProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2a-arrow-top-right-thick"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java index 9195a784c..287f6890a 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java @@ -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 ForwardMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { ThreadHelper.runAsync(() -> { - model.forthSync(1); + BooleanScope.executeExclusive(model.getBusy(), () -> { + model.forthSync(1); + }); }); } @@ -33,7 +36,7 @@ public class ForwardMenuProvider implements BrowserMenuLeafProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2a-arrow-right"); } @@ -48,7 +51,7 @@ public class ForwardMenuProvider implements BrowserMenuLeafProvider { } @Override - public boolean isActive(BrowserFileSystemTabModel model, List entries) { + public boolean isActive(BrowserFileSystemTabModel model) { return model.getHistory().canGoForthProperty().get(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java index 77465c75a..2e9d831d0 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java @@ -12,11 +12,10 @@ 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.app.util.OptionsBuilder; -import io.xpipe.core.FileKind; +import io.xpipe.app.ext.FileKind; +import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.app.platform.OptionsBuilder; import io.xpipe.core.FilePath; -import io.xpipe.core.OsType; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; @@ -27,7 +26,7 @@ import java.util.List; public class NewItemMenuProvider implements BrowserMenuBranchProvider { @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2p-plus-box-outline"); } @@ -87,7 +86,7 @@ public class NewItemMenuProvider implements BrowserMenuBranchProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultFileIcon()); } @@ -134,7 +133,7 @@ public class NewItemMenuProvider implements BrowserMenuBranchProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultDirectoryIcon()); } @@ -150,7 +149,7 @@ public class NewItemMenuProvider implements BrowserMenuBranchProvider { var linkName = new SimpleStringProperty(); var target = new SimpleStringProperty(); var modal = ModalOverlay.of( - "base.newLink", + "newLink", new OptionsBuilder() .name("linkName") .addString(linkName) @@ -188,11 +187,11 @@ public class NewItemMenuProvider implements BrowserMenuBranchProvider { @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; + return model.getFileSystem().supportsLinkCreation(); } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultFileIcon()); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java index 7a44200f9..f26b838d1 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java @@ -6,8 +6,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; import javafx.scene.input.KeyCode; @@ -22,7 +22,10 @@ public class OpenDirectoryInNewTabMenuProvider implements BrowserMenuLeafProvide public void execute(BrowserFileSystemTabModel model, List entries) { if (model.getBrowserModel() instanceof BrowserFullSessionModel bm) { bm.openFileSystemAsync( - model.getEntry(), m -> entries.getFirst().getRawFileEntry().getPath(), null); + model.getEntry(), + null, + m -> entries.getFirst().getRawFileEntry().getPath(), + null); } } @@ -34,7 +37,7 @@ public class OpenDirectoryInNewTabMenuProvider implements BrowserMenuLeafProvide } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2f-folder-open-outline"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryMenuProvider.java index 39504cf13..d3fbffa07 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryMenuProvider.java @@ -7,7 +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.util.LabelGraphic; +import io.xpipe.app.platform.LabelGraphic; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; @@ -24,7 +24,7 @@ public class OpenDirectoryMenuProvider implements BrowserMenuLeafProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2f-folder-open"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java index bd3c64087..5dcca14a2 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java @@ -7,7 +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.util.LabelGraphic; +import io.xpipe.app.platform.LabelGraphic; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; @@ -24,7 +24,7 @@ public class OpenFileDefaultMenuProvider implements BrowserMenuLeafProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2b-book-open-variant"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java index dc2bba783..df84b83c5 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java @@ -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 io.xpipe.core.OsType; import javafx.beans.value.ObservableValue; @@ -27,13 +27,13 @@ public class OpenFileWithMenuProvider implements BrowserMenuLeafProvider { @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return OsType.getLocal() == OsType.WINDOWS + return OsType.ofLocal() == OsType.WINDOWS && entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2b-book-open-page-variant-outline"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java index 0877bb038..b4bb219e0 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java @@ -7,7 +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.util.LabelGraphic; +import io.xpipe.app.platform.LabelGraphic; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; @@ -44,7 +44,7 @@ public class OpenNativeFileDetailsMenuProvider implements BrowserMenuLeafProvide } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2f-folder-information-outline"); } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalInDirectoryMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalInDirectoryMenuProvider.java index fbdbcbf90..5a6e13cfe 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalInDirectoryMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalInDirectoryMenuProvider.java @@ -1,15 +1,13 @@ package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserActionProvider; -import io.xpipe.app.browser.action.impl.OpenTerminalActionProvider; import io.xpipe.app.browser.file.BrowserEntry; 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.ext.FileKind; +import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.app.util.LabelGraphic; -import io.xpipe.core.FileKind; import io.xpipe.core.FilePath; import javafx.beans.value.ObservableValue; @@ -32,16 +30,13 @@ public class OpenTerminalInDirectoryMenuProvider implements BrowserMenuLeafProvi ? List.of(model.getCurrentDirectory().getPath()) : Collections.singletonList((FilePath) null); for (var dir : dirs) { - var name = (dir != null ? dir + " - " : "") + model.getName().getValue(); - model.openTerminalAsync(name, dir, model.getFileSystem().getShell().orElseThrow(), dirs.size() == 1); + var name = (model.getFileSystem().supportsTerminalWorkingDirectory() && dir != null ? dir + " - " : "") + + model.getName().getValue(); + model.openTerminalAsync( + name, dir, model.getFileSystem().getRawShellControl().orElseThrow(), dirs.size() == 1); } } - @Override - public Class getDelegateActionProvider() { - return OpenTerminalActionProvider.class; - } - @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); @@ -52,7 +47,7 @@ public class OpenTerminalInDirectoryMenuProvider implements BrowserMenuLeafProvi } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2c-console"); } @@ -72,7 +67,7 @@ public class OpenTerminalInDirectoryMenuProvider implements BrowserMenuLeafProvi } @Override - public boolean isActive(BrowserFileSystemTabModel model, List entries) { + public boolean isActive(BrowserFileSystemTabModel model) { var t = AppPrefs.get().terminalType().getValue(); return t != null; } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java index 854551bff..b8ad48c64 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java @@ -7,8 +7,8 @@ import io.xpipe.app.browser.file.BrowserFileTransferMode; 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; import javafx.scene.input.KeyCode; @@ -34,16 +34,28 @@ public class PasteMenuProvider implements BrowserMenuLeafProvider { return; } - model.dropFilesIntoAsync( - target, - files.stream() - .map(browserEntry -> browserEntry.getRawFileEntry()) - .toList(), - BrowserFileTransferMode.COPY); + var isDuplication = files.size() == 1 + && target.getPath() + .equals(files.getFirst().getRawFileEntry().getPath().getParent()); + if (isDuplication) { + model.duplicateFile(files.getFirst().getRawFileEntry()); + } else { + model.dropFilesIntoAsync( + target, + files.stream() + .map(browserEntry -> browserEntry.getRawFileEntry()) + .toList(), + BrowserFileTransferMode.COPY); + } } @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + var clipboard = BrowserClipboard.retrieveCopy(); + if (clipboard == null) { + return false; + } + return (entries.size() == 1 && entries.stream() .allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) @@ -51,7 +63,7 @@ public class PasteMenuProvider implements BrowserMenuLeafProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2c-content-paste"); } @@ -76,7 +88,7 @@ public class PasteMenuProvider implements BrowserMenuLeafProvider { } @Override - public boolean isActive(BrowserFileSystemTabModel model, List entries) { + public boolean isActive(BrowserFileSystemTabModel model) { return BrowserClipboard.retrieveCopy() != null; } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java index 0fb176217..b3b1ddd33 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java @@ -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 RefreshDirectoryMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { ThreadHelper.runAsync(() -> { - model.refreshSync(); + BooleanScope.executeExclusive(model.getBusy(), () -> { + model.refreshSync(); + }); }); } @@ -33,7 +36,7 @@ public class RefreshDirectoryMenuProvider implements BrowserMenuLeafProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdmz-refresh"); } @@ -48,7 +51,7 @@ public class RefreshDirectoryMenuProvider implements BrowserMenuLeafProvider { } @Override - public boolean isActive(BrowserFileSystemTabModel model, List entries) { + public boolean isActive(BrowserFileSystemTabModel model) { return !model.getInOverview().get(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java index 4b1b43964..afb458fda 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java @@ -5,8 +5,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; import javafx.scene.input.KeyCode; @@ -33,7 +33,7 @@ public class RenameMenuProvider implements BrowserMenuLeafProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2r-rename-box"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java index 98efc84a3..b3cff50d6 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java @@ -6,10 +6,10 @@ import io.xpipe.app.browser.menu.BrowserMenuCategory; import io.xpipe.app.browser.menu.MultiExecuteMenuProvider; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.FileEntry; +import io.xpipe.app.ext.FileKind; +import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.process.CommandBuilder; import io.xpipe.app.process.ShellDialects; -import io.xpipe.app.util.LabelGraphic; -import io.xpipe.core.FileKind; import io.xpipe.core.OsType; import javafx.beans.value.ObservableValue; @@ -53,7 +53,7 @@ public class RunFileMenuProvider extends MultiExecuteMenuProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2p-play"); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java index 7e77d08b5..4fe616439 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java @@ -9,8 +9,8 @@ import io.xpipe.app.browser.menu.BrowserApplicationPathMenuProvider; 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.app.process.OsFileSystem; -import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.FilePath; import javafx.beans.value.ObservableValue; @@ -28,7 +28,7 @@ public class BaseUntarMenuProvider implements BrowserApplicationPathMenuProvider } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip"))); } @@ -49,6 +49,10 @@ public class BaseUntarMenuProvider implements BrowserApplicationPathMenuProvider @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + if (model.getFileSystem().getShell().isEmpty()) { + return false; + } + if (gz) { return entries.stream() .allMatch(entry -> entry.getRawFileEntry() diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java index 31e01bdb4..0729fa726 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java @@ -9,8 +9,8 @@ import io.xpipe.app.browser.menu.BrowserApplicationPathMenuProvider; 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.app.process.OsFileSystem; -import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.OsType; import javafx.beans.value.ObservableValue; @@ -26,7 +26,7 @@ public abstract class BaseUnzipUnixMenuProvider implements BrowserMenuLeafProvid } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip"))); } @@ -55,6 +55,10 @@ public abstract class BaseUnzipUnixMenuProvider implements BrowserMenuLeafProvid @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + if (model.getFileSystem().getShell().isEmpty()) { + return false; + } + return entries.stream() .allMatch(entry -> entry.getRawFileEntry().getPath().toString().endsWith(".zip")) diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java index dd25c2aad..f7b20e6f2 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java @@ -8,8 +8,8 @@ import io.xpipe.app.browser.icon.BrowserIcons; 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.app.process.OsFileSystem; -import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.OsType; import javafx.beans.value.ObservableValue; @@ -25,7 +25,7 @@ public abstract class BaseUnzipWindowsActionProvider implements BrowserMenuLeafP } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip"))); } @@ -49,6 +49,10 @@ public abstract class BaseUnzipWindowsActionProvider implements BrowserMenuLeafP @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + if (model.getFileSystem().getShell().isEmpty()) { + return false; + } + return entries.stream() .allMatch(entry -> entry.getRawFileEntry().getPath().toString().endsWith(".zip")) diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java index 615f7863c..a810b9472 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java @@ -6,34 +6,34 @@ import io.xpipe.app.browser.menu.*; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.util.CommandSupport; -import io.xpipe.app.util.LabelGraphic; -import io.xpipe.core.FileKind; +import io.xpipe.app.ext.FileKind; +import io.xpipe.app.platform.LabelGraphic; import io.xpipe.core.OsType; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import javafx.scene.control.TextField; +import lombok.SneakyThrows; + import java.util.List; public class CompressMenuProvider implements BrowserMenuBranchProvider { @Override public void init(BrowserFileSystemTabModel model) throws Exception { + if (model.getFileSystem().getShell().isEmpty()) { + return; + } + var sc = model.getFileSystem().getShell().orElseThrow(); - var foundTar = CommandSupport.findProgram(sc, "tar"); - model.getCache().getInstalledApplications().put("tar", foundTar.isPresent()); - - if (sc.getOsType() != OsType.WINDOWS) { - var found = CommandSupport.findProgram(sc, "zip"); - model.getCache().getInstalledApplications().put("zip", found.isPresent()); - } + sc.view().isInPath("tar", true); + sc.view().isInPath("zip", true); } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return new LabelGraphic.IconGraphic("mdi2a-archive"); } @@ -49,6 +49,10 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + if (model.getFileSystem().getShell().isEmpty()) { + return false; + } + var ext = List.of("zip", "tar", "tar.gz", "tgz", "rar", "xar"); if (entries.stream().anyMatch(browserEntry -> ext.stream().anyMatch(s -> browserEntry .getRawFileEntry() @@ -100,7 +104,7 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { public void execute(BrowserFileSystemTabModel model, List entries) { var name = new SimpleStringProperty(directory ? entries.getFirst().getFileName() : null); var modal = ModalOverlay.of( - "base.archiveName", + "archiveName", Comp.of(() -> { var creationName = new TextField(); creationName.textProperty().bindBidirectional(name); @@ -141,7 +145,7 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { } @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon() { return directory ? new LabelGraphic.IconGraphic("mdi2f-file-tree") : new LabelGraphic.IconGraphic("mdi2f-file-outline"); @@ -214,8 +218,9 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { } @Override - public boolean isActive(BrowserFileSystemTabModel model, List entries) { - return model.getCache().getInstalledApplications().get("tar"); + @SneakyThrows + public boolean isActive(BrowserFileSystemTabModel model) { + return model.getFileSystem().getShell().orElseThrow().view().isInPath("tar", true); } @Override diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java index 932b0bf74..143a73adc 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java @@ -33,14 +33,14 @@ public class UntarActionProvider implements BrowserActionProvider { var args = "x" + (gz ? "z" : "") + "f"; c.add(args); c.addFile(entry.getRawFileEntry().getPath()); - if (toDirectory) { - c.add("-C").addFile(target); - } if (toDirectory) { model.getFileSystem().mkdirs(target); } sc.command(c) - .withWorkingDirectory(model.getCurrentDirectory().getPath()) + .withWorkingDirectory( + toDirectory + ? target + : model.getCurrentDirectory().getPath()) .execute(); } model.refreshSync(); diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java index bbecc3593..6f07dd570 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java @@ -3,9 +3,9 @@ package io.xpipe.app.browser.menu.impl.compress; 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.ext.FileKind; import io.xpipe.app.process.CommandBuilder; import io.xpipe.app.process.ShellDialects; -import io.xpipe.core.FileKind; import io.xpipe.core.FilePath; import io.xpipe.core.OsType; diff --git a/app/src/main/java/io/xpipe/app/comp/Comp.java b/app/src/main/java/io/xpipe/app/comp/Comp.java index 89d35967d..54188b6c2 100644 --- a/app/src/main/java/io/xpipe/app/comp/Comp.java +++ b/app/src/main/java/io/xpipe/app/comp/Comp.java @@ -1,17 +1,17 @@ package io.xpipe.app.comp; import io.xpipe.app.comp.augment.Augment; -import io.xpipe.app.comp.augment.GrowAugment; import io.xpipe.app.comp.base.TooltipHelper; import io.xpipe.app.core.AppI18n; -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 javafx.application.Platform; -import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; import javafx.geometry.Orientation; +import javafx.scene.Node; import javafx.scene.control.Separator; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCombination; @@ -67,10 +67,6 @@ public abstract class Comp> { return of(() -> new Separator(Orientation.HORIZONTAL)); } - public static Comp> vseparator() { - return of(() -> new Separator(Orientation.VERTICAL)); - } - @SuppressWarnings("unchecked") public > T apply(Augment augment) { if (augments == null) { @@ -80,11 +76,11 @@ public abstract class Comp> { return (T) this; } - public Comp prefWidth(int width) { + public Comp prefWidth(double width) { return apply(struc -> struc.get().setPrefWidth(width)); } - public Comp prefHeight(int height) { + public Comp prefHeight(double height) { return apply(struc -> struc.get().setPrefHeight(height)); } @@ -116,11 +112,11 @@ public abstract class Comp> { return apply(struc -> struc.get().setMinHeight(height)); } - public Comp maxWidth(int width) { + public Comp maxWidth(double width) { return apply(struc -> struc.get().setMaxWidth(width)); } - public Comp maxHeight(int height) { + public Comp maxHeight(double height) { return apply(struc -> struc.get().setMaxHeight(height)); } @@ -156,10 +152,6 @@ public abstract class Comp> { }); } - public Comp disable(boolean o) { - return disable(new ReadOnlyBooleanWrapper(o)); - } - public Comp disable(ObservableValue o) { return apply(struc -> { var region = struc.get(); @@ -211,7 +203,63 @@ public abstract class Comp> { } public Comp grow(boolean width, boolean height) { - return apply(GrowAugment.create(width, height)); + return apply(struc -> { + struc.get().parentProperty().addListener((c, o, n) -> { + if (o instanceof Region) { + if (width) { + struc.get().prefWidthProperty().unbind(); + } + if (height) { + struc.get().prefHeightProperty().unbind(); + } + } + + bindGrow(struc.get(), n, width, height); + }); + + bindGrow(struc.get(), struc.get().getParent(), width, height); + }); + } + + private void bindGrow(Region r, Node parent, boolean width, boolean height) { + if (!(parent instanceof Region p)) { + return; + } + + if (width) { + r.prefWidthProperty() + .bind(Bindings.createDoubleBinding( + () -> { + var val = p.getWidth() + - p.getInsets().getLeft() + - p.getInsets().getRight(); + if (val <= 0) { + return Region.USE_COMPUTED_SIZE; + } + + // Floor to prevent rounding issues which cause an infinite growing + return Math.floor(val); + }, + p.widthProperty(), + p.insetsProperty())); + } + if (height) { + r.prefHeightProperty() + .bind(Bindings.createDoubleBinding( + () -> { + var val = p.getHeight() + - p.getInsets().getTop() + - p.getInsets().getBottom(); + if (val <= 0) { + return Region.USE_COMPUTED_SIZE; + } + + // Floor to prevent rounding issues which cause an infinite growing + return Math.floor(val); + }, + p.heightProperty(), + p.insetsProperty())); + } } public Comp tooltip(ObservableValue text) { diff --git a/app/src/main/java/io/xpipe/app/comp/README.md b/app/src/main/java/io/xpipe/app/comp/README.md index 4f445f51b..f599e526e 100644 --- a/app/src/main/java/io/xpipe/app/comp/README.md +++ b/app/src/main/java/io/xpipe/app/comp/README.md @@ -1,8 +1,6 @@ -# FxComps - Compound Components for JavaFX +# Compound Components -The FxComps library provides a new approach to creating JavaFX interfaces and -offers a quicker and more robust user interface development workflow. -This library is compatible and can be used with any other JavaFX library. +As a basis, JavaFX nodes are created and manage via comps (compound components). ## Principles @@ -12,11 +10,6 @@ It is advantageous to define a certain component to be a factory that can create an instances of a JavaFX Node each time it is called. By using this factory architecture, the scene contents can be rebuilt entirely by invoking the root component factory. -See the [hot reload](#Hot-Reload) section on how this can be used. - -Of course, if a component is a compound component that has children, -the parent factory has to incorporate the child factories into its creation process. -This can be done in fxcomps. #### A comp should produce a transparent representation of Regions and Controls @@ -86,44 +79,3 @@ If you for example bind your IDE Hot Reload to F4 and your Scene reload listener you can almost instantly apply any changes made to your GUI code without restarting. You can also implement a similar solution to also reload your stylesheets and translations. -## Library contents - -Aside from the base classes needed to implement the principles listed above, -this library also comes with a few very basic Comp implementations and some Augments. -These are very general implementations and can be seen as example implementations. - -#### Comps - -- [HorizontalComp](src/main/java/io/xpipe/fxcomps/comp/HorizontalComp.java) / - [VerticalComp](src/main/java/io/xpipe/fxcomps/comp/VerticalComp.java): Simple Comp implementation to create a - HBox/VBox using Comps as input -- [StackComp](src/main/java/io/xpipe/fxcomps/comp/StackComp.java): Simple Comp implementation to easily create a stack - pane using Comps as input -- [StackComp](src/main/java/io/xpipe/fxcomps/comp/LabelComp.java): Simple Comp implementation for a label - -#### Augments - -- [GrowAugment](src/main/java/io/xpipe/fxcomps/augment/GrowAugment.java): Binds the width/height of a Comp to its - parent, adjusted for parent padding -- [PopupMenuComp](src/main/java/io/xpipe/fxcomps/augment/PopupMenuAugment.java): Allows you to show a context menu when - a comp is left-clicked in addition to right-click - -## Creating a basic comp - -As the central idea of this library is that you create your own Comps, it is designed to be very simple: - -````java - var b = Comp.of(() -> new Button("Button")); - var l = Comp.of(() -> new Label("Label")); - - // Create an HBox factory and apply some Augments to it - var layoutFactory = new HorizontalComp(List.of(b, l)) - .apply(struc -> struc.get().setAlignment(Pos.CENTER)) - .apply(GrowAugment.create(true, true)) - .styleClass("layout"); - - // You can now create node instances of your layout - var region = layoutFactory.createRegion(); -```` - -Most simple Comp definitions can be defined inline with the `Comp.of(...)` method. diff --git a/app/src/main/java/io/xpipe/app/comp/augment/GrowAugment.java b/app/src/main/java/io/xpipe/app/comp/augment/GrowAugment.java deleted file mode 100644 index cafdb5349..000000000 --- a/app/src/main/java/io/xpipe/app/comp/augment/GrowAugment.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.xpipe.app.comp.augment; - -import io.xpipe.app.comp.CompStructure; - -import javafx.beans.binding.Bindings; -import javafx.scene.Node; -import javafx.scene.layout.Region; - -public class GrowAugment> implements Augment { - - private final boolean width; - private final boolean height; - - private GrowAugment(boolean width, boolean height) { - this.width = width; - this.height = height; - } - - public static > GrowAugment create(boolean width, boolean height) { - return new GrowAugment<>(width, height); - } - - private void bind(Region r, Node parent) { - if (!(parent instanceof Region p)) { - return; - } - - if (width) { - r.prefWidthProperty() - .bind(Bindings.createDoubleBinding( - () -> { - var val = p.getWidth() - - p.getInsets().getLeft() - - p.getInsets().getRight(); - if (val <= 0) { - return Region.USE_COMPUTED_SIZE; - } - - // Floor to prevent rounding issues which cause an infinite growing - return Math.floor(val); - }, - p.widthProperty(), - p.insetsProperty())); - } - if (height) { - r.prefHeightProperty() - .bind(Bindings.createDoubleBinding( - () -> { - var val = p.getHeight() - - p.getInsets().getTop() - - p.getInsets().getBottom(); - if (val <= 0) { - return Region.USE_COMPUTED_SIZE; - } - - // Floor to prevent rounding issues which cause an infinite growing - return Math.floor(val); - }, - p.heightProperty(), - p.insetsProperty())); - } - } - - @Override - public void augment(S struc) { - struc.get().parentProperty().addListener((c, o, n) -> { - if (o instanceof Region) { - if (width) { - struc.get().prefWidthProperty().unbind(); - } - if (height) { - struc.get().prefHeightProperty().unbind(); - } - } - - bind(struc.get(), n); - }); - - bind(struc.get(), struc.get().getParent()); - } -} diff --git a/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java index f6ce4503c..f7e162dab 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java @@ -3,12 +3,10 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; import io.xpipe.app.core.AppLayoutModel; -import io.xpipe.app.core.AppProperties; import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.util.PlatformThread; -import io.xpipe.core.OsType; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; @@ -21,8 +19,6 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; -import org.apache.commons.lang3.SystemUtils; - import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -48,31 +44,6 @@ public class AppLayoutComp extends Comp { var multi = new MultiContentComp(map, true); multi.styleClass("background"); - multi.apply(struc -> { - struc.get() - .opacityProperty() - .bind(Bindings.createDoubleBinding( - () -> { - // Only Windows 11 has colored background support - if (OsType.getLocal() == OsType.WINDOWS && !SystemUtils.IS_OS_WINDOWS_11) { - return 1.0; - } - - if (OsType.getLocal() == OsType.LINUX) { - return 1.0; - } - - // On macOS, we don't have a transparent background in dev mode - if (OsType.getLocal() == OsType.MACOS - && AppProperties.get().isDevelopmentEnvironment()) { - return 1.0; - } - - return AppPrefs.get().performanceMode().get() ? 1.0 : 0.95; - }, - AppPrefs.get().performanceMode())); - }); - var pane = new BorderPane(); var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries(), model.getQueueEntries()); StackPane multiR = (StackPane) multi.createRegion(); diff --git a/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java b/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java index 7b9c8893d..df359bad5 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java @@ -6,8 +6,9 @@ import io.xpipe.app.core.*; import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.core.window.AppMainWindow; import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.platform.ColorHelper; +import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.app.util.PlatformThread; import javafx.animation.*; import javafx.beans.property.SimpleBooleanProperty; @@ -40,12 +41,15 @@ public class AppMainWindowContentComp extends SimpleComp { var loadingIcon = new ImageView(); loadingIcon.setFitWidth(80); loadingIcon.setFitHeight(80); - loadingIcon.setOpacity(0.9); - var color = - AppPrefs.get() != null && AppPrefs.get().theme().getValue().isDark() - ? Color.web("#0b898aff").darker() - : Color.web("#0b898aff"); + var dark = + AppPrefs.get() != null && AppPrefs.get().theme().getValue().isDark(); + loadingIcon.setOpacity(dark ? 0.95 : 0.93); + + var color = AppPrefs.get() != null + ? ColorHelper.withOpacity( + AppPrefs.get().theme().getValue().getEmphasisColor().get(), dark ? 0.7 : 0.85) + : Color.TRANSPARENT; DropShadow shadow = new DropShadow(); shadow.setRadius(10); shadow.setColor(color); @@ -126,7 +130,7 @@ public class AppMainWindowContentComp extends SimpleComp { overlay.addListener((ListChangeListener) c -> { if (c.next() && c.wasAdded()) { - AppMainWindow.getInstance().focus(); + AppMainWindow.get().focus(); // Close blocking modal windows var childWindows = Window.getWindows().stream() diff --git a/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java index b7e30cd5b..747b8ab16 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java @@ -3,9 +3,10 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; import io.xpipe.app.comp.SimpleCompStructure; -import io.xpipe.app.util.LabelGraphic; -import io.xpipe.app.util.PlatformThread; +import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.app.platform.PlatformThread; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.css.Size; @@ -37,6 +38,12 @@ public class ButtonComp extends Comp> { this.listener = listener; } + public ButtonComp(ObservableValue name, LabelGraphic graphic, Runnable listener) { + this.name = name; + this.graphic = new ReadOnlyObjectWrapper<>(graphic); + this.listener = listener; + } + @Override public CompStructure